一、这份索引解决什么
卷一 48 节各自给了字段、公式、不变量与一份可跑脚本,但散在 16 章里。这份附录把规格字段汇成一张表、把机制整数公式汇成一张表、把四条不变量汇成一张表、把数据集与生成脚本汇成一张表,最后给出 S / P / W 三层的完整复现命令。全卷只读同一份真源 config/play_spec.json;本卷的净增量在**「可执行」——同一份规格被四处消费,产出逐字相同**的摘要。
字段与公式都以整数为准:违反「预算与阈值一律整数、随机一律自实现可播种 PRNG、config/ 禁浮点」三条纪律里的任何一条,跨语言对照必然崩。
二、卷内文件命名约定
全卷文件一律带 gd_mech_ / mech 前缀,与后续两卷(gd_play_ / gd_proto_)严格不串卷:
| 目录 | 命名 | 例 |
|---|---|---|
config/ | play_spec.json(唯一真源,只读)+ gd_mech_<NN>_<用途>.json | config/gd_mech_06_perturb.json |
sim/ | gd_mech_<NN>_<用途>.py | sim/gd_mech_03_trajectory.py |
core/src/ashdeck/ | 共享底座 types / rng / cards / combat / digest.ts(只读) | core/src/ashdeck/combat.ts |
core/test/ | gd_mech_<NN>_<用途>.test.ts | core/test/gd_mech_02_validate.test.ts |
web/ | build_mech<NN>.mjs / verify_mech<NN>.mjs | web/verify_mech05.mjs |
out/ | mech<NN>_<用途>.{json,csv,svg} 与 web/dist/mech<NN>.html、out/shots/*.png | out/mech03_trajectory.csv |
out/ 下的产物名以各节正文代码块头注释里的 file: 为准(下表给的是约定名与用途)。底座五个模块 types / rng / cards / combat / digest 全卷只读,各章只在它之上写自己那一节的脚本与断言。
三、玩法规格字段表(config/play_spec.json)
全卷唯一真源就是下面这份文件,字段全部为整数或字符串,无浮点:
{
"spec_version": 1,
"id": "ashdeck",
"name": "灰烬牌库",
"seed": 20261014,
"rng": {
"algo": "lehmer-minstd",
"mod": 2147483647,
"mul": 48271
},
"params": {
"playerHp": 60,
"enemyHp": 50,
"enemyAttack": 6,
"energyMax": 3,
"handSize": 5,
"turnLimit": 20,
"rewardGold": 12,
"punishHp": 4
},
"cards": [
{ "id": "strike", "cost": 1, "damage": 6, "block": 0, "count": 5, "rarity": "common" },
{ "id": "heavy", "cost": 2, "damage": 14, "block": 0, "count": 3, "rarity": "uncommon" },
{ "id": "jab", "cost": 0, "damage": 3, "block": 0, "count": 2, "rarity": "common" },
{ "id": "guard", "cost": 1, "damage": 0, "block": 5, "count": 2, "rarity": "common" }
],
"goals": {
"primary": "enemyHp<=0",
"secondary": "playerHp>0",
"limit": "turn<=turnLimit"
},
"invariants": [
"cardCount==deckSize",
"damageConserved",
"energyInRange",
"blockNonNegative"
],
"strategy": ["p0", "p0", "p0", "p0", "e", "p0", "p0", "p0", "p0", "e", "p0", "p0", "p0", "p0", "e", "p0", "p0", "p0", "p0", "e", "p0", "p0", "p0", "p0", "e"]
}
| 字段 | 类型 | 取值域 | 作用 | 在哪节 |
|---|---|---|---|---|
spec_version | int | ≥ 1 | 规格版本,驱动迁移 | 2.3 |
id | string | 非空 | 工程名(ashdeck) | 2.1 |
name | string | 非空 | 显示名(灰烬牌库) | 2.1 |
seed | int | 0 … rng.mod−1 | PRNG 种子 | 9.3 |
rng.algo | string | lehmer-minstd | 随机算法名(自实现) | 9.3 |
rng.mod | int | 2147483647 | Lehmer 模数 | 9.3 |
rng.mul | int | 48271 | Lehmer 乘数 | 9.3 |
params.playerHp | int | > 0 | 玩家初始生命 | 1.2 |
params.enemyHp | int | > 0 | 敌人初始生命(主目标) | 1.2 |
params.enemyAttack | int | ≥ 0 | 回合末敌人攻击力 | 1.2 |
params.energyMax | int | ≥ 0 | 每回合能量上限 | 1.2 |
params.handSize | int | ≥ 0 | 手牌上限 | 1.2 |
params.turnLimit | int | > 0 | 回合上限(超时判负) | 1.2 |
params.rewardGold | int | ≥ 0 | 胜利奖励金币 | 8.1 |
params.punishHp | int | ≥ 0 | 惩罚扣血 | 8.2 |
cards[].id | string | 非空 | 卡牌标识 | 1.1 |
cards[].cost | int | ≥ 0 | 打出耗能 | 1.2 |
cards[].damage | int | ≥ 0 | 伤害值 | 1.1 |
cards[].block | int | ≥ 0 | 格挡值 | 1.1 |
cards[].count | int | > 0 | 卡池张数 | 1.2 |
cards[].rarity | string | 非空 | 稀有度(随机结构用) | 9.1 |
goals.primary | string | 表达式 | 主目标(enemyHp<=0) | 1.2 |
goals.secondary | string | 表达式 | 次目标(playerHp>0) | 1.2 |
goals.limit | string | 表达式 | 约束(turn<=turnLimit) | 1.2 |
invariants[] | string[] | 四条 | 不变量清单 | 10.2 |
strategy[] | string[] | p<idx> / e | 基线命令序列 | 3.1 |
deck_size 不是字段,是派生量:Σ cards[].count = 5 + 3 + 2 + 2 = 12。
四、机制公式表(全整数)
| 量 | 公式 | 出处 |
|---|---|---|
| 牌库大小 | deck_size = Σ cards[].count | 3.2 |
| 能量收支 | 回合开始 energy = energyMax;出牌 energy -= cost;若 cost > energy 则跳过该牌 | 4.1 |
| 伤害结算 | d = min(card.damage, enemyHp);enemyHp -= d;dealt += d;enemyHp <= 0 → 胜 | 4.1 |
| 格挡吸收 | absorbed = min(block, enemyAttack);block -= absorbed;playerHp = max(0, playerHp - (enemyAttack - absorbed));回合末 block = 0 | 4.1 |
| Lehmer PRNG | state = state * 48271 % 2147483647 | 9.3 |
| Fisher-Yates 洗牌 | state = seed % MOD or 1;for i = n-1 … 1: state = rngNext(state); j = state % (i+1); swap(i, j) | 9.3 |
| 伤害守恒量 | dealt == params.enemyHp - enemyHp | 3.2 |
| 胜率千分比 | winPermille = wins * 1000 // total(整数地板除) | 6.1 / 14.2 |
| 相变临界宽度 | 序参量跨阈值(如胜率千分比 ≥ 500 掉到 ≤ 200)的参数区间宽度 width = hi - lo(整数格) | 6.2 |
| 策略空间熵 | H = -Σ p_i · log2 p_i,×1000 取整后为整数 | 14.2 |
随机序列可核对:种子 20261014 的前 5 个 PRNG 状态是 914347409, 1379866695, 1192438993, 1218440562, 62244266;shuffle([1..8], 42) 的结果是 [8, 2, 4, 3, 1, 6, 5, 7]——两语言同算法同种子必须同序。
五、不变量清单(四条)
| 不变量 | 含义 | 检验方法 | 在哪节 |
|---|---|---|---|
cardCount==deckSize | 牌数守恒 | 每步后 len(hand) + len(draw) + len(discard) == Σ count | 3.2 / 4.2 / 10.2 |
damageConserved | 伤害守恒 | dealt == params.enemyHp - enemyHp | 3.2 / 4.2 / 10.2 |
energyInRange | 能量有界 | 0 ≤ energy ≤ params.energyMax | 4.2 / 10.2 |
blockNonNegative | 格挡非负 | block ≥ 0 | 4.2 / 10.2 |
四条不变量在 S 层与 P 层各断言一次:S 层报守恒误差(整数,恒为 0),P 层由 invariantsHold(spec, snap) 判定,负例必须被判不成立(人为把 cardCount 改 11、energy 改超上限、block 改 −1,函数都返回 false)。
六、数据集索引
下表按「产物 / 生成脚本 / 在哪节」列出各节的 S 层与 W 层产物;out/ 下的确切文件名以该节 file: 头注释为准:
| 产物(约定名) | 生成脚本 | 在哪节 |
|---|---|---|
config/play_spec.json | 手写真源(只读) | 2.1 |
out/mech01_anatomy.json | sim/gd_mech_01_anatomy.py | 1.1 |
out/mech01_resources.csv | sim/gd_mech_01_resources.py | 1.2 |
out/mech03_trajectory.csv | sim/gd_mech_03_trajectory.py | 3.1 |
out/mech03_conservation.csv | sim/gd_mech_03_conservation.py | 3.2 |
out/mech03_readable.csv / out/mech03_hp.svg | sim/gd_mech_03_readable.py | 3.3 |
out/mech06_sensitivity.csv | sim/gd_mech_06_sensitivity.py | 6.1 |
out/mech06_phase.csv / out/mech06_phase.svg | sim/gd_mech_06_phase.py | 6.2 |
out/mech07_loop_gain.json | sim/gd_mech_07_loop_gain.py | 7.2 |
out/mech08_rhythm.csv | sim/gd_mech_08_rhythm.py | 8.1 |
out/mech08_frustration.json | sim/gd_mech_08_frustration.py | 8.2 |
out/mech09_variance.csv | sim/gd_mech_09_variance.py | 9.2 |
out/mech10_reachability.csv | sim/gd_mech_10_reachability.py | 10.1 |
out/mech12_coupling.json | sim/gd_mech_12_coupling.py | 12.1 |
out/mech14_entropy.json / out/mech14_spectrum.svg | sim/gd_mech_14_entropy.py | 14.2 |
out/mech15_digest.json | sim/gd_mech_15_digest.py | 15.1 |
out/mech16_assembly.json | sim/gd_mech_16_assembly.py | 16.1 |
web/dist/mech05.html | web/build_mech05.mjs | 5.1 |
out/shots/mech05_*.png | web/verify_mech05.mjs | 5.2 |
P 层不产出数据文件,产出的是 core/test/gd_mech_<NN>_*.test.ts 里的一组断言;W 层产出的是 web/dist/mech<NN>.html 与截图。
七、可运行性命令速查
S 层(Python,逐个跑,不要一次传多个文件):
# snippet: S 层用全路径解释器;shell 的 for 循环逐个执行,避免只跑第一个文件
cd "$HOME/.gdbook/ashdeck"
for f in sim/gd_mech_*.py; do /opt/homebrew/bin/python3 "$f" || exit 1; done
P 层(TypeScript,必须写全 glob;node --test <目录> 不发现 .ts):
# snippet: node --test 接收 glob 而非目录,必须加引号让 node 自己展开
cd "$HOME/.gdbook/ashdeck"
node --test 'core/test/gd_mech_*.test.ts'
W 层(Playwright 无头真玩,先 build 再 verify):
# snippet: 复用全局 Playwright 必须经 createRequire;verify 产出截图并断言 canvas 已绘制
cd "$HOME/.gdbook/ashdeck"
node web/build_mech05.mjs
NODE_PATH=/opt/homebrew/lib/node_modules node web/verify_mech05.mjs --page web/dist/mech05.html --shot out/shots/mech05.png
B 层(软告警,不硬失败):由各章 S 层脚本的扰动段产出 JSON,例如 config/gd_mech_06_perturb.json 由 sim/gd_mech_06_sensitivity.py 写出;无独立命令,数字来自本机实跑。
八、跨语言摘要的可验证点
「一份规格四处消费」的可验证落点是 digest:S 层 sim/ashlib.py 的 digest(snap) 与 P 层 core/src/ashdeck/digest.ts 的 digestSnap(s) 必须逐字一致(键按字母序、JSON 无空格分隔)。下面这段是 P 层摘要内核,全文 26 行,与工作区 core/src/ashdeck/digest.ts 逐字节一致:
// file: core/src/ashdeck/digest.ts
// tier: P
// verify: node --test 'core/test/gd_mech_base.test.ts'
//
// 跨语言摘要。键按字母序插入,与 sim/ashlib.py 的 digest(sort_keys=True,
// separators=(",", ":"))逐字一致——这是「三处消费同一份规格」的可验证点。
import type { Snapshot } from "./types.ts";
export function digestSnap(s: Snapshot): string {
const out: Record<string, unknown> = {
block: s.block,
cardCount: s.cardCount,
damageConserved: s.damageConserved,
dealt: s.dealt,
enemyHp: s.enemyHp,
energy: s.energy,
hand: s.hand,
over: s.over,
played: s.played,
playerHp: s.playerHp,
turn: s.turn,
won: s.won,
};
return JSON.stringify(out);
}
用规格自带的基线策略跑一局,两侧摘要都是下面这一行(实跑输出):
{"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}
判读:4 回合取胜,打出 9 张、累计 50 点伤害(恰好清空敌人 50 血),余能量 1,玩家剩 48 血。整数类量可以写「逐位一致」;感知类派生量按声明容差断言,正文不写「逐位相等」。
九、一键复现与排错顺序
- 先清槽:跑本卷前用
gd_extract.py --book 玩法机制与可执行规格 --restore覆盖out/blocks.json,否则读到的是上一卷的块。 - 结构门禁:
/opt/homebrew/bin/python3 precheck_gd.py --book 玩法机制与可执行规格 --prefix gd-mech- --expect 50,要求 FAIL 0 / WARN 0。 - 块一致性:
/opt/homebrew/bin/python3 check_blocks_gd.py --book 玩法机制与可执行规格,要求「不一致 0 / 缺文件 0」。 - 三层真跑:S 层
for循环逐个跑、P 层写全 glob、W 层 build + verify;任一exit != 0即停。 - 构建:所有写盘停止后单次、非 quiet、后台 timeout ≥ 1800000 跑
hugo --gc --minify,要求 exit 0 / 0 ERROR / 0 WARN / 0REF_NOT_FOUND。
排错顺序固定为规格 → S 层 → P 层 → W 层 → 构建:先确认 config/play_spec.json 无浮点、spec_version 与 id 正确,再看 S 层脚本 exit 0 且守恒误差为 0,接着 P 层 fail == 0,然后 W 层无头断言全绿,最后才谈构建与产物。digest 对不上时,先查两侧 JSON 分隔符是否都无空格(Python 需 separators=(",", ":")),再查键是否都按字母序。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。