生成种子地图

在部分生存游戏都会在开局通过生成 seed(种子) 来做地图动态生成, 该篇章就是说明其原理和作用

首先大部分种子值是并没有要求长度(不能为空), 更进一步来说就是只要是字符串就可以作为地图种子(只要能被哈希)

hash(哈希) 就是构建地图种子的核心关键, 通过字符串生成沙盒世界的 伪随机种子(Seed), 转化流程如下

  1. 把输入字符串全部转为 UTF-8 编码字节数组

  2. 使用哈希算法(FNV1a,MurmurHash3,MD5 等哈希算法)

  3. 截取哈希结果为 int64 长整型(正负数值)

如果对哈希效率有要求可以采用 MurmurHash3 算法, 而如果需要更偏向内部集成的通用性可以采用 MD5 算法

这就是标准生成种子数值的流程, 以 Python 脚本来实现就是如下生成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import hashlib

# int64有符号最大值
INT64_MAX = 9223372036854775807

# 1. 输入的自定义文本
text = "一段没什么意义的文字"

# 2. 生成 UTF-8 的字节数组
bytes = text.encode("utf-8")

# 3. 这里采用 MD5 算法计算得出哈希值, 把 128 位 md5 字节转为超大无符号整数
hash = hashlib.md5(bytes)
hash = int.from_bytes(hash.digest(), byteorder="big")

# 4. 最后取模转为合法 int64 正数种子
# 得出种子数值: 2981560909910880916
seed = hash % INT64_MAX
print(f"得出种子数值: {seed}")

相同输入文本将永远算出同一个 seed, 获取到地图随机种子值(2981560909910880916)

那么接下来的问题就是拿到该值有什么作用? 要怎么去使用这个数值?

哈希与噪音

拿到哈希值之后就需要用到另外的概念 哈希噪音(Hash Nosie), 按照网上说法来看

对网格坐标、像素 UV、区块 ID 做哈希运算, 将离散坐标映射到 0~1 均匀分布随机数, 替代传统噪声的预计算置换表

不需要直接看懂, 对于游戏开发来说只需要知道哈希噪音主要作用

  • 区块坐标作为哈希输入, 每次加载同一区块的哈希输出完全一致, 地形、植被随机分布不会错乱

  • 通过给大地图按照长宽高做切分区块并编号, 然后按照哈希值做种子随机生成确定区块ID是什么地形

简单概括来说, 其实你自己就可以在脑子模拟出来流程, 如下流程来思考即可

  1. 假设目前需要生成一张 100x100 的地图

  2. 按照 10x10 为分隔成不同区块ID(RegionID, 比如 [(0,1),(0,2)…] 的区域列表)

  3. 通过上面提出的全局哈希 seed 值, 再通过 权重算法 对区块赋予 海洋/浅滩/沙滩/平原/森林/草地 等地形类型

而其中提出的权重算法就是利用到 哈希噪音(Hash Nosie) 处理, 这里为什么不干脆直接用随机数来生成?

  • 不可复现: 普通随机数每次运行结果不同, 重进游戏同区块地形会变, 会导致没办法复现具体地形

  • 方便流处理: 启动游戏只需要加载玩家视野的区块, 后续玩家走到哪就拿区块坐标实时算出哈希噪声、生成地形

如果是动态生成的沙盒世界, 可以通过 输入坐标(x,z) + 全局种子 就可以输出 权重 决定是 平地、高山、深海 地形

这里通过 Python 来构建伪代码(实际上该代码可以直接运行), 方便直观看到效果

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
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import hashlib

# int64有符号最大值
INT64_MAX = 9223372036854775807


# 抽象的世界地图类
class World:
# 地图最大宽度
width: int

# 地图最大高度
height: int

# 初始化
def __init__(self, width, height):
self.width = width
self.height = height

# 按照 [[x,y],...] 切分区块
def explode(self, x: int, y: int) -> [int, int]:
size_x = int(self.width / x)
size_y = int(self.height / y)

# 切分区块 [ {x,y},..... ]
region = []
for i in range(size_x):
for j in range(size_y):
region.append((i, j))
return region

# 计算哈希噪音值
def nosie(self, region_x: int, region_y: int, seed: int) -> float:
# 拼接唯一标识字符串, 转化区块的哈希值
combine = f"({region_x},{region_y},{seed})"
bytes = combine.encode("utf-8")
hash = hashlib.md5(bytes)
hash = int.from_bytes(hash.digest(), byteorder="big")

# 将超大哈希整数映射到 0~1 浮点数
# 2 ** 128 是 2 的 128 次方, MD5 算出 128 位二进制摘要, 本质上将 md5 得出的指数就是归一化到 0.0~1.0 之中
# INT64_MAX 是 64 位有符号整数上限, 只能存 64 位信息, MD5 得出的数值会因为 64 位被截断导致噪音分布异常
# 所以通过除以 128 位二进制最大摘要值, 让噪音归一在 0.0~1.0 之中分配散列权重
return hash / (2 ** 128)

# 转化字符串
def __str__(self):
return f"width: {self.width}, height: {self.height}"


# 创建 100x100 的世界地图
world = World(100, 100)
print(f"world = {world}")

# 切分 10x10 区块, 其实就是切分出 (0,0)~(9,9) 的矩阵
region = world.explode(10, 10)
print(f"region = {region}")

# 生成对应种子哈希值
text = "一段没什么意义的文字"
bytes = text.encode("utf-8")
hash = hashlib.md5(bytes)
hash = int.from_bytes(hash.digest(), byteorder="big")
seed = hash % INT64_MAX
print(f"seed = {seed}")

# 获取区块的哈希噪音
print("==================== 噪音区块 ====================")
for (rx, ry) in region[:15]: # 只要打印前面15个区块信息, 太长不好看结果
noise = world.nosie(rx, ry, seed)
terrain = '死区'
if 0.00 <= noise < 0.25:
terrain = "海洋"
elif 0.25 <= noise < 0.35:
terrain = "沙滩"
elif 0.35 <= noise < 0.60:
terrain = "草地"
elif 0.60 <= noise < 1.00:
terrain = "森林"

print(f"region = ({rx},{ry}), noise = {noise}, terrain = {terrain}")

这段 Python 代码运行之后可以获得以下输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
world = width: 100, height: 100
region = [(0, 0), (0, 1), (0, 2), (0, 3), (0, 4), (0, 5), (0, 6), (0, 7), (0, 8), (0, 9), (1, 0), (1, 1), (1, 2), (1, 3), (1, 4), (1, 5), (1, 6), (1, 7), (1, 8), (1, 9), (2, 0), (2, 1), (2, 2), (2, 3), (2, 4), (2, 5), (2, 6), (2, 7), (2, 8), (2, 9), (3, 0), (3, 1), (3, 2), (3, 3), (3, 4), (3, 5), (3, 6), (3, 7), (3, 8), (3, 9), (4, 0), (4, 1), (4, 2), (4, 3), (4, 4), (4, 5), (4, 6), (4, 7), (4, 8), (4, 9), (5, 0), (5, 1), (5, 2), (5, 3), (5, 4), (5, 5), (5, 6), (5, 7), (5, 8), (5, 9), (6, 0), (6, 1), (6, 2), (6, 3), (6, 4), (6, 5), (6, 6), (6, 7), (6, 8), (6, 9), (7, 0), (7, 1), (7, 2), (7, 3), (7, 4), (7, 5), (7, 6), (7, 7), (7, 8), (7, 9), (8, 0), (8, 1), (8, 2), (8, 3), (8, 4), (8, 5), (8, 6), (8, 7), (8, 8), (8, 9), (9, 0), (9, 1), (9, 2), (9, 3), (9, 4), (9, 5), (9, 6), (9, 7), (9, 8), (9, 9)]
seed = 2981560909910880916
==================== 噪音区块 ====================
region = (0,0), noise = 0.32365443124394727, terrain = 沙滩
region = (0,1), noise = 0.02662270242078061, terrain = 海洋
region = (0,2), noise = 0.1563521286181385, terrain = 海洋
region = (0,3), noise = 0.3000059996901416, terrain = 沙滩
region = (0,4), noise = 0.7566015211223611, terrain = 森林
region = (0,5), noise = 0.24234854821564203, terrain = 海洋
region = (0,6), noise = 0.540772269001445, terrain = 草地
region = (0,7), noise = 0.8721267592189645, terrain = 森林
region = (0,8), noise = 0.6164583810054668, terrain = 森林
region = (0,9), noise = 0.5927730031338889, terrain = 草地
region = (1,0), noise = 0.11057403435311021, terrain = 海洋
region = (1,1), noise = 0.5141146939722326, terrain = 草地
region = (1,2), noise = 0.1074019978873469, terrain = 海洋
region = (1,3), noise = 0.6226038866573754, terrain = 森林
region = (1,4), noise = 0.3254825607361817, terrain = 沙滩

只要输入的哈希字符串保持一致, 那么生成地形的地形将永远不会变(要复现地形只需要拿到哈希字符串即可)

噪音算法

上面已经通过 均匀区间权重 实现了简单 2D 像素区块地图/Minecraft 类方块区块/简单 2D 策略地图 地形构建

这种算法实现极其简单且高效, 但是的缺陷也是十分明显: 地形完全随机, 区块之间地形无过渡

这种地形算法的弊端就是会出现相邻区块可能突然从海洋跳到森林(过渡效果突兀)

单纯文字可能没有什么概念, 所以这里按照刚才的 Python 说明这种情况, 这里微调下代码

1
2
3
4
5
6
7
8
9
10
11
12
# 修改之前的权重, 追加火山地形
# 火山地形 + 海岛地形
if 0.00 <= noise < 0.25:
terrain = "海洋"
elif 0.25 <= noise < 0.35:
# terrain = "沙滩"
terrain = "火山坑"
elif 0.35 <= noise < 0.60:
# terrain = "草地"
terrain = "火山"
elif 0.60 <= noise < 1.00:
terrain = "森林"

运行之后就会发现某个地形出现一些突兀的情况

1
2
3
4
5
6
region = (0,0), noise = 0.32365443124394727, terrain = 火山坑
region = (0,1), noise = 0.02662270242078061, terrain = 海洋
region = (0,2), noise = 0.1563521286181385, terrain = 海洋
region = (0,3), noise = 0.3000059996901416, terrain = 火山坑
region = (0,4), noise = 0.7566015211223611, terrain = 森林
region = (0,5), noise = 0.24234854821564203, terrain = 海洋

可以看到火山坑附加没有火山地形, 却突兀的形成了火山喷发之后冷却的形成的火山坑(看起来就有点反常识)

不过部分动态生成地图的游戏可能没有讲究这么多, 所以这种情况是可以被接受, 主要原因如下

  • 高效性能: 只需要单次哈希 + 简单数值判断, 无循环、无插值、无多层噪声叠加, 海量区块同时生成也不会卡顿

  • 维护成本低: 代码极简, 可以快速迭代修改地形权重

  • 而以下游戏玩法不在乎连续地形, 所以直接使用是没问题的

    • 像素回合制策略: 每个区块独立关卡格子, 不需要自然连续大陆
    • 小型挂机放置游戏: 地图仅做背景装饰, 玩家不会细看地形逻辑
    • 极简方块小游戏: 单区块为独立小岛, 区块之间天然隔离, 不存在相邻地形违和问题

但是如果你是打算构建生存建造和追求写实地理逻辑的地形, 那么就需要做更加细致的噪音算法构建

高级算法

上面的简单权重地形算法已经保证构建地形功能, 假设现在编写生存建造游戏, 那么就需要做些高级定制处理, 用到技术如下

  • Worley: 细胞噪音, 地块分割噪声, 把整张地图切割成不规则多边形地块(天然形成独立海岛、大陆分割、湖泊)

  • Simplex: 群系类型噪声, 输出范围 -1.0~1.0, 用于给每个分割好的地块分配环境类型

    • 得出数值 -1.0~-0.6: 沙漠
    • 得出数值 -0.6~-0.2: 草原
    • 得出数值 -0.2~0.3: 森林
    • 得出数值 0.3~0.7: 沼泽
    • 得出数值 0.7~1.0: 雪地

这种用到方法就是 多层噪音 合并构建, 通俗来讲构建流程就是以下顺序

  1. Worley 哈希噪音值首次会地区分配地形, 比如分配大区块为 海洋/陆地/湖泊(封闭小型陆地) 地貌轮廓

  2. Simplex 哈希噪音值二次分配地形群系, 比如按照区间来生成 沙漠/草原/森林/沼泽/雪地 等主要地形

  3. 最后在群系地块内部, 使用哈希噪声生成细节, 比如 树木、岩石、矿石、野生动物刷新点位

这里的海洋区域指代锁死水域, 也就是封闭地区不允许玩家通过(用于地图边界限制), 如果地图是火山类型可以替换成岩浆

按照地理知识逻辑生成的区块地形:

  • 大片连通陆地统一群系

  • 海洋只存在海岛小型陆地, 不会穿插大片沙漠森林

  • 雪地/冻土/岩浆之类成片分布, 不会零散单个地块出现

首先就是 Worley 地貌轮廓分隔处理, 这里的噪音算法就是之前常见的权重算法

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
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import hashlib

# int128 有符号最大值
INT128_MAX = 2 ** 128

# int64 有符号最大值
INT64_MAX = 9223372036854775807


# 基础哈希噪声 = 输出 0.0 ~ 1.0
def hash_noise(x: int, y: int, seed: int) -> float:
combine = f"({x},{y},{seed})"
bytes = combine.encode("utf-8")
hash = hashlib.md5(bytes).digest()
hash = int.from_bytes(hash, byteorder="big")
return hash / INT128_MAX


# Worley细胞噪声, [返回单元格ID,基础地貌类型]
def worley_noise(world_x: int, world_y: int, scale: int, seed: int) -> tuple[int, str]:
noise = hash_noise(world_x // scale, world_y // scale, seed)
cell_id = int(noise * 2000)
dist_val = hash_noise(world_x, world_y, seed)
# 地貌判定, 火山地图只需将"ocean"替换为"lava"
if dist_val < 0.28:
return cell_id, "ocean" # 封锁水域
elif dist_val < 0.36:
return cell_id, "lake" # 内陆湖泊
else:
return cell_id, "land" # 可通行陆地


# 测试生成 100x100 的世界地图
text = "一段没什么意义的文字"
bytes = text.encode("utf-8")
hash = hashlib.md5(bytes)
hash = int.from_bytes(hash.digest(), byteorder="big")
seed = hash % INT64_MAX
print(f"seed = {seed}")

# Worley 划分基础地貌, 这里假设 100x100 的世界地图
for x in range(100):
for y in range(100):
cell_id, terrain_type = worley_noise(x, y, 12, seed)

# 首次地貌轮廓形成之后就是确认是否为封锁水域/湖泊
# 如果是直接返回,跳过二次群系计算, 也就是不需要验证 simplex 算法生成地形
if terrain_type in ("ocean", "lake"):
region = {
"cell_id": cell_id,
"base_terrain": terrain_type,
"biome": None,
"resource": "封锁区域无资源"
}
else:
# todo: 构建 Simplex 算法
region = {
"cell_id": cell_id,
"base_terrain": terrain_type,
"resource": "准备通过 Simplex 算法分配地区资源"
}

print(f"pos = ({x},{y}), cell_id = {cell_id}, terrain_type = {terrain_type}, region = {region}")

注意: Worley 算法划分的坐标区块和最上面说得 Region 权重区块不是一个概念

也就是说最开始的 100x100 地图通过划分 10x10 划分 Region IDWorley 通过 scale 划分局域地貌 不是一个概念

这里的 worley_noise 内部 scale = 单元格缩放尺寸/单个 Worley 多边形地块的边长, 核心代码如下

1
2
3
# 这里传入 scale = 12, 代表地图上每 12×12 世界坐标方块将合并成同一个 Worley 细胞单元格
# world_x // 12 和 world_y // 12 是向下取整除法
noise = hash_noise(world_x // scale, world_y // scale, seed)

上面代码核心作用就以 12 个单位为一个单位, 在 (0~12) 坐标之中的都会获取到相同地貌区块ID(cell_id)

这种方式保证多个地图像素坐落的附近都是相同地貌, 也就形成可以形成独有的地形生态, 举例理解如下

  • 坐标 (0~11, 0~11) → 整除 12 后全部 = (0,0) → 同一个单元格

  • 坐标 (12~23, 0~11) → 整除 12 后全部 = (1,0) → 下一个独立单元格

对于 scale 的取值, 这是需要自定义来确定

  • scale 越大(比如 20~30)

    • 单个 Worley 地块尺寸更大
    • 整张地图地块数量变少
    • 大陆、海岛、湖泊的面积更大, 大板块地貌
  • scale 越小(比如 6~8)

    • 单个地块尺寸很小
    • 地图被切割成大量细碎小块
    • 生成超多小型湖泊和零散小岛, 从而出现地形破碎

建议取中间值 scale = 12, 然后微调这个值来确定最终地貌信息, 之后就是 simplex 算法处理

注意: 只有 terrain_type 为非封锁区域才允许走 simplex 算法处理

利用 Python 重新构建出整体功能, 我这边规划重写整体脚本:

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
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# 简易噪音地图生成器, 无需依赖其他第三方依赖库
import hashlib
import argparse
import json

# int128 有符号最大值
INT128_MAX = 2 ** 128

# int64 有符号最大值
INT64_MAX = 9223372036854775807


# 基础哈希噪声, 输出 0.0 ~ 1.0
def hash_noise(x: int, y: int, seed: int) -> float:
combine_str = f"{seed}_{x}_{y}"
data = combine_str.encode("utf-8")
md5_bytes = hashlib.md5(data).digest()
hash_int = int.from_bytes(md5_bytes, byteorder="big")
return hash_int / INT128_MAX


# Worley 细胞噪声, [返回单元格ID,基础地貌类型]
def worley_noise(world_x: int, world_y: int, scale: int, seed: int) -> tuple[int, str]:
noise = hash_noise(world_x // scale, world_y // scale, seed)
cell_id = int(noise * 2000)
dist_val = hash_noise(world_x, world_y, seed)

# 地貌判定, 火山地图只需将"ocean"替换为"lava"
if dist_val < 0.28:
return cell_id, "ocean" # 封锁水域
elif dist_val < 0.36:
return cell_id, "lake" # 内陆湖泊
else:
return cell_id, "land" # 可通行陆地


# Simplex 噪声映射至 -1.0~1.0
def simplex_noise(x: int, y: int, seed: int) -> float:
n = hash_noise(x, y, seed)
return n * 2.0 - 1.0


# Simplex 噪音值转化成匹配生物群系, 这部分生物群系表可以进行微调
def get_biome_by_simplex(s_val: float) -> str:
if -1.0 <= s_val < -0.6:
return "沙漠"
elif -0.6 <= s_val < -0.2:
return "草原"
elif -0.2 <= s_val < 0.3:
return "森林"
elif 0.3 <= s_val < 0.7:
return "沼泽"
else:
return "雪地"


# 确定最终的资源数值, 这部分资源表可以进行微调
def get_block_resource(x: int, y: int, seed: int, biome: str) -> str:
noise = hash_noise(x, y, seed)
if biome == "森林":
if noise > 0.65:
return "大树"
elif noise > 0.3:
return "灌木"
elif biome == "雪地":
if noise > 0.72:
return "铁矿"
elif biome == "沙漠":
if noise > 0.8:
return "金矿"
return "空地"


# 启动入口
if __name__ == "__main__":
# 创建参数解析器
parser = argparse.ArgumentParser(description="沙盒地图种子转换器, 输入任意字符串输出地图信息")
parser.add_argument("--text", type=str, help="自定义地图种子字符串, 支持中文、数字、英文")
parser.add_argument("--width", type=int, help="自定义地图宽度")
parser.add_argument("--height", type=int, help="自定义地图高度")
parser.add_argument("--scale", type=int, default=12, help="Worley 多边形地块的边长")
parser.add_argument("--output", type=str, default=None, help="输出 JSON 格式文件")

# 获取参数列表
args = parser.parse_args()

# 1. 构建全局种子值
bytes = args.text.encode("utf-8")
hash = hashlib.md5(bytes)
hash = int.from_bytes(hash.digest(), byteorder="big")
seed = hash % INT64_MAX
print(f"seed = {seed}")

# 2. 划分基础地貌
maps = [];
for world_x in range(args.width):
for world_y in range(args.height):
cell_id, terrain_type = worley_noise(world_x, world_y, args.scale, seed)

# 首次地貌轮廓形成之后就是确认是否为封锁水域/湖泊
if terrain_type in ("ocean", "lake"):
maps.append({
"x": world_x,
"y": world_y,
"scale": args.scale,
"cell_id": cell_id,
"base_terrain": terrain_type,
"biome": None,
"resource": "封锁区域无资源"
})
continue # 跳过后续逻辑

# 非封锁区域, 利用 simplex 二次噪音值获取统一生态群系
cell_size = args.scale
cell_center_x = cell_id * cell_size + cell_size // 2
cell_center_y = cell_id * cell_size + cell_size // 2
noise_val = simplex_noise(cell_center_x, cell_center_y, seed)
biome = get_biome_by_simplex(noise_val)

# 最终针对二次哈希噪音获取内部生态资源
resource = get_block_resource(world_x, world_y, seed, biome)
maps.append({
"x": world_x,
"y": world_y,
"scale": args.scale,
"cell_id": cell_id,
"base_terrain": terrain_type,
"biome": biome,
"resource": resource
})

# 3. 确认最后是否可以打印或者输出 JSON
if args.output is None:
for map in maps:
print(map)
else:
with open(args.output, "w") as f:
json.dump(maps, f, indent=8)

这里初步输出内容如下, 可以具体测试和改进下具体功能

1
2
3
4
5
6
7
8
9
10
{'x': 0, 'y': 0, 'scale': 12, 'cell_id': 1601, 'base_terrain': 'land', 'biome': '沼泽', 'resource': '空地'}
{'x': 0, 'y': 1, 'scale': 12, 'cell_id': 1601, 'base_terrain': 'land', 'biome': '沼泽', 'resource': '空地'}
{'x': 0, 'y': 2, 'scale': 12, 'cell_id': 1601, 'base_terrain': 'land', 'biome': '沼泽', 'resource': '空地'}
{'x': 0, 'y': 3, 'scale': 12, 'cell_id': 1601, 'base_terrain': 'land', 'biome': '沼泽', 'resource': '空地'}
{'x': 0, 'y': 4, 'scale': 12, 'cell_id': 1601, 'base_terrain': 'ocean', 'biome': None, 'resource': '封锁区域无资源'}
{'x': 0, 'y': 5, 'scale': 12, 'cell_id': 1601, 'base_terrain': 'land', 'biome': '沼泽', 'resource': '空地'}
{'x': 0, 'y': 6, 'scale': 12, 'cell_id': 1601, 'base_terrain': 'land', 'biome': '沼泽', 'resource': '空地'}
{'x': 0, 'y': 7, 'scale': 12, 'cell_id': 1601, 'base_terrain': 'ocean', 'biome': None, 'resource': '封锁区域无资源'}
{'x': 0, 'y': 8, 'scale': 12, 'cell_id': 1601, 'base_terrain': 'ocean', 'biome': None, 'resource': '封锁区域无资源'}
{'x': 0, 'y': 9, 'scale': 12, 'cell_id': 1601, 'base_terrain': 'land', 'biome': '沼泽', 'resource': '空地'}

内部其实需要修改的是 get_biome_by_simplex(获取地形再分配)get_block_resource(获取区块资源) - 可以用 excel 调整