using Arch.Core; using HSchool.Ai; using HSchool.Content; using HSchool.People; namespace HSchool.Simulation.Tests; public class PersonDayLogTests { private static readonly DateTime TuesdayFive = new(2012, 4, 3, 5, 0, 0, DateTimeKind.Utc); private static readonly DateTime TuesdayNoon = new(2012, 4, 3, 12, 0, 0, DateTimeKind.Utc); private static readonly DateTime TuesdayEvening = new(2012, 4, 3, 22, 0, 0, DateTimeKind.Utc); [Fact] public void TryStartAndFinish_WriteStartAndEndRows() { using var school = OpenCafeteria(TuesdayNoon); Spawn(school, "a", hunger: 0.3f); Assert.True(school.TryStartAction("a", "EatLunch")); Assert.Contains( school.DayLog, row => row.PersonId == "a" && row.Type == PersonLogTypes.ActionStarted && row.ThingDef == "EatLunch"); TickMinutes(school, 16); Assert.Contains( school.DayLog, row => row.PersonId == "a" && row.Type == PersonLogTypes.ActionEnded && row.ThingDef == "EatLunch"); Assert.Equal( "начал: Обед", school.DayLog.First(row => row.Type == PersonLogTypes.ActionStarted).Caption(school.Catalog!, "ru")); } [Fact] public void AfterSix_YesterdaysRowsAreGone() { using var school = OpenCafeteria(TuesdayFive); Spawn(school, "a", hunger: 0.3f); Assert.True(school.TryStartAction("a", "EatLunch")); Assert.NotEmpty(school.DayLog); TickMinutes(school, 61); Assert.True(school.Clock.Time >= new DateTime(2012, 4, 3, 6, 0, 0, DateTimeKind.Utc)); Assert.Empty(school.DayLog); } [Fact] public void SkipEmpty_ClearsYesterdaysRows() { using var school = OpenEmpty(TuesdayEvening); school.AppendDayLog(new PersonLogEvent("f0.c0", TuesdayEvening, PersonLogTypes.ActionStarted, "EatLunch")); Assert.NotEmpty(school.DayLog); var result = school.TrySkipEmpty(); Assert.True(result.Succeeded); Assert.Empty(school.DayLog); } [Fact] public void Pages_DoNotOverlap() { var time = TuesdayNoon; var log = new List(); for (var i = 0; i < 25; i++) { log.Add(new PersonLogEvent("p", time.AddMinutes(i), PersonLogTypes.ActionStarted, "EatLunch")); } var first = PersonLogBrowser.Apply(log, "p", new PersonLogQuery(null, Descending: false, Page: 1, PageSize: 10), catalog: null, "ru"); var second = PersonLogBrowser.Apply(log, "p", new PersonLogQuery(null, Descending: false, Page: 2, PageSize: 10), catalog: null, "ru"); Assert.Equal(25, first.Total); Assert.Equal(10, first.Entries.Count); Assert.Equal(10, second.Entries.Count); Assert.Empty(first.Entries.Select(row => row.Time).Intersect(second.Entries.Select(row => row.Time))); Assert.Equal(time, first.Entries[0].Time); Assert.Equal(time.AddMinutes(10), second.Entries[0].Time); } [Fact] public void Search_FiltersByCaption() { var log = new[] { new PersonLogEvent("p", TuesdayNoon, PersonLogTypes.ActionStarted, "EatLunch"), new PersonLogEvent("p", TuesdayNoon.AddMinutes(1), PersonLogTypes.ActionStarted, "UseToilet"), }; var page = PersonLogBrowser.Apply(log, "p", new PersonLogQuery("toilet", Descending: false, Page: 1, PageSize: 20), catalog: null, "en"); Assert.Equal(1, page.Total); Assert.Equal("action-started", page.Entries[0].Type); Assert.Equal("UseToilet", page.Entries[0].ThingDef); } private static void TickMinutes(School school, int minutes) { for (var i = 0; i < minutes; i++) { school.Tick(0.2d, 5d); } } private static School OpenCafeteria(DateTime start) { var catalog = new CatalogLoader().Load( [CatalogLoader.CorePackId], [ new ContentDocument("core", "defs/actions/sit.jsonc", """{ "defName": "Sit", "abstract": true }"""), new ContentDocument( "core", "defs/actions/eat.jsonc", """ { "defName": "EatLunch", "room": "Cafeteria", "thing": "Chair", "minutes": 15, "need": "Hunger", "needGain": 0.5, "roles": ["student"] } """), new ContentDocument( "core", "defs/needs/hunger.jsonc", """{ "defName": "Hunger", "initial": 1, "decayPerHour": 0, "min": 0, "max": 1 }"""), new ContentDocument("core", "defs/things/chair.jsonc", """{ "defName": "Chair", "actions": ["Sit"] }"""), new ContentDocument("core", "defs/territories/yard.jsonc", """{ "defName": "Yard", "travelMinutes": 1 }"""), new ContentDocument("core", "defs/buildings/main.jsonc", """{ "defName": "Main" }"""), new ContentDocument("core", "defs/floors/floor.jsonc", """{ "defName": "Floor" }"""), new ContentDocument( "core", "defs/rooms/cafeteria.jsonc", """ { "defName": "Cafeteria", "slots": [{ "key": "seats", "thing": "Chair" }], "travelMinutes": 1 } """), new ContentDocument("core", "localizations/ru.jsonc", """{ "EatLunch": "Обед", "ActionStarted": "начал: {0}", "ActionEnded": "закончил: {0}" }"""), ]); var map = new MapLayout { Territory = new TerritoryNode { Id = "yard", Def = "Yard" }, Buildings = [new BuildingNode { Id = "main", Def = "Main" }], Floors = [new FloorNode { Id = "floor-1", Def = "Floor", Building = "main" }], Rooms = [ new RoomNode { Id = "cafe", Def = "Cafeteria", Building = "main", Floor = "floor-1", Slots = [new SlotFill { Key = "seats", Thing = "Chair", Count = 8 }], }, ], Links = [new MapLink { A = "yard", B = "cafe" }], }; MapValidator.Validate(map, catalog); return School.Create(1, "Столовая", start, catalog, map); } private static void Spawn(School school, string id, float hunger) { school.World.Create( new PersonIdentity(id, "f", false, TuesdayNoon, DummyName()), new PersonRoles(true, false, false, null, null, null), new PersonNeeds(new Dictionary(StringComparer.Ordinal) { ["Hunger"] = hunger }), new Presence("cafe", 0f, "cafe", false, []), PersonActivity.Idle); } 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); } private static PersonName DummyName() { var cases = new CaseTable { Nom = "А", Gen = "А", Dat = "А", Acc = "А", Ins = "А", Pre = "А", }; return new PersonName("А", "А", "А", cases, cases, cases); } }