summaryrefslogtreecommitdiff
path: root/src/LivingVillage.Desktop/MapGen.fs
diff options
context:
space:
mode:
Diffstat (limited to 'src/LivingVillage.Desktop/MapGen.fs')
-rw-r--r--src/LivingVillage.Desktop/MapGen.fs346
1 files changed, 300 insertions, 46 deletions
diff --git a/src/LivingVillage.Desktop/MapGen.fs b/src/LivingVillage.Desktop/MapGen.fs
index a9d6c9f..abd4a7e 100644
--- a/src/LivingVillage.Desktop/MapGen.fs
+++ b/src/LivingVillage.Desktop/MapGen.fs
@@ -10,11 +10,14 @@ open LivingVillage.Kernel
module MapGen =
/// 地面码,与既有 ProceduralMap / 渲染映射保持一致。
+ /// P30:大图乡村空区新增 4=水田 / 5=菜畦(成熟 tile 素材;非水故仍可通行)。
type GroundTile =
| Grass = 0
| Water = 1
| Stone = 2
| Peat = 3
+ | PaddyField = 4
+ | VegetablePlot = 5
type Bounds =
{ MinX: int
@@ -67,6 +70,17 @@ module MapGen =
X: int
Y: int }
+ /// P30 树丛种类:竹丛 / 灌木丛;纯视觉层(不写瓦片、不影响通行),仅大图乡村空区生成。
+ type GroveKind =
+ | BambooClump
+ | ShrubClump
+
+ /// P30 树丛:单格树/灌木,成丛由若干相邻格组成;渲染走既有 Bamboo/Shrub 成熟素材。
+ type Grove =
+ { Kind: GroveKind
+ X: int
+ Y: int }
+
type Result =
{ Width: int
Height: int
@@ -79,6 +93,12 @@ module MapGen =
Paths: (int * int) list
Buildings: Building list
Decorations: RiverDecoration list
+ /// P30 散落农舍:与沿河组团民居分列,避免污染组团规模统计;门贴草地/院落路可达。
+ Farmhouses: Building list
+ /// P30 乡村树丛:纯视觉,不入瓦片、不影响通行与可达性。
+ Groves: Grove list
+ /// P30 主路中心线(逐列 y):默认图为笔直 roadY;大图为确定性缓弯(相邻列步长 <=1)。
+ MainRoadCenter: int array
ReachableTiles: int
ReachabilityOk: bool
BridgeCrossingsOk: bool }
@@ -103,6 +123,8 @@ module MapGen =
/// P24:仅对大于 256x192 的图追加沿河民居(泊松散布),保证既有 checksum 不变。
/// P28:大图沿河民居由 10 增至 16(+6 座,宽度 2/3/4 变化),默认图仍为 0、不消耗 RNG。
/// P29:大图额外启用聚落组团发牌种子(独立 clusterRng),默认图保持 0、不进入组团分支。
+ /// P30:大图沿河/沿路民居目标由 16 增至 30(组团 2-5 座为主),并新增乡村空区填充与主路弯曲,
+ /// 全部只在 large 分支执行;64x48/256x192 判定不进入、不消费任何额外 RNG。
let paramsForSize (width: int) (height: int) (seed: uint64) : Params =
let base_ = defaultParams seed
let large = width > 256 || height > 192
@@ -110,7 +132,7 @@ module MapGen =
Width = width
Height = height
RiverCount = max base_.RiverCount (max 1 (height / 96))
- ExtraRiversideHouses = (if large then 16 else 0)
+ ExtraRiversideHouses = (if large then 30 else 0)
DecorativeStones = not large
RiverDecorations = large
ClusterSeed = (if large then seed ^^^ 0xC1A57E2UL else 0UL) }
@@ -254,6 +276,8 @@ module MapGen =
let mutable bridges: (int * int) list = []
let mutable paths: (int * int) list = []
let mutable buildings: Building list = []
+ let mutable farmhouses: Building list = []
+ let mutable groves: Grove list = []
let pathSet = System.Collections.Generic.HashSet<int>()
let addPath x y =
if inMap x y && tiles.[idx x y] <> int GroundTile.Water then
@@ -264,8 +288,29 @@ module MapGen =
structureRng <- nextState
lo + int (raw % uint64 (max 1 (hi - lo + 1)))
let roadY = (core.MinY + core.MaxY) / 2
+ // P30 主路柔化:仅大图把主街按逐点确定性微扰成缓弯(曲率 ±3 格、相邻列步长 <=1,
+ // 遇弯补一格保证 4 连通);默认图仍为笔直主街,故 64x48/256x192 输出逐字节不变。
+ let largeMap = p.ExtraRiversideHouses > 0
+ let roadCenter =
+ let arr = Array.create w roadY
+ if largeMap then
+ let roadSeed = p.Seed ^^^ 0x5E9B0AUL
+ let mutable prev = roadY
+ for x in 1 .. w - 2 do
+ let noise = valueNoise (float32 x) 0.0f roadSeed 130.0f
+ let target = roadY + int (System.Math.Round(3.0 * float (noise - 0.5f) * 2.0))
+ let y = max (prev - 1) (min (prev + 1) target)
+ arr.[x] <- y
+ prev <- y
+ // 两边界补与首/末列相接,避免边缘断头。
+ arr.[0] <- arr.[1]
+ arr.[w - 1] <- arr.[w - 2]
+ arr
+ let roadYAt (x: int) = roadCenter.[max 0 (min (w - 1) x)]
for x in 1 .. w - 2 do
- addPath x roadY
+ if x > 1 && roadYAt x <> roadYAt (x - 1) then
+ addPath x (roadYAt (x - 1))
+ addPath x (roadYAt x)
let bridgeColumns =
[ for river in riverBands do
let mutable picked: int list = []
@@ -284,8 +329,9 @@ module MapGen =
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
+ let roadYb = roadYAt bx
+ let yLo = min (river.CenterY - 1) roadYb
+ let yHi = max (river.CenterY + river.Width) roadYb
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
@@ -311,12 +357,12 @@ module MapGen =
buildings <- building :: buildings
addPath building.DoorX building.DoorY
- // 5b) P24/P28/P29:大图沿河聚落组团。仅在 ExtraRiversideHouses>0 时执行,只落在核心区外
+ // 5b) P24/P28/P29/P30:大图沿河聚落组团。仅在 ExtraRiversideHouses>0 时执行,只落在核心区外
// 草地并避免与既有民居重叠,故 64x48/256x192 输出逐字节不变。
- // P29:不改「沿路随机散点」,而是沿横向石板路(主街 + 河岸路)成排成组落位:
- // 每组 2-4 座、组内 1 格间隙、组间留 4-6 格空档,形成「沿河成排 / 桥头小广场」的
- // 聚落肌理;组发牌使用独立 clusterRng(group seeding),不触碰既有 structureRng,
- // 桥/路/灯笼等基础结构坐标不受影响。宽度仍按 2/3/4 加权(附属小筑/民居/大宅)。
+ // P29:沿横向石板路(主街 + 河岸路)成排成组落位,组发牌用独立 clusterRng(group seeding),
+ // 不触碰既有 structureRng,桥/路/灯笼等基础结构坐标不受影响。宽度按 2/3/4 加权。
+ // P30:目标密度由 16 增至 30,组团规模改为 2-5 座;含基础岸屋的路段优先布组,
+ // 使每座基础民居并入 >=2 座的组团,消除大面积孤立单屋。
if p.ExtraRiversideHouses > 0 then
let footprintFree (left: int) (top: int) (width: int) =
left >= 1 && left + width - 1 <= w - 2 && top >= 1 && top + 1 <= h - 2
@@ -331,6 +377,13 @@ module MapGen =
left < b.Left + b.Width && b.Left < left + width
&& top < b.Top + b.Height && b.Top < top + 2)
// 横向路段的连续格(主街/河岸路),先按 (y,x) 排序再按 group seed 打散成确定性布线顺序。
+ // P30:含既有岸屋的路段优先处理,保证每座基础民居都能被并进 2-5 座的组团。
+ let runHasBase (runY: int) (x0: int) (x1: int) =
+ buildings
+ |> List.exists (fun b ->
+ (b.Top = runY - 2 || b.Top = runY + 1)
+ && b.Left <= x1
+ && b.Left + b.Width - 1 >= x0)
let horizontalRuns =
[ for y in 1 .. h - 2 do
let mutable x = 1
@@ -342,51 +395,230 @@ module MapGen =
if x - x0 >= 4 then yield (y, x0, x - 1)
else
x <- x + 1 ]
- |> List.sortBy (fun (y, x0, _) -> (hash2 y x0 p.ClusterSeed))
+ |> List.sortBy (fun (y, x0, x1) -> ((if runHasBase y x0 x1 then 0 else 1), hash2 y x0 p.ClusterSeed))
let mutable clusterRng = p.ClusterSeed
let nextCluster () =
let nextState, raw = splitmix clusterRng
clusterRng <- nextState
raw
let mutable extraPlaced = 0
- for (runY, runStart, runEnd) in horizontalRuns do
- if extraPlaced < p.ExtraRiversideHouses then
- let runLength = runEnd - runStart + 1
- // 主街长路段多放,河岸短路各放一小簇。
- let quota = min (p.ExtraRiversideHouses - extraPlaced) (if runLength >= 64 then 6 else 3)
- // 默认屋在路北(门贴路南缘);北侧容不下时改南侧(沿用既有岸屋「门在顶排」口径)。
+ // 在一条横向路段的 [lo,hi] 区间内铺一个 2-5 座组团:门贴路缘、组内 1 格间隙、
+ // 宽度按 2/3/4 加权。先做局部试排,只有落满 >=2 座、或能与既有民居并组时才提交,
+ // 从源头消除「孤立单屋」。返回实际落位数。
+ let nearFootprint (a: Building) (b: Building) =
+ let xGap = max (a.Left - (b.Left + b.Width - 1)) (b.Left - (a.Left + a.Width - 1))
+ let yGap = max (a.Top - (b.Top + b.Height - 1)) (b.Top - (a.Top + a.Height - 1))
+ xGap <= 2 && yGap <= 2
+ let placeGroup (runY: int) (lo: int) (hi: int) (desired: int) : int =
+ if hi - lo + 1 < 4 then
+ 0
+ else
let sideFits above =
let top = if above then runY - 2 else runY + 1
- [ runStart .. min runEnd (runStart + 4) ]
- |> List.exists (fun x -> footprintFree x top 3)
+ [ lo .. min hi (lo + 4) ] |> List.exists (fun x -> footprintFree x top 3)
let above = if sideFits true then true else not (sideFits false)
- let mutable x = runStart + int (nextCluster () % 2UL)
- let mutable placedHere = 0
- while placedHere < quota && x + 1 <= runEnd do
- let groupSize = 2 + int (nextCluster () % 3UL)
- for _ in 1 .. groupSize do
- if placedHere < quota && x + 1 <= runEnd then
- let widthRoll = int (nextCluster () % 10UL)
- let buildingWidth = if widthRoll < 5 then 3 elif widthRoll < 8 then 2 else 4
- let doorX = x + buildingWidth / 2
- let doorY = if above then runY - 1 else runY + 1
- let top = if above then doorY - 1 else doorY
- let left = doorX - buildingWidth / 2
- if footprintFree left top buildingWidth && not (overlaps left top buildingWidth) then
- let building =
- { Left = left
- Top = top
- Width = buildingWidth
- Height = 2
- DoorX = doorX
- DoorY = doorY }
- buildings <- building :: buildings
- addPath building.DoorX building.DoorY
- extraPlaced <- extraPlaced + 1
- placedHere <- placedHere + 1
- x <- x + buildingWidth + 1
- // 组间空档:保持可控行距与组团边界。
- x <- x + 4 + int (nextCluster () % 3UL)
+ let mutable x = max lo (min (hi - 1) (lo + int (nextCluster () % 3UL)))
+ let tentative = ResizeArray<Building>()
+ let mutable guard = 0
+ while tentative.Count < desired && x <= hi - 1 && guard < 48 do
+ let widthRoll = int (nextCluster () % 10UL)
+ let buildingWidth = if widthRoll < 5 then 3 elif widthRoll < 8 then 2 else 4
+ let doorX = x + buildingWidth / 2
+ let doorY = if above then runY - 1 else runY + 1
+ let top = if above then doorY - 1 else doorY
+ let left = doorX - buildingWidth / 2
+ let clashes =
+ tentative
+ |> Seq.exists (fun b ->
+ left < b.Left + b.Width && b.Left < left + buildingWidth
+ && top < b.Top + b.Height && b.Top < top + 2)
+ if doorX <= hi && footprintFree left top buildingWidth && not (overlaps left top buildingWidth) && not clashes then
+ tentative.Add
+ { Left = left
+ Top = top
+ Width = buildingWidth
+ Height = 2
+ DoorX = doorX
+ DoorY = doorY }
+ x <- x + buildingWidth + 1
+ else
+ x <- x + 1
+ guard <- guard + 1
+ let connected (t: Building) =
+ (buildings |> List.exists (fun b -> nearFootprint b t))
+ || (tentative |> Seq.exists (fun o -> not (obj.ReferenceEquals(o, t)) && nearFootprint o t))
+ let keep = tentative |> Seq.filter connected |> Seq.toList
+ for building in keep do
+ buildings <- building :: buildings
+ addPath building.DoorX building.DoorY
+ keep.Length
+ // P30 基础岸屋配对:为每座尚孤立的基础民居在同行贴 1 格补一座,保证并入 >=2 座组团。
+ let baseBuildings = buildings
+ let sameBuilding (a: Building) (c: Building) =
+ a.Left = c.Left && a.Top = c.Top && a.Width = c.Width && a.DoorX = c.DoorX && a.DoorY = c.DoorY
+ for b in baseBuildings do
+ let isolated =
+ buildings
+ |> List.forall (fun o -> sameBuilding o b || not (nearFootprint o b))
+ if isolated then
+ let widthRoll = int (nextCluster () % 10UL)
+ let buildingWidth = if widthRoll < 5 then 3 elif widthRoll < 8 then 2 else 4
+ let candidates = [ b.Left + b.Width + 1; b.Left - 1 - buildingWidth ]
+ let mutable done_ = false
+ for left in candidates do
+ if not done_
+ && left >= 1
+ && left + buildingWidth - 1 <= w - 2
+ && footprintFree left b.Top buildingWidth
+ && not (overlaps left b.Top buildingWidth) then
+ let doorX = left + buildingWidth / 2
+ buildings <-
+ { Left = left
+ Top = b.Top
+ Width = buildingWidth
+ Height = 2
+ DoorX = doorX
+ DoorY = b.DoorY }
+ :: buildings
+ addPath doorX b.DoorY
+ done_ <- true
+ for (runY, runStart, runEnd) in horizontalRuns do
+ if extraPlaced < p.ExtraRiversideHouses then
+ let runLength = runEnd - runStart + 1
+ // 长路段容纳多个组团,短河岸路各一个。
+ let groupsForRun = max 1 (min 3 (runLength / 24))
+ for group in 0 .. groupsForRun - 1 do
+ if extraPlaced < p.ExtraRiversideHouses then
+ let lo = runStart + (group * runLength) / groupsForRun
+ let hi = runStart + ((group + 1) * runLength) / groupsForRun - 1
+ let desired = min (p.ExtraRiversideHouses - extraPlaced) (2 + int (nextCluster () % 4UL))
+ let placed = placeGroup runY lo hi desired
+ extraPlaced <- extraPlaced + placed
+
+ // 5d) P30 乡村空区填充:上部/下部空带按确定性网格铺田块(水田/菜畦走成熟 tile
+ // 素材,写入 4/5 号非水瓦片,仍可通行)、树丛(纯视觉)与少量散落农舍(门贴院落路)。
+ // 仅在 large 分支执行:默认图不进入、不消费任何额外 RNG;田块离水面 >=3 格,
+ // 避开路网/核心/民居,故不影响河道连续性与 BFS 可达性。
+ if p.ExtraRiversideHouses > 0 then
+ let mutable countrysideRng = p.ClusterSeed ^^^ 0xF1E1D5EEDUL
+ let nextCountry () =
+ let nextState, raw = splitmix countrysideRng
+ countrysideRng <- nextState
+ raw
+ // 水面垂直距离:河流整幅横贯,故只需按 y 计算;边界列留 99(不会被选中)。
+ let waterDistance = Array.create (w * h) 99
+ for river in riverBands do
+ for x in 1 .. w - 2 do
+ for y in 0 .. h - 1 do
+ let d =
+ if y < river.CenterY then river.CenterY - y
+ elif y > river.CenterY + river.Width - 1 then y - (river.CenterY + river.Width - 1)
+ else 0
+ let i = idx x y
+ if d < waterDistance.[i] then waterDistance.[i] <- d
+ let structureBlocked = Array.create (w * h) false
+ for y in core.MinY .. core.MaxY do
+ for x in core.MinX .. core.MaxX do
+ structureBlocked.[idx x y] <- true
+ for (px, py) in paths do
+ structureBlocked.[idx px py] <- true
+ let markBuilding (b: Building) =
+ for yy in b.Top .. b.Top + b.Height - 1 do
+ for xx in b.Left .. b.Left + b.Width - 1 do
+ if inMap xx yy then structureBlocked.[idx xx yy] <- true
+ for b in buildings do
+ markBuilding b
+ let freeGround (x: int) (y: int) (minWater: int) =
+ inMap x y
+ && tiles.[idx x y] = int GroundTile.Grass
+ && waterDistance.[idx x y] >= minWater
+ && not structureBlocked.[idx x y]
+ let cellW = 18
+ let cellH = 14
+ let tryField (cellX: int) (cellY: int) (roll: uint64) =
+ let fw = 6 + int ((roll >>> 8) % 9UL) // 6..14
+ let fh = 4 + int ((roll >>> 16) % 7UL) // 4..10
+ let maxOffX = max 0 (cellW - 4 - fw)
+ let maxOffY = max 0 (cellH - 4 - fh)
+ let left = cellX + 2 + int ((roll >>> 24) % uint64 (maxOffX + 1))
+ let top = cellY + 2 + int ((roll >>> 32) % uint64 (maxOffY + 1))
+ let isPaddy = (roll >>> 40) % 2UL = 0UL
+ let free =
+ left >= 1
+ && top >= 1
+ && left + fw - 1 <= w - 2
+ && top + fh - 1 <= h - 2
+ && [ for dy in 0 .. fh - 1 do
+ for dx in 0 .. fw - 1 -> (left + dx, top + dy) ]
+ |> List.forall (fun (x, y) -> freeGround x y 3)
+ if free then
+ let code = if isPaddy then int GroundTile.PaddyField else int GroundTile.VegetablePlot
+ for dy in 0 .. fh - 1 do
+ for dx in 0 .. fw - 1 do
+ tiles.[idx (left + dx) (top + dy)] <- code
+ true
+ else
+ false
+ let tryGrove (cellX: int) (cellY: int) (roll: uint64) =
+ let gw = 2 + int ((roll >>> 8) % 3UL) // 2..4
+ let gh = 1 + int ((roll >>> 12) % 2UL) // 1..2
+ let left = cellX + 2 + int ((roll >>> 16) % uint64 (max 1 (cellW - 4 - gw + 1)))
+ let top = cellY + 2 + int ((roll >>> 24) % uint64 (max 1 (cellH - 4 - gh + 1)))
+ let kind = if (roll >>> 32) % 2UL = 0UL then BambooClump else ShrubClump
+ let cells = [ for dy in 0 .. gh - 1 do for dx in 0 .. gw - 1 -> (left + dx, top + dy) ]
+ if cells |> List.forall (fun (x, y) -> freeGround x y 2) then
+ groves <- (cells |> List.map (fun (x, y) -> { Kind = kind; X = x; Y = y })) @ groves
+ true
+ else
+ false
+ let tryFarmhouse (cellX: int) (cellY: int) (roll: uint64) =
+ let fw = 2 + int ((roll >>> 8) % 2UL) // 2..3
+ let left = cellX + 2 + int ((roll >>> 12) % uint64 (max 1 (cellW - 4 - fw + 1)))
+ let top = cellY + 2 + int ((roll >>> 20) % uint64 (max 1 (cellH - 4 - 2 + 1)))
+ let doorX = left + fw / 2
+ let doorY = top + 1
+ let free =
+ left >= 1
+ && top >= 1
+ && left + fw - 1 <= w - 2
+ && top + 1 <= h - 2
+ && [ for dy in 0 .. 1 do
+ for dx in 0 .. fw - 1 -> (left + dx, top + dy) ]
+ |> List.forall (fun (x, y) -> freeGround x y 3)
+ if free then
+ farmhouses <-
+ { Left = left
+ Top = top
+ Width = fw
+ Height = 2
+ DoorX = doorX
+ DoorY = doorY }
+ :: farmhouses
+ addPath doorX doorY
+ // 屋后小菜畦 2 行(留在本网格内,避免与邻格重叠)。
+ let gardenFree =
+ top + 3 <= h - 2
+ && [ for dy in 0 .. 1 do
+ for dx in 0 .. fw - 1 -> (left + dx, top + 2 + dy) ]
+ |> List.forall (fun (x, y) -> freeGround x y 3)
+ if gardenFree then
+ for dy in 0 .. 1 do
+ for dx in 0 .. fw - 1 do
+ tiles.[idx (left + dx) (top + 2 + dy)] <- int GroundTile.VegetablePlot
+ true
+ else
+ false
+ let mutable cellY = 0
+ while cellY < h - 2 do
+ let mutable cellX = 0
+ while cellX < w - 2 do
+ let roll = nextCountry ()
+ let bucket = int (roll % 100UL)
+ if bucket < 58 then tryField cellX cellY roll |> ignore
+ elif bucket < 82 then tryGrove cellX cellY roll |> ignore
+ elif bucket < 84 then tryFarmhouse cellX cellY roll |> ignore
+ cellX <- cellX + cellW
+ cellY <- cellY + cellH
// 5c) P26:河岸装饰(护岸石垒/芦苇/垂柳),仅大图启用。纯视觉层,不写入任何瓦片,
// 故 64x48/256x192 的 tiles checksum 与默认世界完全不受影响;装饰只落在紧邻水面的
@@ -510,7 +742,7 @@ module MapGen =
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 doorsOk = (buildings @ farmhouses) |> 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
@@ -528,6 +760,9 @@ module MapGen =
Paths = paths
Buildings = buildings
Decorations = decorations
+ Farmhouses = farmhouses
+ Groves = groves
+ MainRoadCenter = roadCenter
ReachableTiles = reachableTiles
ReachabilityOk = spawnsOk && doorsOk && bridgesOk
BridgeCrossingsOk = crossingsOk }
@@ -567,6 +802,25 @@ module MapGen =
.Append(decorCode decoration.Kind).Append(':')
.Append(decoration.X).Append(':').Append(decoration.Y).Append(';')
|> ignore
+ builder.Append('|') |> ignore
+ for building in map.Farmhouses do
+ builder
+ .Append(building.Left).Append(':').Append(building.Top).Append(':')
+ .Append(building.Width).Append(':').Append(building.Height).Append(':')
+ .Append(building.DoorX).Append(':').Append(building.DoorY).Append(';')
+ |> ignore
+ builder.Append('|') |> ignore
+ let groveCode = function
+ | BambooClump -> 0
+ | ShrubClump -> 1
+ for grove in map.Groves do
+ builder
+ .Append(groveCode grove.Kind).Append(':')
+ .Append(grove.X).Append(':').Append(grove.Y).Append(';')
+ |> ignore
+ builder.Append('|') |> ignore
+ for y in map.MainRoadCenter do
+ builder.Append(y).Append(',') |> ignore
builder.ToString()
/// 可行走判定:非水,或位于桥面上(桥面仍标记为水但可通行)。