using HSchool.Ai; using HSchool.Content; using HSchool.People; using HSchool.Schedule; using HSchool.Simulation; namespace HSchool.Simulation.Tests; public class AppropriatenessSimulationTests { private static readonly DateTime TuesdayMorning = new(2012, 4, 3, 6, 0, 0, DateTimeKind.Utc); private static readonly DateTime JanuaryMorning = new(2012, 1, 10, 6, 0, 0, DateTimeKind.Utc); private static readonly DateTime JulyMorning = new(2012, 7, 10, 6, 0, 0, DateTimeKind.Utc); [Fact] public void Morning_NoBrightPolicy_WornColorsAreNotBright() { using var school = Open(TuesdayMorning, seed: 31); school.DressRules = school.DressRules with { Students = new DressRulePair(FormPolicies.Regular, ColorPolicies.NoBright), }; MorningDress.Apply(school); foreach (var pupil in school.Roster!.People.Where(person => person.IsStudent)) { foreach (var item in pupil.Items.Where(row => row.Location == ItemLocations.Worn)) { if (item.Color is { } color && school.Catalog!.Colors.TryGetValue(color, out var colorDef)) { Assert.DoesNotContain(ColorTags.Bright, colorDef.Tags, StringComparer.Ordinal); } } } } [Fact] public void FrostMorning_WornOuterExists_JulyHasNoFurCoat() { using var january = Open(JanuaryMorning, seed: 31); MorningDress.Apply(january); Assert.Contains( january.Roster!.People.Where(person => person.IsStudent), person => person.Items.Any(item => item.Location == ItemLocations.Worn && january.Catalog!.Things[item.Def].Layers.Any(layer => layer.Equals(ApparelLayers.Outer, StringComparison.Ordinal)))); using var july = Open(JulyMorning, seed: 31); july.SyncWeather(force: true); MorningDress.Apply(july); Assert.DoesNotContain( july.Roster!.People.SelectMany(person => person.Items), item => item.Location == ItemLocations.Worn && item.Def == "FurCoat"); } [Fact] public void MorningDress_StagesPeKitForTodaysLesson() { var (school, pupilId) = StaffedPeClass(); using (school) { MorningDress.Apply(school); var person = school.Roster!.People.Single(item => item.Id == pupilId); Assert.Contains( person.Items, item => item.Def is "PeShirt" or "PeShorts" && item.Location is ItemLocations.Locker or ItemLocations.Bag); } } [Fact] public void PeBreak_CompletesChangeBeforeLesson() { var (school, pupilId) = StaffedPeClass(); using (school) { MorningDress.Apply(school); AdvanceTo(school, new DateTime(2012, 4, 3, 9, 30, 0, DateTimeKind.Utc)); var person = school.Roster!.People.Single(item => item.Id == pupilId); Assert.Contains( person.Items, item => item.Location == ItemLocations.Worn && item.Def == "PeShirt"); } } [Fact] public void AfterPeBreak_ModeExpectsEveryday() { var (school, pupilId) = StaffedPeClass(); using (school) { MorningDress.Apply(school); AdvanceTo(school, new DateTime(2012, 4, 3, 10, 12, 0, DateTimeKind.Utc)); var person = school.Roster!.People.Single(item => item.Id == pupilId); var schoolClass = school.Roster.Classes.First(row => row.PupilIds.Contains(pupilId)); Assert.Equal(ApparelMode.Everyday, ApparelPresence.ModeAt(school, person, schoolClass)); Assert.True(ApparelDresser.CurrentIssues(school, person, ApparelMode.Everyday).HasFlag(ApparelIssue.PeForbidden)); } } [Fact] public void PeBreak_PupilReachesOwnChangingRoom() { var (school, pupilId) = StaffedPeClass(); using (school) { MorningDress.Apply(school); AdvanceTo(school, new DateTime(2012, 4, 3, 9, 24, 0, DateTimeKind.Utc)); var row = school.CapturePresence().Single(item => item.PersonId == pupilId); var person = school.Roster!.People.Single(item => item.Id == pupilId); var room = person.Female ? "girls-changing-room" : "boys-changing-room"; Assert.Equal(room, row.NodeId); Assert.Empty(row.Path); } } [Fact] public void PeLesson_WearsPeThenReturnsToEveryday() { var (school, pupilId) = StaffedPeClass(); using (school) { MorningDress.Apply(school); AdvanceTo(school, new DateTime(2012, 4, 3, 9, 35, 0, DateTimeKind.Utc)); var person = school.Roster!.People.Single(item => item.Id == pupilId); Assert.Contains( person.Items, item => item.Location == ItemLocations.Worn && item.Def == "PeShirt"); Assert.Contains( person.Items, item => item.Location == ItemLocations.Worn && item.Def == "PeShorts"); Assert.DoesNotContain( person.Items, item => item.Location == ItemLocations.Worn && item.Def is "Shirt" or "TShirt" or "Dress"); AdvanceTo(school, new DateTime(2012, 4, 3, 10, 25, 0, DateTimeKind.Utc)); person = school.Roster.People.Single(item => item.Id == pupilId); Assert.DoesNotContain( person.Items, item => item.Location == ItemLocations.Worn && item.Def == "PeShirt"); Assert.Contains( person.Items, item => item.Location == ItemLocations.Worn && item.Def is "Shirt" or "TShirt" or "Dress" or "Skirt" or "Pants" or "Jeans" or "Trousers"); } } [Fact] public void Boy_DoesNotEnterGirlsChangingRoom() { var (school, pupilId) = StaffedPeClass(forceBoy: true); using (school) { MorningDress.Apply(school); AdvanceTo(school, new DateTime(2012, 4, 3, 9, 18, 0, DateTimeKind.Utc)); var row = school.CapturePresence().Single(item => item.PersonId == pupilId); Assert.NotEqual("girls-changing-room", row.NodeId); } } [Fact] public void ChangeWithoutLocker_StashedItemGoesToBag() { using var school = Open(TuesdayMorning, seed: 31); school.DressRules = school.DressRules with { Students = new DressRulePair(FormPolicies.Regular, ColorPolicies.NoBright), }; var pupil = school.Roster!.People.First(person => person.IsStudent && person.LockerRoomId is null); var items = pupil.Items.ToList(); items.Add(new InventoryItem("TShirt", "Red", 1f, ItemLocations.Worn)); ReplaceItems(pupil, items); ApparelDresser.RedressPerson(school, pupil, ApparelMode.Everyday, includeHome: false); pupil = school.Roster.People.Single(person => person.Id == pupil.Id); Assert.Contains(pupil.Items, item => item.Def == "TShirt" && item.Color == "Red" && item.Location == ItemLocations.Bag); } [Fact] public void RedressPerson_WritesApparelChangedRows() { using var school = Open(TuesdayMorning, seed: 31); school.DressRules = school.DressRules with { Students = new DressRulePair(FormPolicies.Regular, ColorPolicies.NoBright), }; var pupil = school.Roster!.People.First(person => person.IsStudent && person.LockerRoomId is null); var items = pupil.Items.ToList(); items.Add(new InventoryItem("TShirt", "Red", 1f, ItemLocations.Worn)); ReplaceItems(pupil, items); ApparelDresser.RedressPerson(school, pupil, ApparelMode.Everyday, includeHome: false); Assert.Contains( school.DayLog, row => row.PersonId == pupil.Id && row.Type == PersonLogTypes.ApparelChanged && row.ThingDef == "TShirt"); var page = PersonLogBrowser.Apply( school.DayLog, pupil.Id, new PersonLogQuery(null, Descending: true, Page: 1, PageSize: 20), school.Catalog, "ru"); Assert.Contains(page.Entries, entry => entry.Type == PersonLogTypes.ApparelChanged); Assert.Contains( page.Entries, entry => entry.Label.StartsWith("переоделся:", StringComparison.Ordinal)); } [Fact] public void PendingStrictRule_DoesNotApplyUntilNextMorning() { using var school = Open(TuesdayMorning, seed: 31); MorningDress.Apply(school); var before = BrightWornCount(school); school.DressRules = school.DressRules with { PendingStudents = new DressRulePair(FormPolicies.Strict, ColorPolicies.NoBright), }; MorningDress.Apply(school); Assert.Equal(before, BrightWornCount(school)); school.Clock.JumpTo(new DateTime(2012, 4, 4, 6, 0, 0, DateTimeKind.Utc)); ApparelWear.Apply(school, gameMinutes: 0, before: new DateTime(2012, 4, 3, 22, 0, 0, DateTimeKind.Utc)); Assert.Equal(0, BrightWornCount(school)); } private static int BrightWornCount(School school) { var count = 0; foreach (var pupil in school.Roster!.People.Where(person => person.IsStudent)) { foreach (var item in pupil.Items.Where(row => row.Location == ItemLocations.Worn)) { if (item.Color is { } color && school.Catalog!.Colors.TryGetValue(color, out var colorDef) && colorDef.Tags.Contains(ColorTags.Bright, StringComparer.Ordinal)) { count++; } } } return count; } private static void ReplaceItems(Person person, List items) { if (person.Items is IList list && !list.IsReadOnly) { list.Clear(); foreach (var item in items) { list.Add(item); } } } private static (School School, string PupilId) StaffedPeClass(bool forceBoy = false) { var (catalog, map) = Vanilla(); var roster = RosterGenerator.Generate(catalog, map, schoolSeed: 1, "Russia", TuesdayMorning); roster = LockerAssigner.Apply(catalog, map, roster); roster = DressGenerator.EnsureRoster(catalog, roster, schoolSeed: 1, 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 pupil = roster.People .Where(person => schoolClass.PupilIds.Contains(person.Id)) .First(person => forceBoy ? !person.Female : true); var school = School.Create(1, "PE", TuesdayMorning, catalog, map); school.InstallPeople(roster, seed: 1, "Russia", pool, climatePresetId: "TemperateContinental"); 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), new LessonPlacement(schoolClass.Id, "Mathematics", "t1", schoolClass.RoomId, Day: 1, Period: 3), ], [])); school.ConfigurePresence(weekDays: 5, maxDecisionsPerTick: 10_000); return (school, pupil.Id); } private static School Open(DateTime start, int seed) { var (catalog, map) = Vanilla(); var roster = RosterGenerator.Generate(catalog, map, seed, "Russia", start); roster = LockerAssigner.Apply(catalog, map, roster); roster = DressGenerator.EnsureRoster(catalog, roster, seed, start); var pool = ApplicantPool.Create(catalog, roster, seed, "Russia", start); var school = School.Create(1, "Одежда", start, catalog, map); school.InstallPeople(roster, seed, "Russia", pool, climatePresetId: "TemperateContinental"); 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 (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); } }