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. [] 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 ";" [] 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") [] 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) [] 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(map.MainRoadCenter.[x], y) Assert.AreEqual(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(keyOf lanterns, keyOf (cc0LanternTiles map 12)) [] 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") [] 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((0: int), x % spacing) for (x, _) in stalls do Assert.AreEqual(spacing / 2, x % spacing) [] 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(0, lanternCount) Assert.AreEqual(0, stallCount) [] 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(keyOf reeds, keyOf (cc0ReedTiles map 4)) [] 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") [] member _.SameSeedPlacesTheSameElementsAcrossCalls () = // Byte-identical placement under a repeated generation of the same seed. let map2 = MapGen.generateWithSize 256 192 seed Assert.AreEqual(keyOf3 (cc0BridgeTiles map), keyOf3 (cc0BridgeTiles map2)) Assert.AreEqual<(int * int) option>(cc0BoatTile map, cc0BoatTile map2) Assert.AreEqual(keyOf (cc0LanternTiles map 12), keyOf (cc0LanternTiles map2 12)) Assert.AreEqual(keyOf (cc0StallTiles map 12), keyOf (cc0StallTiles map2 12)) Assert.AreEqual(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(MapGen.serialize map, MapGen.serialize map3)