16.3 五卷总收官

五卷写完,最后要做一次总收官:把五卷的交付物按权重折算成整体完成度(D 层清单 + S 层打分),判定是否达可交付线,并诚实列出仍未完成的缺口(卷五方案与附录 A–F)与六项永远无法在命令行复现的出海未知项(平台后台、审核、买量、税务、结算、母语审校)。

站内《上线变现与长线运营》 写的是国内上线与长线运营,站内 SaaS 平台创业口径的小游戏专题 (25 篇)讲的是做小游戏平台本身。本卷两者都不重做——卷五只做跨平台发行与出海。本章是整部《小游戏开发实战》五卷本的最后一节:不重做国内运营方法论,也不写平台创业,只做一件收尾的事——把五卷的交付物摆到一起,算一次总账。

收官要回答的三个问题

一本分五卷、共 250 篇的书,写到最后一节,读者真正想知道的是三件事:

  1. 这套书作为一个整体,完成到什么程度。不是「写了 250 篇」这种篇数,而是「五卷之间的交付链是否接得上、关键交付物是否到位」。
  2. 还差什么。哪些交付物仍是 partial,缺口有多重。
  3. 哪些永远算不出来。有些东西——平台后台的真实数字、审核结果、母语审校结论——命令行里复现不了,必须显式标成未知项(X 层),而不是假装它们能被仿真覆盖。

五卷交付链

这套书的设计是「后一卷吃前一卷的产出」,五卷连成一条链,每卷都有自己独立的目录页:

卷主题目录交付给下一卷的东西
卷一市场品类与产品设计目录品类选择与设计案
卷二引擎实现与体验打磨目录可玩内核与包体预算
卷三上线变现与长线运营目录国内收入链与留存漏斗
卷四商业运营实战目录国内商业运营台账
卷五跨平台发行与出海目录多平台发行与出海落地

D 层:收官清单

下面这份是 D 层配置 config/g16_finale.json:它把五卷的交付物列成 10 项,每项带一个整数权重与一个完成态(done/partial/todo),另列五卷交付链(chain)与出海剩余未知项(unknowns,X 层)。整体完成度 = Σ(权重 × 完成度千分比) // Σ权重,达 readyThresholdPermille 即判可交付。

{
  "version": 1,
  "series": "小游戏开发实战",
  "game": "merge-island",
  "observedAt": "2026-10",
  "readyThresholdPermille": 880,
  "note": "五卷本总收官清单(D 层)。口径是「五卷链路装配」的建模,不是任何真实项目的验收记录。items[] 每项:weight=权重(整数),status=完成态(done/partial/todo,映射 1000/500/0 千分比),volume=归属卷。整体完成度 = Σ(weight×statusPermille) // Σweight(千分比),达 readyThresholdPermille 即判可交付。chain[] 是五卷的交付链(上一卷产出 → 下一卷输入)。unknowns[] 是出海剩余未知项(X 层,需真实业务环境,命令行不可复现)。全整数,无浮点。",
  "items": [
    { "id": "v1-design",  "name": "卷一《市场品类与产品设计》设计案", "volume": "v1", "weight": 16, "status": "done" },
    { "id": "v2-engine",  "name": "卷二《引擎实现与体验打磨》引擎内核", "volume": "v2", "weight": 16, "status": "done" },
    { "id": "v3-launch",  "name": "卷三《上线变现与长线运营》上线运营方案", "volume": "v3", "weight": 16, "status": "done" },
    { "id": "v4-ops",     "name": "卷四《商业运营实战》商业运营手册", "volume": "v4", "weight": 16, "status": "done" },
    { "id": "v5-global",  "name": "卷五《跨平台发行与出海》出海发行方案", "volume": "v5", "weight": 16, "status": "partial" },
    { "id": "one-config", "name": "一份 economy.json 三种消费(Python/TS/浏览器)", "volume": "v1", "weight": 4, "status": "done" },
    { "id": "two-hosts",  "name": "一份逻辑两种宿主(平台抽象层 + mock 宿主)", "volume": "v2", "weight": 4, "status": "done" },
    { "id": "html-proto", "name": "单文件 HTML 可玩原型 + 无头断言验收", "volume": "v2", "weight": 4, "status": "done" },
    { "id": "replay",     "name": "可播种确定性回放(Node 与浏览器同摘要)", "volume": "v3", "weight": 4, "status": "done" },
    { "id": "appendix",   "name": "附录 A–F 全套(配置与清单索引)", "volume": "v5", "weight": 4, "status": "partial" }
  ],
  "chain": [
    { "from": "v1", "to": "v2", "handoff": "品类选择与设计案 → 引擎骨架与体验打磨目标" },
    { "from": "v2", "to": "v3", "handoff": "可玩内核与包体预算 → 上线变现与长线运营方案" },
    { "from": "v3", "to": "v4", "handoff": "国内收入链与留存漏斗 → 投放变现与单位账" },
    { "from": "v4", "to": "v5", "handoff": "国内商业运营台账 → 多平台发行与出海落地" }
  ],
  "unknowns": [
    { "id": "x-platform-backend", "volume": "v5", "text": "各海外平台的真实后台数据(下载、收入、留存)" },
    { "id": "x-review",           "volume": "v5", "text": "真实上架审核结果与驳回(受平台当期政策影响)" },
    { "id": "x-ua",               "volume": "v5", "text": "真实买量投放与海外 ROI(受当地竞价与竞品影响)" },
    { "id": "x-tax",              "volume": "v5", "text": "税务表单受理与预扣(受所在国与平台当期规则影响)" },
    { "id": "x-payout",           "volume": "v5", "text": "真实结算到账与汇率(受银行与外汇市场影响)" },
    { "id": "x-native-review",    "volume": "v5", "text": "母语审校结论与文化适配判断(需母语者与当地视角)" }
  ]
}

S 层:加权完成度打分

下面这段是 S 层脚本,读上面这份清单,把 done/partial/todo 映射成 1000/500/0 千分比,按权重加权算出整体完成度,并列出缺口项、交付链边数与未知项数。它同时读共享 config/economy.json,报告满级产出,证明五卷共用同一套经济模型。全整数、无浮点。

#!/usr/bin/env python3
# file: sim/g16_finale.py
# tier: S
# verify: /opt/homebrew/bin/python3 sim/g16_finale.py --config config/ --out out/
"""五卷本总收官打分(S 层):把五卷的交付物按权重折算成整体完成度,判定是否达可交付线。

读 config/g16_finale.json 的五卷交付清单。每项 status 映射成完成度千分比:
    done → 1000    partial → 500    todo → 0
整体完成度 = Σ(weight × statusPermille) // Σweight(千分比),达 readyThresholdPermille 即判可交付。
另列出「缺口项」(status != done)、五卷交付链(chain)与出海剩余未知项(unknowns,X 层)。
同时读共享 config/economy.json,报告满级产出,证明五卷共用同一经济模型。产出 JSON + CSV + SVG。
全整数、无浮点。真实交付物内容与验收结论需真实业务环境,命令行不可复现(X)。
"""
from __future__ import annotations

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

STATUS_PERMILLE = {"done": 1000, "partial": 500, "todo": 0}
STATUS_COLOR = {"done": "#16a34a", "partial": "#f59e0b", "todo": "#dc2626"}


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


def draw_svg(path: str, items: list, score: int, threshold: int) -> int:
    """纯 Python SVG:每项一根权重条(按完成度着色)+ 一条可交付线。"""
    W, PAD, ROW = 780, 320, 32
    H = 74 + ROW * len(items)
    root = ET.Element("svg", {"xmlns": "http://www.w3.org/2000/svg", "width": str(W),
                              "height": str(H), "viewBox": f"0 0 {W} {H}"})
    ET.SubElement(root, "rect", {"width": str(W), "height": str(H), "fill": "#ffffff"})
    ET.SubElement(root, "text", {"x": "16", "y": "28", "font-size": "15",
                                 "fill": "#111827"}).text = (
        f"《小游戏开发实战》五卷本整体完成度 {score}‰(可交付线 {threshold}‰)")
    span = W - PAD - 24
    total_w = sum(it["weight"] for it in items) or 1
    for i, it in enumerate(items):
        y = 52 + i * ROW
        w = span * it["weight"] // total_w
        ET.SubElement(root, "text", {"x": "16", "y": str(y + 17), "font-size": "11",
                                     "fill": "#374151"}).text = it["name"][:30]
        ET.SubElement(root, "rect", {"x": str(PAD), "y": str(y + 4), "width": str(w),
                                     "height": "16", "rx": "3",
                                     "fill": STATUS_COLOR[it["status"]]})
        ET.SubElement(root, "text", {"x": str(PAD + w + 6), "y": str(y + 16), "font-size": "10",
                                     "fill": "#6b7280"}).text = (
            f'{it["status"]} · 权重 {it["weight"]} · {it["statusPermille"]}‰')
    os.makedirs(os.path.dirname(path), exist_ok=True)
    ET.ElementTree(root).write(path, encoding="utf-8", xml_declaration=True)
    return len(ET.tostring(root))


def main() -> int:
    ap = argparse.ArgumentParser()
    ap.add_argument("--config", default="config/")
    ap.add_argument("--out", default="out/")
    a = ap.parse_args()
    cfg = load(a.config, "g16_finale")
    econ = load(a.config, "economy")
    items = []
    for it in cfg["items"]:
        sp = STATUS_PERMILLE[it["status"]]
        items.append(dict(it, statusPermille=sp, weighted=it["weight"] * sp))
    total_w = sum(it["weight"] for it in items)
    total_weighted = sum(it["weighted"] for it in items)
    score = total_weighted // total_w
    threshold = cfg["readyThresholdPermille"]
    gaps = [it for it in items if it["status"] != "done"]
    e = econ["economy"]
    max_out = e["baseOutputPerMinute"] * (e["levelBonusNum"] ** (e["maxLevel"] - 1)) \
        // (e["levelBonusDen"] ** (e["maxLevel"] - 1))
    digest = (f'items={len(items)},weight={total_w},score={score},ready={str(score >= threshold).lower()};'
              f'chain={len(cfg["chain"])},unknowns={len(cfg["unknowns"])},maxOut={max_out};'
              f'gaps=' + ",".join(f'{g["id"]}:{g["status"]}' for g in gaps))

    os.makedirs(a.out, exist_ok=True)
    svg_bytes = draw_svg(os.path.join(a.out, "g16_finale.svg"), items, score, threshold)
    with open(os.path.join(a.out, "g16_finale.csv"), "w", encoding="utf-8", newline="") as f:
        w = csv.writer(f)
        cols = ("id", "name", "volume", "weight", "status", "statusPermille", "weighted")
        w.writerow(cols)
        for it in items:
            w.writerow([it[k] for k in cols])
    rep = {"version": cfg["version"], "series": cfg["series"], "game": cfg["game"],
           "observedAt": cfg["observedAt"], "itemCount": len(items), "totalWeight": total_w,
           "totalWeighted": total_weighted, "scorePermille": score,
           "readyThresholdPermille": threshold, "ready": score >= threshold,
           "marginPermille": score - threshold,
           "gaps": [{"id": g["id"], "name": g["name"], "status": g["status"], "weight": g["weight"]} for g in gaps],
           "pendingWeight": sum(g["weight"] for g in gaps), "chain": cfg["chain"],
           "unknowns": cfg["unknowns"], "sharedEconomyMaxLevelOutput": max_out,
           "items": items, "digest": digest,
           "svg": os.path.join(a.out, "g16_finale.svg"), "svgBytes": svg_bytes}
    with open(os.path.join(a.out, "g16_finale.json"), "w", encoding="utf-8") as f:
        json.dump(rep, f, ensure_ascii=False, indent=2)
    print(json.dumps({"itemCount": len(items), "totalWeight": total_w, "scorePermille": score,
                      "readyThresholdPermille": threshold, "ready": score >= threshold,
                      "marginPermille": score - threshold, "gaps": rep["gaps"],
                      "pendingWeight": rep["pendingWeight"], "chainEdges": len(cfg["chain"]),
                      "unknownCount": len(cfg["unknowns"]),
                      "sharedEconomyMaxLevelOutput": max_out, "digest": digest,
                      "svgBytes": svg_bytes}, ensure_ascii=False, indent=2))
    return 0


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

真实运行(/opt/homebrew/bin/python3 sim/g16_finale.py --config config/ --out out/):

{
  "itemCount": 10,
  "totalWeight": 100,
  "scorePermille": 900,
  "readyThresholdPermille": 880,
  "ready": true,
  "marginPermille": 20,
  "gaps": [
    {
      "id": "v5-global",
      "name": "卷五《跨平台发行与出海》出海发行方案",
      "status": "partial",
      "weight": 16
    },
    {
      "id": "appendix",
      "name": "附录 A–F 全套(配置与清单索引)",
      "status": "partial",
      "weight": 4
    }
  ],
  "pendingWeight": 20,
  "chainEdges": 4,
  "unknownCount": 6,
  "sharedEconomyMaxLevelOutput": 170,
  "digest": "items=10,weight=100,score=900,ready=true;chain=4,unknowns=6,maxOut=170;gaps=v5-global:partial,appendix:partial",
  "svgBytes": 4039
}

打分结果 scorePermille: 900,可交付线 880,ready: true,余量 marginPermille: 20——刚过线。

缺口与未知项

余量只有 20‰,说明这套书虽然达标,但离「全绿」还有距离。两个缺口项都是 partial:

缺口项权重状态说明
卷五《跨平台发行与出海》出海发行方案16partial卷五仍在收尾,方案本身尚未完全定稿
附录 A–F 全套(配置与清单索引)4partial附录仍在补齐

还有六项是永远无法在命令行复现的出海未知项(X 层),它们不是「待办」,而是「必须承认的边界」:各海外平台的真实后台数据、真实上架审核结果、真实买量投放与 ROI、税务表单受理与预扣、真实结算到账与汇率、母语审校结论与文化适配判断。这些只能靠真实业务环境取得,任何仿真都给不出。

自检表

检查项结果依据
交付物项数10 项S 层 itemCount
总权重100S 层 totalWeight
整体完成度900‰ ≥ 880‰,过S 层 scorePermille
缺口项2 项(均 partial)S 层 gaps
交付链边数4 条(v1→v2→v3→v4→v5)S 层 chainEdges
未知项数6 项(X 层)S 层 unknownCount
五卷共用同一经济模型满级产出 170sharedEconomyMaxLevelOutput

一句话结论:五卷的交付链接得上、整体完成度刚过可交付线,但卷五方案与附录仍有缺口,且有六项出海结论只能靠真实业务环境取得。这正是本书一贯的态度——能算的算清,算不了的标出来,绝不用编造的数字把缺口填平。本节所有打分数字均为本机相对量级,绝对数值不可移植。

阅读导航:上一节:16.2 多平台仿真与验收 · 下一节:回到目录 。

继续阅读

探索更多技术文章

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

全部文章 返回首页

「minigame」更多文章

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