附录 B 配置表字段字典与 schema

pebble/config/ 的字段字典与 schema:economy.json 与 ch00_budget.json 每个字段的类型、含义与取值范围,一份可机读的 schema(D 层),以及一段真跑的校验脚本(S 层)。核心纪律是 config 里禁浮点——增长率写整数比、系数写整数百分比,随机数必须可播种。

pebble/config/ 是这套书的唯一真源:Python 仿真、TypeScript 内核、浏览器原型都只读它。这一页把它讲清楚——有哪些字段、什么类型、什么范围、为什么这么定,并给出一份可机读的 schema 与一段真跑的校验脚本。

一、为什么配置要有纪律

「一份 config 三种消费」能成立的前提是三处算出同一个数字。如果 Python 和 TypeScript 对同一份配置算出不同的数,那「唯一真源」就是假的。跨语言逐位一致有三条硬规则:

  1. 禁浮点。增长率、系数一律写整数比(levelBonusNum / levelBonusDen)或整数百分比(ratePct)。不要写 1.15 这种浮点——浮点幂运算在不同运行时末位可能不一致,取整后就会差 1。
  2. 幂运算在 TS 侧用 BigInt。115^19 已经超过 2^53,用普通 number 会丢精度;Python 侧用原生大整数就没这个问题。
  3. 除法一律整数地板除。Python //,TypeScript Math.floor(a / b) 且 a、b 为整数;上取整用 (a + b - 1) // b,不要用浮点 ceil。

还有一条与随机数有关的纪律:随机数必须可播种。core/ 里禁用 Math.random(),一律用配置里的 seed 加自实现的整数 LCG/PCG。确定性回放是小游戏工程的核心要求——同一份种子加同一串输入,必须每次得到同一局结果,否则第 12 章的「原型回放」就没法做。

二、字段字典

config/economy.json

字段类型含义取值范围单位
economy.baseOutputPerMinuteint1 级每分钟基础产出1 – 1000000个/分钟
economy.levelBonusNumint每级产出乘数的分子100 – 400整数比分子
economy.levelBonusDenint每级产出乘数的分母100 – 400整数比分母
economy.maxLevelint最高等级1 – 999级
retention.decayPctPerDayint次日留存衰减比例1 – 100百分比/天
retention.horizonDaysint留存曲线天数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.firstPackageBudgetKBint首包体积上限4096KB
package.subpackageBudgetKBint分包总体积上限20480KB
package.codeBudgetKBint代码占用512KB
package.atlasAssetKBint单张贴图平均体积96KB
package.audioAssetKBint单段音频平均体积48KB
package.levelConfigKBint单份关卡配置体积6KB
load.coldStartBudgetMsint冷启动时间预算3000毫秒
load.perAssetLoadMsint单资源平均加载耗时12毫秒
session.sessionTargetSecint单局目标时长180秒
session.coreLoopSecint单次核心循环时长30秒
session.feedbackEverySecint正反馈间隔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 是设计的合同——它让「这份配置合不合法」变成一个可以自动回答的问题,而不是靠人肉检查。

环境与全部实跑命令见附录 A 。回到目录页 。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「minigame」更多文章

  1. 附录 F:全书公式、参数与数据集索引
  2. 9.3 同意管理与数据出境
  3. 9.2 COPPA 与儿童向游戏的限制