using HSchool.Ai; using HSchool.Ai.Tests; using HSchool.Content; using HSchool.People; namespace HSchool.Ai.Tests; public class TalkCirclesTests { [Fact] public void BoundToLesson_BlocksChatLeisure() { var (catalog, map, walks) = World(); var corridor = map.Rooms.First(room => room.Def == "Corridor").Id; var decision = DecisionPlanner.Decide( catalog, map, walks, new ActorState( corridor, corridor, false, false, true, false, false, true, corridor, Needs(), Intent.None, Talk: new TalkPlannerContext("p1", new Dictionary(), new Dictionary(), null, false, true, catalog.BehaviorRules)), (_, _) => 0); Assert.NotEqual(TalkActions.Chat, decision.StartAction); Assert.NotEqual(TalkActions.StaffChat, decision.StartAction); } [Fact] public void Student_DoesNotPickStaffChat() { var (catalog, map, walks) = World(); var teachersRoom = map.Rooms.First(room => room.Def == "TeachersRoom").Id; var decision = DecisionPlanner.Decide( catalog, map, walks, new ActorState( teachersRoom, teachersRoom, false, false, true, false, false, false, teachersRoom, Needs(social: 0.2f), Intent.None, Talk: new TalkPlannerContext("p1", new Dictionary(), new Dictionary(), null, false, true, catalog.BehaviorRules)), (_, _) => 0); Assert.NotEqual(TalkActions.StaffChat, decision.StartAction); } [Fact] public void PhoneChat_NotOfferedWithoutPhone() { var (catalog, map, walks) = World(); var corridor = map.Rooms.First(room => room.Def == "Corridor").Id; var decision = DecisionPlanner.Decide( catalog, map, walks, Actor(corridor, hasPhone: false), (_, _) => 0); Assert.NotEqual(TalkActions.PhoneChat, decision.StartAction); } [Fact] public void HasPhone_DetectsBagPhone() { Assert.False(TalkCircles.HasPhone([])); Assert.True(TalkCircles.HasPhone([new InventoryItem("Phone", "Black", 1f, ItemLocations.Bag)])); } [Fact] public void NoSharedLanguage_WeakensOpinionShift() { var (catalog, _, _) = World(); var rules = catalog.BehaviorRules!; var topic = catalog.Topics["TopicStudy"]; var from = Person("a", ["RussianLanguage"]); var to = Person("b", ["English"]); var withLanguage = TalkCircles.OpinionDelta(from, to, topic, 50f, sharedLanguage: true, TalkActions.Chat, ApparelIssue.None, rules, catalog); var without = TalkCircles.OpinionDelta(from, to, topic, 50f, sharedLanguage: false, TalkActions.Chat, ApparelIssue.None, rules, catalog); Assert.True(withLanguage > without); Assert.True(without <= 1); } [Fact] public void HighCommunication_MovesOpinionMoreThanLow_OnSameTopic() { var (catalog, _, _) = World(); var rules = catalog.BehaviorRules!; var topic = catalog.Topics["TopicSport"]; var from = Person("a", ["RussianLanguage"]); var to = Person("b", ["RussianLanguage"]); var high = TalkCircles.OpinionDelta(from, to, topic, 80f, true, TalkActions.Chat, ApparelIssue.None, rules, catalog); var low = TalkCircles.OpinionDelta(from, to, topic, 20f, true, TalkActions.Chat, ApparelIssue.None, rules, catalog); Assert.True(high > low); } [Fact] public void NodeFriendScore_PullsTowardFriends_NotEnemies() { var (catalog, _, _) = World(); var opinions = new Dictionary(StringComparer.Ordinal) { ["friend"] = 50, ["enemy"] = -50, }; var nodes = new Dictionary(StringComparer.Ordinal) { ["friend"] = "corridor-a", ["enemy"] = "corridor-b", }; var context = new TalkPlannerContext("self", opinions, nodes, null, false, true, catalog.BehaviorRules); Assert.True(TalkCircles.NodeFriendScore("corridor-a", "self", context) > 0); Assert.True(TalkCircles.NodeFriendScore("corridor-b", "self", context) < 0); Assert.Equal(0f, TalkCircles.NodeFriendScore("corridor-a", "self", context with { FriendPullActive = false })); } [Fact] public void LunchNodeBlockedByEnemy_OnlyWhenAnotherSeatExists() { var (catalog, _, _) = World(); var opinions = new Dictionary(StringComparer.Ordinal) { ["enemy"] = -50 }; var nodes = new Dictionary(StringComparer.Ordinal) { ["enemy"] = "canteen-1" }; var context = new TalkPlannerContext("self", opinions, nodes, null, false, true, catalog.BehaviorRules); Assert.True(TalkCircles.LunchNodeBlockedByEnemy("canteen-1", "self", context, hasAlternative: true)); Assert.False(TalkCircles.LunchNodeBlockedByEnemy("canteen-1", "self", context, hasAlternative: false)); Assert.False(TalkCircles.LunchNodeBlockedByEnemy("canteen-2", "self", context, hasAlternative: true)); } [Fact] public void GossipAndOutgoing_UseTraitFields_NotHardcodedIdLists() { var (catalog, _, _) = World(); Assert.True(catalog.Traits["Gossip"].TalkOpinionMultiplier > 1f); Assert.Contains(catalog.Traits["Gossip"].TalkTagWeights, bias => bias.Tag.Equals("gossip", StringComparison.Ordinal)); Assert.True(catalog.Traits["Outgoing"].TalkInitiative > 1f); Assert.Equal(1, catalog.Traits["Outgoing"].TalkCircleBonus); Assert.True(TalkCircles.OpinionMultiplier(["Gossip"], catalog) > TalkCircles.OpinionMultiplier([], catalog)); Assert.True(TalkCircles.Initiative(["Outgoing"], catalog) > TalkCircles.Initiative([], catalog)); } [Fact] public void GossipTrait_BiasesPickTopicTowardGossipTag() { var (catalog, _, _) = World(); var gossip = Person("g", ["RussianLanguage"]) with { Traits = ["Gossip"] }; var plain = Person("p", ["RussianLanguage"]); var gossipHits = 0; var plainHits = 0; for (var i = 0; i < 200; i++) { var seed = Seed.Mix(i, 17, Seed.ApparelSalt); var gossipId = TalkCircles.PickTopic(catalog, gossip, age: 14, seed); var plainId = TalkCircles.PickTopic(catalog, plain, age: 14, seed); if (gossipId is not null && catalog.Topics[gossipId].Tags.Contains("gossip", StringComparer.Ordinal)) { gossipHits++; } if (plainId is not null && catalog.Topics[plainId].Tags.Contains("gossip", StringComparer.Ordinal)) { plainHits++; } } Assert.True(gossipHits > plainHits); } [Fact] public void PickTopic_IsNotAlwaysTheLastNameAlphabetically() { var (catalog, _, _) = World(); var picker = Person("a", ["RussianLanguage"]); var picked = new HashSet(StringComparer.Ordinal); for (var i = 0; i < 80; i++) { var id = TalkCircles.PickTopic(catalog, picker, age: 14, Seed.Mix(i, 3, Seed.ApparelSalt)); Assert.NotNull(id); picked.Add(id!); } Assert.True(picked.Count >= 3); } [Fact] public void AppearanceTopic_UsesDressAppropriateness() { Assert.Equal(2, TalkCircles.AppearanceModifier(ApparelIssue.None)); Assert.Equal(-2, TalkCircles.AppearanceModifier(ApparelIssue.Formality)); Assert.Equal(0, TalkCircles.AppearanceModifier(ApparelIssue.Color)); } [Fact] public void CommunicationGain_IsLessThanLessonSkillPerHour() { var (catalog, _, _) = World(); var rules = catalog.BehaviorRules!; Assert.True(rules.TalkSkillPerHour < rules.LessonSkillPerHour); var skill = catalog.Skills["Communication"]; var afterTalkHour = TalkCircles.CommunicationGain(50f, skill, 1f, rules); Assert.Equal(50f + rules.TalkSkillPerHour, afterTalkHour); } [Fact] public void Enemies_AreNotRankedAsInvitees() { var (catalog, _, _) = World(); var opinions = new Dictionary(StringComparer.Ordinal) { ["b"] = 50, ["c"] = -50, }; var initiator = Person("a", []) with { Opinions = opinions }; var ranked = TalkCircles.RankInvitees( initiator, [ new TalkCircles.Candidate("b", "c1", true, false, false), new TalkCircles.Candidate("c", "c1", true, false, false), ], catalog.BehaviorRules); Assert.Equal(["b"], ranked); } private static ActorState Actor(string node, bool hasPhone) { var (catalog, _, _) = World(); return new ActorState( node, node, false, false, true, false, false, false, node, Needs(social: 0.2f), Intent.None, Talk: new TalkPlannerContext("p1", new Dictionary(), new Dictionary(), null, hasPhone, true, catalog.BehaviorRules)); } private static Dictionary Needs(float social = 1f) => new(StringComparer.Ordinal) { ["Social"] = social, ["Hunger"] = 1f, ["Sleep"] = 1f, ["Toilet"] = 1f, }; private static (DefCatalog Catalog, MapLayout Map, WalkGraph Walks) World() { var (catalog, map) = Fixtures.Vanilla(); return (catalog, map, WalkGraph.Build(catalog, map)); } private static Person Person(string id, string[] languages) { var skills = languages.ToDictionary(skill => skill, _ => 60, StringComparer.Ordinal); skills["Communication"] = 50; var cases = new CaseTable { Nom = "A", Gen = "A", Dat = "A", Acc = "A", Ins = "A", Pre = "A", }; return new Person { Id = id, FamilyId = "f1", Female = false, BirthDate = new DateTime(2000, 1, 1, 0, 0, 0, DateTimeKind.Utc), Name = new PersonName("A", "B", "C", cases, cases, cases), IsStudent = true, IsStaff = false, IsParent = false, Numbers = new Dictionary(StringComparer.Ordinal), Choices = new Dictionary(StringComparer.Ordinal), Skills = skills, Traits = [], Needs = new Dictionary(StringComparer.Ordinal), Opinions = new Dictionary(StringComparer.Ordinal), }; } }