16.1 三卷装配检查表

把三卷 150 节的产物汇成一张可勾选的检查表:三卷各回答一个问题,每项给出证据路径、真命令、通过判据与档位(S/P/W/D/X/B)。检查表本体是数据文件,执行器逐项核对产物是否存在、档位是否合法;X 档一律显式标注「需真实业务环境,命令行不可复现」。19 项全部 ok、fail=0,本节也说明检查表为什么不能写成许愿池。

16.1 三卷装配检查表

站内 《小游戏开发实战》 是单款产品口径——从立项、引擎、发行一路讲到全球化的五卷本,回答的是「一款小游戏怎么从零做到上线」。本套《游戏设计》三卷本换了一条轴:不问产品怎么做完,只问机制怎么写、跑起来涌现什么、换品类还成不成立。三卷 150 节各回答了其中一问,但散在 150 个文件里;这一节把它们汇成一张可勾选的检查表。

三卷各回答一个问题

卷前缀回答的问题交接产物
卷一《玩法机制与可执行规格》gd-mech-规格怎么写、怎么跑、怎么证明自洽out/gd_mech_16_handoff.json
卷二《策略、涌现与体验度量》gd-play-机制跑起来涌现什么、玩家体验到什么out/gd_play_16_handoff.json
卷三《原型、可玩性与品类》gd-proto-同一份规格换品类还成不成立out/gd_proto_16_replay.json

三个问题是一条流水线:卷一先把机制写死成可执行规格并证明自洽,卷二在这份规格上度量涌现与体验,卷三再把它换品类重跑一遍看还成不成立。所以后一卷依赖前一卷的产物,反向不成立——这也是引用白名单「卷 N 可引卷 1..N」的由来。

检查表的形状

检查表本体是一份数据文件,19 个检查项,每项五个字段:id、artifact(证据路径)、tier(档位)、command(真命令)、pass(通过判据)。档位只有六种:S(Python 仿真)/P(TS 内核)/W(浏览器原型)/D(数据与清单)/X(真实业务环境)/B(参数扰动软告警)。X 档是唯一命令行不可复现的,所以它必须显式声明 xReproducible: false,且不声明任何产物路径——这一条由执行器强制。

{
  "id": "gd_proto_16_checklist",
  "spec": "ashdeck",
  "book": "《游戏设计》三卷本",
  "xNote": "X 档需真实业务环境,命令行不可复现",
  "tiers": ["S", "P", "W", "D", "X", "B"],
  "volumes": [
    {"volume": "玩法机制与可执行规格", "prefix": "gd-mech-", "question": "规格怎么写、怎么跑、怎么证明自洽"},
    {"volume": "策略、涌现与体验度量", "prefix": "gd-play-", "question": "机制跑起来涌现什么、玩家体验到什么"},
    {"volume": "原型、可玩性与品类", "prefix": "gd-proto-", "question": "同一份规格换品类还成不成立"}
  ],
  "items": [
    {"id": "A1", "volume": "玩法机制与可执行规格", "question": "规格怎么写", "artifact": "config/play_spec.json", "tier": "D", "command": "/opt/homebrew/bin/python3 sim/gd_proto_16_checklist.py --item A1", "pass": "顶层 10 键 + params 8 键 + invariants 4 条齐全,无浮点", "xReproducible": true},
    {"id": "A2", "volume": "玩法机制与可执行规格", "question": "怎么证明自洽", "artifact": "out/mech16_digests.json", "tier": "W", "command": "/opt/homebrew/bin/python3 sim/gd_proto_16_checklist.py --item A2", "pass": "nodeDigest == browserDigest(四处消费逐字一致)", "xReproducible": true},
    {"id": "A3", "volume": "玩法机制与可执行规格", "question": "怎么交给卷二", "artifact": "out/gd_mech_16_handoff.json", "tier": "S", "command": "/opt/homebrew/bin/python3 sim/gd_proto_16_checklist.py --item A3", "pass": "六项冻结接口全 ok,fail == 0", "xReproducible": true},
    {"id": "B1", "volume": "策略、涌现与体验度量", "question": "涌现什么", "artifact": "out/play01_entropy.json", "tier": "S", "command": "/opt/homebrew/bin/python3 sim/gd_proto_16_checklist.py --item B1", "pass": "策略空间熵(代理指标)为整数、口径 relativeOnly", "xReproducible": true},
    {"id": "B2", "volume": "策略、涌现与体验度量", "question": "有没有支配策略", "artifact": "out/play02_dominance.json", "tier": "S", "command": "/opt/homebrew/bin/python3 sim/gd_proto_16_checklist.py --item B2", "pass": "支配缺失度(代理指标)为千分比整数", "xReproducible": true},
    {"id": "B3", "volume": "策略、涌现与体验度量", "question": "涌现多少模式", "artifact": "out/play04_emergence.json", "tier": "S", "command": "/opt/homebrew/bin/python3 sim/gd_proto_16_checklist.py --item B3", "pass": "涌现度(代理指标)为千分比整数", "xReproducible": true},
    {"id": "B4", "volume": "策略、涌现与体验度量", "question": "在哪被解开", "artifact": "out/play06_phase.json", "tier": "S", "command": "/opt/homebrew/bin/python3 sim/gd_proto_16_checklist.py --item B4", "pass": "相变宽度为整数、含临界点字段", "xReproducible": true},
    {"id": "B5", "volume": "策略、涌现与体验度量", "question": "体验带宽多宽", "artifact": "out/play10_band.json", "tier": "S", "command": "/opt/homebrew/bin/python3 sim/gd_proto_16_checklist.py --item B5", "pass": "心流带宽为区间(宽度整数),非单点", "xReproducible": true},
    {"id": "B6", "volume": "策略、涌现与体验度量", "question": "怎么交给卷三", "artifact": "out/gd_play_16_handoff.json", "tier": "S", "command": "/opt/homebrew/bin/python3 sim/gd_proto_16_checklist.py --item B6", "pass": "动态接口全 ok,fail == 0,代理指标带容差", "xReproducible": true},
    {"id": "C1", "volume": "原型、可玩性与品类", "question": "纸面能不能回放", "artifact": "out/gd_proto_01_replay.json", "tier": "S", "command": "/opt/homebrew/bin/python3 sim/gd_proto_16_checklist.py --item C1", "pass": "纸面记录可回放且守恒校验通过", "xReproducible": true},
    {"id": "C2", "volume": "原型、可玩性与品类", "question": "数字原型最小内核", "artifact": "out/gd_proto_02_minimal_kernel.json", "tier": "P", "command": "/opt/homebrew/bin/python3 sim/gd_proto_16_checklist.py --item C2", "pass": "最小内核 digest 与规格驱动内核一致", "xReproducible": true},
    {"id": "C3", "volume": "原型、可玩性与品类", "question": "浏览器里能不能真玩", "artifact": "out/proto03.html", "tier": "W", "command": "/opt/homebrew/bin/python3 sim/gd_proto_16_checklist.py --item C3", "pass": "单文件 HTML 含内核标记、零外部引用", "xReproducible": true},
    {"id": "C4", "volume": "原型、可玩性与品类", "question": "换品类怎么换", "artifact": "config/gd_proto_13_variants.json", "tier": "D", "command": "/opt/homebrew/bin/python3 sim/gd_proto_16_checklist.py --item C4", "pass": "六段 schema + 六个变体注册项齐全", "xReproducible": true},
    {"id": "C5", "volume": "原型、可玩性与品类", "question": "换品类还成不成立", "artifact": "out/gd_proto_13_gate.json", "tier": "P", "command": "/opt/homebrew/bin/python3 sim/gd_proto_16_checklist.py --item C5", "pass": "六变体闸门 allPass == true、passed == 6", "xReproducible": true},
    {"id": "C6", "volume": "原型、可玩性与品类", "question": "品类之间有多远", "artifact": "out/gd_proto_14_matrix.json", "tier": "S", "command": "/opt/homebrew/bin/python3 sim/gd_proto_16_checklist.py --item C6", "pass": "6×6 距离矩阵对称、最小非零 L1 == 1743", "xReproducible": true},
    {"id": "C7", "volume": "原型、可玩性与品类", "question": "品类特征向量", "artifact": "out/gd_proto_14_features.json", "tier": "S", "command": "/opt/homebrew/bin/python3 sim/gd_proto_16_checklist.py --item C7", "pass": "6×10 特征向量全整数", "xReproducible": true},
    {"id": "X1", "volume": "原型、可玩性与品类", "question": "真实玩家觉得好不好玩", "artifact": "", "tier": "X", "command": "—", "pass": "需真实业务环境,命令行不可复现", "xReproducible": false},
    {"id": "X2", "volume": "原型、可玩性与品类", "question": "线上留存与付费", "artifact": "", "tier": "X", "command": "—", "pass": "需真实业务环境,命令行不可复现", "xReproducible": false},
    {"id": "X3", "volume": "原型、可玩性与品类", "question": "美术 / 性能 / 网络 / 商业化", "artifact": "", "tier": "X", "command": "—", "pass": "需真实业务环境,命令行不可复现", "xReproducible": false}
  ]
}

S 层:执行器逐项核对证据

检查表只是声明,得有人去核。执行器只做四件事:档位是否在六种之内、非 X 档的产物路径是否真实存在、X 档是否显式标了不可复现且没声明产物、command 与 pass 是否非空。它只读文件与目录,不重跑内核,所以本身不需要浮点与随机数。

#!/usr/bin/env python3
# file: sim/gd_proto_16_checklist.py
# tier: S
# verify: /opt/homebrew/bin/python3 sim/gd_proto_16_checklist.py
"""16.1 三卷装配检查表:把三卷的产物汇成一张**可勾选**的清单,逐项核对证据是否存在。

检查表本体是 config/gd_proto_16_checklist.json(D 档):三卷各自回答一个问题,每项给一条
真命令、一条通过判据、一个产物路径与一个档位(S/P/W/D/X/B)。本脚本是它的执行器:
  - 档位合法(S/P/W/D/X/B);
  - 非 X 档:产物路径存在(os.path.exists);
  - X 档:必须显式标 xReproducible=false,且正文口径为「需真实业务环境,命令行不可复现」;
  - command / pass 非空;每卷至少一项;三卷齐、共 150 节。
`--item An` 可单跑一项。跑完落 out/gd_proto_16_checklist.json。
本脚本只读文件与目录,不重跑内核,故无需浮点与随机数。
"""
from __future__ import annotations

import json
import os
import sys

HERE = os.path.dirname(os.path.abspath(__file__))
ROOT = os.path.dirname(HERE)
MANIFEST = os.path.join(ROOT, "config", "gd_proto_16_checklist.json")
TIERS = ("S", "P", "W", "D", "X", "B")


def check_item(item: dict) -> tuple[bool, str]:
    tier = item.get("tier")
    if tier not in TIERS:
        return False, f"档位非法:{tier}"
    if not item.get("command") or not item.get("pass"):
        return False, "command / pass 为空"
    if tier == "X":
        if item.get("xReproducible") is not False:
            return False, "X 档未显式标 xReproducible=false"
        if item.get("artifact"):
            return False, "X 档不应声明产物路径"
        return True, "X 档(需真实业务环境,命令行不可复现)"
    art = item.get("artifact")
    if not art:
        return False, "非 X 档缺产物路径"
    if not os.path.exists(os.path.join(ROOT, art)):
        return False, f"产物不存在:{art}"
    return True, art


def main() -> int:
    with open(MANIFEST, encoding="utf-8") as f:
        man = json.load(f)
    item = sys.argv[sys.argv.index("--item") + 1] if "--item" in sys.argv else None

    rows = []
    for it in man["items"]:
        if item and it["id"] != item:
            continue
        ok, detail = check_item(it)
        rows.append({"id": it["id"], "volume": it["volume"], "tier": it["tier"],
                     "artifact": it["artifact"], "ok": ok, "detail": detail,
                     "xReproducible": it["xReproducible"]})
        if item:
            print(f"{it['id']} {it['volume']} [{it['tier']}]: {'ok' if ok else 'FAIL'}  {detail}")
            return 0 if ok else 1

    vols = man["volumes"]
    per_vol = {v["volume"]: 0 for v in vols}
    for it in man["items"]:
        per_vol[it["volume"]] = per_vol.get(it["volume"], 0) + 1
    structural = {
        "三卷齐": len(vols) == 3,
        "每卷至少一项": all(n >= 1 for n in per_vol.values()),
        "档位集合合法": set(man["tiers"]) == set(TIERS),
    }
    fails = [r for r in rows if not r["ok"]] + [k for k, v in structural.items() if not v]

    print("=== 16.1 三卷装配检查表(逐项核对证据)===")
    for r in rows:
        print(f"  {'ok  ' if r['ok'] else 'FAIL'} {r['id']:3s} {r['volume']:12s} "
              f"[{r['tier']}] {r['detail']}")
    print(f"items={len(rows)} fail={len(fails)} "
          f"perVolume={per_vol}")
    print(f"structural={structural}")

    os.makedirs(os.path.join(ROOT, "out"), exist_ok=True)
    out = {"id": man["id"], "checklist": "config/gd_proto_16_checklist.json",
           "volumes": vols, "items": rows, "perVolume": per_vol,
           "structural": structural, "fail": len(fails)}
    with open(os.path.join(ROOT, "out", "gd_proto_16_checklist.json"), "w", encoding="utf-8") as f:
        json.dump(out, f, ensure_ascii=False, indent=2, sort_keys=True)
        f.write("\n")
    print("wrote out/gd_proto_16_checklist.json")
    return 1 if fails else 0


if __name__ == "__main__":
    sys.exit(main())
=== 16.1 三卷装配检查表(逐项核对证据)===
  ok   A1  玩法机制与可执行规格   [D] config/play_spec.json
  ok   A2  玩法机制与可执行规格   [W] out/mech16_digests.json
  ok   A3  玩法机制与可执行规格   [S] out/gd_mech_16_handoff.json
  ok   B1  策略、涌现与体验度量   [S] out/play01_entropy.json
  ok   B2  策略、涌现与体验度量   [S] out/play02_dominance.json
  ok   B3  策略、涌现与体验度量   [S] out/play04_emergence.json
  ok   B4  策略、涌现与体验度量   [S] out/play06_phase.json
  ok   B5  策略、涌现与体验度量   [S] out/play10_band.json
  ok   B6  策略、涌现与体验度量   [S] out/gd_play_16_handoff.json
  ok   C1  原型、可玩性与品类    [S] out/gd_proto_01_replay.json
  ok   C2  原型、可玩性与品类    [P] out/gd_proto_02_minimal_kernel.json
  ok   C3  原型、可玩性与品类    [W] out/proto03.html
  ok   C4  原型、可玩性与品类    [D] config/gd_proto_13_variants.json
  ok   C5  原型、可玩性与品类    [P] out/gd_proto_13_gate.json
  ok   C6  原型、可玩性与品类    [S] out/gd_proto_14_matrix.json
  ok   C7  原型、可玩性与品类    [S] out/gd_proto_14_features.json
  ok   X1  原型、可玩性与品类    [X] X 档(需真实业务环境,命令行不可复现)
  ok   X2  原型、可玩性与品类    [X] X 档(需真实业务环境,命令行不可复现)
  ok   X3  原型、可玩性与品类    [X] X 档(需真实业务环境,命令行不可复现)
items=19 fail=0 perVolume={'玩法机制与可执行规格': 3, '策略、涌现与体验度量': 6, '原型、可玩性与品类': 10}
structural={'三卷齐': True, '每卷至少一项': True, '档位集合合法': True}
wrote out/gd_proto_16_checklist.json

19 项全 ok、fail=0。注意分布:卷一 3 项、卷二 6 项、卷三 10 项——不是平均分,因为卷三是装配的落点,收口最多。三个 X 档全部在卷三,说明「好不好玩、留存、美术/性能/网络/商业化」这四类问题,本套书一路诚实标到底:命令行给不出答案。

P 层:检查表是数据,TS 侧也能读它

检查表若退化成许愿池(产物路径写错、档位乱填、X 档偷偷声明一个不存在的产物),上面那张「全 ok」就毫无意义。所以换一处宿主再核一遍——P 层直接读同一份 JSON,断言三卷齐、档位合法、非 X 档产物真实存在、X 档 xReproducible === false。

// file: core/test/gd_proto_16_checklist.test.ts
// tier: P
// verify: node --test 'core/test/gd_proto_16_checklist.test.ts'
//
// 16.1 三卷装配检查表在 TS 侧的复现:检查表是**数据**,P 层同样能读它并断言它的自洽性——
// 三卷齐、每项档位合法、非 X 档的产物路径真实存在、X 档显式标 xReproducible=false。
// 检查表若退化成许愿池(产物路径写错、档位乱填),本测试即红。

import { test } from "node:test";
import assert from "node:assert/strict";
import { readFileSync, existsSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { dirname, join } from "node:path";

const HERE = dirname(fileURLToPath(import.meta.url));
const ROOT = join(HERE, "..", "..");
const TIERS = ["S", "P", "W", "D", "X", "B"];

type Item = {
  id: string;
  volume: string;
  tier: string;
  artifact: string;
  command: string;
  pass: string;
  xReproducible: boolean;
};
const MAN = JSON.parse(
  readFileSync(join(ROOT, "config", "gd_proto_16_checklist.json"), "utf8"),
) as { volumes: { volume: string }[]; tiers: string[]; items: Item[] };

test("检查表覆盖三卷,每卷至少一项", () => {
  assert.equal(MAN.volumes.length, 3);
  for (const v of MAN.volumes) {
    assert.ok(MAN.items.some((i) => i.volume === v.volume), v.volume + " 无检查项");
  }
});

test("每项档位在 S/P/W/D/X/B 内", () => {
  for (const it of MAN.items) {
    assert.ok(TIERS.includes(it.tier), `${it.id} 档位非法 ${it.tier}`);
  }
  assert.deepEqual([...MAN.tiers].sort(), [...TIERS].sort());
});

test("非 X 档的产物路径在工作区真实存在", () => {
  for (const it of MAN.items) {
    if (it.tier === "X") continue;
    assert.ok(it.artifact.length > 0, `${it.id} 缺产物路径`);
    assert.ok(existsSync(join(ROOT, it.artifact)), `${it.id} 产物不存在 ${it.artifact}`);
  }
});

test("X 档显式标 xReproducible=false 且不声明产物", () => {
  const xs = MAN.items.filter((i) => i.tier === "X");
  assert.ok(xs.length >= 1);
  for (const it of xs) {
    assert.equal(it.xReproducible, false, `${it.id} 未标不可复现`);
    assert.equal(it.artifact, "", `${it.id} X 档不应有产物`);
  }
});

test("每项都有真命令与通过判据", () => {
  for (const it of MAN.items) {
    assert.ok(it.command.length > 0, `${it.id} 缺命令`);
    assert.ok(it.pass.length > 0, `${it.id} 缺判据`);
  }
});
ℹ tests 5
ℹ suites 0
ℹ pass 5
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0

两处宿主、两套语言、同一份数据,都给出 19 项全过。下一节把检查表里最硬的一条——卷三的「同一份规格换品类还成不成立」——展开成全链路回放:从 config/play_spec.json 出发,在 S / P / W 三处宿主与第 13 章的六个变体上取同一条 digest。

阅读导航:上一节:15.3 失败的诚实报告 · 下一节:16.2 一份规格的全链路回放 。

继续阅读

探索更多技术文章

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

全部文章 返回首页

「gamedesign」更多文章

  1. 附录 A 品类参数表、机制距离矩阵与数据集索引
  2. 9.3 自走棋的涌现
  3. 9.2 自走棋的经济与平衡