using Arch.Core; using HSchool.Ai; using HSchool.Content; using HSchool.People; using HSchool.Schedule; namespace HSchool.Simulation.Tests; public class LessonTeacherPresentTests { private static readonly DateTime TuesdayMorning = new(2012, 4, 3, 6, 0, 0, DateTimeKind.Utc); private static readonly DateTime LessonStart = new(2012, 4, 3, 8, 30, 0, DateTimeKind.Utc); [Fact] public void PupilAndTeacherStanding_GainsSkill() { var (school, homeroom, pupilId, teacherId) = StaffedMath(); using (school) { AdvanceTo(school, LessonStart); SetPlace(school, pupilId, homeroom); SetPlace(school, teacherId, homeroom); var before = SkillOf(school, pupilId, "Mathematics"); LessonLearningSystem.Apply(school, 45); Assert.True(SkillOf(school, pupilId, "Mathematics") > before); } } [Fact] public void TeacherInAnotherRoom_DoesNotGain() { var (school, homeroom, pupilId, teacherId) = StaffedMath(); using (school) { AdvanceTo(school, LessonStart); SetPlace(school, pupilId, homeroom); SetPlace(school, teacherId, "restroom-1"); var before = SkillOf(school, pupilId, "Mathematics"); LessonLearningSystem.Apply(school, 45); Assert.Equal(before, SkillOf(school, pupilId, "Mathematics")); } } [Fact] public void TeacherWalking_DoesNotGain() { var (school, homeroom, pupilId, teacherId) = StaffedMath(); using (school) { AdvanceTo(school, LessonStart); SetPlace(school, pupilId, homeroom); SetPlace(school, teacherId, "corridor-1", path: [homeroom], remaining: 1.5f); var before = SkillOf(school, pupilId, "Mathematics"); LessonLearningSystem.Apply(school, 45); Assert.Equal(before, SkillOf(school, pupilId, "Mathematics")); } } [Fact] public void MissingTeacherId_DoesNotGain() { var (school, homeroom, pupilId, _) = StaffedMath(teacherId: "nobody"); using (school) { AdvanceTo(school, LessonStart); SetPlace(school, pupilId, homeroom); var before = SkillOf(school, pupilId, "Mathematics"); LessonLearningSystem.Apply(school, 45); Assert.Equal(before, SkillOf(school, pupilId, "Mathematics")); } } [Fact] public void NoTeacher_LogsOncePerSubject() { var (school, homeroom, pupilId, teacherId) = StaffedMath(); using (school) { AdvanceTo(school, LessonStart); SetPlace(school, pupilId, homeroom); SetPlace(school, teacherId, "restroom-1"); LessonLearningSystem.Apply(school, 5); LessonLearningSystem.Apply(school, 5); var rows = school.DayLog .Where(row => row.PersonId == pupilId && row.Type == PersonLogTypes.LessonNoTeacher) .ToArray(); Assert.Single(rows); Assert.Equal("Mathematics", rows[0].ThingDef); Assert.Equal( "урок без учителя: Математика", rows[0].Caption(school.Catalog!, "ru")); } } [Fact] public void HungryStillGainsLess_WhenTeacherIsPresent() { var (school, homeroom, firstId, teacherId) = StaffedMath(); using (school) { var schoolClass = school.Roster!.Classes.First(row => row.PupilIds.Contains(firstId)); var secondId = schoolClass.PupilIds.First(id => id != firstId); AdvanceTo(school, LessonStart); SetPlace(school, firstId, homeroom); SetPlace(school, secondId, homeroom); SetPlace(school, teacherId, homeroom); SetNeed(school, firstId, "Hunger", 0.2f); SetNeed(school, secondId, "Hunger", 1f); var hungryBefore = SkillOf(school, firstId, "Mathematics"); var fullBefore = SkillOf(school, secondId, "Mathematics"); LessonLearningSystem.Apply(school, 45); var hungryGain = SkillOf(school, firstId, "Mathematics") - hungryBefore; var fullGain = SkillOf(school, secondId, "Mathematics") - fullBefore; Assert.True(fullGain > 0); Assert.True(hungryGain > 0); Assert.True(fullGain > hungryGain); } } [Fact] public void WeakTeacher_GainsLessThanStrong() { var (school, homeroom, pupilId, teacherId) = StaffedMath(); using (school) { AdvanceTo(school, LessonStart); SetPlace(school, pupilId, homeroom); SetPlace(school, teacherId, homeroom); SetSkill(school, teacherId, "Mathematics", 20); var beforeWeak = SkillOf(school, pupilId, "Mathematics"); LessonLearningSystem.Apply(school, 45); var weakGain = SkillOf(school, pupilId, "Mathematics") - beforeWeak; SetSkill(school, pupilId, "Mathematics", beforeWeak); SetSkill(school, teacherId, "Mathematics", 80); var beforeStrong = SkillOf(school, pupilId, "Mathematics"); LessonLearningSystem.Apply(school, 45); var strongGain = SkillOf(school, pupilId, "Mathematics") - beforeStrong; Assert.True(weakGain > 0); Assert.True(strongGain > weakGain); } } [Fact] public void MissingTeacherSkillKey_StillGains() { var (school, homeroom, pupilId, teacherId) = StaffedMath(); using (school) { AdvanceTo(school, LessonStart); SetPlace(school, pupilId, homeroom); SetPlace(school, teacherId, homeroom); ClearSkill(school, teacherId, "Mathematics"); var before = SkillOf(school, pupilId, "Mathematics"); LessonLearningSystem.Apply(school, 45); Assert.True(SkillOf(school, pupilId, "Mathematics") > before); } } [Fact] public void TeacherSkill_DoesNotRescueAbsence() { var (school, homeroom, pupilId, teacherId) = StaffedMath(); using (school) { AdvanceTo(school, LessonStart); SetPlace(school, pupilId, homeroom); SetPlace(school, teacherId, "restroom-1"); SetSkill(school, teacherId, "Mathematics", 100); var before = SkillOf(school, pupilId, "Mathematics"); LessonLearningSystem.Apply(school, 45); Assert.Equal(before, SkillOf(school, pupilId, "Mathematics")); } } private static (School School, string Homeroom, string PupilId, string TeacherId) StaffedMath( string? teacherId = null) { var (catalog, map) = Vanilla(); var roster = RosterGenerator.Generate(catalog, map, schoolSeed: 1, "Russia", TuesdayMorning); var pool = ApplicantPool.Create(catalog, roster, schoolSeed: 1, "Russia", TuesdayMorning); var hired = Staffing.Hire(catalog, map, roster, pool, pool.Applicants[0].Person.Id, Staffing.TeacherPosition, 1_000_000f); Assert.Equal(StaffingError.None, hired.Error); roster = hired.Roster; pool = hired.Pool; var hiredId = roster.People.First(person => person.IsStaff).Id; var schoolClass = roster.Classes.First(row => row.RoomId is "classroom-101" or "classroom-102" or "classroom-103" or "classroom-104"); var school = School.Create(1, "Урок", TuesdayMorning, catalog, map); school.InstallPeople(roster, seed: 1, "Russia", pool); school.SetTimetable(new Timetable( [new LessonPlacement(schoolClass.Id, "Mathematics", teacherId ?? hiredId, schoolClass.RoomId, Day: 1, Period: 1)], [])); school.ConfigurePresence(weekDays: 5, maxDecisionsPerTick: 10_000); var pupil = schoolClass.PupilIds .Select(id => school.Roster!.People.First(person => person.Id == id)) .First(person => !person.Traits.Contains("Lazy")); return (school, schoolClass.RoomId, pupil.Id, hiredId); } private static void AdvanceTo(School school, DateTime until) { while (school.Clock.Time < until) { school.Tick(0.2d, 5d); } } private static void SetPlace(School school, string personId, string node, string[]? path = null, float remaining = 0f) { var query = new QueryDescription().WithAll(); school.World.Query( in query, (ref PersonIdentity identity, ref Presence presence) => { if (identity.Id.Equals(personId, StringComparison.Ordinal)) { presence = new Presence(node, remaining, node, HeadingHome: false, path ?? []); } }); } private static void SetNeed(School school, string personId, string need, float value) { var query = new QueryDescription().WithAll(); school.World.Query( in query, (ref PersonIdentity identity, ref PersonNeeds needs) => { if (identity.Id.Equals(personId, StringComparison.Ordinal)) { needs.Values[need] = value; } }); } private static float SkillOf(School school, string personId, string skill) { var value = float.NaN; var query = new QueryDescription().WithAll(); school.World.Query( in query, (ref PersonIdentity identity, ref PersonSkills skills) => { if (identity.Id.Equals(personId, StringComparison.Ordinal)) { value = skills.Values.GetValueOrDefault(skill); } }); return value; } private static void SetSkill(School school, string personId, string skill, float value) { var query = new QueryDescription().WithAll(); school.World.Query( in query, (ref PersonIdentity identity, ref PersonSkills skills) => { if (identity.Id.Equals(personId, StringComparison.Ordinal)) { skills.Values[skill] = value; } }); } private static void ClearSkill(School school, string personId, string skill) { var query = new QueryDescription().WithAll(); school.World.Query( in query, (ref PersonIdentity identity, ref PersonSkills skills) => { if (identity.Id.Equals(personId, StringComparison.Ordinal)) { skills.Values.Remove(skill); } }); } private static (DefCatalog Catalog, MapLayout Map) Vanilla() { var root = Path.Combine(AppContext.BaseDirectory, "vanilla"); var documents = new List(); foreach (var path in Directory.EnumerateFiles(root, "*.*", SearchOption.AllDirectories)) { if (!path.EndsWith(".jsonc", StringComparison.OrdinalIgnoreCase) && !path.EndsWith(".json", StringComparison.OrdinalIgnoreCase)) { continue; } var relative = Path.GetRelativePath(root, path).Replace('\\', '/'); documents.Add(new ContentDocument(CatalogLoader.CorePackId, relative, File.ReadAllText(path))); } var catalog = new CatalogLoader().Load([CatalogLoader.CorePackId], documents); var map = CatalogLoader.LastDefaultMap([CatalogLoader.CorePackId], documents); Assert.NotNull(map); return (catalog, map); } }