using HSchool.Content; using HSchool.People; using HSchool.Schedule; namespace HSchool.Simulation.Tests; public class ApparelWearTests { private static readonly DateTime TuesdayMorning = new(2012, 4, 3, 6, 0, 0, DateTimeKind.Utc); private static readonly DateTime TuesdayLesson = new(2012, 4, 3, 8, 30, 0, DateTimeKind.Utc); private static readonly DateTime TuesdayEvening = new(2012, 4, 3, 22, 0, 0, DateTimeKind.Utc); private static readonly DateTime WednesdayMorning = new(2012, 4, 4, 6, 0, 0, DateTimeKind.Utc); [Fact] public void HourOnCampus_DropsWornShirtByWearPerHour() { using var school = OpenStaffed(); AdvanceTo(school, TuesdayLesson); var (person, index) = WornShirt(school, onCampus: true); var rate = school.Catalog!.BehaviorRules!.ApparelWearPerHour; var before = person.Items[index].Condition; TickMinutes(school, 60); var row = school.CapturePresence().Single(item => item.PersonId == person.Id); Assert.NotNull(row.NodeId); Assert.Equal(before - rate, person.Items[index].Condition, precision: 4); } [Fact] public void HourOnCampus_LeavesBagAndLockerShirtsAlone() { using var school = OpenStaffed(); AdvanceTo(school, TuesdayLesson); var (person, wornIndex) = WornShirt(school, onCampus: true); var bag = person.Items[wornIndex] with { Location = ItemLocations.Bag, Condition = 1f }; var locker = person.Items[wornIndex] with { Location = ItemLocations.Locker, Condition = 1f }; AddItem(person, bag); AddItem(person, locker); TickMinutes(school, 60); Assert.True(person.Items[wornIndex].Condition < 1f); Assert.Equal(1f, person.Items.Single(item => item.Location == ItemLocations.Bag && item.Def == bag.Def).Condition); Assert.Equal(1f, person.Items.Single(item => item.Location == ItemLocations.Locker && item.Def == locker.Def).Condition); } [Fact] public void WearWithoutCrossingABand_DoesNotMarkPeopleChanged() { using var school = OpenStaffed(); AdvanceTo(school, TuesdayLesson); var changed = false; for (var i = 0; i < 60; i++) { changed |= school.Tick(0.2d, 5d); } Assert.False(changed); } [Fact] public void CrossingAConditionBand_MarksPeopleChanged() { using var school = OpenStaffed(); AdvanceTo(school, TuesdayLesson); var (person, index) = WornShirt(school, onCampus: true); SetItem(person, index, person.Items[index] with { Condition = 0.755f }); var changed = false; for (var i = 0; i < 60; i++) { changed |= school.Tick(0.2d, 5d); } Assert.True(person.Items[index].Condition < 0.75f); Assert.True(changed); } [Fact] public void RaggedWornShirt_IsReplacedByWorkMorning() { using var school = OpenEmpty(TuesdayEvening); var (person, index) = WornShirt(school); var def = person.Items[index].Def; var color = person.Items[index].Color; SetItem(person, index, person.Items[index] with { Condition = 0.05f }); var result = school.TrySkipEmpty(); Assert.True(result.Succeeded); Assert.True(result.PeopleChanged); Assert.Equal(WednesdayMorning, school.Clock.Time); Assert.Equal(def, person.Items[index].Def); Assert.Equal(color, person.Items[index].Color); Assert.Equal(1f, person.Items[index].Condition); Assert.Contains( school.DayLog, row => row.PersonId == person.Id && row.Type == PersonLogTypes.ApparelReplaced && row.ThingDef == def); var caption = school.DayLog.First(row => row.PersonId == person.Id).Caption(school.Catalog!, "ru"); Assert.StartsWith("получил новую ", caption, StringComparison.Ordinal); Assert.Contains(school.Catalog!.Label("ru", school.Catalog.Things[def]), caption, StringComparison.Ordinal); } [Fact] public void SkipThroughTheNight_MatchesALivedNight() { using var skipped = OpenEmpty(TuesdayEvening); using var lived = OpenEmpty(TuesdayEvening); var skippedShirt = WornShirt(skipped); var livedShirt = WornShirt(lived); SetItem(skippedShirt.Person, skippedShirt.Index, skippedShirt.Person.Items[skippedShirt.Index] with { Condition = 0.05f }); SetItem(livedShirt.Person, livedShirt.Index, livedShirt.Person.Items[livedShirt.Index] with { Condition = 0.05f }); Assert.True(skipped.TrySkipEmpty().Succeeded); while (lived.Clock.Time < WednesdayMorning) { lived.Tick(0.2d, 5d); } Assert.Equal(skipped.Clock.Time, lived.Clock.Time); Assert.Equal(1f, skippedShirt.Person.Items[skippedShirt.Index].Condition); Assert.Equal(1f, livedShirt.Person.Items[livedShirt.Index].Condition); Assert.Equal(skippedShirt.Person.Items[skippedShirt.Index].Def, livedShirt.Person.Items[livedShirt.Index].Def); Assert.Equal( skipped.DayLog.Select(row => (row.Type, row.ThingDef)).OrderBy(row => row.ThingDef, StringComparer.Ordinal), lived.DayLog.Select(row => (row.Type, row.ThingDef)).OrderBy(row => row.ThingDef, StringComparer.Ordinal)); } [Fact] public void ConditionCaption_FollowsTheCatalogBands() { var catalog = Vanilla().Catalog; Assert.Equal("ApparelConditionIntact", ApparelCondition.BandId(catalog.BehaviorRules, 1f)); Assert.Equal("ApparelConditionWorn", ApparelCondition.BandId(catalog.BehaviorRules, 0.5f)); Assert.Equal("ApparelConditionTorn", ApparelCondition.BandId(catalog.BehaviorRules, 0.2f)); Assert.Equal("ApparelConditionRags", ApparelCondition.BandId(catalog.BehaviorRules, 0.1f)); Assert.Equal("целая", ApparelCondition.Label(catalog, "ru", 1f)); Assert.Equal("висит лохмотьями", ApparelCondition.Label(catalog, "ru", 0.1f)); Assert.Equal("intact", ApparelCondition.Label(catalog, "en", 1f)); } private static (Person Person, int Index) WornShirt(School school, bool onCampus = false) { var here = onCampus ? school.CapturePresence() .Where(row => row.NodeId is not null) .Select(row => row.PersonId) .ToHashSet(StringComparer.Ordinal) : null; foreach (var person in school.Roster!.People) { if (here is not null && !here.Contains(person.Id)) { continue; } for (var i = 0; i < person.Items.Count; i++) { if (person.Items[i].Location.Equals(ItemLocations.Worn, StringComparison.Ordinal) && (person.Items[i].Def.Equals("Shirt", StringComparison.Ordinal) || person.Items[i].Def.Equals("TShirt", StringComparison.Ordinal))) { return (person, i); } } } throw new InvalidOperationException("No worn Shirt in the roster."); } private static void SetItem(Person person, int index, InventoryItem item) { if (person.Items is not IList list || list.IsReadOnly) { throw new InvalidOperationException("Items is not a mutable list."); } list[index] = item; } private static void AddItem(Person person, InventoryItem item) { if (person.Items is not IList list || list.IsReadOnly) { throw new InvalidOperationException("Items is not a mutable list."); } list.Add(item); } private static void TickMinutes(School school, int minutes) { for (var i = 0; i < minutes; i++) { school.Tick(0.2d, 5d); } } private static void AdvanceTo(School school, DateTime until) { while (school.Clock.Time < until) { school.Tick(0.2d, 5d); } } private static School OpenStaffed() { 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, "PhysicalEducation", "t2", "gym-hall", Day: 1, Period: 2), ], [])); school.ConfigurePresence(weekDays: 5, maxDecisionsPerTick: 10_000); return school; } private static School OpenEmpty(DateTime start) { var (catalog, map) = Vanilla(); var roster = RosterGenerator.Generate(catalog, map, schoolSeed: 1, "Russia", start); var pool = ApplicantPool.Create(catalog, roster, schoolSeed: 1, "Russia", start); var school = School.Create(1, "Износ ночь", start, catalog, map); school.InstallPeople(roster, seed: 1, "Russia", pool); school.ConfigurePresence(weekDays: 5, maxDecisionsPerTick: 10_000); return school; } 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); } }