summaryrefslogtreecommitdiff
path: root/src/LivingVillage.Desktop.Tests/P40ArtTests.fs
diff options
context:
space:
mode:
Diffstat (limited to 'src/LivingVillage.Desktop.Tests/P40ArtTests.fs')
-rw-r--r--src/LivingVillage.Desktop.Tests/P40ArtTests.fs66
1 files changed, 66 insertions, 0 deletions
diff --git a/src/LivingVillage.Desktop.Tests/P40ArtTests.fs b/src/LivingVillage.Desktop.Tests/P40ArtTests.fs
index 549640e..15e6601 100644
--- a/src/LivingVillage.Desktop.Tests/P40ArtTests.fs
+++ b/src/LivingVillage.Desktop.Tests/P40ArtTests.fs
@@ -23,6 +23,47 @@ type P40ArtTests () =
x >= 0 && x < map.Width && y >= 0 && y < map.Height
&& map.Tiles.[indexOf x y] = int MapGen.GroundTile.Stone
+ // --- P40 boat awning: solid arch silhouette (atlas pixel decode) -----------
+ // The atlas is written by scripts/make-cc0-art.py as non-interlaced 8-bit RGBA
+ // with filter byte 0 on every scanline, so a tiny decoder is enough to assert
+ // the awning is a filled arch with a continuous ridge (not a row of 1px posts).
+ let atlasRgba () : int * int * (int -> int -> (int * int * int * int)) =
+ let path = System.IO.Path.Combine(System.AppContext.BaseDirectory, cc0TileAtlasRelativePath)
+ let bytes = System.IO.File.ReadAllBytes path
+ let be32 (o: int) =
+ (int bytes.[o] <<< 24) ||| (int bytes.[o + 1] <<< 16) ||| (int bytes.[o + 2] <<< 8) ||| int bytes.[o + 3]
+ let width = be32 16
+ let height = be32 20
+ let mutable offset = 8
+ let mutable idat = System.IO.MemoryStream()
+ while offset < bytes.Length do
+ let length = be32 offset
+ let tag = System.Text.Encoding.ASCII.GetString(bytes, offset + 4, 4)
+ if tag = "IDAT" then idat.Write(bytes, offset + 8, length)
+ offset <- offset + 12 + length
+ use z = new System.IO.Compression.ZLibStream(new System.IO.MemoryStream(idat.ToArray()), System.IO.Compression.CompressionMode.Decompress)
+ use ms = new System.IO.MemoryStream()
+ z.CopyTo ms
+ let decompressed = ms.ToArray()
+ let stride = width * 4
+ let pixels = Array.zeroCreate<byte> (width * height * 4)
+ for y in 0 .. height - 1 do
+ let rowStart = y * (stride + 1) + 1 // skip the per-row filter byte (always 0 here)
+ Array.blit decompressed rowStart pixels (y * stride) stride
+ let get (x: int) (y: int) =
+ if x < 0 || x >= width || y < 0 || y >= height then (0, 0, 0, 0)
+ else
+ let i = (y * width + x) * 4
+ (int pixels.[i], int pixels.[i + 1], int pixels.[i + 2], int pixels.[i + 3])
+ width, height, get
+
+ let isBambooOpaque (r: int, g: int, b: int, a: int) =
+ a > 0
+ && ((r, g, b) = (44, 58, 34)
+ || (r, g, b) = (86, 104, 62)
+ || (r, g, b) = (28, 38, 22)
+ || (r, g, b) = (22, 30, 17))
+
[<TestMethod>]
member _.PavingCoversTrafficNetworkAndDropsIsolatedScatter () =
let paving = cc0PavingTiles map
@@ -102,3 +143,28 @@ type P40ArtTests () =
for index in [ cc0TileIndexPaving; cc0TileIndexRedLantern ] do
Assert.IsTrue(index >= 0 && index < cc0TileCount, sprintf "P40 tile index %d out of atlas" index)
Assert.AreEqual<int>(21, cc0TileCount)
+
+ [<TestMethod>]
+ member _.BoatAwningIsASolidArchWithAContinuousRidge () =
+ let width, _, pixel = atlasRgba ()
+ Assert.IsTrue(width >= (cc0TileIndexBoatRight + 1) * 32, "atlas must cover the boat tiles")
+ for tile in [ cc0TileIndexBoatLeft; cc0TileIndexBoatRight ] do
+ let ox = tile * 32
+ // Ridge: y in [1,2], x in [6,25] must have no transparent column break.
+ for x in 6 .. 25 do
+ for y in 1 .. 2 do
+ let (_, _, _, a) = pixel (ox + x) y
+ Assert.IsTrue(a > 0, sprintf "boat tile %d ridge pixel (%d,%d) is transparent" tile x y)
+ // Solid arch: below the arch line, every x in [4,27] has opaque bamboo.
+ for x in 4 .. 27 do
+ let hasBamboo =
+ [ 0 .. 31 ]
+ |> List.exists (fun y -> isBambooOpaque (pixel (ox + x) y))
+ Assert.IsTrue(hasBamboo, sprintf "boat tile %d column %d has no opaque bamboo" tile x)
+ // The two halves join: the arch top at the tile boundary is the same row.
+ let topRow (ox: int) (x: int) =
+ [ 0 .. 31 ] |> List.tryFind (fun y -> isBambooOpaque (pixel (ox + x) y))
+ Assert.AreEqual<Option<int>>(
+ topRow (cc0TileIndexBoatLeft * 32) 31,
+ topRow (cc0TileIndexBoatRight * 32) 0,
+ "left/right awning halves must meet at the same arch height (no broken arc)")