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 } Inventory = Map.empty 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 } Mind = { Needs = needs Personality = personalityOfSeed 1UL Action = Wander Target = actionTarget Wander ActionAge = 0L EffectDone = false HungerFlagged = false Memory = [] } } NoHost = { Reserved = 0UL } Npcs = [| mkNpc 0 mem0; mkNpc 1 mem1 |] Events = [] Rumors = [] Annals = [] } 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 _.OneDayEqualsTicksPerDayConstant () = // 守卫:1 模拟日 = 24h * 3600s * 60tick/s = 5,184,000 tick if Sim.ticksPerDay <> 5_184_000L then Assert.Fail($"ticksPerDay={Sim.ticksPerDay}, expected 5184000") [] member _.RelationHalfLifeEqualsOneDay () = // HALF_LIFE 必须 = 1 模拟日(修复单位错误:原 86400L 实为 24 模拟分钟) if Sim.relationHalfLifeTicks <> Sim.ticksPerDay then Assert.Fail($"relationHalfLifeTicks={Sim.relationHalfLifeTicks}, ticksPerDay={Sim.ticksPerDay}") [] member _.RelationDecayWeightAtHalfLifeIsHalf () = // 真半衰语义:age = halfLife 处权重恰为 0.5(旧式 exp(-age/halfLife) 给出 e^-1≈0.368) let now = 2L * Sim.relationHalfLifeTicks let w = Sim.relationDecayWeight now (now - Sim.relationHalfLifeTicks) if abs (w - 0.5f) > 1e-4f then Assert.Fail($"weight at one half-life = {w}, expected 0.5") [] member _.RelationMatrixMatchesHandComputedDecay () = // 单次闲聊记忆(tick=0, valence=0.3),now = 1 个半衰期 → 真半衰权重恰 0.5 → 双向累加 2 * 0.3 * 0.5 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 * 0.5f 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})") [] member _.ChatMemorySurvivesRoutineMemoryChurn () = let initial = [ RelationHarness.chatted 1L 1 ] let full = [ 1 .. Sim.memoryCapacity - 1 ] |> List.fold (fun mem i -> Sim.recordMemory (int64 (i + 1)) Meal mem) initial let retained = Sim.recordMemory (int64 (Sim.memoryCapacity + 1)) Meal full if retained.Length > Sim.memoryCapacity then Assert.Fail("memory exceeds capacity") if not (retained |> List.exists (fun e -> match e.Kind with Chatted _ -> true | _ -> false)) then Assert.Fail("chat memory must survive routine-memory churn") [] member _.NpcAvoidsImmediateRepeatChatPartner () = let needs = { Hunger = 100.0f; Energy = 100.0f; Social = 0.0f; Money = 100.0f } let personality = { Drive = 0.0f Aggression = 0.0f Extraversion = 1.0f Honesty = 0.0f Greed = 0.0f } let plaza = Sim.actionTarget Wander let npc id pos action age effectDone memory = { Id = NpcId id Pos = pos Inventory = Map.empty Mind = { Needs = needs Personality = personality Action = action Target = Sim.actionTarget action ActionAge = age EffectDone = effectDone HungerFlagged = false Memory = memory } } let self = npc 0 plaza Wander Sim.minActionTicks true [ RelationHarness.chatted 0L 1 ] let nearest = npc 1 { X = plaza.X + 10.0f; Y = plaza.Y } Work 0L false [] let alternate = npc 2 { X = plaza.X + 20.0f; Y = plaza.Y } Work 0L false [] let world = { Tick = 1L Time = 0.0 Rng = Rng.ofSeed 55UL Avatar = { Pos = plaza Mind = { Needs = needs Personality = personality Action = Wander Target = actionTarget Wander ActionAge = 0L EffectDone = false HungerFlagged = false Memory = [] } } NoHost = { Reserved = 0UL } Npcs = [| self; nearest; alternate |] Events = [] Rumors = [] Annals = [] } let next = Sim.step RelationHarness.zeroInput world match List.tryHead next.Npcs.[0].Mind.Memory with | Some entry -> match entry.Kind with | Chatted(NpcId partner) -> if partner <> 2 then Assert.Fail("a recent partner must not monopolize the next chat") | kind -> Assert.Fail($"expected a chat memory, got {kind}") | None -> Assert.Fail("expected a chat memory")