summaryrefslogtreecommitdiff
path: root/src/LivingVillage.Desktop.Tests/P47SceneDetailTests.fs
blob: ac8bf80f0f5d62479bd8ea6bfd49f2298035d42f (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
namespace LivingVillage.Desktop.Tests

open Microsoft.VisualStudio.TestTools.UnitTesting
open LivingVillage.Desktop

/// P47 scene detail:
///  * water reflections reuse waterFrameTickAt phase and only land on water;
///  * bank reeds / floating props are deterministic map+tick pure functions;
///  * the near foreground roof layer uses one unified shadow direction.
[<TestClass>]
type P47SceneDetailTests () =

    let map = MapGen.generate (MapGen.defaultParams 42UL)
    let assertInt (expected: int) (actual: int) (message: string) =
        Assert.IsTrue((expected = actual), sprintf "%s: expected %d actual %d" message expected actual)

    [<TestMethod>]
    member _.ReflectionHelpersAreDeterministicAndBounded () =
        assertInt 3 (fst SceneDetail.shadowOffsetPx) "shadow dx"
        assertInt 4 (snd SceneDetail.shadowOffsetPx) "shadow dy"
        // alpha strictly decreases with distance and never goes negative.
        let alphas = [ for d in 1 .. 6 -> SceneDetail.reflectionAlphaAt d ]
        Assert.IsTrue(alphas |> List.forall (fun a -> a >= 96), "reflection alpha must stay legible (>= 96)")
        Assert.IsTrue(alphas.[0] > alphas.[1] && alphas.[1] > alphas.[2], "alpha must decrease with distance")
        // wobble is exactly the 3-phase water ramp mapped to -1/0/+1 and is deterministic.
        for tick in [ 0L; 32L; 64L; 96L ] do
            for x in [ 0; 3; 9 ] do
                for y in [ 0; 2 ] do
                    let expected =
                        match VillageArt.waterFrameTickAt tick x y with
                        | 0 -> -1
                        | 1 -> 0
                        | _ -> 1
                    assertInt expected (SceneDetail.reflectionWobbleAt tick x y) "wobble must reuse waterFrameTickAt"
                    Assert.IsTrue(abs (SceneDetail.reflectionWobbleAt tick x y) <= 1, "wobble bounded to +/-1px")

    [<TestMethod>]
    member _.ReflectionsOnlyLandOnWaterBelowALightSource () =
        let sources = SceneDetail.lightSourceTiles map
        Assert.IsTrue(not (List.isEmpty sources), "the generator map must have bank light sources")
        let streaks = SceneDetail.reflectionStreaks map
        Assert.IsTrue(not (List.isEmpty streaks), "there must be at least one reflection streak")
        for (x, y, alpha, _) in streaks do
            Assert.IsTrue(SceneDetail.isWaterSurface map x y, sprintf "reflection (%d,%d) must sit on a reflective surface" x y)
            Assert.IsTrue(alpha >= 96, "reflection alpha must stay legible (>= 96)")
        // every streak sits in the same column as a source, within stream range + bridge/own-structure gap.
        let sourceSet = sources |> Set.ofList
        for (x, y, _, _) in streaks do
            Assert.IsTrue(
                [ 1 .. SceneDetail.reflectionRange + SceneDetail.reflectionGap ]
                |> List.exists (fun d -> Set.contains (x, y - d) sourceSet),
                sprintf "reflection (%d,%d) must be in the column below a source" x y)
        // each light source keeps its kind so the renderer can tint per kind.
        for (_, _, kind) in SceneDetail.lightSources map do
            Assert.IsTrue(kind = SceneDetail.BridgeLantern || kind = SceneDetail.RoadLantern || kind = SceneDetail.RedLantern,
                          "light source kind must be one of the three known kinds")
        // at least one source must produce a full >= 3 tile vertical reflection (leader acceptance).
        let longest =
            SceneDetail.lightSources map
            |> List.map (fun (x, y, _) -> SceneDetail.reflectionLength map x y)
            |> List.fold max 0
        Assert.IsTrue(longest >= 3, sprintf "the longest reflection must span >= 3 tiles, got %d" longest)

    [<TestMethod>]
    member _.BankReedsAndFloatersAreDenseAndDeterministic () =
        let reeds = SceneDetail.bankReedTiles map
        Assert.IsTrue(not (List.isEmpty reeds), "bank reeds must be non-empty")
        Assert.IsTrue(reeds |> List.distinct |> List.length = reeds.Length, "reeds must be distinct")
        for (x, y) in reeds do
            Assert.IsTrue(not (SceneDetail.isWater map x y), "reeds sit on land")
            let nearWater =
                SceneDetail.isWater map (x - 1) y
                || SceneDetail.isWater map (x + 1) y
                || SceneDetail.isWater map x (y - 1)
                || SceneDetail.isWater map x (y + 1)
            Assert.IsTrue(nearWater, "reeds hug the water edge")
            Assert.IsTrue(x % 4 <> 0, "extra reeds interleave the existing spacing-4 set")
        let floats = SceneDetail.floatingProps map
        Assert.IsTrue(not (List.isEmpty floats), "floating props must be non-empty")
        Assert.IsTrue(floats |> List.distinct |> List.length = floats.Length, "floaters must be distinct")
        for (x, y, _) in floats do
            Assert.IsTrue(SceneDetail.isWater map x y && SceneDetail.isWater map (x + 1) y,
                          "floaters sit on open water")
        // both float kinds appear across the map.
        let kinds = floats |> List.map (fun (_, _, k) -> k) |> List.distinct
        Assert.IsTrue(kinds.Length >= 1, "float kinds present")

    [<TestMethod>]
    member _.FloatDriftAndFrameAreBoundedAndMoveWithTick () =
        for tick in [ 0L; 100L; 5000L ] do
            for seed in [ 0; 7; 31 ] do
                let dx, dy = SceneDetail.floatDrift tick seed
                Assert.IsTrue(dx >= -2 && dx <= 2, sprintf "float dx %d out of [-2,2]" dx)
                Assert.IsTrue(dy >= -1 && dy <= 1, sprintf "float dy %d out of [-1,1]" dy)
                assertInt (SceneDetail.floatFrame tick seed) (SceneDetail.floatFrame tick seed) "frame deterministic"
                Assert.IsTrue(SceneDetail.floatFrame tick seed >= 0 && SceneDetail.floatFrame tick seed <= 1, "frame in 0/1")
        let driftOverTime = [ for t in 0L .. 40L -> SceneDetail.floatDrift t 3 ]
        Assert.IsTrue(driftOverTime |> List.distinct |> List.length > 1, "drift must move with tick")

    [<TestMethod>]
    member _.ForegroundRooftopsAreStaggeredAndCoverTheWidth () =
        let width, height = 1280, 720
        let rows = SceneDetail.foregroundRooftops width height 0L
        Assert.IsTrue(rows.Length >= width / 32, "foreground spans the viewport width")
        for (_, y, w) in rows do
            let stagger = (height - 64) - y
            Assert.IsTrue(stagger >= 0 && stagger <= 24 && stagger % 8 = 0,
                          sprintf "stagger %d must be one of 0/8/16/24" stagger)
            assertInt 32 w "foreground roof cell width"
        // deterministic for the same tick, and slowly drifts across ticks.
        Assert.IsTrue(SceneDetail.foregroundRooftops width height 0L = SceneDetail.foregroundRooftops width height 0L)
        Assert.IsTrue(SceneDetail.foregroundRooftops width height 0L <> SceneDetail.foregroundRooftops width height 240L)

    [<TestMethod>]
    member _.FloaterAtlasContract () =
        assertInt 96 FloaterArt.atlasWidth "floater atlas width"
        assertInt 16 FloaterArt.atlasHeight "floater atlas height"
        assertInt 24 FloaterArt.cellWidth "floater cell width"
        assertInt 16 FloaterArt.cellHeight "floater cell height"
        for kind in [ FloaterArt.duckIndex; FloaterArt.leafIndex ] do
            for frame in 0 .. FloaterArt.frameCount - 1 do
                let rect = FloaterArt.sourceRectangle kind frame
                Assert.IsTrue(rect.X >= 0 && rect.X + rect.Width <= FloaterArt.atlasWidth, "source rect inside atlas")
                assertInt FloaterArt.cellHeight rect.Height "source cell height"
        // four distinct cells, duck and leaf never share a frame cell.
        let cells =
            [ for kind in [ FloaterArt.duckIndex; FloaterArt.leafIndex ] do
                  for frame in 0 .. FloaterArt.frameCount - 1 -> FloaterArt.sourceRectangle kind frame ]
        Assert.IsTrue(cells |> List.distinct |> List.length = 4, "four distinct floater cells")