using Arch.Core; using HSchool.Content; using HSchool.People; using HSchool.Schedule; namespace HSchool.Simulation.Tests; public class DecisionTests { private static readonly DateTime TuesdayMorning = new(2012, 4, 3, 6, 0, 0, DateTimeKind.Utc); [Fact] public void ZeroToilet_LeavesClass_ReachesRestroom_AndReturns() { var (school, homeroom, pupilId) = StaffedFirstFloorClass(); using (school) { AdvanceTo(school, new DateTime(2012, 4, 3, 8, 30, 0, DateTimeKind.Utc)); SetNeed(school, pupilId, "Toilet", 0f); Assert.Equal(0f, NeedOf(school, pupilId, "Toilet")); school.QueueDecision(pupilId); if (!WaitUntil(school, pupilId, row => school.Map!.NodeDef(row.NodeId ?? "") == "Restroom", 25)) { var row = school.CapturePresence().Single(item => item.PersonId == pupilId); Assert.Fail( $"never reached a restroom: node={row.NodeId} dest={row.DestinationId} goal={row.GoalKind}/{row.GoalId}/{row.GoalAction} action={row.ActionId} toilet={NeedOf(school, pupilId, "Toilet")} path={string.Join(",", row.Path)}"); } Assert.True(WaitUntil(school, pupilId, row => row.ActionId == "UseToilet", 8)); Assert.True(WaitUntil( school, pupilId, row => row.NodeId == homeroom && row.Path.Count == 0 && row.RemainingMinutes <= 0 && row.ActionId is null, 25)); } } [Fact] public void NeedJustBelowThreshold_StaysInClass() { var (school, homeroom, pupilId) = StaffedFirstFloorClass(); using (school) { AdvanceTo(school, new DateTime(2012, 4, 3, 8, 30, 0, DateTimeKind.Utc)); SetNeed(school, pupilId, "Toilet", 0.34f); school.QueueDecision(pupilId); AdvanceTo(school, new DateTime(2012, 4, 3, 8, 38, 0, DateTimeKind.Utc)); var row = school.CapturePresence().Single(item => item.PersonId == pupilId); Assert.Equal(homeroom, row.NodeId); Assert.Empty(row.Path); Assert.NotEqual("UseToilet", row.ActionId); Assert.Equal("Duty", row.GoalKind); } } [Fact] public void TwoEqualNeeds_FinishTheToiletAction() { var (school, _, pupilId) = StaffedFirstFloorClass(); using (school) { AdvanceTo(school, new DateTime(2012, 4, 3, 8, 30, 0, DateTimeKind.Utc)); SetNeed(school, pupilId, "Toilet", 0f); school.QueueDecision(pupilId); Assert.True(WaitUntil(school, pupilId, row => row.ActionId == "UseToilet", 25)); SetNeed(school, pupilId, "Hunger", 0f); school.QueueDecision(pupilId); school.Tick(0.2d, 5d); Assert.Equal("UseToilet", ActivityOf(school, pupilId)); } } [Fact] public void BreakPicksLeisure_LessonDoesNot() { var (school, homeroom, pupilId) = TwoHomeroomLessons(); using (school) { AdvanceTo(school, new DateTime(2012, 4, 3, 8, 45, 0, DateTimeKind.Utc)); var inLesson = school.CapturePresence().Single(item => item.PersonId == pupilId); Assert.Equal(homeroom, inLesson.NodeId); Assert.Equal("Duty", inLesson.GoalKind); Assert.Null(inLesson.ActionId); AdvanceTo(school, new DateTime(2012, 4, 3, 9, 18, 0, DateTimeKind.Utc)); var onBreak = school.CapturePresence().Single(item => item.PersonId == pupilId); Assert.Equal("Leisure", onBreak.GoalKind); Assert.True( school.Map!.NodeDef(onBreak.NodeId ?? "") is "Corridor" || school.Map.NodeDef(onBreak.DestinationId ?? "") is "Corridor" || onBreak.ActionId is "Chat" or "RecessRest" or "WalkCorridor"); Assert.True( school.Map!.NodeDef(onBreak.NodeId ?? "") is "Corridor" || school.Map.NodeDef(onBreak.DestinationId ?? "") is "Corridor" || onBreak.ActionId is "Chat" or "RecessRest" or "WalkCorridor"); } } [Fact] public void HungryPupil_GainsLessSkillDuringTheLesson() { var (school, _, firstId) = StaffedFirstFloorClass(); using (school) { var schoolClass = school.Roster!.Classes.First(row => row.PupilIds.Contains(firstId)); var secondId = schoolClass.PupilIds.First(id => id != firstId); AdvanceTo(school, new DateTime(2012, 4, 3, 8, 30, 0, DateTimeKind.Utc)); SetNeed(school, firstId, "Hunger", 0.2f); SetNeed(school, secondId, "Hunger", 1f); var hungryBefore = SkillOf(school, firstId, "Mathematics"); var fullBefore = SkillOf(school, secondId, "Mathematics"); AdvanceTo(school, new DateTime(2012, 4, 3, 9, 15, 0, DateTimeKind.Utc)); 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 DecisionCap_DefersTheOverflow() { var (school, _, firstId) = StaffedFirstFloorClass(); using (school) { var secondId = school.Roster!.People.First(person => person.IsStudent && person.Id != firstId).Id; AdvanceTo(school, new DateTime(2012, 4, 3, 8, 45, 0, DateTimeKind.Utc)); school.Tick(0.2d, 5d); Assert.Equal(0, school.PendingDecisionCount); school.ConfigurePresence(weekDays: 5, maxDecisionsPerTick: 1); school.QueueDecision(firstId); school.QueueDecision(secondId); Assert.Equal(2, school.PendingDecisionCount); school.Tick(0.2d, 5d); Assert.Equal(1, school.PendingDecisionCount); } } [Fact] public void SameSeedAndActions_MatchAfterAWeek() { var (catalog, map) = Vanilla(); using var a = OpenStaffed(catalog, map, seed: 9); using var b = OpenStaffed(catalog, map, seed: 9); var until = new DateTime(2012, 4, 10, 9, 20, 0, DateTimeKind.Utc); PlayTo(a, until); PlayTo(b, until); Assert.Equal(StateFingerprint(a), StateFingerprint(b)); } private static (School School, string Homeroom, string PupilId) StaffedFirstFloorClass() { var (catalog, map) = Vanilla(); var school = OpenStaffed(catalog, map, seed: 1); var homeroomClass = school.Roster!.Classes.First(row => row.RoomId is "classroom-101" or "classroom-102" or "classroom-103" or "classroom-104"); var pupil = homeroomClass.PupilIds .Select(id => school.Roster.People.First(person => person.Id == id)) .First(person => !person.Traits.Contains("Lazy")); return (school, homeroomClass.RoomId, pupil.Id); } private static (School School, string Homeroom, string PupilId) TwoHomeroomLessons() { 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 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", "t1", schoolClass.RoomId, Day: 1, Period: 1), new LessonPlacement(schoolClass.Id, "Mathematics", "t1", schoolClass.RoomId, Day: 1, Period: 2), ], [])); 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); } private static School OpenStaffed(DefCatalog catalog, MapLayout map, int seed) { var roster = RosterGenerator.Generate(catalog, map, seed, "Russia", TuesdayMorning); var pool = ApplicantPool.Create(catalog, roster, seed, "Russia", TuesdayMorning); var schoolClass = roster.Classes.First(row => row.RoomId is "classroom-101" or "classroom-102" or "classroom-103" or "classroom-104"); var school = School.Create(seed, "Решения", TuesdayMorning, catalog, map); school.InstallPeople(roster, seed, "Russia", pool); school.SetTimetable(new Timetable( [ new LessonPlacement(schoolClass.Id, "Mathematics", "t1", schoolClass.RoomId, Day: 1, Period: 1), new LessonPlacement(schoolClass.Id, "PhysicalEducation", "t2", "gym-hall", Day: 1, Period: 2), ], [])); school.ConfigurePresence(weekDays: 5, maxDecisionsPerTick: 10_000); return school; } private static void AdvanceTo(School school, DateTime until) { while (school.Clock.Time < until) { school.Tick(0.2d, 5d); } } private static void PlayTo(School school, DateTime until) { while (school.Clock.Time < until) { if (school.PeekSkipEmpty().Allowed) { school.TrySkipEmpty(); continue; } school.Tick(0.2d, 5d); } } private static bool WaitUntil(School school, string personId, Func match, int minutes) { for (var i = 0; i < minutes; i++) { school.Tick(0.2d, 5d); var row = school.CapturePresence().Single(item => item.PersonId == personId); if (match(row)) { return true; } } return false; } 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 NeedOf(School school, string personId, string need) { var value = float.NaN; var query = new QueryDescription().WithAll(); school.World.Query(in query, (ref PersonIdentity identity, ref PersonNeeds needs) => { if (identity.Id.Equals(personId, StringComparison.Ordinal)) { value = needs.Values.GetValueOrDefault(need, float.NaN); } }); return 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 string? ActivityOf(School school, string personId) { string? found = null; var query = new QueryDescription().WithAll(); school.World.Query(in query, (ref PersonIdentity identity, ref PersonActivity activity) => { if (identity.Id.Equals(personId, StringComparison.Ordinal)) { found = activity.ActionId; } }); return found; } private static string StateFingerprint(School school) { var needs = new Dictionary(StringComparer.Ordinal); var skills = new Dictionary(StringComparer.Ordinal); var needQuery = new QueryDescription().WithAll(); school.World.Query(in needQuery, (ref PersonIdentity identity, ref PersonNeeds personNeeds, ref PersonSkills personSkills) => { needs[identity.Id] = string.Join(",", personNeeds.Values.OrderBy(pair => pair.Key, StringComparer.Ordinal).Select(pair => $"{pair.Key}:{pair.Value:0.###}")); skills[identity.Id] = string.Join(",", personSkills.Values.OrderBy(pair => pair.Key, StringComparer.Ordinal).Select(pair => $"{pair.Key}:{pair.Value:0.###}")); }); return string.Join( "|", school.CapturePresence() .OrderBy(row => row.PersonId, StringComparer.Ordinal) .Select(row => $"{row.PersonId}:{row.NodeId ?? "-"}:{row.RemainingMinutes:0.###}:{row.DestinationId ?? "-"}:{(row.HeadingHome ? "1" : "0")}:{string.Join(",", row.Path)}:{row.ActionId ?? "-"}:{row.GoalKind ?? "-"}:{needs.GetValueOrDefault(row.PersonId)}:{skills.GetValueOrDefault(row.PersonId)}")); } 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); } }