diff options
Diffstat (limited to 'src/LivingVillage.Desktop.Tests/P47SceneDetailTests.fs')
| -rw-r--r-- | src/LivingVillage.Desktop.Tests/P47SceneDetailTests.fs | 117 |
1 files changed, 117 insertions, 0 deletions
diff --git a/src/LivingVillage.Desktop.Tests/P47SceneDetailTests.fs b/src/LivingVillage.Desktop.Tests/P47SceneDetailTests.fs new file mode 100644 index 0000000..4ff0a66 --- /dev/null +++ b/src/LivingVillage.Desktop.Tests/P47SceneDetailTests.fs @@ -0,0 +1,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") |
