summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHermes Leader <v@v>2026-09-22 08:41:14 +0800
committerHermes Leader <v@v>2026-09-22 08:41:14 +0800
commitaa3d749ed2a63f9409290c564649f9718756fd6e (patch)
tree864a7f3d8d750158513e71739091e72204d23da3
parent1fb9f9de8f7a6e9124f9a650e44b2f27ed2cac7a (diff)
parent6afc9041e9aeb9f51a3b9ea73b5a6d493862ee1c (diff)
downloadliving-village-aa3d749ed2a63f9409290c564649f9718756fd6e.tar.gz
merge: P24 512x384 民居密度/村民可辨(writer hash 保留)
-rw-r--r--docs/维护说明.md25
-rw-r--r--src/LivingVillage.Desktop.Tests/MapGenTests.fs32
-rw-r--r--src/LivingVillage.Desktop/Game.fs14
-rw-r--r--src/LivingVillage.Desktop/MapGen.fs55
-rw-r--r--src/LivingVillage.Desktop/VillageArt.fs21
5 files changed, 137 insertions, 10 deletions
diff --git a/docs/维护说明.md b/docs/维护说明.md
index b9bd9fe..79ca370 100644
--- a/docs/维护说明.md
+++ b/docs/维护说明.md
@@ -388,6 +388,20 @@ dotnet src/LivingVillage.Headless/bin/Release/net8.0/LivingVillage.Headless.dll
- 取证钩子:`LV_RIVERSCAPE_WIDTH`/`LV_RIVERSCAPE_HEIGHT`(默认 256x192)把生成器世界扩到
512x384 跑巡游/性能;`LV_MAPGEN_SHOT=1` + `LV_MAPGEN_WIDTH/HEIGHT/SEED` 出整图。
+## 民居密度参数(P24)
+
+- `MapGen.Params.ExtraRiversideHouses`:大图额外沿河民居数量,默认 0;`paramsForSize` 仅对
+ `width>256 || height>192` 设为 **10**。512x384 在原有 8 座桥点民居上再按现有路网泊松散布
+ 10 座(`structureRng` 确定性),**共 18 座**,门全部接入路网且 BFS 可达;64x48/256x192 仍
+ 4 座、checksum 6042052 / 1284656962 不变。
+- 民居渲染改用 256x192 同款部件:黛瓦(`RoofRidgeSprite`/`RoofEaveSprite`)+ 木墙/窗
+ (`WallWoodSprite`/`WallWindowSprite`)+ 门(`DoorSprite`),替换原黑白矩形色块。
+- 村民可见性证据钩子 `LV_NPC_VISIBLE_LOG=1`:每秒打印 `visible-npcs=K of 30`;512x384 巡游中
+ 观测到最大 30/30 全部在屏(`/tmp/opencode/lv-p24-tour.txt`)。
+- 取证:`/tmp/opencode/lv-p24-art/`(`p24-map-512x384-seed{4242,7}.png` 整图、
+ `p24-zoom-houses.png`/`p24-zoom-villagers.png` 局部放大含民居与村民、`p24-tour-0{1,2,3}.png`
+ tour 页、`tour512/` 全量帧),白块回归 0 命中。
+
## 已验证命令
```bash
@@ -473,6 +487,14 @@ timeout 280 xvfb-run -a -s "-screen 0 1280x720x24" env LV_AUTOPLAY_SAMPLE=1 \
LV_UNLOCK_FPS=1 LV_PERF_SUMMARY=1 \
dotnet src/LivingVillage.Desktop/bin/Release/net8.0/LivingVillage.Desktop.dll
+# P24 512x384 民居密度取证:整图 + 巡游(记录帧;LV_NPC_VISIBLE_LOG 打印可见村民数)
+timeout 240 xvfb-run -a -s "-screen 0 1280x720x24" env LV_AUTOPLAY_SAMPLE=1 \
+ LV_RIVERSCAPE_TOUR=1 LV_RIVERSCAPE_WIDTH=512 LV_RIVERSCAPE_HEIGHT=384 \
+ LV_UNLOCK_FPS=1 LV_NPC_VISIBLE_LOG=1 LV_AUTOPLAY_RECORD=1 LV_RECORD_EVERY=120 \
+ LV_RECORD_NAME=p24-tour- LV_RECORD_DIR=/tmp/opencode/lv-p24-art/tour512 \
+ dotnet src/LivingVillage.Desktop/bin/Release/net8.0/LivingVillage.Desktop.dll
+# 局部放大由 tour512 帧裁 480x360 后 2x 最近邻放大(含民居+村民)
+
# P20 第三步:默认生成器世界的昼夜巡游录制(真走位,含桥面行走帧;p20c-* 命名)
for pair in day:12.0 night:20.75; do name=${pair%%:*}; hour=${pair##*:}; \
timeout 150 xvfb-run -a -s "-screen 0 1280x720x24" env LV_AUTOPLAY_SAMPLE=1 \
@@ -525,6 +547,9 @@ python3 scripts/check-no-white-blocks.py evidence/night-run
- `/tmp/opencode/lv-p13/evidence/`(P13:`walk-idle-frames.png`、`water-3phase.png`、
`smoke-3phase.png`、`water-2frame.png`、`smoke-2frame.png`、`day-panorama.png`、
`night-lantern.png`、`day-night-compare.png`、`walk-real-run.png`、`day-run/`、`night-run/`)
+- `/tmp/opencode/lv-p24-art/`(P24 512x384:`p24-map-512x384-seed{4242,7}.png` 整图、
+ `p24-zoom-houses.png`/`p24-zoom-villagers.png` 局部放大、`p24-tour-0{1,2,3}.png` tour 页、
+ `tour512/` 全量巡游帧;规格要求的 `/tmp/lv-p24-art` 被沙箱 external_directory deny,落此路径)
- `/tmp/opencode/lv-p23-art/`(P23 512x384:`p23-map-512x384-seed{4242,7}.png` 整图真实渲染、
`p23-culled-512x384-seed{4242,7}.png` 视口裁剪、`p23-detail-512x384-seed{4242,7}.png` 1:1 近景、
`tour512/` 巡游帧)
diff --git a/src/LivingVillage.Desktop.Tests/MapGenTests.fs b/src/LivingVillage.Desktop.Tests/MapGenTests.fs
index 4d11dd1..4a09865 100644
--- a/src/LivingVillage.Desktop.Tests/MapGenTests.fs
+++ b/src/LivingVillage.Desktop.Tests/MapGenTests.fs
@@ -229,6 +229,38 @@ type MapGenTests () =
Assert.IsTrue(visited.[y * map.Width + x], sprintf "bridge tile (%d,%d) unreachable at 512x384" x y)
Assert.IsTrue(map.Buildings.Length >= map.Rivers.Length, "riverside building per band at 512x384")
+ // ---- P24:512x384 沿河民居加密(默认图不受影响) ----
+
+ [<TestMethod>]
+ member _.ExtraHouseDensityOnlyEnabledForLargeMaps () =
+ Assert.AreEqual<int>(0, (MapGen.paramsForSize 64 48 (uint64 1)).ExtraRiversideHouses)
+ Assert.AreEqual<int>(0, (MapGen.paramsForSize 256 192 (uint64 1)).ExtraRiversideHouses)
+ Assert.IsTrue((MapGen.paramsForSize 512 384 (uint64 1)).ExtraRiversideHouses > 0)
+
+ [<TestMethod>]
+ member _.LargeMapHasAtLeastTwelveHousesWithReachableDoors () =
+ let map = MapGen.generateWithSize 512 384 (uint64 4242)
+ Assert.IsTrue(map.Buildings.Length >= 12, sprintf "512x384 must place >=12 houses, saw %d" map.Buildings.Length)
+ let startX, startY = map.Spawns.Head
+ let visited = MapGen.floodFill map startX startY
+ for building in map.Buildings do
+ Assert.IsTrue(MapGen.isWalkable map building.DoorX building.DoorY, sprintf "door (%d,%d) must be walkable" building.DoorX building.DoorY)
+ Assert.IsTrue(visited.[building.DoorY * map.Width + building.DoorX], sprintf "door (%d,%d) unreachable" building.DoorX building.DoorY)
+ Assert.IsTrue(map.ReachabilityOk, "512x384 reachability must hold with extra houses")
+
+ [<TestMethod>]
+ member _.LargeMapHousePlacementIsDeterministic () =
+ let a = MapGen.generateWithSize 512 384 (uint64 4242)
+ let b = MapGen.generateWithSize 512 384 (uint64 4242)
+ Assert.AreEqual<int>(a.Buildings.Length, b.Buildings.Length)
+ Assert.IsTrue(a.Buildings = b.Buildings)
+ Assert.AreEqual<string>(MapGen.serialize a, MapGen.serialize b)
+
+ [<TestMethod>]
+ member _.DefaultSizeHouseCountUnchanged () =
+ Assert.AreEqual<int>(4, (MapGen.generateWithSize 64 48 (uint64 4242)).Buildings.Length)
+ Assert.AreEqual<int>(4, (MapGen.generateWithSize 256 192 (uint64 4242)).Buildings.Length)
+
/// 锁定当前默认尺寸的生成结果,防止 P23 参数适配无意改变 64x48 / 256x192。
[<TestMethod>]
member _.DefaultSizeChecksumsArePinned () =
diff --git a/src/LivingVillage.Desktop/Game.fs b/src/LivingVillage.Desktop/Game.fs
index 8309867..bcc4094 100644
--- a/src/LivingVillage.Desktop/Game.fs
+++ b/src/LivingVillage.Desktop/Game.fs
@@ -215,6 +215,8 @@ type LivingVillageGame() as this =
let mutable avatarMoving = false
let unlockFps = Environment.GetEnvironmentVariable("LV_UNLOCK_FPS") = "1"
let perfSummaryMode = Environment.GetEnvironmentVariable("LV_PERF_SUMMARY") = "1"
+ // P24 证据钩子:每秒打印屏幕内可见 NPC 数,证明大世界村民确实被渲染。
+ let npcVisibleLog = Environment.GetEnvironmentVariable("LV_NPC_VISIBLE_LOG") = "1"
let mutable fpsSamples: float list = []
let mutable fpsFrames = 0
let mutable fpsSeconds = 0.0
@@ -751,6 +753,18 @@ type LivingVillageGame() as this =
if perfSummaryMode then
fpsSamples <- fps :: fpsSamples
printfn $"fps={fps:F1} tick={world.Tick} pos=({world.Avatar.Pos.X:F0},{world.Avatar.Pos.Y:F0})"
+ if npcVisibleLog then
+ let viewport = this.GraphicsDevice.Viewport
+ let margin = float32 Sim.tilePixels
+ let visible =
+ world.Npcs
+ |> Array.filter (fun npc ->
+ let sx = npc.Pos.X - camera.X
+ let sy = npc.Pos.Y - camera.Y
+ sx >= -margin && sx <= float32 viewport.Width + margin
+ && sy >= -margin && sy <= float32 viewport.Height + margin)
+ |> Array.length
+ printfn $"visible-npcs={visible} of {world.Npcs.Length}"
prevKb <- kb
override this.EndRun() =
diff --git a/src/LivingVillage.Desktop/MapGen.fs b/src/LivingVillage.Desktop/MapGen.fs
index ce9bccf..1e63ed2 100644
--- a/src/LivingVillage.Desktop/MapGen.fs
+++ b/src/LivingVillage.Desktop/MapGen.fs
@@ -30,7 +30,9 @@ module MapGen =
RiverWidth: int
CoreSide: int
SpawnColumns: int
- SpawnRows: int }
+ SpawnRows: int
+ /// P24:大图额外沿河民居数量(沿现有路网泊松散布)。默认 0,64x48/256x192 不受影响。
+ ExtraRiversideHouses: int }
/// P20 第二步:一条贯穿全图的东西向河道(每列恰好一段,宽度下限 RiverWidth)。
type River =
@@ -70,16 +72,20 @@ module MapGen =
RiverWidth = 3
CoreSide = 14
SpawnColumns = 5
- SpawnRows = 6 }
+ SpawnRows = 6
+ ExtraRiversideHouses = 0 }
/// P23 尺寸自适应参数:河道数量随高度增长(约每 96 行一条),其余沿用默认。
/// 64x48 / 256x192 仍得到 2 条河道,与既有输出逐字节一致;512x384 得到 4 条。
+ /// P24:仅对大于 256x192 的图追加沿河民居(泊松散布),保证既有 checksum 不变。
let paramsForSize (width: int) (height: int) (seed: uint64) : Params =
let base_ = defaultParams seed
+ let large = width > 256 || height > 192
{ base_ with
Width = width
Height = height
- RiverCount = max base_.RiverCount (max 1 (height / 96)) }
+ RiverCount = max base_.RiverCount (max 1 (height / 96))
+ ExtraRiversideHouses = (if large then 10 else 0) }
// ---- splitmix64 与 value noise(无外部依赖、无时钟) ----
@@ -275,6 +281,49 @@ module MapGen =
buildings <- building :: buildings
addPath building.DoorX building.DoorY
+ // 5b) P24:大图沿河民居加密。从既有路网瓦片出发,向四邻定向落位 3x2 白墙黛瓦民居,
+ // 门开向相邻路格并接入路网(BFS 可达);仅在 ExtraRiversideHouses>0 时执行,
+ // 且只在核心区外的草地落位,故 64x48/256x192 输出逐字节不变。
+ if p.ExtraRiversideHouses > 0 then
+ let footprintFree (left: int) (top: int) =
+ left >= 1 && left + 2 <= w - 2 && top >= 1 && top + 1 <= h - 2
+ && [ for dx in 0 .. 2 do
+ for dy in 0 .. 1 -> (left + dx, top + dy) ]
+ |> List.forall (fun (x, y) ->
+ tiles.[idx x y] = int GroundTile.Grass
+ && not (inCore x y))
+ let overlaps (left: int) (top: int) =
+ buildings
+ |> List.exists (fun b ->
+ left < b.Left + b.Width && b.Left < left + 3
+ && top < b.Top + b.Height && b.Top < top + 2)
+ let directions = [ (0, -1); (0, 1); (-1, 0); (1, 0) ]
+ let mutable extraPlaced = 0
+ let mutable extraAttempts = 0
+ let maxAttempts = p.ExtraRiversideHouses * 400
+ while extraPlaced < p.ExtraRiversideHouses && extraAttempts < maxAttempts && paths.Length > 0 do
+ extraAttempts <- extraAttempts + 1
+ let nextState, raw = splitmix structureRng
+ structureRng <- nextState
+ if paths.Length > 0 then
+ let pathX, pathY = List.item (int (raw % uint64 paths.Length)) paths
+ let dirX, dirY = directions.[int ((raw >>> 32) % 4UL)]
+ let doorX = pathX + dirX
+ let doorY = pathY + dirY
+ let left = doorX - 1
+ let top = doorY - 1
+ if footprintFree left top && not (overlaps left top) then
+ let building =
+ { Left = left
+ Top = top
+ Width = 3
+ Height = 2
+ DoorX = doorX
+ DoorY = doorY }
+ buildings <- building :: buildings
+ addPath building.DoorX building.DoorY
+ extraPlaced <- extraPlaced + 1
+
// 6) 30 个 spawn:核心区内的规则网格。
let sc = max 2 p.SpawnColumns
let sr = max 2 p.SpawnRows
diff --git a/src/LivingVillage.Desktop/VillageArt.fs b/src/LivingVillage.Desktop/VillageArt.fs
index 3ad5b19..12c17ba 100644
--- a/src/LivingVillage.Desktop/VillageArt.fs
+++ b/src/LivingVillage.Desktop/VillageArt.fs
@@ -616,13 +616,20 @@ module VillageArt =
for (x, y) in map.Bridges do
spriteBatch.Draw(textures.TileAtlas, XnaRectangle(tileX x, tileY y, cell, cell), bridgeSource, XnaColor.White)
for building in map.Buildings do
- for row in building.Top .. building.Top + building.Height - 1 do
- // 白墙朝向道路一行(门所在行),其余为黑瓦屋顶,两侧河岸朝向一致。
- let color =
- if row = building.DoorY then XnaColor(236, 232, 220)
- else XnaColor(48, 52, 60)
- fillRect spriteBatch pixel color (tileX building.Left) (tileY row) (building.Width * cell) (cell)
- fillRect spriteBatch pixel (XnaColor(96, 60, 42)) (tileX building.DoorX) (tileY building.DoorY) cell cell
+ let left = building.Left
+ let right = building.Left + building.Width - 1
+ // P24:改用 256x192 同款民宅部件——黛瓦(脊/檐)+ 木墙/窗 + 门。
+ for row in building.Top .. building.DoorY - 1 do
+ let roofSprite = if row = building.Top then RoofRidgeSprite else RoofEaveSprite
+ let source = tileSourceRectangle roofSprite
+ for x in left .. right do
+ spriteBatch.Draw(textures.TileAtlas, XnaRectangle(tileX x, tileY row, cell, cell), source, XnaColor.White)
+ for x in left .. right do
+ let wallSprite =
+ if x = building.DoorX then DoorSprite
+ elif (x - left) % 2 = 0 then WallWindowSprite
+ else WallWoodSprite
+ spriteBatch.Draw(textures.TileAtlas, XnaRectangle(tileX x, tileY building.DoorY, cell, cell), tileSourceRectangle wallSprite, XnaColor.White)
/// 桥头灯笼瓦片:每座桥每列的最上/最下一格(桥两端),用于挂灯笼与夜间暖光。
let bridgeLanternTiles (map: MapGen.Result) : (int * int) list =