16.3 交付与后续
站内 《小游戏开发实战》
五卷本收官时给的是一份「上线清单」——渠道、买量、留存、出海,落点是一款产品真的上线。本套《游戏设计》三卷本收官给的不是上线清单,而是一份可复用的机制工具链:150 节全部围绕同一份 config/play_spec.json 展开,每节的可运行代码都落在同一套目录结构里,换个规格就能当模板用。这一节清点交付物、说明怎么用、划清能力的边界,并交代后续可以往哪走。
交付物:150 节加一条工具链
三卷共 150 节正文(16 章 × 3 节 + 目录 + 附录,每卷 50 节),加上卷三这最后一章。工具链不是「一堆散脚本」,而是七个各有分工的目录:
| 目录 | 放什么 | 档位 |
|---|---|---|
config/ | 规格与清单:play_spec.json、变体注册表、装配检查表、交付清单 | D |
sim/ | Python 仿真与核对脚本 | S |
core/src/ashdeck/ | 本体内核:类型 / PRNG / 卡牌 / 战斗 / digest | P |
core/genre/ | 五个品类内核:塔防 / 三消 / 平台跳跃 / 自走棋 / 放置 | P |
core/test/ | 内核测试 | P |
web/ | 浏览器原型的构建与验证 | W |
out/ | 全部产物:单文件 HTML、截图、核对用的 JSON | — |
交付清单本身也是数据,把「三卷齐、合计 150 节、工具链目录齐、各卷交接产物在、基线 digest 未漂移」这几条写成可核对的字段。下面这份清单的 baseDigest 就是那条贯穿三卷的字符串——从卷一第一次算出来到卷三收官,一个字节都没漂。
{
"id": "gd_proto_16_deliverable",
"spec": "ashdeck",
"book": "《游戏设计》三卷本",
"totalSections": 150,
"baseDigest": "{\"block\":0,\"cardCount\":12,\"damageConserved\":true,\"dealt\":50,\"enemyHp\":0,\"energy\":1,\"hand\":[\"strike\",\"jab\",\"heavy\"],\"over\":true,\"played\":9,\"playerHp\":48,\"turn\":4,\"won\":true}",
"volumes": [
{
"volume": "玩法机制与可执行规格",
"prefix": "gd-mech-",
"sections": 50,
"question": "规格怎么写、怎么跑、怎么证明自洽",
"handoff": "out/gd_mech_16_handoff.json"
},
{
"volume": "策略、涌现与体验度量",
"prefix": "gd-play-",
"sections": 50,
"question": "机制跑起来涌现什么、玩家体验到什么",
"handoff": "out/gd_play_16_handoff.json"
},
{
"volume": "原型、可玩性与品类",
"prefix": "gd-proto-",
"sections": 50,
"question": "同一份规格换品类还成不成立",
"handoff": "out/gd_proto_16_replay.json"
}
],
"toolchain": [
"config",
"sim",
"core/src/ashdeck",
"core/genre",
"core/test",
"web",
"out"
],
"howToUse": [
"从卷一 1.1 读起:先看机制如何被写成规格字段,再看 S / P / W 三处消费。",
"想复现某一步:读该节的 file: 代码块,按 verify: 里的命令在本机跑一遍。",
"想换品类:改 config/play_spec.json 的 rules / params 段,或参照 config/gd_proto_13_variants.json 加一个变体。",
"想当模板:把 config/play_spec.json 换成自己的规格,core/ 与 web/ 工具链不改即可复用。"
],
"limits": [
"机制侧可算:守恒、不变量、确定性回放、策略空间、涌现度、相变宽度、品类距离。",
"主观体验(好不好玩 / 心流是否真的发生)只能用代理指标做相对比较,禁绝对化。",
"美术 / 性能 / 网络 / 商业化需真实业务环境,命令行不可复现(X 档)。",
"真实玩家测试数据、线上留存与付费,本套一律不覆盖。"
],
"next": [
"把六品类变体扩到真实规则集(每个品类补到 30+ 条规则)并重跑闸门。",
"把机制距离矩阵与《风格与品类美术实战》的风格距离矩阵做联合聚类。",
"把代理指标接到真实玩家测试数据上,做机制量到体验量的回归。"
]
}
怎么用:三条入口
清单里 howToUse 那四条其实收敛成三条入口。照着读——从卷一 1.1 起,先看机制怎么被写成规格字段,再看它在 S / P / W 三处被消费;照着复现——每节的代码块都带 file: 路径与 verify: 命令,拷到本机就能跑;换成自己的规格——改 config/play_spec.json 的 rules 与 params 段,core/ 与 web/ 一行不动即可复用。核对脚本把这三条都验了一遍:
#!/usr/bin/env python3
# file: sim/gd_proto_16_handoff.py
# tier: S
# verify: /opt/homebrew/bin/python3 sim/gd_proto_16_handoff.py
"""16.3 交付与后续(S 层):三卷总收官的可核对形态。
三卷回答三个问题(规格怎么写 / 跑起来涌现什么 / 换品类还成不成立),交付物是同一份
config/play_spec.json 与同一条工具链。本脚本读 config/gd_proto_16_deliverable.json,逐条核对:
① 三卷齐、各 50 节、合计 150 节;
② 工具链目录全部存在(config / sim / core/src/ashdeck / core/genre / core/test / web / out);
③ 每卷的交接产物存在,且卷一卷二的冻结接口 fail == 0;
④ 交付清单里的基线 digest 与现算一致(规格没被偷偷改过);
⑤ howToUse / limits / next 三段非空。
落 out/gd_proto_16_handoff.json。全整数、无浮点、无 random。
"""
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_deliverable.json")
sys.path.insert(0, HERE)
import ashlib # noqa: E402
def load_json(path):
with open(path, encoding="utf-8") as f:
return json.load(f)
def main() -> int:
spec = ashlib.load_spec()
man = load_json(MANIFEST)
vols = man["volumes"]
base = ashlib.digest(ashlib.run_spec(spec))
checks = {
"三卷齐": len(vols) == 3,
"合计 150 节": sum(v["sections"] for v in vols) == man["totalSections"] == 150,
"工具链目录齐": all(os.path.isdir(os.path.join(ROOT, d)) for d in man["toolchain"]),
"各卷交接产物存在": all(os.path.exists(os.path.join(ROOT, v["handoff"])) for v in vols),
"基线 digest 未漂移": man["baseDigest"] == base,
"howToUse 非空": len(man["howToUse"]) >= 1,
"limits 非空": len(man["limits"]) >= 1,
"next 非空": len(man["next"]) >= 1,
}
# 卷一卷二的冻结接口必须仍 fail == 0
for v in vols[:2]:
h = load_json(os.path.join(ROOT, v["handoff"]))
checks[v["volume"] + " 接口 fail==0"] = h.get("fail", 1) == 0
bad = [k for k, val in checks.items() if not val]
print("=== 16.3 三卷总收官 · 交付物核对 ===")
for k, val in checks.items():
print(f" {'ok ' if val else 'FAIL'} {k}")
for v in vols:
print(f" {v['volume']:16s} {v['sections']} 节 {v['question']}")
print(f"totalSections={man['totalSections']} baseDigest={base}")
print(f"fail={len(bad)} {'(交付物齐备)' if not bad else bad}")
out = {"id": man["id"], "book": man["book"], "spec": spec["id"],
"volumes": vols, "totalSections": man["totalSections"],
"toolchain": man["toolchain"], "baseDigest": base,
"howToUse": man["howToUse"], "limits": man["limits"], "next": man["next"],
"checks": checks, "fail": len(bad)}
os.makedirs(os.path.join(ROOT, "out"), exist_ok=True)
with open(os.path.join(ROOT, "out", "gd_proto_16_handoff.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_handoff.json")
return 1 if bad else 0
if __name__ == "__main__":
raise SystemExit(main())
=== 16.3 三卷总收官 · 交付物核对 ===
ok 三卷齐
ok 合计 150 节
ok 工具链目录齐
ok 各卷交接产物存在
ok 基线 digest 未漂移
ok howToUse 非空
ok limits 非空
ok next 非空
ok 玩法机制与可执行规格 接口 fail==0
ok 策略、涌现与体验度量 接口 fail==0
玩法机制与可执行规格 50 节 规格怎么写、怎么跑、怎么证明自洽
策略、涌现与体验度量 50 节 机制跑起来涌现什么、玩家体验到什么
原型、可玩性与品类 50 节 同一份规格换品类还成不成立
totalSections=150 baseDigest={"block":0,"cardCount":12,"damageConserved":true,"dealt":50,"enemyHp":0,"energy":1,"hand":["strike","jab","heavy"],"over":true,"played":9,"playerHp":48,"turn":4,"won":true}
fail=0 (交付物齐备)
wrote out/gd_proto_16_handoff.json
十项全 ok、fail=0。卷一卷二两份交接产物的 fail 仍为 0,意味着后卷没有偷偷改动前卷冻结的接口——这是「后卷依赖前卷」这条白名单规则的运行期证据。
边界:机制侧可算,其余需真实环境
limits 那四条必须原样摆出来,因为它们划的是能力的边界,不是免责声明。机制侧真能算:守恒、不变量、确定性回放、策略空间熵、涌现度、相变宽度、品类距离——这些都有整数口径、可复现。但主观体验(好不好玩、心流是否真的发生)只能用代理指标做相对比较,禁绝对化:本套从不说「提升 N 倍」,只说「A 规格的带宽比 B 宽」。美术、性能、网络、商业化四类问题需要真实业务环境,命令行不可复现,一律进 X 档;真实玩家测试数据、线上留存与付费,本套一律不覆盖。
// file: core/test/gd_proto_16_handoff.test.ts
// tier: P
// verify: node --test 'core/test/gd_proto_16_handoff.test.ts'
//
// 16.3 三卷总收官在 TS 侧的复现:交付清单是**数据**,P 层同样能读它并断言三卷齐、150 节、
// 工具链目录存在、基线 digest 未漂移、各卷交接产物存在。清单若与工作区真值脱节,本测试即红。
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";
import { runSpec } from "../src/ashdeck/combat.ts";
import { digestSnap } from "../src/ashdeck/digest.ts";
import type { Spec } from "../src/ashdeck/types.ts";
const HERE = dirname(fileURLToPath(import.meta.url));
const ROOT = join(HERE, "..", "..");
const SPEC: Spec = JSON.parse(readFileSync(join(ROOT, "config", "play_spec.json"), "utf8"));
const MAN = JSON.parse(
readFileSync(join(ROOT, "config", "gd_proto_16_deliverable.json"), "utf8"),
) as {
totalSections: number;
baseDigest: string;
toolchain: string[];
howToUse: string[];
limits: string[];
next: string[];
volumes: { volume: string; prefix: string; sections: number; handoff: string }[];
};
test("三卷齐、各 50 节、合计 150 节", () => {
assert.equal(MAN.volumes.length, 3);
assert.equal(MAN.totalSections, 150);
assert.equal(MAN.volumes.reduce((a, v) => a + v.sections, 0), 150);
assert.deepEqual(MAN.volumes.map((v) => v.prefix), ["gd-mech-", "gd-play-", "gd-proto-"]);
});
test("工具链目录全部存在", () => {
for (const d of MAN.toolchain) {
assert.ok(existsSync(join(ROOT, d)), "缺目录 " + d);
}
});
test("各卷交接产物存在", () => {
for (const v of MAN.volumes) {
assert.ok(existsSync(join(ROOT, v.handoff)), v.volume + " 缺 " + v.handoff);
}
});
test("交付清单的基线 digest 与现算一致(规格未漂移)", () => {
assert.equal(MAN.baseDigest, digestSnap(runSpec(SPEC)));
});
test("howToUse / limits / next 三段非空", () => {
assert.ok(MAN.howToUse.length >= 1);
assert.ok(MAN.limits.length >= 1);
assert.ok(MAN.next.length >= 1);
});
ℹ tests 5
ℹ pass 5
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
后续:三个方向
next 那三条是顺着能力边界往外推,不是许愿。第一,把六品类变体从「骨架规格」扩到真实规则集(每个品类补到 30+ 条规则)再重跑闸门——现在每份变体只写了几条核心规则,够证明结构同构,不够当成品。第二,把本节工具链算出的机制距离矩阵,与 《游戏美术与技术美术》
那套风格距离矩阵做联合聚类——机制近且风格近的品类,可能是同一批玩家的重叠区。第三,把代理指标接到真实玩家测试数据上,做机制量到体验量的回归;这一步就跨出命令行能覆盖的范围了,需要真实业务环境。
三卷的三个问题在此闭合
卷一问「规格怎么写、怎么跑、怎么证明自洽」,交付的是一份可执行规格 + 三处消费的同一个 digest。卷二问「机制跑起来涌现什么、玩家体验到什么」,交付的是一层代理指标 + 相变与心流的可算形态。卷三问「同一份规格换品类还成不成立」,交付的是六品类实例 + 一道 6/6 闸门 + 一张距离矩阵。三个问题共用一份 config/play_spec.json、一条跨语言 digest、一套目录结构——所以它们不是三个独立专题,是一条流水线。换掉这份规格,整条流水线仍然成立;这正是这套工具链想留给读者的东西:机制可以被写成可跑的规格,规格可以被换品类重跑,而每一次重跑都留下可核对的证据。
阅读导航:上一节:16.2 一份规格的全链路回放 · 下一节:回到目录 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。