summaryrefslogtreecommitdiff
path: root/src/LivingVillage.Desktop.Tests/DesktopTests.fs
blob: 52443e96c3e8affa934ccf0d7b473a57aa1d1498 (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
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
197
198
199
200
201
202
203
204
205
206
207
208
209
210
namespace LivingVillage.Desktop.Tests

open Microsoft.VisualStudio.TestTools.UnitTesting
open LivingVillage.Kernel
open LivingVillage.Kernel.Sim
open LivingVillage.Desktop
open LivingVillage.Desktop.VillagePresentation

module private Harness =

    let atNpc (id: int) (world: World) : World =
        let npc = world.Npcs.[id]
        { world with Avatar = { world.Avatar with Pos = npc.Pos } }

[<TestClass>]
type DesktopTests () =

    [<TestMethod>]
    member _.InteractOpensTheSixIntentMenu () =
        let world = Sim.initialWorldN 42UL 4 |> Harness.atNpc 0
        let next, view = M5Interaction.apply Interact world M5Interaction.initial

        Assert.AreEqual<World>(world, next)
        Assert.AreEqual<M5Panel>(DialoguePanel, view.Panel)
        Assert.AreEqual<int>(6, view.Menu.Value.Options.Length)
        Assert.IsTrue(M5Interaction.titleText view |> fun text -> text.Contains("闲聊"))

    [<TestMethod>]
    member _.SelectingAnIntentUpdatesWorldAndChronicle () =
        let world = Sim.initialWorldN 42UL 4 |> Harness.atNpc 0
        let openedWorld, opened = M5Interaction.apply Interact world M5Interaction.initial
        let next, view = M5Interaction.apply Intent1 openedWorld opened

        Assert.IsTrue(next.Events |> List.exists (fun event ->
            match event.Kind with
            | DialogueEvent _ -> true
            | _ -> false))
        Assert.IsTrue(next.Annals |> List.exists (fun entry ->
            match entry.Kind with
            | DialogueAnnal _ -> true
            | _ -> false))
        Assert.IsTrue(view.Chronicle.Contains("dialogue"))
        Assert.IsTrue(view.Status.Contains("dialogue"))

    [<TestMethod>]
    member _.NeedsAndObservationPanelsUseKernelViews () =
        let world = Sim.initialWorldN 42UL 4 |> Harness.atNpc 0
        let afterStep = Sim.step { Input = { MoveX = 0.0f; MoveY = 0.0f } } world
        let _, needsView = M5Interaction.apply ToggleNeeds afterStep M5Interaction.initial
        let _, observationView = M5Interaction.apply Observe world M5Interaction.initial

        Assert.AreEqual<Needs>(afterStep.Avatar.Mind.Needs, needsView.Needs.Value.Needs)
        Assert.IsTrue(observationView.Observations |> List.exists (fun observation -> observation.Id = NpcId 0))

    [<TestMethod>]
    member _.DialogueAndOverlayViewsRejectWorldCommandsUntilClosed () =
        let world = Sim.initialWorldN 42UL 4 |> Harness.atNpc 0
        let openedWorld, dialogueView = M5Interaction.apply Interact world M5Interaction.initial
        let _, afterObserve = M5Interaction.apply Observe openedWorld dialogueView
        let _, afterNeeds = M5Interaction.apply ToggleNeeds openedWorld dialogueView
        let _, afterChronicle = M5Interaction.apply ShowChronicle openedWorld dialogueView
        let _, needsView = M5Interaction.apply ToggleNeeds world M5Interaction.initial
        let _, interactionAttempt = M5Interaction.apply Interact world needsView
        let _, closedView = M5Interaction.apply ClosePanel world dialogueView

        Assert.AreEqual<M5View>(dialogueView, afterObserve)
        Assert.AreEqual<M5View>(dialogueView, afterNeeds)
        Assert.AreEqual<M5View>(dialogueView, afterChronicle)
        Assert.AreEqual<M5Panel>(NeedsPanel, interactionAttempt.Panel)
        Assert.AreEqual<M5Panel>(WorldPanel, closedView.Panel)

    [<TestMethod>]
    member _.HeadlessSmokeProvesTheThreeHopCausalChain () =
        let result = Program.M5Smoke.run ()

        Assert.IsTrue(result.Passed, result.Failure)
        Assert.AreEqual<int>(6, result.MenuOptions)
        Assert.AreEqual<int>(4, result.RumorPathLength)
        Assert.IsTrue(result.Chronicle.Contains("dialogue"))
        Assert.IsTrue(result.RumorTrace.Contains("receiver=-1"))
        Assert.IsTrue(result.RumorTrace.Contains("depth=3"))

    [<TestMethod>]
    member _.DayNightStateChangesTheRenderProfile () =
        let day = M6Presentation.profileAtTick (6L * Sim.ticksPerDay / 24L)
        let night = M6Presentation.profileAtTick (22L * Sim.ticksPerDay / 24L)

        Assert.AreEqual<M6DayNight>(Day, day.Mode)
        Assert.AreEqual<M6DayNight>(Night, night.Mode)
        Assert.AreNotEqual(day.Background, night.Background)
        Assert.AreNotEqual(day.WorldTint, night.WorldTint)

    [<TestMethod>]
    member _.DesktopBootstrapMovesAvatarOffTheHomeDoorAndSeparatesVillagers () =
        let world = WorldBootstrap.initialWorld 42UL Sim.npcCount
        let avatarPixel : PixelPosition =
            { X = world.Avatar.Pos.X
              Y = world.Avatar.Pos.Y }
        let avatarTile =
            sceneTileFromPixel avatarPixel
        let home = scene.Homes |> List.head
        let renderPlan = VillageArt.sampleRenderPlan ()
        let blockedTiles =
            (renderPlan.Grounds |> List.map (fun (tile, _) -> VillageArt.worldTile renderPlan tile))
            @ VillageArt.planFootprint renderPlan
            |> List.map (fun tile -> tile.X, tile.Y)
            |> Set.ofList
        let sceneTileOf (position: Vec2) : TilePosition =
            sceneTileFromPixel
                { X = position.X
                  Y = position.Y }
        let distanceSquared (first: Vec2) (second: Vec2) : float32 =
            let dx = second.X - first.X
            let dy = second.Y - first.Y
            dx * dx + dy * dy
        let npcTiles = world.Npcs |> Array.map (fun npc -> sceneTileOf npc.Pos)

        Assert.AreEqual<int>(6, avatarTile.X)
        Assert.AreEqual<int>(7, avatarTile.Y)
        Assert.IsFalse(isNearHomeDoor avatarTile home, "The avatar must start beyond the doorway interaction radius.")
        Assert.IsTrue(
            npcTiles |> Array.forall (fun tile -> not (Set.contains (tile.X, tile.Y) blockedTiles)),
            "A villager must not spawn on the house, water, bridge, or garden footprint.")
        Assert.IsTrue(
            world.Npcs
            |> Array.forall (fun npc -> distanceSquared world.Avatar.Pos npc.Pos >= 96.0f * 96.0f),
            "Villagers must not overlap the avatar spawn.")

        for first in 0 .. world.Npcs.Length - 2 do
            for second in first + 1 .. world.Npcs.Length - 1 do
                Assert.IsTrue(
                    distanceSquared world.Npcs.[first].Pos world.Npcs.[second].Pos >= 96.0f * 96.0f,
                    sprintf "villagers %d and %d are too close" first second)

        let repeated = WorldBootstrap.initialWorld 42UL Sim.npcCount
        Assert.AreEqual<World>(world, repeated)

    [<TestMethod>]
    member _.DaylightUsesAWarmHighContrastWorldTint () =
        let day = M6Presentation.profileAtTick (6L * Sim.ticksPerDay / 24L)

        Assert.IsTrue(day.WorldTint.R > day.WorldTint.G)
        Assert.IsTrue(day.WorldTint.G > day.WorldTint.B)

    [<TestMethod>]
    member _.DaylightAutoplayBootstrapStartsAtDawnAndPreservesDefaultStart () =
        let dawnTick = 6L * Sim.ticksPerDay / 24L
        let daylightWorld = WorldBootstrap.initialWorldForDaylight true 42UL Sim.npcCount
        let defaultWorld = WorldBootstrap.initialWorldForDaylight false 42UL Sim.npcCount
        let daylightProfile = M6Presentation.profileAtTick daylightWorld.Tick
        let defaultProfile = M6Presentation.profileAtTick defaultWorld.Tick

        Assert.AreEqual<int64>(dawnTick, daylightWorld.Tick)
        Assert.IsTrue(abs (daylightWorld.Time - float dawnTick * Sim.dtSeconds) < 1e-9)
        Assert.AreEqual<M6DayNight>(Day, daylightProfile.Mode)
        Assert.AreEqual<int64>(0L, defaultWorld.Tick)
        Assert.AreEqual<float>(0.0, defaultWorld.Time)
        Assert.AreEqual<M6DayNight>(Night, defaultProfile.Mode)

// ---- P12 中文每日任务面板 ----

module private TaskPanelHarness =

    let occupationWithTask (taskState: Occupation.TaskState) : Occupation.State =
        let occupation = Occupation.stateOf Occupation.Farmer
        let task =
            (Occupation.dailyTaskOf 42UL 7UL 0L Occupation.Farmer).Value
            |> Occupation.acceptTask
            |> fun active -> if taskState = Occupation.Done then { active with State = Occupation.Done } else { active with State = taskState }
        { occupation with Today = Some task }

[<TestClass>]
type TaskPanelTests () =

    [<TestMethod>]
    member _.TaskPanelWithoutOccupationShowsNoOccupationLine () =
        let world = Sim.initialWorldN 42UL 4
        let _, view = M5Interaction.apply ShowTaskPanel world M5Interaction.initial

        Assert.AreEqual<M5Panel>(TaskPanel, view.Panel)
        Assert.AreEqual<string>("task", M5Interaction.panelName view)
        let lines = M5Interaction.panelLines world view
        Assert.IsTrue(lines |> List.contains "任务", "panel heading expected")
        Assert.IsTrue(lines |> List.contains "无职业")

    [<TestMethod>]
    member _.TaskPanelShowsChineseOccupationNameAndTheThreeStates () =
        let world = Sim.initialWorldN 42UL 4
        for taskState, expected in [ Occupation.Offered, "待接受"; Occupation.Active, "进行中"; Occupation.Done, "已完成" ] do
            let withTask = { M5Interaction.initial with Task = Some (TaskPanelHarness.occupationWithTask taskState) }
            let _, view = M5Interaction.apply ShowTaskPanel world withTask
            let lines = M5Interaction.panelLines world view
            Assert.IsTrue(lines |> List.contains "农夫", $"occupation name missing for {taskState}")
            Assert.IsTrue(lines |> List.exists (fun line -> line.Contains(expected)), $"state {expected} missing in {lines}")

    [<TestMethod>]
    member _.TaskPanelTitleUsesChineseHeading () =
        let world = Sim.initialWorldN 42UL 4
        let _, view = M5Interaction.apply ShowTaskPanel world M5Interaction.initial
        Assert.IsTrue(M5Interaction.titleText view |> fun text -> text.Contains("任务"))

    [<TestMethod>]
    member _.ShowTaskPanelIsRejectedFromDialoguePanelLikeOtherOverlays () =
        let world = Sim.initialWorldN 42UL 4 |> Harness.atNpc 0
        let openedWorld, dialogueView = M5Interaction.apply Interact world M5Interaction.initial
        let _, afterTask = M5Interaction.apply ShowTaskPanel openedWorld dialogueView

        Assert.AreEqual<M5View>(dialogueView, afterTask)
        let _, closed = M5Interaction.apply ClosePanel world { dialogueView with Panel = TaskPanel }
        Assert.AreEqual<M5Panel>(WorldPanel, closed.Panel)