16.1 把三卷装配成完整方案

三卷各写了什么、彼此怎么咬合,是收官章要回答的第一个问题。本节把卷一设计、卷二实现、卷三运营的产出登记成一份装配清单(D 层),声明从立项到长线运营的十段链路、每段引用哪一卷哪几章、交付哪些产物,再用一段 S 层脚本把清单逐条校验,断言三卷章号连续、链路引用不越界、交付物与门禁闭环,产出 JSON、CSV 与一张链路覆盖图。

这是全书三卷一百五十篇的收官章。前面十五章把《合成小岛》从选品类讲到了上线门禁与长线运营,但它们各自成章——真正要交到团队手里的,不是十五份独立的分析,而是一份能被别人接住、能一路跑到上线的完整方案。收官章要回答的就是这件事:三卷怎么装配成一份方案、装配完怎么验收、验收完怎么交付。

先说分工。本站有一篇小程序小游戏开发综述 ,那是一篇约百行的单篇概览,覆盖引擎选型、性能与变现的要点罗列;本套是成体系、可运行的三卷——每一条结论背后都有一份能跑的配置与脚本,收官章更是把三卷的产物装配成一条可复现的链路。概览告诉你「小游戏开发涉及哪些方面」,本套告诉你「每一个方面具体怎么做、怎么验」。另需声明:本章综合引用卷一与卷二的白名单章节,是为了把三卷串成一条链路,不是重复它们的框架。

16.1 把三卷装配成完整方案

16.1.1 装配要解决的是「接得住」

一份方案交付失败,往往不是内容不够,而是接不住:做实现的拿到设计案,发现少了一章;做运营的拿到上线清单,发现某个门禁没有对应产出物;换个人接手,发现「这一段到底归哪一卷、哪几章」说不清。装配要做的,就是把「接得住」变成可核对的项——每段链路引用哪一卷哪几章、产出什么、谁负责、过哪道门,全部登记清楚。

这与卷一收口时的做法一脉相承:卷一的交付物验收与下一卷衔接 已经证明「设计案本身也能被脚本核对」。本节把同一套思路放大到三卷:不是核对一卷的五十篇,而是核对三卷装配后的一条完整链路。

16.1.2 装配清单(D 层)

下面这段是 D 层装配清单。它登记三卷的章号(design 15 章、engine 16 章、launch 16 章)、从立项到长线运营的十段链路(每段引用产出它的卷与章、产出什么产物、谁负责、过哪道门),以及必须齐备的八份交付物与十道验收门。它只是数据——真正逐条核对的是下一节的 S 层脚本。

{
  "version": 1,
  "series": "《小游戏开发实战》",
  "game": "merge-island",
  "observedAt": "2026-10",
  "note": "三卷装配清单(D 层):把卷一设计、卷二实现、卷三运营的产出装配成一份可交付的上线方案。volumes 登记三卷的章号;stages 声明从立项到长线运营的十段链路,每段引用产出它的卷与章;deliverables 列出必须齐备的交付物与责任人、证据;gates 是装配完成后要过的验收门。章标题为摘要,真实标题以各卷正文为准。",
  "volumes": [
    { "id": "design", "name": "市场品类与产品设计", "prefix": "mg-design-", "chapters": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15] },
    { "id": "engine", "name": "引擎实现与体验打磨", "prefix": "mg-engine-", "chapters": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16] },
    { "id": "launch", "name": "上线变现与长线运营", "prefix": "mg-launch-", "chapters": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16] }
  ],
  "stages": [
    { "id": "market-fit", "name": "市场与品类选择", "volume": "design", "chapters": [1, 2, 3], "artifact": "立项一页纸", "owner": "制作人", "gate": "G1" },
    { "id": "design-spec", "name": "玩法与数值设计", "volume": "design", "chapters": [6, 7, 8, 10], "artifact": "设计案", "owner": "主策", "gate": "G2" },
    { "id": "budget-plan", "name": "预算与排期", "volume": "design", "chapters": [11, 13, 14], "artifact": "预算表", "owner": "制作人", "gate": "G3" },
    { "id": "core-loop", "name": "核心循环与状态机", "volume": "engine", "chapters": [5, 6], "artifact": "可运行内核", "owner": "客户端", "gate": "G4" },
    { "id": "platform-adapter", "name": "平台适配层", "volume": "engine", "chapters": [8, 10], "artifact": "一份逻辑两种宿主", "owner": "客户端", "gate": "G5" },
    { "id": "render-perf", "name": "渲染与性能", "volume": "engine", "chapters": [3, 9, 14], "artifact": "性能基线", "owner": "客户端", "gate": "G6" },
    { "id": "monetization", "name": "变现与收入模型", "volume": "launch", "chapters": [1, 2, 3], "artifact": "收入模型", "owner": "商业化", "gate": "G7" },
    { "id": "launch-gate", "name": "上线门禁", "volume": "launch", "chapters": [9, 10], "artifact": "上线清单", "owner": "运营", "gate": "G8" },
    { "id": "growth-retention", "name": "增长与留存", "volume": "launch", "chapters": [6, 7, 8], "artifact": "漏斗看板", "owner": "增长", "gate": "G9" },
    { "id": "ops-loop", "name": "长线运营", "volume": "launch", "chapters": [12, 13], "artifact": "版本列车", "owner": "运营", "gate": "G10" }
  ],
  "deliverables": [
    { "id": "D1", "name": "设计案", "stage": "design-spec", "owner": "主策", "evidence": "out/l16_assembly.json#design-spec", "required": true },
    { "id": "D2", "name": "预算与排期表", "stage": "budget-plan", "owner": "制作人", "evidence": "out/l16_assembly.json#budget-plan", "required": true },
    { "id": "D3", "name": "可运行内核与单测", "stage": "core-loop", "owner": "客户端", "evidence": "core/test/l16_acceptance.test.ts", "required": true },
    { "id": "D4", "name": "平台适配层与兼容矩阵", "stage": "platform-adapter", "owner": "客户端", "evidence": "out/l16_assembly.json#platform-adapter", "required": true },
    { "id": "D5", "name": "收入模型与回本测算", "stage": "monetization", "owner": "商业化", "evidence": "out/l16_fullchain.json", "required": true },
    { "id": "D6", "name": "上线检查清单", "stage": "launch-gate", "owner": "运营", "evidence": "out/l16_assembly.json#launch-gate", "required": true },
    { "id": "D7", "name": "漏斗与留存看板口径", "stage": "growth-retention", "owner": "增长", "evidence": "out/l16_assembly.csv", "required": true },
    { "id": "D8", "name": "可玩原型回放产物", "stage": "ops-loop", "owner": "客户端", "evidence": "web/dist/l16.html", "required": true }
  ],
  "gates": [
    { "id": "G1", "name": "品类通过加权评分线", "stage": "market-fit", "hard": true },
    { "id": "G2", "name": "设计案每章有产出物", "stage": "design-spec", "hard": true },
    { "id": "G3", "name": "预算不超止损线", "stage": "budget-plan", "hard": true },
    { "id": "G4", "name": "核心循环单测全绿", "stage": "core-loop", "hard": true },
    { "id": "G5", "name": "两种宿主同摘要", "stage": "platform-adapter", "hard": true },
    { "id": "G6", "name": "首帧与包体达标", "stage": "render-perf", "hard": true },
    { "id": "G7", "name": "回本日不超窗口", "stage": "monetization", "hard": true },
    { "id": "G8", "name": "上线清单门禁项全过", "stage": "launch-gate", "hard": true },
    { "id": "G9", "name": "D1/D7/D30 达锚点", "stage": "growth-retention", "hard": true },
    { "id": "G10", "name": "版本列车排期无空窗", "stage": "ops-loop", "hard": false }
  ]
}

十段链路横跨三卷:design 三段(选品、设计、预算)、engine 三段(内核、适配、性能)、launch 四段(变现、门禁、增长、运营)。每一段都绑定一道门,其中九道是硬门禁、最后一道「版本列车排期无空窗」是软门禁——它不拦住上线,只提醒运营别把版本排空。

16.1.3 装配校验脚本(S 层)

下面这段是 S 层校验脚本。它读上面那份清单,跑五条检查:V1 三卷章号连续、前缀合法;V2 每段链路引用的卷与章都在对应卷范围内;V3 三卷都被链路覆盖;V4 每份必需交付物都有责任人与证据;V5 门禁与链路一一对应、无悬空。任一条不通过即非 0 退出,产出 JSON、CSV 与一张按卷着色的链路覆盖图。

#!/usr/bin/env python3
# file: sim/l16_assembly.py
# tier: S
# verify: /opt/homebrew/bin/python3 sim/l16_assembly.py --config config/ --out out/
"""三卷装配校验(S 层):读 config/l16_assembly.json,把「一份可交付的上线方案」
拆成可核对的项,逐条真跑:

  V1 三卷登记完整      前缀 / 章号 / 标题齐备,章号连续 1..N
  V2 链路引用合法      每段 stage 引用的卷与章都在对应卷的章号范围内
  V3 链路覆盖          design / engine / launch 三卷各有 stage 引用
  V4 交付物齐备        每份 required 交付物都有 owner 与 evidence,且 stage 存在
  V5 门禁闭环          每个 stage 都有一个 gate,每个 gate 都指向存在的 stage

产出 out/l16_assembly.json + out/l16_assembly.csv + out/l16_assembly.svg。
全整数、无浮点;图表用纯 Python SVG。章标题为摘要,真实标题以各卷正文为准。
"""

from __future__ import annotations

import argparse
import csv
import json
import os
import xml.etree.ElementTree as ET

VOL_COLOR = {"design": "#2563eb", "engine": "#7c3aed", "launch": "#10b981"}


def txt(parent: ET.Element, x: float, y: float, s: str, size: int = 12,
        fill: str = "#374151", anchor: str | None = None) -> None:
    a = {"x": str(x), "y": str(y), "font-size": str(size), "fill": fill}
    if anchor:
        a["text-anchor"] = anchor
    ET.SubElement(parent, "text", a).text = s


def svg(path: str, stages: list[dict], vol_name: dict) -> int:
    """纯 Python SVG:每段 stage 一行,长度正比于它引用的章数,颜色按所属卷区分。"""
    W, PAD, ROW = 760, 240, 34
    H = 96 + ROW * len(stages)
    r = ET.Element("svg", {"xmlns": "http://www.w3.org/2000/svg", "width": str(W),
                           "height": str(H), "viewBox": f"0 0 {W} {H}"})
    ET.SubElement(r, "rect", {"width": str(W), "height": str(H), "fill": "#ffffff"})
    txt(r, 16, 26, "《合成小岛》上线方案装配:链路分段与引用章数(D 层清单)", 15, "#111827")
    for k, (vid, c) in enumerate(VOL_COLOR.items()):
        ET.SubElement(r, "rect", {"x": str(16 + k * 200), "y": "40", "width": "12",
                                  "height": "12", "rx": "2", "fill": c})
        txt(r, 33 + k * 200, 51, vol_name[vid], 11)
    bmax = W - PAD - 96
    hi = max((len(s["chapters"]) for s in stages), default=1) or 1
    for i, s in enumerate(stages):
        y = 96 + i * ROW
        w = bmax * len(s["chapters"]) // hi
        txt(r, 16, y + 20, s["name"])
        ET.SubElement(r, "rect", {"x": str(PAD), "y": str(y + 6), "width": str(w), "height": "18",
                                  "rx": "4", "fill": VOL_COLOR.get(s["volume"], "#6b7280")})
        txt(r, PAD + w + 8, y + 20, f'{s["volume"]} 第 {",".join(str(c) for c in s["chapters"])} 章',
            11, "#6b7280")
    os.makedirs(os.path.dirname(path), exist_ok=True)
    ET.ElementTree(r).write(path, encoding="utf-8", xml_declaration=True)
    return os.path.getsize(path)


def main() -> int:
    ap = argparse.ArgumentParser()
    ap.add_argument("--config", default="config/")
    ap.add_argument("--out", default="out/")
    a = ap.parse_args()
    with open(os.path.join(a.config, "l16_assembly.json"), encoding="utf-8") as f:
        cfg = json.load(f)

    vols = {v["id"]: v for v in cfg["volumes"]}
    vol_name = {v["id"]: v["name"] for v in cfg["volumes"]}
    ch_range = {v["id"]: set(v["chapters"]) for v in cfg["volumes"]}
    stage_ids = {s["id"] for s in cfg["stages"]}
    gate_by_stage = {g["stage"] for g in cfg["gates"]}

    problems: list[str] = []

    # V1 三卷登记完整:前缀 / 章号连续 1..N / 标题非空
    v1 = []
    for v in cfg["volumes"]:
        chs = list(v["chapters"])
        if chs != list(range(1, len(chs) + 1)):
            v1.append(f'{v["id"]} 章号不连续:{chs}')
        if not v["prefix"].endswith("-") or len(v["prefix"]) < 6:
            v1.append(f'{v["id"]} 前缀不合法:{v["prefix"]}')
    problems += v1

    # V2 链路引用合法
    v2 = []
    for s in cfg["stages"]:
        if s["volume"] not in vols:
            v2.append(f'{s["id"]} 引用未知卷 {s["volume"]}')
            continue
        for c in s["chapters"]:
            if c not in ch_range[s["volume"]]:
                v2.append(f'{s["id"]} 引用 {s["volume"]} 第 {c} 章越界')
    problems += v2

    # V3 链路覆盖:三卷各有 stage
    covered = {s["volume"] for s in cfg["stages"]}
    v3 = [] if covered == set(vols) else [f"未覆盖的卷:{sorted(set(vols) - covered)}"]
    problems += v3

    # V4 交付物齐备
    v4 = []
    for d in cfg["deliverables"]:
        if d["stage"] not in stage_ids:
            v4.append(f'{d["id"]} 指向不存在的 stage {d["stage"]}')
        if d["required"] and (not d.get("owner") or not d.get("evidence")):
            v4.append(f'{d["id"]} 缺 owner 或 evidence')
    problems += v4

    # V5 门禁闭环
    v5 = []
    for s in cfg["stages"]:
        if s["id"] not in gate_by_stage:
            v5.append(f'stage {s["id"]} 无门禁')
    for g in cfg["gates"]:
        if g["stage"] not in stage_ids:
            v5.append(f'gate {g["id"]} 指向不存在的 stage {g["stage"]}')
    problems += v5

    checks = [
        {"id": "V1", "name": "三卷登记完整", "pass": not v1, "problems": v1},
        {"id": "V2", "name": "链路引用合法", "pass": not v2, "problems": v2},
        {"id": "V3", "name": "链路覆盖三卷", "pass": not v3, "problems": v3},
        {"id": "V4", "name": "交付物齐备", "pass": not v4, "problems": v4},
        {"id": "V5", "name": "门禁闭环", "pass": not v5, "problems": v5},
    ]
    ok = all(c["pass"] for c in checks)

    stages = []
    for s in cfg["stages"]:
        stages.append({"id": s["id"], "name": s["name"], "volume": s["volume"],
                       "chapters": s["chapters"], "chapterCount": len(s["chapters"]),
                       "artifact": s["artifact"], "owner": s["owner"], "gate": s["gate"]})
    vol_summary = [{"id": v["id"], "name": v["name"], "prefix": v["prefix"],
                    "chapterCount": len(v["chapters"]),
                    "stages": sum(1 for s in stages if s["volume"] == v["id"])}
                   for v in cfg["volumes"]]

    os.makedirs(a.out, exist_ok=True)
    sb = svg(os.path.join(a.out, "l16_assembly.svg"), stages, vol_name)
    summary = {"version": cfg["version"], "source": "sim/l16_assembly.py", "game": cfg["game"],
               "observedAt": cfg["observedAt"], "volumes": vol_summary,
               "stageCount": len(stages), "deliverableCount": len(cfg["deliverables"]),
               "gateCount": len(cfg["gates"]),
               "hardGateCount": sum(1 for g in cfg["gates"] if g["hard"]),
               "checks": checks, "problems": problems, "ok": ok, "svgBytes": sb}
    with open(os.path.join(a.out, "l16_assembly.json"), "w", encoding="utf-8") as f:
        json.dump(summary, f, ensure_ascii=False, indent=2)
    with open(os.path.join(a.out, "l16_assembly.csv"), "w", encoding="utf-8", newline="") as f:
        w = csv.writer(f)
        w.writerow(["id", "name", "volume", "chapters", "chapterCount", "artifact", "owner", "gate"])
        for s in stages:
            w.writerow([s["id"], s["name"], s["volume"], ";".join(str(c) for c in s["chapters"]),
                        s["chapterCount"], s["artifact"], s["owner"], s["gate"]])
    print(json.dumps({"stageCount": len(stages), "deliverableCount": len(cfg["deliverables"]),
                      "gateCount": len(cfg["gates"]), "hardGateCount": summary["hardGateCount"],
                      "volumes": vol_summary, "checks": {c["id"]: c["pass"] for c in checks},
                      "ok": ok, "svgBytes": sb}, ensure_ascii=False, indent=2))
    return 0


if __name__ == "__main__":
    raise SystemExit(main())

本机实跑输出(同机相对量级,绝对数值不可移植):

{
  "stageCount": 10,
  "deliverableCount": 8,
  "gateCount": 10,
  "hardGateCount": 9,
  "volumes": [
    { "id": "design", "name": "市场品类与产品设计", "prefix": "mg-design-", "chapterCount": 15, "stages": 3 },
    { "id": "engine", "name": "引擎实现与体验打磨", "prefix": "mg-engine-", "chapterCount": 16, "stages": 3 },
    { "id": "launch", "name": "上线变现与长线运营", "prefix": "mg-launch-", "chapterCount": 16, "stages": 4 }
  ],
  "checks": { "V1": true, "V2": true, "V3": true, "V4": true, "V5": true },
  "ok": true,
  "svgBytes": 3021
}

五条检查全 PASS。装配的意义在这一行 "ok": true 里:它不是「我觉得写完了」,而是「三卷的章号对得上、每一段链路都能指到具体章节、八份交付物都有主、十道门都能回到具体 stage」。任何一处笔误——把 engine 的某段误引成第 17 章、漏给一份交付物配证据——都会让对应的 V 项报 FAIL。

16.1.4 三卷在链路里的分工

把十段链路按卷归拢,三卷的分工一目了然:

卷链路分段交付的核心产物
卷一 市场品类与产品设计市场与品类选择、玩法与数值设计、预算与排期立项一页纸、设计案、预算表
卷二 引擎实现与体验打磨核心循环与状态机、平台适配层、渲染与性能可运行内核、一份逻辑两种宿主、性能基线
卷三 上线变现与长线运营变现与收入模型、上线门禁、增长与留存、长线运营收入模型、上线清单、漏斗看板、版本列车

卷一回答「做什么」,卷二回答「怎么做出来」,卷三回答「怎么赚、怎么留、怎么活得久」。三卷之间不是并列的三本书,而是一条流水线的三段:卷一的产物是卷二的输入(设计案约束选型),卷二的产物是卷三的输入(可运行内核才有留存与收入可谈),卷三的产物又反哺卷一的迭代(收入与留存数据决定下一版做什么)。下一节把这条流水线真正跑一遍。

小结

  • 收官章的第一件事是装配:把三卷十五份分析变成一份能被接住、能跑到上线的完整方案。
  • D 层装配清单登记三卷章号、十段链路、八份交付物与十道门;S 层脚本跑 V1–V5 五条检查,逐条真跑。
  • 本机实跑:三卷 15/16/16 章、链路 10 段(design 3 / engine 3 / launch 4)、交付物 8 份、门禁 10 道(硬 9 软 1),五条检查全 PASS。
  • 三卷是一条流水线的三段,不是并列的三本书;本节的装配清单把这条流水线登记成可核对的项。

阅读导航:上一节:目录 · 下一节:16.2 全链路仿真验收 。

继续阅读

探索更多技术文章

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

全部文章 返回首页

「minigame」更多文章

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