From d3c0ef17cb1c4d30c1d586e2a5b616f58bd3d875 Mon Sep 17 00:00:00 2001 From: "Somhairle H. Marisol" Date: Tue, 22 Sep 2026 02:43:39 +0800 Subject: feat(mapgen): 河道连续性不变量 + 桥/石板路网/沿河民居(P20 第二步) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 河道改为贯穿全图连续水带(每列一段、宽度 >= RiverWidth),置于核心区上下 两侧且不相交;新增不变量测试:逐列水段数恒定、每段宽度下限、任意水格 >=2 个水邻居(禁止断裂缺口与孤立水点)。 - Result 新增 Rivers/Bridges/Paths/Buildings/BridgeCrossingsOk;每河 2 座桥、 横贯主路 + 垂直连接路 + 远岸路 + 白墙黑瓦民居(门朝路)。 - 公开 isWalkable/floodFill;ReachabilityOk 现覆盖 30 spawn + 所有门 + 所有桥面, BridgeCrossingsOk 保证每桥两岸同连通域。 - 回归:Desktop 115->119、Kernel 90 不变;final_digest 与基线一致。 --- src/LivingVillage.Desktop/Game.fs | 50 +++++---- src/LivingVillage.Desktop/MapGen.fs | 186 +++++++++++++++++++++++++------- src/LivingVillage.Desktop/VillageArt.fs | 40 +++++-- 3 files changed, 211 insertions(+), 65 deletions(-) (limited to 'src/LivingVillage.Desktop') diff --git a/src/LivingVillage.Desktop/Game.fs b/src/LivingVillage.Desktop/Game.fs index 64dc3e2..5d2ba54 100644 --- a/src/LivingVillage.Desktop/Game.fs +++ b/src/LivingVillage.Desktop/Game.fs @@ -690,48 +690,58 @@ type LivingVillageGame() as this = printfn "menu-shot=done frames=%d" menuShotFrame this.Exit() - /// P20 证据:把一次 MapGen 绘制渲染到离屏 target 并落盘,返回绘制瓦片数。 - member private this.RenderMapGen (drawFitted: bool) : int * Texture2D = - let viewport = this.GraphicsDevice.Viewport - use target = new RenderTarget2D(this.GraphicsDevice, viewport.Width, viewport.Height) + /// P20 证据:把一次 MapGen 绘制渲染到指定尺寸离屏 target,返回绘制瓦片数与纹理。 + member private this.RenderMapGen (targetWidth: int) (targetHeight: int) (camX: int) (camY: int) (drawFitted: bool) : int * Texture2D = + use target = new RenderTarget2D(this.GraphicsDevice, targetWidth, targetHeight) this.GraphicsDevice.SetRenderTarget target this.GraphicsDevice.Clear(Color(18, 26, 20)) - let camX = max 0 ((mapGenResult.Width * Sim.tilePixels - viewport.Width) / 2) - let camY = max 0 ((mapGenResult.Height * Sim.tilePixels - viewport.Height) / 2) spriteBatch.Begin() let drawn = if drawFitted then - VillageArt.drawMapFitted spriteBatch artTextures viewport.Width viewport.Height mapGenResult Color.White + VillageArt.drawMapFitted spriteBatch artTextures pixel targetWidth targetHeight mapGenResult Color.White else - VillageArt.drawMapViewport spriteBatch artTextures (Vector2(float32 camX, float32 camY)) viewport.Width viewport.Height mapGenResult 0L Color.White + VillageArt.drawMapViewport spriteBatch artTextures pixel (Vector2(float32 camX, float32 camY)) targetWidth targetHeight mapGenResult 0L Color.White spriteBatch.End() this.GraphicsDevice.SetRenderTarget null - let data = Array.zeroCreate (viewport.Width * viewport.Height) + let data = Array.zeroCreate (targetWidth * targetHeight) target.GetData(data) - let texture = new Texture2D(this.GraphicsDevice, viewport.Width, viewport.Height) + let texture = new Texture2D(this.GraphicsDevice, targetWidth, targetHeight) texture.SetData(data) (drawn, texture) - /// P20 证据:整图等比渲染 + 1:1 视口裁剪帧各存一张,打印绘制计数后退出。 + /// P20 证据:整图放大渲染(每瓦片 6px)+ 1:1 视口裁剪帧各存一张,打印计数后退出。 member private this.DrawMapGenShot () = System.IO.Directory.CreateDirectory recordDirectory |> ignore let viewport = this.GraphicsDevice.Viewport - let fittedDrawn, fittedTexture = this.RenderMapGen true + let mapCell = 6 + let fittedDrawn, fittedTexture = this.RenderMapGen (mapGenResult.Width * mapCell) (mapGenResult.Height * mapCell) 0 0 true use _fitted = fittedTexture - use stream = System.IO.File.Create(sprintf "%s/mapgen-%dx%d-seed%d.png" recordDirectory mapGenResult.Width mapGenResult.Height (int mapGenResult.Seed)) - fittedTexture.SaveAsPng(stream, viewport.Width, viewport.Height) - printfn "mapgen-fitted drawn=%d viewport=%dx%d" fittedDrawn viewport.Width viewport.Height + use stream = System.IO.File.Create(sprintf "%s/p20b-map-%dx%d-seed%d.png" recordDirectory mapGenResult.Width mapGenResult.Height (int mapGenResult.Seed)) + fittedTexture.SaveAsPng(stream, fittedTexture.Width, fittedTexture.Height) + printfn "mapgen-fitted drawn=%d png=%dx%d" fittedDrawn fittedTexture.Width fittedTexture.Height - let camX = max 0 ((mapGenResult.Width * Sim.tilePixels - viewport.Width) / 2) - let camY = max 0 ((mapGenResult.Height * Sim.tilePixels - viewport.Height) / 2) - let culledDrawn, culledTexture = this.RenderMapGen false + let clampRange maximum value = max 0 (min (max 0 maximum) value) + let camX = clampRange (mapGenResult.Width * Sim.tilePixels - viewport.Width) ((mapGenResult.Width * Sim.tilePixels - viewport.Width) / 2) + let camY = clampRange (mapGenResult.Height * Sim.tilePixels - viewport.Height) ((mapGenResult.Height * Sim.tilePixels - viewport.Height) / 2) + let culledDrawn, culledTexture = this.RenderMapGen viewport.Width viewport.Height camX camY false use _culled = culledTexture - use culledStream = System.IO.File.Create(sprintf "%s/mapgen-culled-%dx%d-seed%d.png" recordDirectory mapGenResult.Width mapGenResult.Height (int mapGenResult.Seed)) + use culledStream = System.IO.File.Create(sprintf "%s/p20b-culled-%dx%d-seed%d.png" recordDirectory mapGenResult.Width mapGenResult.Height (int mapGenResult.Seed)) culledTexture.SaveAsPng(culledStream, viewport.Width, viewport.Height) let total = mapGenResult.Width * mapGenResult.Height let visible = MapGen.visibleTileCount mapGenResult.Width mapGenResult.Height camX camY viewport.Width viewport.Height printfn "mapgen-cull drawn=%d visible=%d total=%d culled=%.1f%%" culledDrawn visible total (100.0 * (1.0 - float culledDrawn / float total)) + match mapGenResult.Bridges with + | (bx, by) :: _ -> + let detailCamX = clampRange (mapGenResult.Width * Sim.tilePixels - viewport.Width) (bx * Sim.tilePixels - viewport.Width / 2) + let detailCamY = clampRange (mapGenResult.Height * Sim.tilePixels - viewport.Height) (by * Sim.tilePixels - viewport.Height / 2) + let detailDrawn, detailTexture = this.RenderMapGen viewport.Width viewport.Height detailCamX detailCamY false + use _detail = detailTexture + use detailStream = System.IO.File.Create(sprintf "%s/p20b-detail-%dx%d-seed%d.png" recordDirectory mapGenResult.Width mapGenResult.Height (int mapGenResult.Seed)) + detailTexture.SaveAsPng(detailStream, detailTexture.Width, detailTexture.Height) + printfn "mapgen-detail drawn=%d bridge=(%d,%d) camera=(%d,%d)" detailDrawn bx by detailCamX detailCamY + | [] -> () + let smallWatch = System.Diagnostics.Stopwatch.StartNew() let smallMap = MapGen.generateWithSize 64 48 (uint64 4242) smallWatch.Stop() @@ -742,7 +752,7 @@ type LivingVillageGame() as this = this.GraphicsDevice.SetRenderTarget frameTarget for _ in 1 .. 30 do spriteBatch.Begin() - VillageArt.drawMapViewport spriteBatch artTextures (Vector2(float32 camX, float32 camY)) viewport.Width viewport.Height mapGenResult 0L Color.White |> ignore + VillageArt.drawMapViewport spriteBatch artTextures pixel (Vector2(float32 camX, float32 camY)) viewport.Width viewport.Height mapGenResult 0L Color.White |> ignore spriteBatch.End() this.GraphicsDevice.SetRenderTarget null frameWatch.Stop() diff --git a/src/LivingVillage.Desktop/MapGen.fs b/src/LivingVillage.Desktop/MapGen.fs index 4f59f84..c59e6bd 100644 --- a/src/LivingVillage.Desktop/MapGen.fs +++ b/src/LivingVillage.Desktop/MapGen.fs @@ -32,6 +32,20 @@ module MapGen = SpawnColumns: int SpawnRows: int } + /// P20 第二步:一条贯穿全图的东西向河道(每列恰好一段,宽度下限 RiverWidth)。 + type River = + { CenterY: int + Width: int } + + /// 沿河民居色块:白墙黑瓦矩形屋顶,门开向石板路。 + type Building = + { Left: int + Top: int + Width: int + Height: int + DoorX: int + DoorY: int } + type Result = { Width: int Height: int @@ -39,8 +53,13 @@ module MapGen = Tiles: int array Core: Bounds Spawns: (int * int) list + Rivers: River list + Bridges: (int * int) list + Paths: (int * int) list + Buildings: Building list ReachableTiles: int - ReachabilityOk: bool } + ReachabilityOk: bool + BridgeCrossingsOk: bool } /// 默认仍是现状游戏尺寸 64x48,核心 14 格、5x6=30 个 spawn。 let defaultParams (seed: uint64) : Params = @@ -125,21 +144,22 @@ module MapGen = if multiOctave widthScale x y p.Seed < 0.18f && not (inCore x y) then tiles.[idx x y] <- int GroundTile.Peat - // 2) 河道:RiverCount 条自西向东随机游走,宽度 RiverWidth。 - let mutable rng = p.Seed ^^^ 0xBEEFUL - for _ in 1 .. max 0 p.RiverCount do - let nextState, rawStartY = splitmix rng - rng <- nextState - let mutable y = 2 + int (rawStartY % uint64 (max 1 (h - 4))) - for x in 2 .. w - 3 do - let nextStep, rawStep = splitmix rng - rng <- nextStep - let drift = int (rawStep % 3UL) - 1 - y <- max 2 (min (h - 3) (y + drift)) - for dy in 0 .. p.RiverWidth - 1 do - let yy = y + dy - if yy <= h - 2 && not (inCore x yy) then - tiles.[idx x yy] <- int GroundTile.Water + // 2) 河道:连续贯穿全图(每列恰好一段),置于核心区上下两侧,宽度恒 >= RiverWidth。 + let riverWidth = max 3 p.RiverWidth + let riverBands = + [ for i in 0 .. max 0 p.RiverCount - 1 do + if i % 2 = 0 then + let centerY = max 1 (min (h - 1 - riverWidth) (min (core.MinY - riverWidth - 2) (h / 4))) + if centerY + riverWidth - 1 < core.MinY then + yield { CenterY = centerY; Width = riverWidth } + else + let centerY = max 1 (min (h - 1 - riverWidth) (max (core.MaxY + 2) (3 * h / 4))) + if centerY > core.MaxY then + yield { CenterY = centerY; Width = riverWidth } ] + for river in riverBands do + for x in 1 .. w - 2 do + for dy in 0 .. river.Width - 1 do + tiles.[idx x (river.CenterY + dy)] <- int GroundTile.Water // 3) 核心活动区整平为草地(生成器保证可通行)。 for y in core.MinY .. core.MaxY do @@ -151,7 +171,7 @@ module MapGen = let minSq = minDist * minDist let attempts = max 100 ((w * h) / 16) let mutable placed: (int * int) list = [] - let mutable scatterRng = rng + let mutable scatterRng = p.Seed ^^^ 0xC0FFEEUL for _ in 1 .. attempts do let nextState, raw = splitmix scatterRng scatterRng <- nextState @@ -167,7 +187,70 @@ module MapGen = tiles.[idx x y] <- int GroundTile.Stone placed <- (x, y) :: placed - // 5) 30 个 spawn:核心区内的规则网格。 + // 5) 交通:桥 + 石板路网 + 沿河民居(白墙黑瓦矩形色块,门朝远岸路)。 + let mutable structureRng = p.Seed ^^^ 0x51AB1EUL + let inMap x y = x >= 0 && x < w && y >= 0 && y < h + let mutable bridges: (int * int) list = [] + let mutable paths: (int * int) list = [] + let mutable buildings: Building list = [] + let pathSet = System.Collections.Generic.HashSet() + let addPath x y = + if inMap x y && tiles.[idx x y] <> int GroundTile.Water then + tiles.[idx x y] <- int GroundTile.Stone + if pathSet.Add(idx x y) then paths <- (x, y) :: paths + let nextBetween lo hi = + let nextState, raw = splitmix structureRng + structureRng <- nextState + lo + int (raw % uint64 (max 1 (hi - lo + 1))) + let roadY = (core.MinY + core.MaxY) / 2 + for x in 1 .. w - 2 do + addPath x roadY + let bridgeColumns = + [ for river in riverBands do + let mutable picked: int list = [] + for _ in 1 .. 2 do + let mutable bx = nextBetween 4 (max 4 (w - 5)) + let mutable guard = 0 + while List.contains bx picked && guard < 8 do + bx <- nextBetween 4 (max 4 (w - 5)) + guard <- guard + 1 + picked <- bx :: picked + yield (river, bx) ] + for river, bx in bridgeColumns do + for dx in 0 .. 1 do + if bx + dx <= w - 2 then + for dy in 0 .. river.Width - 1 do + let yy = river.CenterY + dy + if inMap (bx + dx) yy then + bridges <- (bx + dx, yy) :: bridges + let yLo = min (river.CenterY - 1) roadY + let yHi = max (river.CenterY + river.Width) roadY + for y in yLo .. yHi do + addPath bx y + let farY = if river.CenterY < core.MinY then river.CenterY - 2 else river.CenterY + river.Width + 1 + for x in max 1 (bx - 7) .. min (w - 2) (bx + 7) do + addPath x farY + let buildingWidth = 3 + nextBetween 0 1 + let doorX = min (w - 3) (max 2 (bx + 3)) + let building = + if river.CenterY < core.MinY then + { Left = max 1 (doorX - buildingWidth / 2) + Top = max 1 (farY - 2) + Width = buildingWidth + Height = 2 + DoorX = doorX + DoorY = max 1 (farY - 1) } + else + { Left = max 1 (doorX - buildingWidth / 2) + Top = farY + 1 + Width = buildingWidth + Height = 2 + DoorX = doorX + DoorY = farY + 1 } + buildings <- building :: buildings + addPath building.DoorX building.DoorY + + // 6) 30 个 spawn:核心区内的规则网格。 let sc = max 2 p.SpawnColumns let sr = max 2 p.SpawnRows let spanX = max 1 (core.MaxX - core.MinX - 2) @@ -179,8 +262,9 @@ module MapGen = (core.MinX + 1 + (c * spanX) / (sc - 1), core.MinY + 1 + (r * spanY) / (sr - 1)) ] - // 6) BFS(4 邻接、非水可走):从核心中心洪泛,校验全部 spawn 可达。 - let walkable x y = tiles.[idx x y] <> int GroundTile.Water + // 7) BFS(4 邻接、桥面可走):校验 spawn / 门 / 桥与两岸全部连通。 + let bridgeSet = System.Collections.Generic.HashSet(bridges |> List.map (fun (x, y) -> idx x y)) + let walkable x y = tiles.[idx x y] <> int GroundTile.Water || bridgeSet.Contains(idx x y) let flood startX startY = let visited = Array.create (w * h) false let queue = System.Collections.Generic.Queue() @@ -202,25 +286,14 @@ module MapGen = queue.Enqueue ni visited - let carve (sx: int) (sy: int) = - let mutable x = sx - while x <> cx do - x <- x + sign (cx - x) - if tiles.[idx x sy] = int GroundTile.Water then tiles.[idx x sy] <- int GroundTile.Grass - let mutable y = sy - while y <> cy do - y <- y + sign (cy - y) - if tiles.[idx x y] = int GroundTile.Water then tiles.[idx x y] <- int GroundTile.Grass - - let mutable visited = flood cx cy - let unreachable = spawns |> List.filter (fun (x, y) -> not visited.[idx x y]) - for spawn in unreachable do - carve (fst spawn) (snd spawn) - if not unreachable.IsEmpty then - visited <- flood cx cy - + let visited = flood (fst spawns.Head) (snd spawns.Head) + let spawnsOk = spawns |> List.forall (fun (x, y) -> visited.[idx x y]) + let doorsOk = buildings |> List.forall (fun b -> visited.[idx b.DoorX b.DoorY]) + let bridgesOk = bridges |> List.forall (fun (x, y) -> visited.[idx x y]) + let crossingsOk = + bridgeColumns + |> List.forall (fun (river, bx) -> visited.[idx bx (river.CenterY - 1)] && visited.[idx bx (river.CenterY + river.Width)]) let reachableTiles = visited |> Array.filter id |> Array.length - let reachabilityOk = spawns |> List.forall (fun (x, y) -> visited.[idx x y]) { Width = w Height = h @@ -228,12 +301,47 @@ module MapGen = Tiles = tiles Core = core Spawns = spawns + Rivers = riverBands + Bridges = bridges + Paths = paths + Buildings = buildings ReachableTiles = reachableTiles - ReachabilityOk = reachabilityOk } + ReachabilityOk = spawnsOk && doorsOk && bridgesOk + BridgeCrossingsOk = crossingsOk } let generateWithSize (width: int) (height: int) (seed: uint64) : Result = generate { defaultParams seed with Width = width; Height = height } + /// 可行走判定:非水,或位于桥面上(桥面仍标记为水但可通行)。 + let isWalkable (map: Result) (x: int) (y: int) : bool = + if x < 0 || x >= map.Width || y < 0 || y >= map.Height then + false + else + map.Tiles.[y * map.Width + x] <> int GroundTile.Water + || (map.Bridges |> List.exists (fun (bx, by) -> bx = x && by = y)) + + /// 从给定起点做 4 邻接洪泛(承认桥面),返回逐瓦片可达标记。 + let floodFill (map: Result) (startX: int) (startY: int) : bool array = + let visited = Array.create (map.Width * map.Height) false + let queue = System.Collections.Generic.Queue() + if isWalkable map startX startY then + let start = startY * map.Width + startX + visited.[start] <- true + queue.Enqueue start + while queue.Count > 0 do + let cur = queue.Dequeue() + let x = cur % map.Width + let y = cur / map.Width + for dx, dy in [ (1, 0); (-1, 0); (0, 1); (0, -1) ] do + let nx = x + dx + let ny = y + dy + if isWalkable map nx ny then + let ni = ny * map.Width + nx + if not visited.[ni] then + visited.[ni] <- true + queue.Enqueue ni + visited + // ---- 视口裁剪(纯函数):只绘制可见瓦片,与 drawWorld 现有窗口一致 ---- /// 返回含端点的可见瓦片范围 (x0, y0, x1, y1);空视口返回 (0,0,-1,-1)。 diff --git a/src/LivingVillage.Desktop/VillageArt.fs b/src/LivingVillage.Desktop/VillageArt.fs index 51e17e0..19c9aa6 100644 --- a/src/LivingVillage.Desktop/VillageArt.fs +++ b/src/LivingVillage.Desktop/VillageArt.fs @@ -724,9 +724,34 @@ module VillageArt = /// P20 证据用:把整张 MapGen 地图等比缩放到视口内绘制,返回绘制的瓦片数。 /// 仅用于 `LV_MAPGEN_SHOT` 整图取证,不参与正常游戏循环。 + let private fillRect (spriteBatch: SpriteBatch) (pixel: Texture2D) (color: XnaColor) (x: int) (y: int) (w: int) (h: int) = + spriteBatch.Draw(pixel, XnaRectangle(x, y, max 1 w, max 1 h), color) + + /// 整图证据里叠加桥面与沿河民居(白墙黑瓦色块 + 门)。 + let private drawMapStructures + (spriteBatch: SpriteBatch) + (textures: ArtTextures) + (pixel: Texture2D) + (tileX: int -> int) + (tileY: int -> int) + (cell: int) + (map: MapGen.Result) = + let bridgeSource = tileSourceRectangle BridgeSprite + 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 drawMapFitted (spriteBatch: SpriteBatch) (textures: ArtTextures) + (pixel: Texture2D) (viewportWidth: int) (viewportHeight: int) (map: MapGen.Result) @@ -737,6 +762,8 @@ module VillageArt = let cell = max 1 (int (float32 Sim.tilePixels * scale)) let offsetX = (viewportWidth - map.Width * cell) / 2 let offsetY = (viewportHeight - map.Height * cell) / 2 + let tileX x = offsetX + x * cell + let tileY y = offsetY + y * cell let spriteOf code = match code with | 1 -> WaterSprite @@ -746,13 +773,15 @@ module VillageArt = for y in 0 .. map.Height - 1 do for x in 0 .. map.Width - 1 do let code = map.Tiles.[y * map.Width + x] - spriteBatch.Draw(textures.TileAtlas, XnaRectangle(offsetX + x * cell, offsetY + y * cell, cell, cell), tileSourceRectangle (spriteOf code), tint) + spriteBatch.Draw(textures.TileAtlas, XnaRectangle(tileX x, tileY y, cell, cell), tileSourceRectangle (spriteOf code), tint) + drawMapStructures spriteBatch textures pixel tileX tileY cell map map.Width * map.Height /// P20 证据用:按相机做视口裁剪,只绘制可见瓦片(1:1 像素),返回实际绘制数。 let drawMapViewport (spriteBatch: SpriteBatch) (textures: ArtTextures) + (pixel: Texture2D) (camera: XnaVector2) (viewportWidth: int) (viewportHeight: int) @@ -761,6 +790,8 @@ module VillageArt = (tint: XnaColor) : int = let x0, y0, x1, y1 = MapGen.visibleTileRange map.Width map.Height (int camera.X) (int camera.Y) viewportWidth viewportHeight + let tileX x = x * Sim.tilePixels - int camera.X + let tileY y = y * Sim.tilePixels - int camera.Y let spriteOf (x: int) (y: int) code = match code with | 1 -> @@ -775,12 +806,9 @@ module VillageArt = for y in y0 .. y1 do for x in x0 .. x1 do let code = map.Tiles.[y * map.Width + x] - spriteBatch.Draw( - textures.TileAtlas, - XnaRectangle(x * Sim.tilePixels - int camera.X, y * Sim.tilePixels - int camera.Y, Sim.tilePixels, Sim.tilePixels), - tileSourceRectangle (spriteOf x y code), - tint) + spriteBatch.Draw(textures.TileAtlas, XnaRectangle(tileX x, tileY y, Sim.tilePixels, Sim.tilePixels), tileSourceRectangle (spriteOf x y code), tint) drawn <- drawn + 1 + drawMapStructures spriteBatch textures pixel tileX tileY Sim.tilePixels map drawn let drawCharacter -- cgit v1.2.3