1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
|
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)
[<TestClass>]
type RelationTests () =
[<TestMethod>]
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")
[<TestMethod>]
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")
[<TestMethod>]
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")
[<TestMethod>]
member _.RelationHalfLifeEqualsOneDay () =
// HALF_LIFE 必须 = 1 模拟日(修复单位错误:原 86400L 实为 24 模拟分钟)
if Sim.relationHalfLifeTicks <> Sim.ticksPerDay then
Assert.Fail($"relationHalfLifeTicks={Sim.relationHalfLifeTicks}, ticksPerDay={Sim.ticksPerDay}")
[<TestMethod>]
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")
[<TestMethod>]
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")
[<TestMethod>]
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")
[<TestMethod>]
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")
[<TestMethod>]
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})")
|