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
|
namespace LivingVillage.Desktop.Tests
open Microsoft.VisualStudio.TestTools.UnitTesting
open LivingVillage.Kernel.Sim
open LivingVillage.Desktop
open LivingVillage.Desktop.VillageArt
/// P40 art-deepening slice:
/// * paving slabs follow the traffic network (paths / doorways / bridges) and the
/// isolated decorative scatter is dropped, so cc0 ground reads as coherent bands;
/// * red door lanterns complement the stone road lanterns with a distinct form,
/// both feeding the existing P14 premultiplied-alpha night-glow path;
/// * every placement stays a pure function of the map, so the same seed yields
/// byte-identical positions across day and night frames.
[<TestClass>]
type P40ArtTests () =
let seed = 4242UL
let map = MapGen.generateWithSize 256 192 seed
let indexOf (x: int) (y: int) = y * map.Width + x
let isStone (x: int) (y: int) =
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
Assert.IsTrue(paving.Count > 0, "seed 4242 should pave something")
// Every path / bridge anchor tile that is stone must be paved.
for (x, y) in map.Paths @ map.Bridges do
if isStone x y then
Assert.IsTrue(paving.Contains(indexOf x y), sprintf "path/bridge stone (%d,%d) must be paved" x y)
// Every paved tile must be stone and touch the network (itself or a 4-neighbour).
let anchors = (map.Paths @ map.Bridges) |> List.map (fun (x, y) -> indexOf x y) |> Set.ofList
let anchorAt (x: int) (y: int) =
x >= 0 && x < map.Width && y >= 0 && y < map.Height && Set.contains (indexOf x y) anchors
for i in paving do
let x = i % map.Width
let y = i / map.Width
Assert.IsTrue(isStone x y, "paving must only replace stone tiles")
Assert.IsTrue(
anchorAt x y || anchorAt (x - 1) y || anchorAt (x + 1) y || anchorAt x (y - 1) || anchorAt x (y + 1),
sprintf "paved tile (%d,%d) is disconnected from the traffic network" x y)
// The isolated decorative scatter must NOT be paved (that is the whole point).
Assert.IsTrue(paving.Count < (map.Tiles |> Array.filter (fun c -> c = int MapGen.GroundTile.Stone) |> Array.length),
"paving must be a strict subset of all stone (scatter dropped)")
[<TestMethod>]
member _.PavingIsDeterministicForTheSameSeed () =
let map2 = MapGen.generateWithSize 256 192 seed
let a = cc0PavingTiles map |> Set.toList |> List.sort |> List.map (fun i -> sprintf "%d" i) |> String.concat ";"
let b = cc0PavingTiles map2 |> Set.toList |> List.sort |> List.map (fun i -> sprintf "%d" i) |> String.concat ";"
Assert.AreEqual<string>(a, b)
[<TestMethod>]
member _.RedLanternsHangAtDoorwaysAndAreDeterministic () =
let lanterns = cc0RedLanternTiles map
let doors =
(map.Buildings @ map.Farmhouses)
|> List.map (fun b -> (b.DoorX, b.DoorY))
|> List.distinct
Assert.IsTrue(lanterns.Length > 0, "seed 4242 should hang red lanterns")
Assert.AreEqual<int>(doors.Length, lanterns.Length)
for (x, y) in lanterns do
Assert.IsTrue(List.contains (x, y) doors, "a red lantern hangs exactly at a doorway")
Assert.AreEqual<string>(
(lanterns |> List.sort |> List.map (fun (x, y) -> sprintf "%d:%d" x y) |> String.concat ";"),
(cc0RedLanternTiles map |> List.sort |> List.map (fun (x, y) -> sprintf "%d:%d" x y) |> String.concat ";"))
[<TestMethod>]
member _.RedAndStoneLanternsFeedTheSameNightGlowPath () =
let cc0Lights = nightLightTilesWith true map |> Set.ofList
let fallbackLights = nightLightTilesWith false map |> Set.ofList
// cc0 mode adds both the road stone lanterns and the red door lanterns.
for t in cc0RedLanternTiles map do
Assert.IsTrue(Set.contains t cc0Lights, "red lantern must be a cc0 night light")
for t in cc0LanternTiles map 12 do
Assert.IsTrue(Set.contains t cc0Lights, "road lantern must be a cc0 night light")
// Red lanterns hang at doorways, which are already house lights, so the
// fallback set may legitimately contain that tile; what must NOT leak is
// the road stone lantern set (a cc0-only placement).
for t in cc0LanternTiles map 12 do
Assert.IsFalse(Set.contains t fallbackLights, "road lantern must not leak into the fallback light set")
Assert.IsTrue(nightLightTiles map = nightLightTilesWith false map, "nightLightTiles keeps the old set")
Assert.IsTrue(Set.isSubset fallbackLights cc0Lights, "cc0 light set is a superset of the fallback set")
[<TestMethod>]
member _.SameSeedPlacementsAreStableAcrossGeneration () =
// Day and night frames reuse the same map; re-generating it must reproduce
// every P40 placement byte-for-byte (no clock, no hidden randomness).
let map2 = MapGen.generateWithSize 256 192 seed
Assert.AreEqual<string>(
(cc0PavingTiles map |> Set.toList |> List.sort |> List.map string |> String.concat ";"),
(cc0PavingTiles map2 |> Set.toList |> List.sort |> List.map string |> String.concat ";"))
Assert.AreEqual<string>(
(cc0RedLanternTiles map |> List.sort |> List.map (fun (x, y) -> sprintf "%d:%d" x y) |> String.concat ";"),
(cc0RedLanternTiles map2 |> List.sort |> List.map (fun (x, y) -> sprintf "%d:%d" x y) |> String.concat ";"))
[<TestMethod>]
member _.P40AtlasIndicesAreInRange () =
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)")
|