15.3 交付物验收与下一卷衔接

交付不是把文档发出去,而是逐项核对能不能被接住。本节把评审、装配、交付三份配置串成一次总验收,覆盖结构验收与内容验收两条线,用一段 S 层脚本产出可读的验收报告,并断言卷一五十篇的文件名与 slug 白名单一致,最后把五个衔接点登记好,把接力棒递给卷二。

本节目标:把评审、装配、交付三份配置串成一次端到端总验收,产出可读的验收报告,并把卷一的结论交接给卷二。

和本章前两节一样,站内那批 PRD 原型文档 是单品的可交互原型口径;本节收束的是卷一前十四章的产出——把它装配成一份可评审、可验收、可交付的设计案,并在交付前做最后一次逐项核对。

前两节分别解决了「评审要什么」和「设计案怎么摆」。这一节回答最后一个问题:这份设计案怎么算「交付合格」。交付不是把文档打包发出去,而是逐项核对——结构对不对、材料齐不齐、命名一致不一致——都能被一个脚本回答。

15.3 交付物验收与下一卷衔接

15.3.1 验收是交付前的最后一次核对

交付失败往往不是内容不行,而是接不住:开发拿到设计案发现少了一章、引用了一个不存在的文件名、或者验收标准各说各话。验收要做的是把「接得住」变成可核对的项,它有两条线:

  • 结构验收:卷一的五十篇齐不齐,文件名与 slug 是否严格一致,命名规则有没有越界。
  • 内容验收:评审材料是否三件套齐备、设计案装配是否每章有产出物、白名单与装配清单是否交叉一致。

这两条线合起来,就是一份可交给下一位接手者的验收报告。它的价值不在「证明我做完了」,而在「证明下一个人能无缝接上」。

15.3.2 验收基线:白名单与验收项

下面这份是 D 层配置:它硬编码了卷一全部五十篇的 slug 白名单,登记了三条验收项(A1 评审材料、A2 设计案装配、A3 交付白名单),以及五个卷二衔接点。白名单写死在配置里,是刻意的——它是「文件名 = slug」这条规则的唯一基准,交付时拿实际文件名去对,多一个少一个立刻暴露。

{
  "version": 1,
  "game": "merge-island",
  "note": "卷一交付验收基线(D 层)。硬编码卷一 50 篇 slug 白名单,作为交付前「文件名 = slug」的断言基准;另登记三份配置的验收项与卷二衔接点。",
  "volume": "市场品类与产品设计",
  "prefix": "mg-design-",
  "expectedTotal": 50,
  "expectedSections": 45,
  "whitelist50": [
    "mg-design-toc", "mg-design-intro", "mg-design-preface", "mg-design-appendix-a", "mg-design-appendix-b",
    "mg-design-01-1-platform-landscape", "mg-design-01-2-distribution-mechanism", "mg-design-01-3-market-size-growth",
    "mg-design-02-1-rank-structure-genre-share", "mg-design-02-2-competitor-teardown-method", "mg-design-02-3-differentiation-opportunity",
    "mg-design-03-1-genre-taxonomy", "mg-design-03-2-genre-business-matrix", "mg-design-03-3-genre-scoring-sensitivity",
    "mg-design-04-1-package-size-budget", "mg-design-04-2-first-load-cold-start", "mg-design-04-3-platform-capability-review",
    "mg-design-05-1-user-persona-scenario", "mg-design-05-2-motivation-payer-tier", "mg-design-05-3-lifecycle-retention-structure",
    "mg-design-06-1-core-loop-breakdown", "mg-design-06-2-loop-duration-feedback", "mg-design-06-3-minimal-playable-prototype",
    "mg-design-07-1-level-structure", "mg-design-07-2-difficulty-flow-curve", "mg-design-07-3-level-calibration-generation",
    "mg-design-08-1-progression-curve", "mg-design-08-2-progress-one-more-run", "mg-design-08-3-offline-earnings-return",
    "mg-design-09-1-social-surface", "mg-design-09-2-viral-k-factor", "mg-design-09-3-social-pressure-protection",
    "mg-design-10-1-iaa-ad-format-placement", "mg-design-10-2-iap-hybrid-structure", "mg-design-10-3-monetization-experience-balance",
    "mg-design-11-1-one-pager-greenlight-doc", "mg-design-11-2-budget-schedule-estimate", "mg-design-11-3-greenlight-stop-loss",
    "mg-design-12-1-prototype-scope-goal", "mg-design-12-2-playability-metric-test", "mg-design-12-3-prototype-iteration-decision",
    "mg-design-13-1-revenue-dau-arpdau", "mg-design-13-2-hybrid-revenue-structure", "mg-design-13-3-revenue-forecast-scenario",
    "mg-design-14-1-staffing-effort-estimate", "mg-design-14-2-outsourcing-asset-cost", "mg-design-14-3-milestone-risk-buffer",
    "mg-design-15-1-review-material-process", "mg-design-15-2-design-doc-assembly", "mg-design-15-3-deliverable-acceptance-handoff"
  ],
  "checks": [
    {"id": "A1", "name": "评审材料齐备", "source": "ch15_review", "rule": "每项材料均有 owner / dueMilestone / passCriteria,且 owner 在角色表内"},
    {"id": "A2", "name": "设计案装配完整", "source": "ch15_design_doc", "rule": "15 章均有产出物,45 个节 slug 命名与归属章号一致"},
    {"id": "A3", "name": "交付清单与白名单一致", "source": "ch15_acceptance", "rule": "50 篇 slug 数量与命名规则一致,无重复、无越界"}
  ],
  "handoff": {
    "nextVolume": "引擎实现与体验打磨",
    "nextPrefix": "mg-engine-",
    "note": "卷二尚未落盘,此处只登记衔接点标题,不写跨卷 relref",
    "entryPoints": [
      {"from": "设计案 §2 平台约束页", "to": "卷二 1.1 引擎与框架选型", "carry": "包体预算与首屏约束作为选型评分的硬约束"},
      {"from": "设计案 §3 玩法页", "to": "卷二 5.1 固定步长与确定性", "carry": "核心循环时长与反馈密度作为循环节奏输入"},
      {"from": "设计案 §5 原型页", "to": "卷二 10.1 平台 API 抽象", "carry": "最小可玩原型的宿主依赖作为抽象层清单"},
      {"from": "设计案 §5 资源页", "to": "卷二 2.2 分包与资源管线", "carry": "外包素材规格与图集粒度作为打包输入"},
      {"from": "设计案 §6 交付页", "to": "卷二 15.1 逻辑单元测试", "carry": "验收项作为卷二测试用例的种子"}
    ]
  }
}

handoff.note 特别写明「卷二尚未落盘,只登记衔接点标题,不写跨卷 relref」。这是有意的纪律:衔接点先写标题,等卷二真的落盘再补链接,否则会出现指向不存在页面的死链。

15.3.3 总验收脚本:三份配置串成一次核对

三条验收项分别依赖三份配置,如果各跑各的脚本,验收就成了三次独立操作。更好的做法是把它们串成一次总验收:一个脚本读三份配置、跑三条验收项、产出一份可读报告。

下面这段是 S 层总验收脚本。它读 ch15_review.json、ch15_design_doc.json、ch15_acceptance.json,跑 A1/A2/A3,产出汇总 JSON 与一份逐条 PASS/FAIL 的验收报告文本,并断言卷一五十篇 slug 白名单的数量、命名规则,以及与装配清单的交叉一致性。

#!/usr/bin/env python3
# file: sim/ch15_acceptance.py
# tier: S
# verify: /opt/homebrew/bin/python3 sim/ch15_acceptance.py --config config/ --out out/
"""卷一交付总验收:读 ch15_review.json / ch15_design_doc.json / ch15_acceptance.json
三份配置,跑三条验收项 A1/A2/A3,产出汇总 JSON 与一份可读的验收报告文本
(每条 PASS/FAIL),并断言卷一 50 篇 slug 白名单:数量、命名规则、与装配清单交叉一致。
任一验收项失败即非 0 退出。产出 out/ch15_acceptance.json + out/ch15_acceptance_report.txt。
"""

from __future__ import annotations

import argparse
import json
import os
import re
import sys

SLUG_RE = re.compile(r"^mg-design-(\d{2})-([1-3])-([a-z0-9]+(?:-[a-z0-9]+)*)$")
FRONT_BACK = {"mg-design-toc", "mg-design-intro", "mg-design-preface",
              "mg-design-appendix-a", "mg-design-appendix-b"}


def load(cfg_dir, name):
    with open(os.path.join(cfg_dir, f"{name}.json"), encoding="utf-8") as f:
        return json.load(f)


def check_a1(review):
    """评审材料:每项材料三件套齐备且 owner 在角色表内。"""
    roles = {r["id"] for r in review["roles"]}
    bad = [f"{m['id']}: 三件套缺项或 owner 非法" for m in review["materials"]
           if not (m.get("owner") in roles and (m.get("dueMilestone") or "").strip()
                   and isinstance(m.get("passCriteria"), list) and m["passCriteria"])]
    return (not bad), f"材料 {len(review['materials'])} 项 / 角色 {len(roles)} 个", bad


def check_a2(doc):
    """设计案装配:15 章各 3 节,45 节 slug 命名与章号一致,每章有产出物。"""
    bad, seen = [], set()
    if sorted(c.get("ch") for c in doc["chapters"]) != list(range(1, 16)):
        bad.append("章号不是 1..15")
    for ch in doc["chapters"]:
        secs = ch.get("sections") or []
        if len(secs) != 3:
            bad.append(f"第 {ch['ch']} 章节数 {len(secs)} != 3")
        for s in secs:
            if s in seen:
                bad.append(f"节 slug 重复:{s}")
            seen.add(s)
            m = SLUG_RE.match(s)
            if not m or int(m.group(1)) != ch["ch"]:
                bad.append(f"节 slug 不合规或章号错位:{s}")
        if not (ch.get("outputs") or []):
            bad.append(f"第 {ch['ch']} 章无产出物")
    if len(seen) != 45:
        bad.append(f"节 slug 合计 {len(seen)} != 45")
    return (not bad), f"章 {len(doc['chapters'])} / 节 slug {len(seen)}", bad


def check_a3(acc, doc):
    """交付白名单:50 篇数量、命名规则,且与装配清单的 45 节交叉一致。"""
    bad, wl = [], acc["whitelist50"]
    if len(wl) != 50:
        bad.append(f"白名单 {len(wl)} != 50")
    if len(set(wl)) != len(wl):
        bad.append("白名单有重复")
    secs = [s for s in wl if s not in FRONT_BACK]
    if len(secs) != 45:
        bad.append(f"节 slug {len(secs)} != 45")
    for s in secs:
        m = SLUG_RE.match(s)
        if not m or not (1 <= int(m.group(1)) <= 15):
            bad.append(f"节 slug 不合规或越界:{s}")
    for s in wl:
        if not s.startswith(acc["prefix"]):
            bad.append(f"slug 前缀错:{s}")
    doc_secs = {s for ch in doc["chapters"] for s in (ch.get("sections") or [])}
    if doc_secs != set(secs):
        bad.append("白名单与装配清单不一致")
    if FRONT_BACK - set(wl):
        bad.append(f"缺前后附 slug:{sorted(FRONT_BACK - set(wl))}")
    return (not bad), f"白名单 {len(wl)} 篇(节 {len(secs)} + 前后附 {len(FRONT_BACK)})", bad


def main() -> int:
    ap = argparse.ArgumentParser()
    ap.add_argument("--config", default="config/")
    ap.add_argument("--out", default="out/")
    args = ap.parse_args()

    review = load(args.config, "ch15_review")
    doc = load(args.config, "ch15_design_doc")
    acc = load(args.config, "ch15_acceptance")

    results = []
    for cid, name, fn in (("A1", "评审材料齐备", lambda: check_a1(review)),
                          ("A2", "设计案装配完整", lambda: check_a2(doc)),
                          ("A3", "交付清单与白名单一致", lambda: check_a3(acc, doc))):
        ok, detail, bad = fn()
        results.append({"id": cid, "name": name, "pass": ok, "detail": detail, "problems": bad})

    ok = all(r["pass"] for r in results)
    ho = acc["handoff"]
    os.makedirs(args.out, exist_ok=True)
    summary = {"source": "sim/ch15_acceptance.py", "volume": acc["volume"], "prefix": acc["prefix"],
               "expectedTotal": acc["expectedTotal"], "results": results,
               "handoff": {"nextVolume": ho["nextVolume"], "nextPrefix": ho["nextPrefix"],
                           "entryPointCount": len(ho["entryPoints"])}, "ok": ok}
    with open(os.path.join(args.out, "ch15_acceptance.json"), "w", encoding="utf-8") as f:
        json.dump(summary, f, ensure_ascii=False, indent=2)

    lines = [f"卷一《{acc['volume']}》交付验收报告",
             f"白名单基线:{acc['expectedTotal']} 篇(前缀 {acc['prefix']})", "-" * 56]
    for r in results:
        lines.append(f"[{'PASS' if r['pass'] else 'FAIL'}] {r['id']} {r['name']}({r['detail']})")
        lines += [f"        - {p}" for p in r["problems"]]
    lines.append("-" * 56)
    lines.append(f"卷二衔接:{ho['nextVolume']}(前缀 {ho['nextPrefix']}),衔接点 {len(ho['entryPoints'])} 个")
    lines += [f"  {e['from']} -> {e['to']}:{e['carry']}" for e in ho["entryPoints"]]
    lines.append(f"总判定:{'通过(全部 PASS)' if ok else '不通过(存在 FAIL)'}")
    text = "\n".join(lines) + "\n"
    with open(os.path.join(args.out, "ch15_acceptance_report.txt"), "w", encoding="utf-8") as f:
        f.write(text)
    sys.stdout.write(text)
    return 0 if ok else 1


if __name__ == "__main__":
    sys.exit(main())

本机实跑输出(脚本同时把这段文本写入 out/ch15_acceptance_report.txt):

卷一《市场品类与产品设计》交付验收报告
白名单基线:50 篇(前缀 mg-design-)
--------------------------------------------------------
[PASS] A1 评审材料齐备(材料 12 项 / 角色 5 个)
[PASS] A2 设计案装配完整(章 15 / 节 slug 45)
[PASS] A3 交付清单与白名单一致(白名单 50 篇(节 45 + 前后附 5))
--------------------------------------------------------
卷二衔接:引擎实现与体验打磨(前缀 mg-engine-),衔接点 5 个
  设计案 §2 平台约束页 -> 卷二 1.1 引擎与框架选型:包体预算与首屏约束作为选型评分的硬约束
  设计案 §3 玩法页 -> 卷二 5.1 固定步长与确定性:核心循环时长与反馈密度作为循环节奏输入
  设计案 §5 原型页 -> 卷二 10.1 平台 API 抽象:最小可玩原型的宿主依赖作为抽象层清单
  设计案 §5 资源页 -> 卷二 2.2 分包与资源管线:外包素材规格与图集粒度作为打包输入
  设计案 §6 交付页 -> 卷二 15.1 逻辑单元测试:验收项作为卷二测试用例的种子
总判定:通过(全部 PASS)

三条验收项全 PASS。这份报告本身就是交付物的一部分——把它随设计案一起交出去,接手者不用重新核对结构,扫一眼报告就知道哪些项已经过了、白名单基准是什么、五个衔接点分别把什么带过去。A3 尤其关键:它断言的是「五十篇白名单」与「装配清单的四十五个节」交叉一致,任何一处笔误都会让两边对不上而报 FAIL。

15.3.4 交接:把接力棒递给卷二

卷一是「做什么」,卷二《引擎实现与体验打磨》是「怎么做出来」。交接不是一句「下一卷见」,而是把卷一的结论映射到卷二的具体章节,让卷二一开篇就知道自己接住了什么。配置里登记了五个衔接点:

从(卷一产出)到(卷二章节)带过去什么
设计案 §2 平台约束页卷二 1.1 引擎与框架选型包体预算与首屏约束,作为选型评分的硬约束
设计案 §3 玩法页卷二 5.1 固定步长与确定性核心循环时长与反馈密度,作为循环节奏输入
设计案 §5 原型页卷二 10.1 平台 API 抽象最小可玩原型的宿主依赖,作为抽象层清单
设计案 §5 资源页卷二 2.2 分包与资源管线外包素材规格与图集粒度,作为打包输入
设计案 §6 交付页卷二 15.1 逻辑单元测试验收项,作为卷二测试用例的种子

五个衔接点覆盖了卷二的选型、循环、平台抽象、资源管线、测试五条主线。特别注意第五个:卷一的验收项直接变成卷二的测试用例种子——本节写的 A1/A2/A3 是结构验收,卷二会把「核心循环跑得对、存档版本能迁移」这类行为验收接上去,两卷的验收口径由此连成一条线。

衔接点只写标题、不写跨卷链接,是因为卷二尚未落盘。等卷二就位,这些标题才会变成真正的 relref。先留标题、后补链接,是跨卷协作里避免死链的基本纪律。

15.3.5 卷一的收束

回看卷一这十五章,它走的是一条从外部约束收敛到内部交付的路径:

  • 第 1–3 章向外看:市场有多大、榜单长什么样、品类怎么选。
  • 第 4–5 章看约束与用户:平台允许做什么、给谁做。
  • 第 6–9 章向内设计:核心循环、关卡、成长、社交。
  • 第 10–14 章算账:怎么变现、值不值得做、要多少人多少时间。
  • 第 15 章收口:把前十四章的结论装配成可评审、可验收、可交付的设计案。

这条路径的意义在于:卷一的每一个结论,都不是孤立的观点,而是可以被核对、被引用、被装配进设计案的产出物。这正是本套书「可运行」主张在卷一这一端的落点——不只是代码能跑,设计案本身也能被脚本核对。

小结

  • 交付不是把文档发出去,而是逐项核对能不能被接住;验收有结构验收与内容验收两条线。
  • D 层配置硬编码卷一五十篇 slug 白名单与三条验收项;S 层脚本把三份配置串成一次总验收,产出可读的验收报告文本。
  • 本机实跑:A1/A2/A3 全 PASS,白名单五十篇(节 45 + 前后附 5),卷二衔接点五个。
  • 交接把卷一结论映射到卷二五个章节;衔接点先写标题、后补链接,跨卷引用不留死链。
  • 卷一十五章从市场收敛到交付,每个结论都是可核对、可引用、可装配的产出物。

卷一到此结束。卷二《引擎实现与体验打磨》会从引擎选型开始,把这本设计案一行行变成能跑起来的代码。

阅读导航:上一节:设计案装配 · 下一节:回到目录 。

继续阅读

探索更多技术文章

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

全部文章 返回首页

「minigame」更多文章

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