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
|
namespace LivingVillage.Desktop.Tests
open Microsoft.VisualStudio.TestTools.UnitTesting
open LivingVillage.Kernel.Sim
open LivingVillage.Desktop
open LivingVillage.Desktop.VillageArt
/// P39 Jiangnan element slice: the self-drawn CC0 elements appended after P34
/// (bridge / boat / lantern / stall / reeds) must place deterministically on the
/// generated world, must not cover water with land props (boat excluded), and
/// must go through the same night-glow path as the P29/P32 lanterns.
[<TestClass>]
type P39ArtTests () =
let seed = 4242UL
let map = MapGen.generateWithSize 256 192 seed
let keyOf (tiles: (int * int) list) =
tiles |> List.sort |> List.map (fun (x, y) -> sprintf "%d:%d" x y) |> String.concat ";"
let keyOf3 (tiles: (int * int * int) list) =
tiles |> List.sort |> List.map (fun (x, y, i) -> sprintf "%d:%d:%d" x y i) |> String.concat ";"
[<TestMethod>]
member _.BridgeTilesComeFromBridgeColumnsAndKeepApproachOffWater () =
// Every bridge column contributes a centre tile on the bridge and an end
// tile one row above; the end tile is the approach, never inside the river.
Assert.IsTrue(map.Bridges.Length > 0, "seed 4242 generated no bridge")
let placed = cc0BridgeTiles map
let bridgeColumns = map.Bridges |> List.map fst |> List.distinct |> List.sort
let placedColumns = placed |> List.map (fun (x, _, _) -> x) |> List.distinct |> List.sort
Assert.AreEqual<(int list)>(bridgeColumns, placedColumns)
let indices = placed |> List.map (fun (_, _, i) -> i) |> Set.ofList
Assert.IsTrue(Set.isSubset (Set.ofList [ cc0TileIndexBridgeCenter; cc0TileIndexBridgeEnd ]) indices)
for (x, _, _) in placed do
Assert.IsTrue(map.Bridges |> List.exists (fun (bx, _) -> bx = x), "bridge tile must sit on a bridge column")
[<TestMethod>]
member _.BoatSitsOnOpenWaterAndIsDeterministic () =
match cc0BoatTile map with
| None -> Assert.Fail "seed 4242 should place a boat"
| Some (x, y) ->
let water (tx: int) (ty: int) = map.Tiles.[ty * map.Width + tx] = int MapGen.GroundTile.Water
Assert.IsTrue(water x y && water (x + 1) y, "boat hull must float on water")
Assert.IsTrue(x >= 1 && x < map.Width - 2, "boat must leave room for its right half")
Assert.AreEqual<(int * int) option>(cc0BoatTile map, cc0BoatTile map)
[<TestMethod>]
member _.LanternsFollowTheMainRoadAtFixedSpacing () =
let lanterns = cc0LanternTiles map 12
Assert.IsTrue(lanterns.Length > 0, "road lanterns should appear along the main road")
for (x, y) in lanterns do
Assert.AreEqual<int>(map.MainRoadCenter.[x], y)
Assert.AreEqual<int>(0, x % 12)
Assert.IsTrue(map.Tiles.[y * map.Width + x] <> int MapGen.GroundTile.Water)
// Deterministic across calls (same seed/map -> byte-identical).
Assert.AreEqual<string>(keyOf lanterns, keyOf (cc0LanternTiles map 12))
[<TestMethod>]
member _.StallsAndLanternsDoNotShareTiles () =
let lanterns = cc0LanternTiles map 12 |> Set.ofList
let stalls = cc0StallTiles map 12
Assert.IsTrue(stalls.Length > 0, "road stalls should appear along the main road")
for stall in stalls do
Assert.IsFalse(Set.contains stall lanterns, "a stall must not overlap a lantern")
[<TestMethod>]
member _.StallsAndLanternsDoNotOverlapForOddAndEvenSpacing () =
// The stall column is offset by `spacing / 2` from the lanterns. For any
// spacing >= 2 the two residue classes (0 and spacing/2) are distinct, so
// they never collide — this must hold for both even and odd spacing, since
// integer truncation of spacing/2 on odd values was a suspected edge case.
let oddSpacings = [ 3; 5; 7; 11; 13 ]
let evenSpacings = [ 2; 4; 6; 8; 12 ]
for spacing in oddSpacings @ evenSpacings do
let lanterns = cc0LanternTiles map spacing |> Set.ofList
let stalls = cc0StallTiles map spacing
Assert.IsTrue(lanterns.Count > 0, sprintf "spacing %d should place lanterns" spacing)
Assert.IsTrue(stalls.Length > 0, sprintf "spacing %d should place stalls" spacing)
for stall in stalls do
let message = sprintf "spacing %d: stall at (%d,%d) overlapped a lantern" spacing (fst stall) (snd stall)
Assert.IsFalse(Set.contains stall lanterns, message)
// The residue classes are exactly {0} and {spacing/2}, so no even/odd
// spacing can put a stall and a lantern on the same column.
for (x, _) in lanterns do
Assert.AreEqual<int>((0: int), x % spacing)
for (x, _) in stalls do
Assert.AreEqual<int>(spacing / 2, x % spacing)
[<TestMethod>]
member _.DegenerateSpacingPlacesNothing () =
// spacing = 1 would make spacing/2 = 0, putting stalls on the same residue
// class as lanterns (a real overlap). Both helpers guard spacing < 2 and
// return an empty set instead of overlapping.
for spacing in [ 0; 1; -3 ] do
let lanternCount: int = (cc0LanternTiles map spacing).Length
let stallCount: int = (cc0StallTiles map spacing).Length
Assert.AreEqual<int>(0, lanternCount)
Assert.AreEqual<int>(0, stallCount)
[<TestMethod>]
member _.ReedsHugTheWaterBank () =
let reeds = cc0ReedTiles map 4
Assert.IsTrue(reeds.Length > 0, "reeds should line the banks")
let water (tx: int) (ty: int) =
tx >= 0 && tx < map.Width && ty >= 0 && ty < map.Height
&& map.Tiles.[ty * map.Width + tx] = int MapGen.GroundTile.Water
for (x, y) in reeds do
Assert.IsFalse(water x y, "reeds are placed on land")
Assert.IsTrue(water x (y - 1) || water x (y + 1), "reeds must touch the water bank")
Assert.AreEqual<string>(keyOf reeds, keyOf (cc0ReedTiles map 4))
[<TestMethod>]
member _.NightLightTilesIncludeRoadLanterns () =
// The P39 stone lanterns reuse the P29/P32 night-glow path, so every road
// lantern must be a light source in the night frame (cc0 mode). The
// backwards-compatible `nightLightTiles` keeps the old light set.
let cc0Lights = nightLightTilesWith true map |> Set.ofList
let fallbackLights = nightLightTiles map |> Set.ofList
for lantern in cc0LanternTiles map 12 do
Assert.IsTrue(Set.contains lantern cc0Lights, "road lantern must contribute night light in cc0 mode")
Assert.IsFalse(Set.contains lantern fallbackLights, "fallback light set must stay unchanged")
[<TestMethod>]
member _.SameSeedPlacesTheSameElementsAcrossCalls () =
// Byte-identical placement under a repeated generation of the same seed.
let map2 = MapGen.generateWithSize 256 192 seed
Assert.AreEqual<string>(keyOf3 (cc0BridgeTiles map), keyOf3 (cc0BridgeTiles map2))
Assert.AreEqual<(int * int) option>(cc0BoatTile map, cc0BoatTile map2)
Assert.AreEqual<string>(keyOf (cc0LanternTiles map 12), keyOf (cc0LanternTiles map2 12))
Assert.AreEqual<string>(keyOf (cc0StallTiles map 12), keyOf (cc0StallTiles map2 12))
Assert.AreEqual<string>(keyOf (cc0ReedTiles map 4), keyOf (cc0ReedTiles map2 4))
// Bridge/boat/lantern/stall/reed placement only depends on the map layout
// and fixed spacing (road lanterns are geometric, not seeded), so two seeds
// that happen to carve the same river still place the same elements. What
// must hold is that placement is a pure function of the map: regenerating
// the map from its own seed reproduces every position byte-for-byte.
let map3 = MapGen.generateWithSize map.Width map.Height map.Seed
Assert.AreEqual<string>(MapGen.serialize map, MapGen.serialize map3)
|