summaryrefslogtreecommitdiff
path: root/src/LivingVillage.Desktop.Tests/P47SceneDetailTests.fs
blob: 4ff0a6688da1e6f29124a097c25a169a82b6d11e (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
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 >= 0), "reflection alpha must be >= 0")
        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.isWater map x y, sprintf "reflection (%d,%d) must sit on water" x y)
            Assert.IsTrue(alpha > 0, "reflection alpha must be positive")
        // every streak sits directly below its source within reflectionRange.
        let sourceSet = sources |> Set.ofList
        for (x, y, _) in streaks do
            Assert.IsTrue([ 1 .. SceneDetail.reflectionRange ] |> List.exists (fun d -> Set.contains (x, y - d) sourceSet),
                          sprintf "reflection (%d,%d) must be under a source" x y)

    [<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")