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
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
|
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 } }
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})")
[<TestMethod>]
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")
[<TestMethod>]
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 }
NoHost = { Reserved = 0UL }
Npcs = [| self; nearest; alternate |]
Events = [] }
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")
|