using Arch.Core; using HSchool.Ai; using HSchool.Content; using HSchool.People; using HSchool.Simulation; namespace HSchool.Simulation.Tests; public class TalkCircleTests { private static readonly DateTime TuesdayMorning = new(2012, 4, 3, 6, 0, 0, DateTimeKind.Utc); [Fact] public void FriendsOnBreakInCorridor_FormTalkCircle() { var (school, first, second) = TwoPupilsOnBreak(); using (school) { OpinionStore.Set(first, second.Id, 55); OpinionStore.Set(second, first.Id, 55); PlaceAt(school, first.Id, "corridor-1"); PlaceAt(school, second.Id, "corridor-1"); Assert.True(school.TryStartAction(first.Id, TalkActions.Chat)); var rows = school.CapturePresence(); Assert.Equal(TalkActions.Chat, rows.Single(row => row.PersonId == first.Id).ActionId); Assert.Equal(TalkActions.Chat, rows.Single(row => row.PersonId == second.Id).ActionId); var circle = school.TalkCircleOf(first.Id); Assert.NotNull(circle); Assert.Contains(first.Id, circle.MemberIds); Assert.Contains(second.Id, circle.MemberIds); Assert.False(string.IsNullOrEmpty(circle.TopicId)); Assert.Equal(circle.MemberIds, school.TalkCircleOf(second.Id)?.MemberIds); Assert.Null(school.TalkCircleOf("missing")); } } [Fact] public void EnemiesOnBreak_DoNotFormCircle() { var (school, first, second) = TwoPupilsOnBreak(); using (school) { OpinionStore.Set(first, second.Id, -55); OpinionStore.Set(second, first.Id, -55); PlaceAt(school, first.Id, "corridor-1"); PlaceAt(school, second.Id, "corridor-1"); Assert.False(school.TryStartAction(first.Id, TalkActions.Chat)); Assert.Null(school.CapturePresence().Single(row => row.PersonId == first.Id).ActionId); } } [Fact] public void FifthPerson_DoesNotJoinFullCircle() { var (school, pupils) = FivePupilsOnBreak(); using (school) { foreach (var pupil in pupils.Take(4)) { PlaceAt(school, pupil.Id, "corridor-1"); } PlaceAt(school, pupils[0].Id, "corridor-1"); Assert.True(school.TryStartAction(pupils[0].Id, TalkActions.Chat)); PlaceAt(school, pupils[4].Id, "corridor-1"); Assert.False(school.TryStartAction(pupils[4].Id, TalkActions.Chat)); Assert.Null(school.CapturePresence().Single(row => row.PersonId == pupils[4].Id).ActionId); } } [Fact] public void StaffInTeachersRoom_CanStaffChat() { var (school, staff) = TwoStaffOnBreak(); using (school) { PlaceAt(school, staff[0].Id, "teachers-room"); PlaceAt(school, staff[1].Id, "teachers-room"); Assert.True(school.TryStartAction(staff[0].Id, TalkActions.StaffChat)); Assert.Equal(TalkActions.StaffChat, ActivityOf(school, staff[0].Id)); Assert.Equal(TalkActions.StaffChat, ActivityOf(school, staff[1].Id)); } } [Fact] public void Pupil_CannotStaffChat() { var (school, first, _) = TwoPupilsOnBreak(); using (school) { PlaceAt(school, first.Id, "teachers-room"); Assert.False(school.TryStartAction(first.Id, TalkActions.StaffChat)); } } [Fact] public void TwoWithPhoneOnBreak_CanPhoneChat() { var (school, first, second) = TwoPupilsWithPhonesOnBreak(); using (school) { PlaceAt(school, first.Id, "corridor-1"); PlaceAt(school, second.Id, "corridor-1"); Assert.True(school.TryStartAction(first.Id, TalkActions.PhoneChat)); Assert.Equal(TalkActions.PhoneChat, ActivityOf(school, first.Id)); Assert.Equal(TalkActions.PhoneChat, ActivityOf(school, second.Id)); } } [Fact] public void WithoutPhone_CannotPhoneChat() { var (school, first, second) = TwoPupilsOnBreak(); using (school) { Assert.False(TalkCircles.HasPhone(first.Items)); PlaceAt(school, first.Id, "corridor-1"); PlaceAt(school, second.Id, "corridor-1"); Assert.False(school.TryStartAction(first.Id, TalkActions.PhoneChat)); } } [Fact] public void TalkCircle_WritesTopicToLogAndShiftsOpinion() { var (school, first, second) = TwoPupilsOnBreak(); using (school) { OpinionStore.Set(first, second.Id, 10); PlaceAt(school, first.Id, "corridor-1"); PlaceAt(school, second.Id, "corridor-1"); Assert.True(school.TryStartAction(first.Id, TalkActions.Chat)); while (ActivityOf(school, first.Id) is not null) { school.Tick(0.2d, 5d); } Assert.NotEqual(10, OpinionStore.Get(first, second.Id)); Assert.Contains( school.DayLog, row => row.PersonId == first.Id && row.Type.Equals(PersonLogTypes.TalkEnded, StringComparison.Ordinal) && row.ThingDef is not null); } } [Fact] public void TalkCircleFinish_MarksTickPeopleChangedSoSaveWritesOpinions() { var (school, first, second) = TwoPupilsOnBreak(); using (school) { OpinionStore.Set(first, second.Id, 10); PlaceAt(school, first.Id, "corridor-1"); PlaceAt(school, second.Id, "corridor-1"); Assert.True(school.TryStartAction(first.Id, TalkActions.Chat)); var peopleChanged = false; while (ActivityOf(school, first.Id) is not null) { peopleChanged |= school.Tick(0.2d, 5d); } Assert.True(peopleChanged); Assert.NotEqual(10, OpinionStore.Get(first, second.Id)); } } [Fact] public void PendingStudyOnlyToday_DoesNotChangeCurrentGamesCircle() { var (school, first, second) = TwoPupilsOnBreak(); using (school) { OpinionStore.Set(first, second.Id, 55); OpinionStore.Set(second, first.Id, 55); PlaceAt(school, first.Id, "corridor-1"); PlaceAt(school, second.Id, "corridor-1"); Assert.True(school.TryStartAction(first.Id, TalkActions.Chat)); var live = school.TalkCirclesById.Values.Single(); live.TopicId = "TopicGames"; school.SpeechRules = school.SpeechRules.WithPending(SpeechPolicies.StudyOnly, null); Assert.Equal(SpeechPolicies.Free, school.SpeechRules.Students); TalkCircleSystem.Apply(school, 0.5d); Assert.Equal("TopicGames", school.TalkCircleOf(first.Id)?.TopicId); Assert.Equal(SpeechPolicies.StudyOnly, school.SpeechRules.PendingStudents); } } [Fact] public void AfterWorkMorning_NoRudePolicy_StudentsDoNotStartRudeTopic() { var (school, first, second) = TwoPupilsOnBreak(); using (school) { school.SpeechRules = school.SpeechRules.WithPending(SpeechPolicies.NoRude, null); MorningDress.Apply(school); Assert.Equal(SpeechPolicies.NoRude, school.SpeechRules.Students); Assert.Null(school.SpeechRules.PendingStudents); TalkCircleSystem.Interrupt(school, first.Id); TalkCircleSystem.Interrupt(school, second.Id); PlaceAt(school, first.Id, "corridor-1"); PlaceAt(school, second.Id, "corridor-1"); OpinionStore.Set(first, second.Id, 55); OpinionStore.Set(second, first.Id, 55); var started = 0; var classmates = school.Roster!.Classes.First(row => row.RoomId == "classroom-101").PupilIds .Select(id => school.Roster.People.First(person => person.Id == id)) .ToArray(); foreach (var initiator in classmates) { TalkCircleSystem.Interrupt(school, initiator.Id); PlaceAt(school, initiator.Id, "corridor-1"); PlaceAt(school, first.Id, "corridor-1"); PlaceAt(school, second.Id, "corridor-1"); if (!school.TryStartAction(initiator.Id, TalkActions.Chat)) { continue; } started++; var circle = school.TalkCircleOf(initiator.Id); Assert.NotNull(circle); Assert.False( school.Catalog!.Topics[circle.TopicId].Tags.Contains(TopicTags.Rude, StringComparer.Ordinal), circle.TopicId); TalkCircleSystem.Interrupt(school, initiator.Id); } Assert.True(started > 0); } } private static (School School, Person First, Person Second) TwoPupilsWithPhonesOnBreak() { var (catalog, map) = Vanilla(); var school = OpenSchool(catalog, map, seed: 45); var withPhone = school.Roster!.People .Where(person => person.IsStudent && TalkCircles.HasPhone(person.Items)) .Take(2) .ToArray(); Assert.Equal(2, withPhone.Length); return (school, withPhone[0], withPhone[1]); } private static (School School, Person First, Person Second) TwoPupilsOnBreak() { var (catalog, map) = Vanilla(); var school = OpenSchool(catalog, map, seed: 42); var schoolClass = school.Roster!.Classes.First(row => row.RoomId == "classroom-101"); var pupils = schoolClass.PupilIds .Select(id => school.Roster.People.First(person => person.Id == id)) .Take(2) .ToArray(); return (school, pupils[0], pupils[1]); } private static (School School, IReadOnlyList Pupils) FivePupilsOnBreak() { var (catalog, map) = Vanilla(); var school = OpenSchool(catalog, map, seed: 43); var schoolClass = school.Roster!.Classes.First(row => row.RoomId == "classroom-101"); var pupils = schoolClass.PupilIds .Select(id => school.Roster.People.First(person => person.Id == id)) .Take(5) .ToArray(); return (school, pupils); } private static (School School, Person[] Staff) TwoStaffOnBreak() { const float cap = 100_000f; var (catalog, map) = Vanilla(); var school = OpenSchool(catalog, map, seed: 44); var roster = school.Roster!; var pool = school.Applicants!; for (var i = 0; i < 2; i++) { var hired = Staffing.Hire( catalog, map, roster, pool, pool.Applicants[0].Person.Id, Staffing.TeacherPosition, cap); Assert.Equal(StaffingError.None, hired.Error); roster = hired.Roster; pool = hired.Pool; } school.ApplyStaffing(roster, pool); var staff = school.Roster!.People.Where(person => person.IsStaff).Take(2).ToArray(); Assert.Equal(2, staff.Length); return (school, staff); } private static School OpenSchool(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 == "classroom-101"); var school = School.Create(seed, "Talk", TuesdayMorning, catalog, map); school.InstallPeople(roster, seed, "Russia", pool); school.SetTimetable(new HSchool.Schedule.Timetable( [ new HSchool.Schedule.LessonPlacement(schoolClass.Id, "Mathematics", "t1", schoolClass.RoomId, Day: 1, Period: 1), ], [])); school.ConfigurePresence(weekDays: 5, maxDecisionsPerTick: 10_000); AdvanceTo(school, new DateTime(2012, 4, 3, 9, 18, 0, DateTimeKind.Utc)); return school; } private static void AdvanceTo(School school, DateTime until) { while (school.Clock.Time < until) { school.Tick(0.2d, 5d); } } private static void PlaceAt(School school, string personId, string nodeId) { var query = new QueryDescription().WithAll(); school.World.Query(in query, (ref PersonIdentity identity, ref Presence presence, ref PersonActivity activity, ref Intent intent) => { if (identity.Id.Equals(personId, StringComparison.Ordinal)) { presence = new Presence(nodeId, 0f, nodeId, false, []); activity = PersonActivity.Idle; intent = Intent.None; } }); } private static string? ActivityOf(School school, string personId) => school.CapturePresence().Single(row => row.PersonId == personId).ActionId; 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; } documents.Add(new ContentDocument( CatalogLoader.CorePackId, Path.GetRelativePath(root, path).Replace('\\', '/'), File.ReadAllText(path))); } var catalog = new CatalogLoader().Load([CatalogLoader.CorePackId], documents); var map = CatalogLoader.LastDefaultMap([CatalogLoader.CorePackId], documents); Assert.NotNull(map); return (catalog, map); } }