这一页解决一个问题:卷三里出现的每一段代码、每一份配置、每一个数据集,在
pebble/里叫什么、放在哪、怎么跑。 它是一张可检索的清单,也是一份复现手册——比卷二附录 C 多出一份「全书公式与参数索引」。
一、这份索引解决什么
卷三的每一节都至少带一个 S / P / W 层的可运行产物。产物多了就需要一张总表,回答三件事:
- 这个产物叫什么名字、放在哪个目录——按 16 章列出
pebble/下的文件,命名规则统一,一眼能定位。 - 它属于哪一层、怎么验证——六层口径(S / P / W / D / X / B)各自的复现命令不同,这一页把它们并排写出来。
- 它依赖什么环境——两个
python3、Node 26、Playwright 复用,任何一处不对都会让「代码看起来错了」,其实只是环境错了。
贯穿全书的仍是《合成小岛》(merge-island)与工具链 pebble。卷三沿用前两卷的两条主张:一份 config,三种消费、一份逻辑,两种宿主;config/ 是唯一真源,Python 仿真、TS 内核、浏览器原型读同一份数字。
二、卷三工具链命名约定
这是卷三最容易踩的坑,先讲清楚。
卷一用 ch00–ch15 前缀,卷二改用 e<章号>_ 前缀,卷三一律用 l<章号>_ 前缀(两位章号,l01…l16):
config/l03_ltv.json # 卷三的配置
sim/l03_ltv.py # 卷三的仿真
core/src/l03/revenue.ts # 卷三的内核源码目录
core/test/l03_revenue.test.ts # 卷三的测试
web/build_l03_calculator.mjs # 卷三的单文件 HTML 构建脚本
web/verify_l03_calculator.mjs # 卷三的 Playwright 无头验收
为什么必须换前缀:node --test 接收的是 glob,不是目录。若卷三继续沿用旧前缀,通配符会同时匹配到两卷的测试文件,一条命令里混进另一卷的断言,输出就不可信了。换成 l<章号>_ 之后:
- 卷一:
node --test 'core/test/ch*.test.ts' - 卷二:
node --test 'core/test/e*.test.ts' - 卷三:
node --test 'core/test/l*.test.ts'
三个 glob 互不重叠。注意要带上两位章号与下划线(l04_ 而不是裸 l*),复现命令里写全 l04_*.test.ts 这样的精确前缀,才不会被无关文件污染。
各章文件对照表
下表把卷三 16 章各自的产物按目录列出。同一章在 config/、sim/、core/test/ 之间尽量保持同名,便于对照;core/src/l<NN>/ 是该章的内核源码目录。
| 章 | 主题 | config | sim | core/src | core/test | web |
|---|---|---|---|---|---|---|
| 1 | 广告变现 | config/l01_*.json | sim/l01_*.py | core/src/l01/ | core/test/l01_*.test.ts | web/build_l01_ads.mjs · web/verify_l01_ads.mjs |
| 2 | 内购与混合变现 | config/l02_*.json | sim/l02_*.py | core/src/l02/ | core/test/l02_*.test.ts | web/build_l02_hybrid.mjs · web/verify_l02_hybrid.mjs |
| 3 | 收入建模与 LTV | config/l03_*.json | sim/l03_*.py | core/src/l03/ | core/test/l03_*.test.ts | web/build_l03_calculator.mjs · web/verify_l03_calculator.mjs |
| 4 | 定价与促销 | config/l04_*.json | sim/l04_*.py | core/src/l04/ | core/test/l04_*.test.ts | web/build_l04_discount.mjs · web/verify_l04_discount.mjs |
| 5 | 买量与素材 | config/l05_*.json | sim/l05_*.py | core/src/l05/ | core/test/l05_*.test.ts | web/build_l05_consistency.mjs · web/verify_l05_consistency.mjs |
| 6 | 自然增长与裂变 | config/l06_*.json | sim/l06_*.py | core/src/l06/ | core/test/l06_*.test.ts | web/build_l06_traffic.mjs · web/verify_l06_traffic.mjs |
| 7 | 留存漏斗与召回 | config/l07_*.json | sim/l07_*.py | core/src/l07/ | core/test/l07_*.test.ts | web/build_l07_joint.mjs · web/verify_l07_joint.mjs |
| 8 | 实验与显著性 | config/l08_*.json | sim/l08_*.py | core/src/l08/ | core/test/l08_*.test.ts | web/build_l08_experiment.mjs · web/verify_l08_experiment.mjs |
| 9 | 发布与灰度 | config/l09_*.json | sim/l09_*.py | core/src/l09/ | core/test/l09_*.test.ts | — |
| 10 | 合规与防沉迷 | config/l10_*.json | sim/l10_*.py | core/src/l10/ | core/test/l10_*.test.ts | — |
| 11 | 数据体系 | config/l11_*.json | sim/l11_*.py | core/src/l11/ | core/test/l11_*.test.ts | — |
| 12 | 活动与赛季运营 | config/l12_*.json | sim/l12_*.py | core/src/l12/ | core/test/l12_*.test.ts | web/build_l12_season.mjs · web/verify_l12_season.mjs |
| 13 | 长线运营与退场 | config/l13_*.json | sim/l13_*.py | core/src/l13/ | core/test/l13_*.test.ts | — |
| 14 | 数据表现与迭代 | config/l14_*.json | sim/l14_*.py | core/src/l14/ | core/test/l14_*.test.ts | — |
| 15 | 失败模式复盘 | config/l15_*.json | sim/l15_*.py | core/src/l15/ | core/test/l15_*.test.ts | web/build_l15_switchboard.mjs · web/verify_l15_switchboard.mjs |
| 16 | 全链路装配 | config/l16_*.json | sim/l16_*.py | core/src/l16/ | core/test/l16_*.test.ts | web/build_l16_replay.mjs · web/verify_l16_replay.mjs |
实测规模:config/ 48 个 l*.json、sim/ 53 个 l*.py(其中 5 个 *_bench.py 属 B 层)、core/test/ 26 个 l*.test.ts、web/ 11 对 build_l*.mjs + verify_l*.mjs、web/dist/ 11 个 l*.html、out/ 146 个产物文件(*.json / *.csv / *.svg)。同一章的 config/ / sim/ / core/ / web/ 四类文件用同一个 <name> 串联,这是「一份 config 三种消费」在文件系统上的体现。
三、环境速查
| 项 | 值 | 说明 |
|---|---|---|
| Python(S 层) | 3.14.6,/opt/homebrew/bin/python3 | 带 numpy 2.5.1 / pandas 3.0.3 / PIL 12.3.0 |
| Python(系统) | 3.9.6,/usr/bin/python3 | 无 numpy/pandas,正文命令一律写全路径 |
| Node | 26.5.0 | 原生跑 TypeScript,strip-only,禁 enum / namespace / 参数属性 / 装饰器 |
| Playwright | 1.59.1,Chromium 已缓存 | 装在 /opt/homebrew/lib/node_modules,用 NODE_PATH + createRequire 复用 |
| matplotlib / scipy | 未安装 | 图表走纯 Python SVG(xml.etree);显著性用自实现 erf / probit |
| tsc / tsx / vitest / npm 包 | 均不使用 | 无 package.json、无 npm install |
两个高频陷阱,先记住:
- 用错
python3会静默降级——S 层仿真依赖 numpy / pandas,系统 3.9.6 没有这些库,import pandas直接报ModuleNotFoundError。命令一律写全路径/opt/homebrew/bin/python3。 NODE_PATH对 ESMimport无效——W 层脚本里必须createRequire(import.meta.url)之后require("playwright"),不能写import { chromium } from "playwright"。
下面这段是 S 层环境自检脚本 sim/ch00_env_check.py(与卷一、卷二共用)。它依次确认解释器、依赖与 Node 版本,是整套书稿的第一条可运行命令:
# file: sim/ch00_env_check.py
# tier: S
# verify: /opt/homebrew/bin/python3 sim/ch00_env_check.py
"""环境自检:确认《小游戏开发实战》三卷需要的运行时都在,并打印 pebble 的能力阶梯。
本机有两个 python3,不要混:
/opt/homebrew/bin/python3 = 3.14.6(带 numpy / pandas / PIL),S 层仿真一律用它;
/usr/bin/python3 = 3.9.6(系统自带,无 numpy/pandas),只能跑最朴素的脚本。
"""
from __future__ import annotations
import os
import shutil
import subprocess
import sys
EXPECT_PY = "/opt/homebrew/bin/python3"
def main() -> int:
ok = True
# /opt/homebrew/bin/python3 是符号链接,比较时解析到真实二进制再判断
real = os.path.realpath(sys.executable)
print(f"python {sys.version.split()[0]} ({real})")
if real != os.path.realpath(EXPECT_PY):
print(f" 注意:正文脚本一律用 {EXPECT_PY},当前解释器不同")
try:
import numpy
import pandas
print(f"numpy {numpy.__version__}")
print(f"pandas {pandas.__version__}")
except ImportError as e: # noqa: BLE001
ok = False
print(f"FAIL 缺依赖:{e}(S 层需要 numpy 与 pandas)")
node = shutil.which("node")
if not node:
ok = False
print("FAIL node 未安装(P 层需要 Node 26 或更高)")
else:
ver = subprocess.run([node, "-v"], capture_output=True, text=True).stdout.strip()
major = int(ver.lstrip("v").split(".")[0])
note = "" if major >= 26 else " (低于 26,原生 TypeScript 不可用)"
print(f"node {ver}{note}")
ok = ok and major >= 26
print("pebble 能力阶梯:S 仿真 / P 内核 / W 网页 / D 静态 / X 不可复现 / B 基准")
print("OK 环境就绪" if ok else "FAIL 环境不满足")
return 0 if ok else 1
if __name__ == "__main__":
raise SystemExit(main())
四、六层可运行性口径与复现命令
每一层都有固定的复现命令。<NN> 换成章号(如 l03),<name> 换成该章的文件名,命令均在 pebble/ 下执行:
| 层 | 含义 | 命令 |
|---|---|---|
| S 仿真 | Python 读 config 跑仿真,产出 out/*.json / .csv / .svg | `for f in sim/l |
| P 内核 | TypeScript 内核 + node --test 断言 | node --test 'core/test/l<NN>_*.test.ts' |
| W 网页 | 单文件 HTML 构建 + Playwright 无头验收 | node web/build_l<NN>_<name>.mjs && NODE_PATH=/opt/homebrew/lib/node_modules node web/verify_l<NN>_<name>.mjs web/dist/l<NN>.html |
| D 静态 | config schema、检查清单、规则表离线校验 | /opt/homebrew/bin/python3 sim/ch00_validate_config.py --config config/ --schema config/ch00_schema.json |
| X 不可复现 | 真实平台后台、真机帧率、审核结果、真实出价 | 无命令——正文每处已标注 |
| B 基准 | *_bench.py 软告警基准 | `for f in sim/*_bench.py; do /opt/homebrew/bin/python3 “$f” –config config/ –out out/ |
为什么 S 层要写 for f in …; do …; done 而不是 python3 sim/l03_*.py:shell 会把通配符展开成多个文件名传给同一个 python3,只有第一个脚本真的执行,其余被当成参数吃掉。逐个跑或用 for 循环才安全。node --test 自己展开 glob,不受影响。
五、全书公式与参数索引
下面按章列出核心公式与关键参数名。参数名一律取自对应 config/l<NN>_*.json 的键,金额一律用整数(分 / 0.1 分 deciFen)、比例用整数百分比 / 千分比,全程地板除、无浮点,故同一份 config 在 Python、TS 内核、浏览器三处逐位一致。
- 第 1 章 广告变现:
impressionsPerDau = requestsPerDau × fillPermille // 1000;revenueDeciFen = requestsPerDau × fillPermille × ecpmFen // 100000。参数requestsPerDau/fillPermille/ecpmFen/floorFen(填充率与 eCPM 反向,收益是乘积的内部最大值)。 - 第 2 章 内购与混合变现:内购按
priceFen × buyersPerDauPermille折算人均;混合协同把 IAA 与 IAP 的adRequestsPerDau/fillPct/ecpmFen/arppuFen放进同一份base,扫描adLoadSweep看总收益峰值。参数priceTiers/buyersPerDauPermille/synergy。 - 第 3 章 收入建模与 LTV:
iaaArpdauDeciFen = adRequestsPerDau × fillPct × ecpmFen // 10000;iapArpdauDeciFen = arppuFen × payRatePermille // 100;arpdauDeciFen = iaa + iap;dayArpuDeciFen(d) = arpdauDeciFen × retentionPermille[d] // 1000;ltvDeciFen(D) = Σ_{d=0..D-1} dayArpuDeciFen(d);paybackDay = min{ D : ltvDeciFen(D) ≥ cpaFen × 10 }。参数arpdauDeciFen/cpaFen/horizonDays/retentionPermille。 - 第 4 章 定价与促销:弧弹性绝对值
|E|‰ = ΔQ × (P1 + P2) // ((Q1 + Q2) × ΔP)(|E| > 1000为富有弹性);discountPct = (listPriceFen − priceFen) × 100 // listPriceFen;净收入 = 毛收入 − 需求前置 + 参考价侵蚀 + 鲸鱼流失三项副作用。参数listPriceFen/priceFen/units/pullForwardPct。 - 第 5 章 买量与素材:
cpiFen = cpmFen × 1000 // (ctrPermille × ipmPermille);activePermille(d) = Σ_{t=0..d} retentionPermille(t) // 1000;ltvDeciFen(d) = arpdauDeciFen × activePermille(d) // 1000;roasPermille = ltvDeciFen(d) × 100 // cpiFen(1000即回本)。参数cpmFen/ctrPermille/ipmPermille/arpdauDeciFen(每日留存按 D1/D7/D30 锚点整数线性插值)。 - 第 6 章 自然增长与裂变:
ampl = kPermille // viralCycleDays;viralNew = 上一日新增 × ampl // 1000;DAU = 上日 DAU × retentionPermille // 1000 + 当日新增;有效获客成本 = 买量花费 // 全部新增。参数kPermille/viralCycleDays/retentionPermille/paidNewPerDay/wordOfMouthPerDay(裂变放大的是「上一日新增流」而非存量 DAU)。 - 第 7 章 留存漏斗与召回:风险分 = 触发特征权重之和,按
minScore分档,以「高风险」为预测正类算精确率 / 召回率 / 提升度;reached = 高风险人数 × reachPermille // 1000;reactivated = reached × reactivatePermille // 1000;ROI = 回收 // 成本。参数churnBasePermille/triggerIfChurnPermille/triggerIfStayPermille/reachPermille/reactivatePermille/reactivatedValueDeciFen/costCent。 - 第 8 章 实验与显著性:两比例 z 检验,
normal_cdf(z) = 0.5 × (1 + erf(z / √2)),erf用 A&S 7.1.26 自实现(误差 < 1.5e-7),probit用 Acklam 逆正态近似(scipy 未装,须自实现);按基线率 / MDE / α / 功效算每臂最小样本量。参数baselineRatePermille/mdePermille/alphaPermille/powerPermille/dailyUsersPerArm/observed。 - 第 9 章 发布与灰度:新版本按
rampSteps五档(1% / 5% / 20% / 50% / 100%)逐步放量,每档观察一段时间;崩溃率越thresholds上限或留存低于下限即回滚。参数rampSteps/thresholds/defectRatePermille/dauPerDay。 - 第 10 章 合规与防沉迷:红线规则为子串匹配(大小写归一)分级处置,
severityLevels决定处置等级;防沉迷按实名 / 时段 / 时长的规则表过滤。参数severityLevels/rules/texts。 - 第 11 章 数据体系:指标树北极星在根,向下分解为 L1 / L2;每条边带
contributionPermille,同一父节点下子权重之和必须为 1000;叶子给raw/target/direction,归一到 0–1000 的scorePermille,内部节点由子节点加权回算。参数northStar/nodes/contributionPermille/direction(up越高越好 /down越低越好)。 - 第 12 章 活动与赛季运营:每周自然衰减
weeklyDecayPermille,季末冲刺加成sprintBoostPermille,季间衔接offSeasonDecayPermille;阶梯达成率由种子化 LCG 生成积分分布后统计。参数economy/season/ladder/targetIndexPermille。 - 第 13 章 长线运营与退场:
dau每周按weeklyDecayPermille衰减且不低于floorDau;周收入= dau × arpdauDeciFen × 7;周成本= 固定成本 + dau × 变动成本 × 7;利润 = 收入 − 成本;退场触发 = 连续lossWeeks周利润为负,或dau跌破dauFloorForSunset。参数weeklyDecayPermille/floorDau/arpdauDeciFen/fixedWeeklyCostDeciFen/variableCostPerDauDeciFen/sunsetRules。 - 第 14 章 数据表现与迭代:所有指标以「基线 = 1000」的指数表示,每周自然衰减
decayPermillePerWeek,迭代在某周注入一次持久增量liftPermille。参数baseline/decayPermillePerWeek/iterations。 - 第 15 章 失败模式复盘:以一组健康
baseline为参照,failures里每条用一个patch覆盖若干参数,喂进第 5 章(买量 ROI)与第 7 章(留存)同一套模型,看它跑出什么坏数字。参数baseline/failures/budgetFen。 - 第 16 章 全链路装配:回放录制由
seed+count+spanMs+cols确定性生成,时间戳均匀铺在(0, spanMs]、整数毫秒升序,下标由整数 LCG 产生;Node 侧与浏览器侧读同一份参数、跑同一段core/src/l16/replay.ts,终局摘要与digest必须逐位相同。参数seed/count/spanMs/cols/startValue/mergeAt。
六、一键复现与排错顺序
复现任意一节的标准动作:把该节的 file: 块还原到 pebble/<file>(工作区已就绪),再跑它 verify: 里那条命令。建议的整卷复现顺序是:
- 环境自检:
/opt/homebrew/bin/python3 sim/ch00_env_check.py,确认解释器、numpy / pandas 与 Node ≥ 26 都在。 - S 层:
for f in sim/l*.py; do /opt/homebrew/bin/python3 "$f" --config config/ --out out/ || exit 1; done,跑通全部仿真并落out/。 - P 层:
node --test 'core/test/l*.test.ts',全部内核断言通过。 - W 层:对第 1–8、12、15、16 章逐个
node web/build_l<NN>_*.mjs,再NODE_PATH=/opt/homebrew/lib/node_modules node web/verify_l<NN>_*.mjs web/dist/l<NN>.html无头验收。 - B 层:
for f in sim/*_bench.py; do /opt/homebrew/bin/python3 "$f" --config config/ --out out/ || exit 1; done,看软告警基准。
数字对不上时的排错顺序:
- 解释器对吗? 用了系统 3.9.6 会在
import pandas处报ModuleNotFoundError,看起来像「代码错了」,其实是解释器错了。命令一律写/opt/homebrew/bin/python3。 node版本对吗? 低于 26 就没有原生 TypeScript,.ts直接跑不起来。- glob 加引号了吗?
node --test 'core/test/l04_*.test.ts'的引号不能省,否则 shell 先展开、再被node --test当多个参数处理。node --test <目录>不会发现.ts,必须传 glob。 - 前缀对吗? 卷三是
l<NN>_,不是卷一的ch<NN>_或卷二的e<NN>_。写错前缀会匹配不到文件(空跑)或误匹配另一卷。 - Playwright 预热了吗? W 层未用
createRequire复用全局包时,无头浏览器起不来,canvas 截图为空白——这是环境问题,不是渲染错。 - 配置被改过吗? 这正是「一份 config 三种消费」该有的行为——改
config/l03_*.json里的参数,sim/、core/、web/三处结果都会跟着变。 - 是不是「同机相对量级」? 正文标了「同机相对量级,绝对数值不可移植」的地方,绝对值依赖机器,只有相对关系可移植。
- 还不对,那就是书错了。 请告诉我具体的节号与数字。
环境口径与卷一、卷二的实跑命令另见卷一的附录 A 与卷二的附录 C 。回到卷三目录页 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。