summaryrefslogtreecommitdiff
path: root/src/LivingVillage.Desktop/MapGen.fs
blob: 4f59f8439d4deec6886e252edd228fff5eb17de5 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
namespace LivingVillage.Desktop

open LivingVillage.Kernel

/// P20 第一步:确定性大地图生成器的参数化纯函数片。
/// 单一入口 `generate`:seed + 尺寸 -> 瓦片网格(0 grass / 1 water / 2 stone / 3 peat)。
/// 组成:splitmix64 随机游走河道 + 2-3 octave value noise 地形 + 泊松式布点装饰,
/// 核心活动区先整平为草地,再用 BFS 校验 30 个 spawn 全部可达核心(不可达则确定性修整)。
/// 全部为纯函数:同参两次生成逐字节一致,不含时钟/随机源。
module MapGen =

    /// 地面码,与既有 ProceduralMap / 渲染映射保持一致。
    type GroundTile =
        | Grass = 0
        | Water = 1
        | Stone = 2
        | Peat = 3

    type Bounds =
        { MinX: int
          MinY: int
          MaxX: int
          MaxY: int }

    type Params =
        { Width: int
          Height: int
          Seed: uint64
          RiverCount: int
          RiverWidth: int
          CoreSide: int
          SpawnColumns: int
          SpawnRows: int }

    type Result =
        { Width: int
          Height: int
          Seed: uint64
          Tiles: int array
          Core: Bounds
          Spawns: (int * int) list
          ReachableTiles: int
          ReachabilityOk: bool }

    /// 默认仍是现状游戏尺寸 64x48,核心 14 格、5x6=30 个 spawn。
    let defaultParams (seed: uint64) : Params =
        { Width = 64
          Height = 48
          Seed = seed
          RiverCount = 2
          RiverWidth = 3
          CoreSide = 14
          SpawnColumns = 5
          SpawnRows = 6 }

    // ---- splitmix64 与 value noise(无外部依赖、无时钟) ----

    let private splitmix (state: uint64) : uint64 * uint64 =
        let nextState = state + 0x9E3779B97F4A7C15UL
        let mutable z = nextState
        z <- (z ^^^ (z >>> 30)) * 0xBF58476D1CE4E5B9UL
        z <- z ^^^ (z >>> 27)
        z <- z * 0x94D049BB133111EBUL
        z <- z ^^^ (z >>> 31)
        (nextState, z)

    let private hash2 (x: int) (y: int) (seed: uint64) : uint64 =
        let base_ = seed ^^^ ((uint64 x * 0x4D3B9UL) + (uint64 y * 0x1D5UL))
        snd (splitmix base_)

    let private valueNoise (fx: float32) (fy: float32) (seed: uint64) (scale: float32) : float32 =
        let xf = fx / scale
        let yf = fy / scale
        let x0 = int (floor xf)
        let y0 = int (floor yf)
        let tx = xf - float32 x0
        let ty = yf - float32 y0
        let smooth (t: float32) = t * t * (3.0f - 2.0f * t)
        let sx = smooth tx
        let sy = smooth ty
        let v00 = float32 (hash2 x0 y0 seed % 1000UL) / 1000.0f
        let v10 = float32 (hash2 (x0 + 1) y0 seed % 1000UL) / 1000.0f
        let v01 = float32 (hash2 x0 (y0 + 1) seed % 1000UL) / 1000.0f
        let v11 = float32 (hash2 (x0 + 1) (y0 + 1) seed % 1000UL) / 1000.0f
        (v00 * (1.0f - sx) + v10 * sx) * (1.0f - sy) + (v01 * (1.0f - sx) + v11 * sx) * sy

    /// 2-3 octave 叠加;特征尺度随地图宽度等比缩放,保证小图/大图观感一致。
    let private multiOctave (widthScale: float32) (x: int) (y: int) (seed: uint64) : float32 =
        let xf = float32 x
        let yf = float32 y
        let a = valueNoise xf yf seed (96.0f * widthScale)
        let b = valueNoise xf yf (seed + 1UL) (38.0f * widthScale)
        let c = valueNoise xf yf (seed + 2UL) (13.0f * widthScale)
        0.55f * a + 0.3f * b + 0.15f * c

    // ---- 生成 ----

    let private index (width: int) (x: int) (y: int) : int = y * width + x

    let tileAt (result: Result) (x: int) (y: int) : GroundTile =
        enum<GroundTile> result.Tiles.[index result.Width x y]

    /// 参数化生成。尺寸下限 8x8;核心区始终先整平为草地,再 BFS 校验可达性。
    let generate (p: Params) : Result =
        let w = max 8 p.Width
        let h = max 8 p.Height
        let tiles = Array.zeroCreate<int> (w * h) // 0 = Grass
        let idx x y = index w x y

        let side = max 6 (min (min w h) p.CoreSide)
        let cx = w / 2
        let cy = h / 2
        let core =
            { MinX = max 1 (cx - side / 2)
              MinY = max 1 (cy - side / 2)
              MaxX = min (w - 2) (cx + side / 2)
              MaxY = min (h - 2) (cy + side / 2) }
        let inCore x y =
            x >= core.MinX && x <= core.MaxX && y >= core.MinY && y <= core.MaxY

        // 1) 多八度噪声:低洼草地转泥炭(跳过核心区)。
        let widthScale = float32 w / 64.0f
        for y in 0 .. h - 1 do
            for x in 0 .. w - 1 do
                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

        // 3) 核心活动区整平为草地(生成器保证可通行)。
        for y in core.MinY .. core.MaxY do
            for x in core.MinX .. core.MaxX do
                tiles.[idx x y] <- int GroundTile.Grass

        // 4) 泊松式布点:最小间距随尺寸缩放,只在草地打石头装饰(跳过核心区)。
        let minDist = max 3 (w / 24)
        let minSq = minDist * minDist
        let attempts = max 100 ((w * h) / 16)
        let mutable placed: (int * int) list = []
        let mutable scatterRng = rng
        for _ in 1 .. attempts do
            let nextState, raw = splitmix scatterRng
            scatterRng <- nextState
            let x = int (raw % uint64 w)
            let y = int ((raw >>> 20) % uint64 h)
            let far =
                placed
                |> List.forall (fun (px, py) ->
                    let dx = px - x
                    let dy = py - y
                    dx * dx + dy * dy > minSq)
            if far && not (inCore x y) && tiles.[idx x y] = int GroundTile.Grass then
                tiles.[idx x y] <- int GroundTile.Stone
                placed <- (x, y) :: placed

        // 5) 30 个 spawn:核心区内的规则网格。
        let sc = max 2 p.SpawnColumns
        let sr = max 2 p.SpawnRows
        let spanX = max 1 (core.MaxX - core.MinX - 2)
        let spanY = max 1 (core.MaxY - core.MinY - 2)
        let spawns =
            [ for r in 0 .. sr - 1 do
                for c in 0 .. sc - 1 do
                    yield
                        (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
        let flood startX startY =
            let visited = Array.create (w * h) false
            let queue = System.Collections.Generic.Queue<int>()
            if walkable startX startY then
                let start = idx startX startY
                visited.[start] <- true
                queue.Enqueue start
            while queue.Count > 0 do
                let cur = queue.Dequeue()
                let x = cur % w
                let y = cur / w
                for dx, dy in [ (1, 0); (-1, 0); (0, 1); (0, -1) ] do
                    let nx = x + dx
                    let ny = y + dy
                    if nx >= 0 && nx < w && ny >= 0 && ny < h then
                        let ni = idx nx ny
                        if not visited.[ni] && walkable nx ny then
                            visited.[ni] <- true
                            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 reachableTiles = visited |> Array.filter id |> Array.length
        let reachabilityOk = spawns |> List.forall (fun (x, y) -> visited.[idx x y])

        { Width = w
          Height = h
          Seed = p.Seed
          Tiles = tiles
          Core = core
          Spawns = spawns
          ReachableTiles = reachableTiles
          ReachabilityOk = reachabilityOk }

    let generateWithSize (width: int) (height: int) (seed: uint64) : Result =
        generate { defaultParams seed with Width = width; Height = height }

    // ---- 视口裁剪(纯函数):只绘制可见瓦片,与 drawWorld 现有窗口一致 ----

    /// 返回含端点的可见瓦片范围 (x0, y0, x1, y1);空视口返回 (0,0,-1,-1)。
    let visibleTileRange (mapWidth: int) (mapHeight: int) (cameraX: int) (cameraY: int) (viewportWidth: int) (viewportHeight: int) : int * int * int * int =
        let x0 = max 0 (cameraX / Sim.tilePixels)
        let y0 = max 0 (cameraY / Sim.tilePixels)
        let x1 = min (mapWidth - 1) ((cameraX + viewportWidth) / Sim.tilePixels + 1)
        let y1 = min (mapHeight - 1) ((cameraY + viewportHeight) / Sim.tilePixels + 1)
        if mapWidth <= 0 || mapHeight <= 0 || x1 < x0 || y1 < y0 then (0, 0, -1, -1) else (x0, y0, x1, y1)

    let visibleTileCount (mapWidth: int) (mapHeight: int) (cameraX: int) (cameraY: int) (viewportWidth: int) (viewportHeight: int) : int =
        let x0, y0, x1, y1 = visibleTileRange mapWidth mapHeight cameraX cameraY viewportWidth viewportHeight
        if x1 < x0 || y1 < y0 then 0 else (x1 - x0 + 1) * (y1 - y0 + 1)