summaryrefslogtreecommitdiff
path: root/src/LivingVillage.Kernel.Tests/RelationTests.fs
blob: 0f9de831e6f0731af55a8ca80902962ee8a07769 (plain)
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})")