From c1242ee391a25d5e5664befb5efe84b3e471ffd2 Mon Sep 17 00:00:00 2001 From: "Somhairle H. Marisol" Date: Sat, 19 Sep 2026 11:07:07 +0800 Subject: feat: M3b 关系网络派生 — Sim.relationMatrix 双向累加+时间衰减(halfLife 86400tick, |r|>0.5 阈值) + headless --dump-relations 与 --batch K D 验收(chats max/avg、relcnt 差异、无NaN) + Desktop L 键关系视图(暂停+快照, 绿/红线粗细∝|r|, cyan NPC 点) + LV_STEPS_PER_FRAME 调试快进 + 衰减/矩阵/30npc 对称测试(19/19) + xvfb 截图验证 PASS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../LivingVillage.Kernel.Tests.fsproj | 1 + src/LivingVillage.Kernel.Tests/RelationTests.fs | 122 +++++++++++++++++++++ 2 files changed, 123 insertions(+) create mode 100644 src/LivingVillage.Kernel.Tests/RelationTests.fs (limited to 'src/LivingVillage.Kernel.Tests') diff --git a/src/LivingVillage.Kernel.Tests/LivingVillage.Kernel.Tests.fsproj b/src/LivingVillage.Kernel.Tests/LivingVillage.Kernel.Tests.fsproj index d1d3c19..65e50db 100644 --- a/src/LivingVillage.Kernel.Tests/LivingVillage.Kernel.Tests.fsproj +++ b/src/LivingVillage.Kernel.Tests/LivingVillage.Kernel.Tests.fsproj @@ -7,6 +7,7 @@ + diff --git a/src/LivingVillage.Kernel.Tests/RelationTests.fs b/src/LivingVillage.Kernel.Tests/RelationTests.fs new file mode 100644 index 0000000..f1d9673 --- /dev/null +++ b/src/LivingVillage.Kernel.Tests/RelationTests.fs @@ -0,0 +1,122 @@ +namespace LivingVillage.Kernel.Tests + +open Microsoft.VisualStudio.TestTools.UnitTesting +open LivingVillage.Kernel +open LivingVillage.Kernel.Sim + +module RelationHarness = + + let zeroInput = { Input = { MoveX = 0.0f; MoveY = 0.0f } } + + let runN (seed: uint64) (count: int) (ticks: int64) : World = + let mutable w = Sim.initialWorldN seed count + for _ in 1L .. ticks do + w <- Sim.step zeroInput w + w + + let handWorld (tick: int64) (mem0: MemoryEvent list) (mem1: MemoryEvent list) : World = + let needs = { Hunger = 100.0f; Energy = 100.0f; Social = 100.0f; Money = 50.0f } + let mkNpc id mem = + { Id = NpcId id + Pos = { X = 0.0f; Y = 0.0f } + Mind = + { Needs = needs + Personality = personalityOfSeed 1UL + Action = Wander + Target = actionTarget Wander + ActionAge = 0L + EffectDone = false + HungerFlagged = false + Memory = mem } } + { Tick = tick + Time = 0.0 + Rng = Rng.ofSeed 1UL + Avatar = { Pos = { X = 0.0f; Y = 0.0f } } + NoHost = { Reserved = 0UL } + Npcs = [| mkNpc 0 mem0; mkNpc 1 mem1 |] + Events = [] } + + let chatted (tick: int64) (partner: int) : MemoryEvent = + { Tick = tick; Kind = Chatted(NpcId partner); Valence = Sim.chatValence } + + let matricesEqual (a: float32[,]) (b: float32[,]) : bool = + Array2D.length1 a = Array2D.length1 b + && Array2D.length2 a = Array2D.length2 b + && (let mutable eq = true + for i in 0 .. Array2D.length1 a - 1 do + for j in 0 .. Array2D.length2 a - 1 do + if a.[i, j] <> b.[i, j] then eq <- false + eq) + +[] +type RelationTests () = + + [] + member _.RelationMatrixSameSeedIsByteIdentical () = + let wa = RelationHarness.runN 42UL 30 600000L + let wb = RelationHarness.runN 42UL 30 600000L + let ma = Sim.relationMatrix wa + let mb = Sim.relationMatrix wb + if wa.Tick <> wb.Tick then Assert.Fail("ticks must match") + Assert.IsTrue(RelationHarness.matricesEqual ma mb, "same seed must yield byte-identical relation matrix") + + [] + member _.RelationDecayWeightNewExceedsOld () = + let now = 10L * Sim.relationHalfLifeTicks + let fresh = Sim.relationDecayWeight now now + let old = Sim.relationDecayWeight now (now - 2L * Sim.relationHalfLifeTicks) + let older = Sim.relationDecayWeight now (now - 4L * Sim.relationHalfLifeTicks) + if fresh <> 1.0f then Assert.Fail("weight of an event at now must be 1") + if old >= fresh then Assert.Fail("older valence must weigh less than newer valence") + if older >= old then Assert.Fail("weight must strictly decrease with age") + + [] + member _.RelationMatrixMatchesHandComputedDecay () = + // 单次闲聊记忆(tick=0, valence=0.3),now = 1 个半衰期 → 双向累加 2 * 0.3 * e^-1 + let w = RelationHarness.handWorld Sim.relationHalfLifeTicks + [ RelationHarness.chatted 0L 1 ] [ RelationHarness.chatted 0L 0 ] + let m = Sim.relationMatrix w + let expected = 2.0f * Sim.chatValence * float32 (System.Math.Exp(-1.0)) + if abs (m.[0, 1] - expected) > 1e-6f then Assert.Fail($"m[0,1]={m.[0, 1]} expected={expected}") + if abs (m.[1, 0] - expected) > 1e-6f then Assert.Fail($"m[1,0]={m.[1, 0]} expected={expected}") + if m.[0, 0] <> 0.0f then Assert.Fail("diagonal must be zero") + if m.[1, 1] <> 0.0f then Assert.Fail("diagonal must be zero") + + [] + member _.RelationMatrixPrefersRecentChats () = + let now = 4L * Sim.relationHalfLifeTicks + let freshWorld = + RelationHarness.handWorld now + [ RelationHarness.chatted now 1 ] [ RelationHarness.chatted now 0 ] + let staleWorld = + RelationHarness.handWorld now + [ RelationHarness.chatted (now - 4L * Sim.relationHalfLifeTicks) 1 ] + [ RelationHarness.chatted (now - 4L * Sim.relationHalfLifeTicks) 0 ] + let mf = Sim.relationMatrix freshWorld + let ms = Sim.relationMatrix staleWorld + if mf.[0, 1] <= ms.[0, 1] then Assert.Fail("recent chat memory must outweigh stale chat memory") + if ms.[0, 1] >= Sim.relationThreshold then Assert.Fail("stale single chat must stay below relation threshold") + + [] + member _.RelationCountsRespectThreshold () = + let now = 4L * Sim.relationHalfLifeTicks + // 双向各 1 次新闲聊 → 矩阵 2*0.3 = 0.6 > 0.5 → 有关系 + let w = RelationHarness.handWorld now + [ RelationHarness.chatted now 1 ] [ RelationHarness.chatted now 0 ] + let m = Sim.relationMatrix w + let counts = Sim.relationCounts m + if counts.[0] <> 1 then Assert.Fail($"npc0 relation count {counts.[0]}, expected 1") + if counts.[1] <> 1 then Assert.Fail($"npc1 relation count {counts.[1]}, expected 1") + + [] + member _.RelationMatrixFromSimulationIsSymmetricNoNaN () = + let w = RelationHarness.runN 42UL 30 600000L + let m = Sim.relationMatrix w + let n = Array2D.length1 m + if n <> 30 then Assert.Fail($"expected 30 npcs, got {n}") + for i in 0 .. n - 1 do + if m.[i, i] <> 0.0f then Assert.Fail($"diagonal must be zero at {i}") + for j in 0 .. n - 1 do + if m.[i, j] <> m.[j, i] then Assert.Fail($"matrix must be symmetric at ({i},{j})") + if System.Single.IsNaN m.[i, j] then Assert.Fail($"NaN at ({i},{j})") + if System.Single.IsInfinity m.[i, j] then Assert.Fail($"infinite at ({i},{j})") -- cgit v1.2.3