summaryrefslogtreecommitdiff
path: root/src/LivingVillage.Desktop.Tests
diff options
context:
space:
mode:
authorSomhairle H. Marisol <[email protected]>2026-09-22 12:10:42 +0800
committerSomhairle H. Marisol <[email protected]>2026-09-22 12:10:42 +0800
commita70a057fce7afa5787b2d4a037d7016f1120ef6a (patch)
treece5a24cf07dbbfd46cee48cbec561cc23349f48e /src/LivingVillage.Desktop.Tests
parent3cd5a635d12474647c4cf288a5cc4acb4e0eb1b5 (diff)
downloadliving-village-a70a057fce7afa5787b2d4a037d7016f1120ef6a.tar.gz
feat(desktop): P29 512 聚落组团 + 夜景灯光径向光晕
- MapGen:仅大图新增 ClusterSeed(group seeding)与组团布屋——512 沿横向石板路 (主街+河岸路)成排成组:组内 2-4 座/1 格间隙、组间 4-6 格空档,宽度 2/3/4 分层; 垂柳改为确定性成丛(2-3 株/丛、丛间 3-5 格空档)。独立 clusterRng 不触碰 structureRng,默认图仍不消费 RNG,64x48=6042052 / 256x192=1284656962 锁定不变。 - VillageArt:新增 lanternFalloff(二次 1-(d/r)^2,半径 0 处最大、边缘 0、严格单调无平台); lanternGlowLayers 按该衰减逐环累积 alpha;drawNightGlows 对桥头灯笼+民居门窗画半径 4 tile 的柔和径向光晕,整图取证与实机 DrawWorldView 共用同一套纯函数。 - 回归 +6:组团行/成丛/衰减/光源确定性;Desktop 151、Kernel 92 全绿,Release 0/0, final_digest=953775FAEB2F… 3 次一致。证据帧见 art/p29-*。
Diffstat (limited to 'src/LivingVillage.Desktop.Tests')
-rw-r--r--src/LivingVillage.Desktop.Tests/MapGenTests.fs79
-rw-r--r--src/LivingVillage.Desktop.Tests/PrototypeTests.fs30
2 files changed, 109 insertions, 0 deletions
diff --git a/src/LivingVillage.Desktop.Tests/MapGenTests.fs b/src/LivingVillage.Desktop.Tests/MapGenTests.fs
index b737707..d83a144 100644
--- a/src/LivingVillage.Desktop.Tests/MapGenTests.fs
+++ b/src/LivingVillage.Desktop.Tests/MapGenTests.fs
@@ -276,6 +276,85 @@ type MapGenTests () =
Assert.AreEqual<int>(4, (MapGen.generateWithSize 64 48 (uint64 4242)).Buildings.Length)
Assert.AreEqual<int>(4, (MapGen.generateWithSize 256 192 (uint64 4242)).Buildings.Length)
+ // ---- P29:512 聚落组团(group seeding 仅大图启用;行内成组、组间留档) ----
+
+ [<TestMethod>]
+ member _.ClusterSeedOnlyEnabledForLargeMaps () =
+ Assert.AreEqual<uint64>(0UL, (MapGen.paramsForSize 64 48 (uint64 4242)).ClusterSeed)
+ Assert.AreEqual<uint64>(0UL, (MapGen.paramsForSize 256 192 (uint64 4242)).ClusterSeed)
+ Assert.AreNotEqual<uint64>(0UL, (MapGen.paramsForSize 512 384 (uint64 4242)).ClusterSeed)
+ Assert.AreEqual<uint64>(0UL, (MapGen.defaultParams (uint64 4242)).ClusterSeed)
+
+ [<TestMethod>]
+ member _.LargeMapHousesFormClusteredRowsWithControlledGaps () =
+ let map = MapGen.generateWithSize 512 384 (uint64 4242)
+ let rowClusters (row: MapGen.Building list) =
+ let sorted = row |> List.sortBy (fun b -> b.Left)
+ let clusters = ResizeArray<MapGen.Building list>()
+ let mutable current: MapGen.Building list = []
+ let mutable previousEnd = System.Int32.MinValue
+ for building in sorted do
+ if current <> [] && building.Left - previousEnd <= 2 then
+ current <- building :: current
+ else
+ if current <> [] then clusters.Add(List.rev current)
+ current <- [ building ]
+ previousEnd <- building.Left + building.Width - 1
+ if current <> [] then clusters.Add(List.rev current)
+ List.ofSeq clusters
+ let clusteredRows = map.Buildings |> List.groupBy (fun b -> b.Top) |> List.map (snd >> rowClusters)
+ let clusterSizes = clusteredRows |> List.collect (List.map List.length)
+ Assert.IsTrue(clusterSizes |> List.exists (fun n -> n >= 3), "at least one row-sized house cluster")
+ // 行内出现多个簇 => 组间留有可控空档,而不是一整排等距。
+ let rowCount = map.Buildings |> List.map (fun b -> b.Top) |> List.distinct |> List.length
+ Assert.IsTrue(clusterSizes.Length > rowCount, "rows must split into separated groups")
+ for clustersInRow in clusteredRows do
+ match clustersInRow with
+ | first :: rest ->
+ let mutable previous = first
+ for cluster in rest do
+ let prevEnd = previous |> List.map (fun b -> b.Left + b.Width - 1) |> List.max
+ let nextStart = cluster |> List.map (fun b -> b.Left) |> List.min
+ Assert.IsTrue(nextStart - prevEnd >= 3, "groups must be separated by a controlled gap")
+ previous <- cluster
+ | [] -> ()
+
+ /// 组团参数下,512 world 同 seed 两次生成逐字节一致(checksum 且序列化 diff 为空)。
+ [<TestMethod>]
+ member _.LargeMapClusterLayoutChecksumIsEmptyDiffAcrossRuns () =
+ let a = MapGen.generateWithSize 512 384 (uint64 4242)
+ let b = MapGen.generateWithSize 512 384 (uint64 4242)
+ Assert.IsTrue(a.Tiles = b.Tiles, "tiles must be byte-identical across runs")
+ Assert.AreEqual<int64>(checksum a, checksum b)
+ Assert.AreEqual<string>(MapGen.serialize a, MapGen.serialize b)
+ Assert.IsTrue(a.Buildings = b.Buildings)
+ Assert.IsTrue(a.Decorations = b.Decorations)
+
+ /// P29 垂柳成丛:岸边出现相邻 2-3 株的柳丛,且不再是均匀逐格散布。
+ [<TestMethod>]
+ member _.LargeMapWillowsGrowInGroves () =
+ let map = MapGen.generateWithSize 512 384 (uint64 4242)
+ let willows = map.Decorations |> List.filter (fun d -> d.Kind = MapGen.Willow)
+ Assert.IsTrue(willows.Length > 0, "large map must place willows")
+ let groveSizes =
+ willows
+ |> List.groupBy (fun d -> d.Y)
+ |> List.collect (fun (_, row) ->
+ let xs = row |> List.map (fun d -> d.X) |> List.sort
+ let sizes = ResizeArray<int>()
+ let mutable current = 0
+ let mutable previous = System.Int32.MinValue
+ for x in xs do
+ if previous >= 0 && x - previous = 1 then
+ current <- current + 1
+ else
+ if current > 0 then sizes.Add current
+ current <- 1
+ previous <- x
+ if current > 0 then sizes.Add current
+ List.ofSeq sizes)
+ Assert.IsTrue(groveSizes |> List.exists (fun n -> n >= 2), "willows must form groves of adjacent trunks")
+
// ---- P25:大图不产孤立石板 tile(装饰石块默认开启仅限小图) ----
[<TestMethod>]
diff --git a/src/LivingVillage.Desktop.Tests/PrototypeTests.fs b/src/LivingVillage.Desktop.Tests/PrototypeTests.fs
index 7f67280..7b35837 100644
--- a/src/LivingVillage.Desktop.Tests/PrototypeTests.fs
+++ b/src/LivingVillage.Desktop.Tests/PrototypeTests.fs
@@ -663,6 +663,36 @@ type PrototypeTests () =
Assert.IsTrue(row.[0] = center && row.[radius] < center, "core is brightest, rim is dimmer")
[<TestMethod>]
+ member _.LanternFalloffIsMaxAtCoreAndMonotonicWithoutPlateau () =
+ // P29:夜景灯光衰减函数——半径 0 处最大(=1),向半径边缘严格单调递减至 0,无平台。
+ let radius = 96.0f
+ Assert.AreEqual<float32>(1.0f, VillageArt.lanternFalloff 0.0f radius)
+ Assert.AreEqual<float32>(0.0f, VillageArt.lanternFalloff radius radius)
+ Assert.AreEqual<float32>(0.0f, VillageArt.lanternFalloff (radius + 7.0f) radius)
+ Assert.IsTrue(abs (VillageArt.lanternFalloff 0.0f 0.0f) < 1e-6f, "zero radius must contribute no light")
+ let samples = [ for i in 0 .. int radius -> VillageArt.lanternFalloff (float32 i) radius ]
+ for value in samples do
+ Assert.IsTrue(value >= 0.0f && value <= 1.0f, "falloff must stay within [0,1]")
+ samples
+ |> List.pairwise
+ |> List.iter (fun (a, b) -> Assert.IsTrue(b < a, sprintf "falloff must strictly decrease (no plateau): %f then %f" a b))
+ // 二次衰减:一半半径处约为 0.75,且严格高于线性 0.5。
+ Assert.IsTrue(abs (VillageArt.lanternFalloff (radius / 2.0f) radius - 0.75f) < 1e-4f)
+
+ [<TestMethod>]
+ member _.NightLightTilesCombineBridgeLanternsAndHouseWindows () =
+ // P29:夜景光源 = 桥头灯笼 + 民居门窗暖光,确定性纯函数。
+ let map = MapGen.generateWithSize 512 384 (uint64 4242)
+ let lights = VillageArt.nightLightTiles map
+ Assert.IsTrue(lights.Length > 0, "large map must emit night lights")
+ Assert.IsTrue(lights.Length > map.Buildings.Length, "windows add light sources beyond the houses themselves")
+ Assert.AreEqual<(int * int) list>(lights, VillageArt.nightLightTiles map)
+ for lantern in VillageArt.bridgeLanternTiles map do
+ Assert.IsTrue(lights |> List.contains lantern, sprintf "bridge lantern (%d,%d) must be a light source" (fst lantern) (snd lantern))
+ for building in map.Buildings do
+ Assert.IsTrue(lights |> List.contains (building.DoorX, building.DoorY), "every house door must carry warm light")
+
+ [<TestMethod>]
member _.CharacterFeetSitOnTheTileGroundLine () =
Assert.AreEqual<int>(16, VillageArt.characterFeetOffset)
let destination = VillageArt.characterDestination { X = 100.0f; Y = 200.0f } (Vector2(0.0f, 0.0f))