From 4993bc37cb00356b83cb4f8a2e4a92fc2fe77922 Mon Sep 17 00:00:00 2001 From: "Somhairle H. Marisol" Date: Sat, 19 Sep 2026 03:20:56 +0800 Subject: feat: M3a 30 NPC 社区 — initialWorldN 网格布局 + Wander 寻伴闲聊 v1(ChatInit 事件队列 + 双方 Chatted 记忆 valence+0.3 + Social+40) + headless 聊天统计; 修复 clamp 泛型比较装箱(30npc 1天 63.5s→15.7s, 输出逐字节一致) + M3a 对拍/闲聊对称测试 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/LivingVillage.Kernel.Tests/DeterminismTests.fs | 60 +++++++++++++++++++++- 1 file changed, 58 insertions(+), 2 deletions(-) (limited to 'src/LivingVillage.Kernel.Tests') diff --git a/src/LivingVillage.Kernel.Tests/DeterminismTests.fs b/src/LivingVillage.Kernel.Tests/DeterminismTests.fs index 0e1766e..972ce19 100644 --- a/src/LivingVillage.Kernel.Tests/DeterminismTests.fs +++ b/src/LivingVillage.Kernel.Tests/DeterminismTests.fs @@ -153,12 +153,12 @@ type DeterminismTests () = [] member _.NpcNeedsDecayOverTimeAndActionsDifferAcrossSeeds () = let w0 = Sim.initialWorld 42UL - let npc0 = List.exactlyOne w0.Npcs + let npc0 = w0.Npcs |> Array.exactlyOne let zero = { Input = { MoveX = 0.0f; MoveY = 0.0f } } let mutable w = w0 for _ in 1 .. 500 do w <- Sim.step zero w - let npc1 = List.exactlyOne w.Npcs + let npc1 = w.Npcs |> Array.exactlyOne let n0 = npc0.Mind.Needs let n1 = npc1.Mind.Needs Assert.IsTrue(n1.Hunger < n0.Hunger, "hunger must decay") @@ -241,3 +241,59 @@ type DeterminismTests () = | None -> () Assert.IsTrue(sawEmpty, "memory must start empty") Assert.IsTrue(sawEvent, "expected a valence memory event within 6000 ticks") + + [] + member _.Npc30SameSeedTracesAreTickByTickEqualAndEventsDrain () = + let zero = { Input = { MoveX = 0.0f; MoveY = 0.0f } } + let mutable a = Sim.initialWorldN 42UL 30 + let mutable b = Sim.initialWorldN 42UL 30 + for t in 1L .. 30000L do + a <- Sim.step zero a + b <- Sim.step zero b + if a <> b then Assert.Fail($"30-npc worlds diverged at tick {t}") + if not a.Events.IsEmpty then Assert.Fail($"Events queue not drained at tick {t}") + Assert.IsTrue(30000L = a.Tick) + + [] + member _.Npc30ChatsRecordSymmetricPositiveMemories () = + let zero = { Input = { MoveX = 0.0f; MoveY = 0.0f } } + let mutable w = Sim.initialWorldN 42UL 30 + let mutable prev0 = w.Npcs.[0].Mind.Action + let mutable symmetryChecked = false + let mutable chatObserved = 0 + let total = 600000L + for _ in 1L .. total do + w <- Sim.step zero w + for npc in w.Npcs do + if npc.Mind.Action = Chat then chatObserved <- chatObserved + 1 + let npc0 = w.Npcs.[0] + if not symmetryChecked && npc0.Mind.Action = Chat && prev0 <> Chat then + match List.tryHead npc0.Mind.Memory with + | Some entry -> + match entry.Kind with + | Chatted partner -> + if partner = npc0.Id then Assert.Fail("chat partner must differ from self") + let partnerNpc = w.Npcs |> Array.find (fun n -> n.Id = partner) + let partnerAlsoChatted = + partnerNpc.Mind.Memory + |> List.exists (fun e -> match e.Kind with Chatted p -> p = npc0.Id | _ -> false) + if not partnerAlsoChatted then + Assert.Fail("chat memory must be recorded symmetrically for both partners") + symmetryChecked <- true + | _ -> Assert.Fail($"chat start must record a Chatted memory, got {entry.Kind}") + | None -> Assert.Fail("chat start must record a Chatted memory") + prev0 <- npc0.Mind.Action + Assert.IsTrue(symmetryChecked, "expected npc0 to start at least one chat within the run") + Assert.IsTrue(chatObserved > 0, "expected some NPC to be chatting during the run") + let mutable chattedMemories = 0 + for npc in w.Npcs do + Assert.IsTrue(npc.Mind.Memory.Length <= Sim.memoryCapacity, "memory exceeds ring capacity") + for e in npc.Mind.Memory do + match e.Kind with + | Chatted partner -> + chattedMemories <- chattedMemories + 1 + if partner = npc.Id then Assert.Fail("chat partner must differ from self") + if e.Valence <> Sim.chatValence then + Assert.Fail($"chat valence must be {Sim.chatValence}, got {e.Valence}") + | _ -> () + Assert.IsTrue(chattedMemories > 0, "expected Chatted memories after the run") -- cgit v1.2.3