pebble/config/是这套书的唯一真源:Python 仿真、TypeScript 内核、浏览器原型都只读它。这一页把它讲清楚——有哪些字段、什么类型、什么范围、为什么这么定,并给出一份可机读的 schema 与一段真跑的校验脚本。
一、为什么配置要有纪律
「一份 config 三种消费」能成立的前提是三处算出同一个数字。如果 Python 和 TypeScript 对同一份配置算出不同的数,那「唯一真源」就是假的。跨语言逐位一致有三条硬规则:
- 禁浮点。增长率、系数一律写整数比(
levelBonusNum/levelBonusDen)或整数百分比(ratePct)。不要写1.15这种浮点——浮点幂运算在不同运行时末位可能不一致,取整后就会差 1。 - 幂运算在 TS 侧用
BigInt。115^19已经超过2^53,用普通number会丢精度;Python 侧用原生大整数就没这个问题。 - 除法一律整数地板除。Python
//,TypeScriptMath.floor(a / b)且 a、b 为整数;上取整用(a + b - 1) // b,不要用浮点ceil。
还有一条与随机数有关的纪律:随机数必须可播种。core/ 里禁用 Math.random(),一律用配置里的 seed 加自实现的整数 LCG/PCG。确定性回放是小游戏工程的核心要求——同一份种子加同一串输入,必须每次得到同一局结果,否则第 12 章的「原型回放」就没法做。
二、字段字典
config/economy.json
| 字段 | 类型 | 含义 | 取值范围 | 单位 |
|---|---|---|---|---|
economy.baseOutputPerMinute | int | 1 级每分钟基础产出 | 1 – 1000000 | 个/分钟 |
economy.levelBonusNum | int | 每级产出乘数的分子 | 100 – 400 | 整数比分子 |
economy.levelBonusDen | int | 每级产出乘数的分母 | 100 – 400 | 整数比分母 |
economy.maxLevel | int | 最高等级 | 1 – 999 | 级 |
retention.decayPctPerDay | int | 次日留存衰减比例 | 1 – 100 | 百分比/天 |
retention.horizonDays | int | 留存曲线天数 | 1 – 3650 | 天 |
levelBonusNum / levelBonusDen = 115 / 100 表示每级产出乘 1.15。等级 L 的产出是 (base × num^(L-1)) // den^(L-1),全程整数。用整数比而不是 1.15,是为了让 S 层与 P 层算出逐位相同的数。
config/ch00_budget.json
| 字段 | 类型 | 含义 | 取值(示例) | 单位 |
|---|---|---|---|---|
package.firstPackageBudgetKB | int | 首包体积上限 | 4096 | KB |
package.subpackageBudgetKB | int | 分包总体积上限 | 20480 | KB |
package.codeBudgetKB | int | 代码占用 | 512 | KB |
package.atlasAssetKB | int | 单张贴图平均体积 | 96 | KB |
package.audioAssetKB | int | 单段音频平均体积 | 48 | KB |
package.levelConfigKB | int | 单份关卡配置体积 | 6 | KB |
load.coldStartBudgetMs | int | 冷启动时间预算 | 3000 | 毫秒 |
load.perAssetLoadMs | int | 单资源平均加载耗时 | 12 | 毫秒 |
session.sessionTargetSec | int | 单局目标时长 | 180 | 秒 |
session.coreLoopSec | int | 单次核心循环时长 | 30 | 秒 |
session.feedbackEverySec | int | 正反馈间隔 | 15 | 秒 |
这份配置支撑序 里那段「约束怎么变成数字」的仿真。它把「首包多大、冷启动多快、单局多长」翻译成资源名额,全部是整数,符合第一节的纪律。
三、schema(D 层)
字段字典是给人看的,schema 是给机器看的。下面这份 D 层 schema 把每个字段的类型与范围写成可校验的结构,S 层脚本读它来校验真实配置。它没有注释语法,所以用块前 HTML 注释声明 file: 与 tier::
{
"schemaVersion": 1,
"rules": {
"noFloat": true,
"integerOnly": true,
"seededRandom": true,
"note": "config 里禁浮点;增长率写整数比,系数写整数百分比;随机数必须可播种"
},
"files": {
"economy.json": {
"economy": {
"baseOutputPerMinute": { "type": "int", "min": 1, "max": 1000000, "unit": "个/分钟", "desc": "1 级每分钟基础产出" },
"levelBonusNum": { "type": "int", "min": 100, "max": 400, "unit": "整数比分子", "desc": "每级产出乘数分子" },
"levelBonusDen": { "type": "int", "min": 100, "max": 400, "unit": "整数比分母", "desc": "每级产出乘数分母" },
"maxLevel": { "type": "int", "min": 1, "max": 999, "unit": "级", "desc": "最高等级" }
},
"retention": {
"decayPctPerDay": { "type": "int", "min": 1, "max": 100, "unit": "百分比/天", "desc": "次日留存衰减比例" },
"horizonDays": { "type": "int", "min": 1, "max": 3650, "unit": "天", "desc": "留存曲线天数" }
}
}
}
}
rules 段是给人读的纪律声明;files 段是可机读的字段规格——每个字段声明 type(目前只有 int)、min / max 范围、unit 单位与 desc 含义。嵌套的 economy / retention 是分组。
四、校验脚本(S 层)
下面这段 S 层脚本读 schema 校验真实配置,把第一节的三条纪律逐条检查出来:字段必须齐全、值必须是整数(浮点直接失败)、必须落在范围内、整数比必须为正。它真跑,失败即非零退出:
# file: sim/ch00_validate_config.py
# tier: S
# verify: /opt/homebrew/bin/python3 sim/ch00_validate_config.py --config config/ --schema config/ch00_schema.json
"""配置表校验:用 config/ch00_schema.json 校验 config/economy.json。
三条硬规则各对应一段检查:
1. 禁浮点 —— 任何数值字段若为 float 直接失败;
2. 必填与范围 —— schema 里声明的字段必须存在且落在 [min, max];
3. 整数比 —— levelBonusNum / levelBonusDen 必须都是正整数。
真实校验已有配置,失败即非零退出。
"""
from __future__ import annotations
import argparse
import json
import os
def load(path: str) -> dict:
with open(path, encoding="utf-8") as f:
return json.load(f)
def walk(schema_node: dict, data_node: dict, prefix: str, errs: list[str]) -> None:
for key, spec in schema_node.items():
if not isinstance(spec, dict) or "type" not in spec:
# 嵌套分组(如 economy / retention)
sub = data_node.get(key)
if not isinstance(sub, dict):
errs.append(f"缺分组 {prefix}{key}")
continue
walk(spec, sub, f"{prefix}{key}.", errs)
continue
if key not in data_node:
errs.append(f"缺字段 {prefix}{key}")
continue
val = data_node[key]
if isinstance(val, bool) or not isinstance(val, int):
errs.append(f"{prefix}{key} 非整数:{val!r}(config 禁浮点)")
continue
if val < spec["min"] or val > spec["max"]:
errs.append(f"{prefix}{key}={val} 越界 [{spec['min']}, {spec['max']}]")
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--config", default="config/")
ap.add_argument("--schema", default="config/ch00_schema.json")
args = ap.parse_args()
schema = load(args.schema)
errs: list[str] = []
for fname, groups in schema["files"].items():
path = os.path.join(args.config, fname)
if not os.path.isfile(path):
errs.append(f"配置文件不存在:{fname}")
continue
data = load(path)
walk(groups, data, f"{fname}:", errs)
# 规则三:整数比分子分母必须为正
eco = load(os.path.join(args.config, "economy.json")).get("economy", {})
for k in ("levelBonusNum", "levelBonusDen"):
if isinstance(eco.get(k), int) and eco[k] <= 0:
errs.append(f"economy.{k} 必须为正整数,实为 {eco[k]}")
if errs:
for e in errs:
print(f"FAIL {e}")
print(f"== 配置校验 FAIL {len(errs)}")
return 1
print("== 配置校验 PASS:字段齐全、全整数、在范围内、整数比为正")
return 0
if __name__ == "__main__":
raise SystemExit(main())
本机实跑(校验通过,economy.json 的 6 个字段全部合法):
== 配置校验 PASS:字段齐全、全整数、在范围内、整数比为正
五、怎么用它
改任何一份配置之后,跑一遍校验再跑仿真:
# snippet: 改配置后的标准动作
/opt/homebrew/bin/python3 sim/ch00_validate_config.py --config config/ --schema config/ch00_schema.json
/opt/homebrew/bin/python3 sim/ch00_constraint_budget.py --config config/ --out out/
如果校验报 非整数,说明你在 config 里写了浮点,请改成整数比或整数百分比;如果报 越界,说明某个值超出了 schema 声明的范围,要么改值,要么在 schema 里说明为什么放宽范围。schema 是设计的合同——它让「这份配置合不合法」变成一个可以自动回答的问题,而不是靠人肉检查。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。