using Arch.Core; using HSchool.Ai; using HSchool.Content; using HSchool.People; namespace HSchool.Simulation.Tests; public class PeopleInSchoolTests { private static readonly DateTime Start = new(2012, 4, 3, 6, 0, 0, DateTimeKind.Utc); [Fact] public void InstallPeople_FillsHomeroomsAndLeavesJobsEmpty() { var (catalog, map) = Vanilla(); var demand = SchoolDemand.From(catalog, map); var roster = RosterGenerator.Generate(catalog, map, schoolSeed: 1, "Russia", Start); var pool = ApplicantPool.Create(catalog, roster, schoolSeed: 1, "Russia", Start); using var school = School.Create(1, "Полная", Start, catalog, map); school.InstallPeople(roster, seed: 1, "Russia", pool); school.Tick(1d / 20d, 5d); Assert.Same(roster, school.Roster); Assert.Equal(11, roster.Classes.Count); Assert.Equal(demand.Seats.Count, roster.People.Count(person => person.IsStudent)); Assert.Equal(0, roster.People.Count(person => person.IsStaff)); Assert.True(RosterFit.Matches(roster, demand)); Assert.Equal(catalog.StaffingRules!.PoolSize, school.Applicants!.Applicants.Count); var peopleQuery = new QueryDescription().WithAll(); var classesQuery = new QueryDescription().WithAll(); Assert.Equal(roster.People.Count, school.World.CountEntities(in peopleQuery)); Assert.Equal(11, school.World.CountEntities(in classesQuery)); } [Fact] public void Tick_AcrossAWeekBoundary_RefreshesTheApplicantPool() { var start = new DateTime(2012, 3, 31, 6, 0, 0, DateTimeKind.Utc); var (catalog, map) = Vanilla(); var roster = RosterGenerator.Generate(catalog, map, schoolSeed: 1, "Russia", start); var pool = ApplicantPool.Create(catalog, roster, schoolSeed: 1, "Russia", start); using var school = School.Create(1, "Неделя", start, catalog, map); school.InstallPeople(roster, seed: 1, "Russia", pool); var monday = new DateTime(2012, 4, 2, 6, 0, 0, DateTimeKind.Utc); var changed = school.Tick((monday - start).TotalMinutes / 5d, 5d); Assert.True(changed); Assert.NotNull(school.Applicants); Assert.NotEqual(pool.Week, school.Applicants.Week); Assert.Equal(pool.Applicants.Count, school.Applicants.Applicants.Count); var peopleQuery = new QueryDescription().WithAll(); Assert.Equal(roster.People.Count, school.World.CountEntities(in peopleQuery)); } [Fact] public void OffCampusNeeds_DoNotMoveOnTick() { var (catalog, map) = Vanilla(); var roster = RosterGenerator.Generate(catalog, map, schoolSeed: 2, "Russia", Start); using var school = School.Create(2, "Нужды", Start, catalog, map); school.InstallPeople(roster, seed: 2); for (var i = 0; i < 20; i++) { school.Tick(1d / 20d, 5d); } var hunger = new List(); var query = new QueryDescription().WithAll(); school.World.Query(in query, (ref PersonNeeds needs) => hunger.Add(needs.Values["Hunger"])); Assert.NotEmpty(hunger); Assert.All(hunger, value => Assert.Equal(1f, value)); } [Fact] public void NeedDecay_DropsWhenTheDefSaysSo() { var catalog = new CatalogLoader().Load( [CatalogLoader.CorePackId], [ new ContentDocument( CatalogLoader.CorePackId, "defs/needs/hunger.jsonc", """{ "defName": "Hunger", "initial": 1, "decayPerHour": 1, "min": 0, "max": 1 }"""), ]); var world = World.Create(); try { world.Create( new PersonNeeds(new Dictionary(StringComparer.Ordinal) { ["Hunger"] = 1f }), new Presence("yard", 0f, "yard", false, [])); NeedDecay.Apply(world, catalog, gameMinutes: 60); var query = new QueryDescription().WithAll(); world.Query(in query, (ref PersonNeeds needs) => Assert.Equal(0f, needs.Values["Hunger"])); } finally { World.Destroy(world); } } [Fact] public void SameSeed_InstallsTheSameNames() { var (catalog, map) = Vanilla(); var first = RosterGenerator.Generate(catalog, map, schoolSeed: 9, "Russia", Start); var second = RosterGenerator.Generate(catalog, map, schoolSeed: 9, "Russia", Start); using var a = School.Create(9, "А", Start, catalog, map); using var b = School.Create(9, "Б", Start, catalog, map); a.InstallPeople(first, 9); b.InstallPeople(second, 9); Assert.Equal( first.People.Select(person => $"{person.Name.Surname} {person.Name.Given} {person.Name.Patronymic}"), second.People.Select(person => $"{person.Name.Surname} {person.Name.Given} {person.Name.Patronymic}")); } [Fact] public void TickPastFirstSeptember_GraduatesTheOldestYearAndKeepsCapacity() { var catalog = Catalog(); var map = Classrooms(4); var roster = RosterGenerator.Generate(catalog, map, schoolSeed: 1, "Russia", Start); var demand = SchoolDemand.From(catalog, map); var oldest = roster.Classes.Max(schoolClass => schoolClass.Year); var graduated = roster.Classes .Where(schoolClass => schoolClass.Year == oldest) .SelectMany(schoolClass => schoolClass.PupilIds) .ToHashSet(StringComparer.Ordinal); using var school = School.Create(1, "Четыре кабинета", Start, catalog, map); school.InstallPeople(roster, seed: 1, "Russia"); var sept = new DateTime(2012, 9, 1, 0, 0, 0, DateTimeKind.Utc); var changed = school.Tick((sept - Start).TotalMinutes / 5d, 5d); Assert.True(changed); Assert.True(school.Clock.Time >= sept); Assert.NotNull(school.Roster); Assert.True(RosterFit.Matches(school.Roster, demand)); Assert.Equal(roster.People.Count(person => person.IsStudent), school.Roster.People.Count(person => person.IsStudent)); Assert.DoesNotContain(school.Roster.People, person => graduated.Contains(person.Id)); Assert.Contains(school.Roster.Classes, schoolClass => schoolClass.Year == 1); var peopleQuery = new QueryDescription().WithAll(); var classesQuery = new QueryDescription().WithAll(); Assert.Equal(school.Roster.People.Count, school.World.CountEntities(in peopleQuery)); Assert.Equal(school.Roster.Classes.Count, school.World.CountEntities(in classesQuery)); } [Fact] public void TickOnCreateDateOfFirstSeptember_DoesNotIntake() { var start = new DateTime(2012, 9, 1, 6, 0, 0, DateTimeKind.Utc); var catalog = Catalog(); var map = Classrooms(4); var roster = RosterGenerator.Generate(catalog, map, schoolSeed: 3, "Russia", start); using var school = School.Create(3, "Первое сентября", start, catalog, map); school.InstallPeople(roster, seed: 3, "Russia"); var years = roster.Classes.Select(schoolClass => schoolClass.Year).OrderBy(year => year).ToArray(); var changed = school.Tick(1d / 20d, 5d); Assert.False(changed); Assert.Same(roster, school.Roster); Assert.Equal(years, school.Roster!.Classes.Select(schoolClass => schoolClass.Year).OrderBy(year => year)); } private static DefCatalog Catalog() { var root = Path.Combine(AppContext.BaseDirectory, "vanilla"); return new CatalogLoader().Load([CatalogLoader.CorePackId], PackDocumentsFrom(root)); } private static MapLayout Classrooms(int count, int desks = 16) { var rooms = new RoomNode[count]; for (var i = 0; i < count; i++) { rooms[i] = new RoomNode { Id = $"classroom-{i:00}", Def = "Classroom", Building = "main", Floor = "floor-1", Label = $"{101 + i}", Seats = desks, }; } return new MapLayout { Rooms = rooms }; } private static (DefCatalog Catalog, MapLayout Map) Vanilla() { var root = Path.Combine(AppContext.BaseDirectory, "vanilla"); var documents = PackDocumentsFrom(root); var catalog = new CatalogLoader().Load([CatalogLoader.CorePackId], documents); var map = CatalogLoader.LastDefaultMap([CatalogLoader.CorePackId], documents); Assert.NotNull(map); return (catalog, map); } private static List PackDocumentsFrom(string packRoot) { var documents = new List(); foreach (var path in Directory.EnumerateFiles(packRoot, "*.*", SearchOption.AllDirectories)) { if (!path.EndsWith(".jsonc", StringComparison.OrdinalIgnoreCase) && !path.EndsWith(".json", StringComparison.OrdinalIgnoreCase)) { continue; } var relative = Path.GetRelativePath(packRoot, path).Replace('\\', '/'); documents.Add(new ContentDocument(CatalogLoader.CorePackId, relative, File.ReadAllText(path))); } return documents; } }