Persist talk opinions and pick topics by weight.

A finished circle never set Tick's peopleChanged bit, so people.json kept pre-talk numbers until morning drift. PickTopic compared a 0..total roll to a 0..1 cursor, so gossip almost never changed the subject.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Leonid Pershin
2026-08-20 13:34:34 +03:00
co-authored by Cursor
parent 8d07b3606d
commit b38d0bb628
9 changed files with 298 additions and 2 deletions
+106
View File
@@ -113,6 +113,112 @@ public class TalkCirclesTests
Assert.True(high > low);
}
[Fact]
public void NodeFriendScore_PullsTowardFriends_NotEnemies()
{
var (catalog, _, _) = World();
var opinions = new Dictionary<string, int>(StringComparer.Ordinal)
{
["friend"] = 50,
["enemy"] = -50,
};
var nodes = new Dictionary<string, string>(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<string, int>(StringComparer.Ordinal) { ["enemy"] = -50 };
var nodes = new Dictionary<string, string>(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<string>(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()
{