diff options
| author | Somhairle H. Marisol <[email protected]> | 2026-09-28 16:33:33 +0800 |
|---|---|---|
| committer | Somhairle H. Marisol <[email protected]> | 2026-09-28 16:33:33 +0800 |
| commit | 3732fece2285e386c1a5ba65aa45a31e467e1217 (patch) | |
| tree | 2a62bb39186ee2af04663859a2bf5c21d42a119e /src | |
| parent | b94ae560c652e8a5e759630acc079d834673c95a (diff) | |
| download | living-village-3732fece2285e386c1a5ba65aa45a31e467e1217.tar.gz | |
test(p40): 连贯带/同seed/夜光回归 + 运行时证据帧
- P40ArtTests 6 例:连贯带覆盖路网且抑制孤立散布、摆放确定性、红灯笼落门口、
红/石灯笼同光晕路径、同 seed 重生成一致、P40 索引在界内
- AssetPackTests 索引列表加 19/20;fsproj 注册 P40ArtTests
- scripts/make-p40-closeups.py 乌篷船 2x NEAREST 特写
- docs/维护说明.md 追加 P40 小节;证据 4 帧白块检查 0 命中,重复运行 byte-identical
- Release 全量 Desktop 201/201、Kernel 119/119 exit 0
Diffstat (limited to 'src')
| -rw-r--r-- | src/LivingVillage.Desktop.Tests/AssetPackTests.fs | 6 | ||||
| -rw-r--r-- | src/LivingVillage.Desktop.Tests/LivingVillage.Desktop.Tests.fsproj | 1 | ||||
| -rw-r--r-- | src/LivingVillage.Desktop.Tests/P40ArtTests.fs | 104 |
3 files changed, 109 insertions, 2 deletions
diff --git a/src/LivingVillage.Desktop.Tests/AssetPackTests.fs b/src/LivingVillage.Desktop.Tests/AssetPackTests.fs index 85fc99c..2a61255 100644 --- a/src/LivingVillage.Desktop.Tests/AssetPackTests.fs +++ b/src/LivingVillage.Desktop.Tests/AssetPackTests.fs @@ -43,7 +43,7 @@ type AssetPackTests () = [<TestMethod>] member _.Cc0StructureTileIndicesFitTheAtlasAndWaterHasTwoPhases () = - // Every P34/P39 self-drawn cell must sit inside the 19-tile atlas band. + // Every P34/P39/P40 self-drawn cell must sit inside the atlas band. let indices = [ cc0TileIndexWaterA cc0TileIndexWaterB @@ -59,7 +59,9 @@ type AssetPackTests () = cc0TileIndexBoatRight cc0TileIndexStoneLantern cc0TileIndexStall - cc0TileIndexReeds ] + cc0TileIndexReeds + cc0TileIndexPaving + cc0TileIndexRedLantern ] for index in indices do Assert.IsTrue(index >= 0 && index < cc0TileCount, sprintf "cc0 tile index %d out of atlas" index) let width, _ = pngDimensions (assetPath cc0TileAtlasRelativePath) diff --git a/src/LivingVillage.Desktop.Tests/LivingVillage.Desktop.Tests.fsproj b/src/LivingVillage.Desktop.Tests/LivingVillage.Desktop.Tests.fsproj index aae83a1..46bede8 100644 --- a/src/LivingVillage.Desktop.Tests/LivingVillage.Desktop.Tests.fsproj +++ b/src/LivingVillage.Desktop.Tests/LivingVillage.Desktop.Tests.fsproj @@ -20,6 +20,7 @@ <Compile Include="P37ProfessionTests.fs" /> <Compile Include="P38HudIconTests.fs" /> <Compile Include="P39ArtTests.fs" /> + <Compile Include="P40ArtTests.fs" /> <Compile Include="SampleTests.fs" /> </ItemGroup> diff --git a/src/LivingVillage.Desktop.Tests/P40ArtTests.fs b/src/LivingVillage.Desktop.Tests/P40ArtTests.fs new file mode 100644 index 0000000..549640e --- /dev/null +++ b/src/LivingVillage.Desktop.Tests/P40ArtTests.fs @@ -0,0 +1,104 @@ +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 + + [<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) |
