附录 A 玩法规格字段表、机制公式与数据集索引

《游戏设计》卷一《玩法机制与可执行规格》附录:把全卷 16 章散落的玩法规格字段、机制整数公式、四条不变量与数据集汇成四张表,给出卷内文件命名约定与 S / P / W 三层完整复现命令,并附一段与工作区逐字节一致的跨语言摘要内核,作为「一份规格四处消费」的可验证落点。

一、这份索引解决什么

卷一 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>_<用途>.jsonconfig/gd_mech_06_perturb.json
sim/gd_mech_<NN>_<用途>.pysim/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.tscore/test/gd_mech_02_validate.test.ts
web/build_mech<NN>.mjs / verify_mech<NN>.mjsweb/verify_mech05.mjs
out/mech<NN>_<用途>.{json,csv,svg} 与 web/dist/mech<NN>.html、out/shots/*.pngout/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_versionint≥ 1规格版本,驱动迁移2.3
idstring非空工程名(ashdeck)2.1
namestring非空显示名(灰烬牌库)2.1
seedint0 … rng.mod−1PRNG 种子9.3
rng.algostringlehmer-minstd随机算法名(自实现)9.3
rng.modint2147483647Lehmer 模数9.3
rng.mulint48271Lehmer 乘数9.3
params.playerHpint> 0玩家初始生命1.2
params.enemyHpint> 0敌人初始生命(主目标)1.2
params.enemyAttackint≥ 0回合末敌人攻击力1.2
params.energyMaxint≥ 0每回合能量上限1.2
params.handSizeint≥ 0手牌上限1.2
params.turnLimitint> 0回合上限(超时判负)1.2
params.rewardGoldint≥ 0胜利奖励金币8.1
params.punishHpint≥ 0惩罚扣血8.2
cards[].idstring非空卡牌标识1.1
cards[].costint≥ 0打出耗能1.2
cards[].damageint≥ 0伤害值1.1
cards[].blockint≥ 0格挡值1.1
cards[].countint> 0卡池张数1.2
cards[].raritystring非空稀有度(随机结构用)9.1
goals.primarystring表达式主目标(enemyHp<=0)1.2
goals.secondarystring表达式次目标(playerHp>0)1.2
goals.limitstring表达式约束(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[].count3.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 = 04.1
Lehmer PRNGstate = state * 48271 % 21474836479.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 - enemyHp3.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) == Σ count3.2 / 4.2 / 10.2
damageConserved伤害守恒dealt == params.enemyHp - enemyHp3.2 / 4.2 / 10.2
energyInRange能量有界0 ≤ energy ≤ params.energyMax4.2 / 10.2
blockNonNegative格挡非负block ≥ 04.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.jsonsim/gd_mech_01_anatomy.py1.1
out/mech01_resources.csvsim/gd_mech_01_resources.py1.2
out/mech03_trajectory.csvsim/gd_mech_03_trajectory.py3.1
out/mech03_conservation.csvsim/gd_mech_03_conservation.py3.2
out/mech03_readable.csv / out/mech03_hp.svgsim/gd_mech_03_readable.py3.3
out/mech06_sensitivity.csvsim/gd_mech_06_sensitivity.py6.1
out/mech06_phase.csv / out/mech06_phase.svgsim/gd_mech_06_phase.py6.2
out/mech07_loop_gain.jsonsim/gd_mech_07_loop_gain.py7.2
out/mech08_rhythm.csvsim/gd_mech_08_rhythm.py8.1
out/mech08_frustration.jsonsim/gd_mech_08_frustration.py8.2
out/mech09_variance.csvsim/gd_mech_09_variance.py9.2
out/mech10_reachability.csvsim/gd_mech_10_reachability.py10.1
out/mech12_coupling.jsonsim/gd_mech_12_coupling.py12.1
out/mech14_entropy.json / out/mech14_spectrum.svgsim/gd_mech_14_entropy.py14.2
out/mech15_digest.jsonsim/gd_mech_15_digest.py15.1
out/mech16_assembly.jsonsim/gd_mech_16_assembly.py16.1
web/dist/mech05.htmlweb/build_mech05.mjs5.1
out/shots/mech05_*.pngweb/verify_mech05.mjs5.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 血。整数类量可以写「逐位一致」;感知类派生量按声明容差断言,正文不写「逐位相等」。

九、一键复现与排错顺序

  1. 先清槽:跑本卷前用 gd_extract.py --book 玩法机制与可执行规格 --restore 覆盖 out/blocks.json,否则读到的是上一卷的块。
  2. 结构门禁:/opt/homebrew/bin/python3 precheck_gd.py --book 玩法机制与可执行规格 --prefix gd-mech- --expect 50,要求 FAIL 0 / WARN 0。
  3. 块一致性:/opt/homebrew/bin/python3 check_blocks_gd.py --book 玩法机制与可执行规格,要求「不一致 0 / 缺文件 0」。
  4. 三层真跑:S 层 for 循环逐个跑、P 层写全 glob、W 层 build + verify;任一 exit != 0 即停。
  5. 构建:所有写盘停止后单次、非 quiet、后台 timeout ≥ 1800000 跑 hugo --gc --minify,要求 exit 0 / 0 ERROR / 0 WARN / 0 REF_NOT_FOUND。

排错顺序固定为规格 → S 层 → P 层 → W 层 → 构建:先确认 config/play_spec.json 无浮点、spec_version 与 id 正确,再看 S 层脚本 exit 0 且守恒误差为 0,接着 P 层 fail == 0,然后 W 层无头断言全绿,最后才谈构建与产物。digest 对不上时,先查两侧 JSON 分隔符是否都无空格(Python 需 separators=(",", ":")),再查键是否都按字母序。

继续阅读

探索更多技术文章

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

全部文章 返回首页

「gamedesign」更多文章

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