diff options
| author | Hermes Leader <v@v> | 2026-09-22 08:41:14 +0800 |
|---|---|---|
| committer | Hermes Leader <v@v> | 2026-09-22 08:41:14 +0800 |
| commit | aa3d749ed2a63f9409290c564649f9718756fd6e (patch) | |
| tree | 864a7f3d8d750158513e71739091e72204d23da3 | |
| parent | 1fb9f9de8f7a6e9124f9a650e44b2f27ed2cac7a (diff) | |
| parent | 6afc9041e9aeb9f51a3b9ea73b5a6d493862ee1c (diff) | |
| download | living-village-aa3d749ed2a63f9409290c564649f9718756fd6e.tar.gz | |
merge: P24 512x384 民居密度/村民可辨(writer hash 保留)
| -rw-r--r-- | docs/维护说明.md | 25 | ||||
| -rw-r--r-- | src/LivingVillage.Desktop.Tests/MapGenTests.fs | 32 | ||||
| -rw-r--r-- | src/LivingVillage.Desktop/Game.fs | 14 | ||||
| -rw-r--r-- | src/LivingVillage.Desktop/MapGen.fs | 55 | ||||
| -rw-r--r-- | src/LivingVillage.Desktop/VillageArt.fs | 21 |
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 = |
