站内《上线变现与长线运营》 写的是国内上线与长线运营,站内 SaaS 平台创业口径的小游戏专题 (25 篇)讲的是做小游戏平台本身。本卷两者都不重做——卷五只做跨平台发行与出海。本章是整部《小游戏开发实战》五卷本的最后一节:不重做国内运营方法论,也不写平台创业,只做一件收尾的事——把五卷的交付物摆到一起,算一次总账。
收官要回答的三个问题
一本分五卷、共 250 篇的书,写到最后一节,读者真正想知道的是三件事:
- 这套书作为一个整体,完成到什么程度。不是「写了 250 篇」这种篇数,而是「五卷之间的交付链是否接得上、关键交付物是否到位」。
- 还差什么。哪些交付物仍是
partial,缺口有多重。 - 哪些永远算不出来。有些东西——平台后台的真实数字、审核结果、母语审校结论——命令行里复现不了,必须显式标成未知项(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:
| 缺口项 | 权重 | 状态 | 说明 |
|---|---|---|---|
| 卷五《跨平台发行与出海》出海发行方案 | 16 | partial | 卷五仍在收尾,方案本身尚未完全定稿 |
| 附录 A–F 全套(配置与清单索引) | 4 | partial | 附录仍在补齐 |
还有六项是永远无法在命令行复现的出海未知项(X 层),它们不是「待办」,而是「必须承认的边界」:各海外平台的真实后台数据、真实上架审核结果、真实买量投放与 ROI、税务表单受理与预扣、真实结算到账与汇率、母语审校结论与文化适配判断。这些只能靠真实业务环境取得,任何仿真都给不出。
自检表
| 检查项 | 结果 | 依据 |
|---|---|---|
| 交付物项数 | 10 项 | S 层 itemCount |
| 总权重 | 100 | S 层 totalWeight |
| 整体完成度 | 900‰ ≥ 880‰,过 | S 层 scorePermille |
| 缺口项 | 2 项(均 partial) | S 层 gaps |
| 交付链边数 | 4 条(v1→v2→v3→v4→v5) | S 层 chainEdges |
| 未知项数 | 6 项(X 层) | S 层 unknownCount |
| 五卷共用同一经济模型 | 满级产出 170 | sharedEconomyMaxLevelOutput |
一句话结论:五卷的交付链接得上、整体完成度刚过可交付线,但卷五方案与附录仍有缺口,且有六项出海结论只能靠真实业务环境取得。这正是本书一贯的态度——能算的算清,算不了的标出来,绝不用编造的数字把缺口填平。本节所有打分数字均为本机相对量级,绝对数值不可移植。
阅读导航:上一节:16.2 多平台仿真与验收 · 下一节:回到目录 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。