附录 E 全书公式、参数与数据集索引

《小游戏开发实战》卷四《商业运营实战》的复现索引:按 16 章列出 pebble/ 下全部 config、sim、core、web 文件,写清本卷工具链的 o<章号>_ 前缀约定、两个 python3 与 Node 26 的环境速查、六层可运行性口径的逐层复现命令,并给出全书公式、参数与数据集索引。

这一页解决一个问题:卷四里出现的每一段代码、每一份配置、每一个数据集,在 pebble/ 里叫什么、放在哪、怎么跑。 它是一张可检索的清单,也是一份复现手册。

一、这份索引解决什么

卷四 16 章共落了 311 个文件、17940 行(含 S / P / W / D / B 五类产物,X 层没有产物)。产物一多就需要一张总表,回答三件事:

  1. 这个产物叫什么、放在哪个目录——第二节按 16 章列出 pebble/ 下的全部文件,命名规则统一,一眼能定位。
  2. 它属于哪一层、怎么验证——六层口径(S / P / W / D / X / B)的复现命令各不相同,第四节把它们并排写出来。
  3. 它依赖什么环境——两个 python3、Node 26、全局 Playwright,任何一处不对都会让「代码看起来错了」,其实只是环境错了。

贯穿全书的仍是《合成小岛》(merge-island)与工具链 pebble,沿用前三卷的两条主张:一份 config,三种消费、一份逻辑,两种宿主。

二、卷四工具链命名约定

卷一用 ch00–ch15 前缀,卷二改用 e<章号>_ 前缀,卷三用 l<章号>_ 前缀,卷四一律用 o<章号>_ 前缀(两位章号,o01…o16):

config/o09_settlement.json          # 配置
sim/o09_settlement.py               # 仿真(S 层)
core/src/o09/settlement.ts          # 内核源码目录(P 层)
core/test/o09_settlement.test.ts    # 内核测试(P 层)
web/build_o09_calculator.mjs        # 单文件 HTML 构建脚本(W 层)
web/verify_o09_calculator.mjs       # Playwright 无头验收(W 层)

为什么必须换前缀:node --test 接收的是 glob,不是目录。若卷四继续沿用旧前缀,通配符会同时匹配到几卷的测试文件,一条命令里混进别的卷的断言,输出就不可信了。换成 o<章号>_ 之后:

# snippet: 四卷的测试 glob 前缀互不重叠,node --test 不会跨卷误匹配
node --test 'core/test/ch*.test.ts'   # 卷一
node --test 'core/test/e*.test.ts'    # 卷二(注意 economy 也以 e 开头)
node --test 'core/test/l*.test.ts'    # 卷三
node --test 'core/test/o*.test.ts'    # 卷四

四个 glob 互不重叠。复现时还要把章号写全(o04_*.test.ts 而不是裸 o*),免得无关文件混进同一次断言。

各章文件对照表

下表把卷四 16 章各自的产物按目录列出。同一章在 config/、sim/、core/test/ 之间尽量保持同名,这是「一份 config 三种消费」在文件系统上的体现;core/src/o<NN>/ 是该章的内核源码目录,out/ 是该章 S 层仿真的产物件数。

章configsimcore/testwebout
101_bid、01_coldstart、01_hierarchy01_bid、01_coldstart、01_coldstart_bench、01_hierarchy01_bid.test、01_coldstart.test、01_hierarchy.test—10
202_bid_choice、02_ramp、02_stoploss02_bid_choice、02_ramp、02_stoploss、02_stoploss_bench02_bid_choice.test、02_ramp.test—10
303_fatigue、03_line、03_matrix03_fatigue、03_fatigue_bench、03_line、03_matrix03_fatigue.test、03_line.test、03_matrix.test—10
404_att、04_recon、04_window04_att、04_recon、04_recon_bench、04_window04_att.test、04_recon.test、04_window.test—10
505_adapters、05_mediation、05_waterfall05_adapter_score、05_mediation、05_waterfall、05_waterfall_bench05_adapter.test、05_mediation.test、05_waterfall.test—11
606_adslot、06_ecpm、06_funnel06_adslot_matrix、06_ecpm_tier、06_funnel_bench、06_funnel_diag06_adslot.test、06_ecpm.test—10
707_order、07_pay_channel、07_recon07_order_flow、07_pay_matrix、07_recon07_order.test、07_pay.test、07_recon.test—9
808_adcopy、08_consent、08_minorad08_adcopy_scan、08_consent、08_minorad、08_minorad_bench08_adcopy.test、08_consent.test—10
909_channel_pkg、09_channel_tier、09_settlement09_channel_tier、09_pkg_validate、09_settlement09_pkg.test、09_settlement.testbuild_o09_calculator.mjs、verify_o09_calculator.mjs9
1010_clauses、10_decision、10_leverage10_clauses_check、10_decision、10_leverage10_clauses.test—9
1111_license_graph、11_material、11_reverse11_license_graph、11_material、11_reverse11_license.test、11_material.test—9
1212_cashflow、12_netpayout、12_settlement12_cashflow、12_cashflow_bench、12_netpayout、12_settlement12_settlement.test—10
1313_capacity、13_routing、13_sop13_capacity_plan、13_routing_matrix、13_sop_rhythm13_capacity.test、13_routing.test、13_sop.test—9
1414_decision、14_monthly、14_unit14_decision、14_monthly、14_monthly_bench、14_unit14_decision.test、14_unit.test—10
1515_failure、15_matrix、15_retro15_failure、15_matrix、15_retro15_matrix.test—9
1616_delivery、16_handbook、16_recon16_delivery、16_recon、16_recon_bench16_handbook.test、16_recon.test—7

表内省略扩展名:config/ 是 .json,sim/ 是 .py(*_bench 属 B 层),core/test/ 是 .test.ts,web/ 是 .mjs。第 9 章的 web 列是两个文件,也是全书唯一的 W 层落点。

实测规模:config/ 48 个、sim/ 57 个(其中 7 个 *_bench.py)、core/src/ 16 个 o<NN>/ 目录、core/test/ 35 个、web/ 2 个、web/dist/ 1 个 o09.html、out/ 152 个——共 311 个文件、17940 行,由第六节的清点脚本在本机实跑得到。

三、环境速查

项值说明
Python(S 层)3.14.6,/opt/homebrew/bin/python3带 numpy / pandas / PIL
Python(系统)3.9.6,/usr/bin/python3无 numpy / pandas,命令一律写全路径
Node26.5.0原生跑 TypeScript,strip-only,禁 enum / namespace / 参数属性 / 装饰器
Playwright1.59.1,Chromium 已缓存装在 /opt/homebrew/lib/node_modules,用 NODE_PATH + createRequire 复用
matplotlib / scipy未安装图表走纯 Python SVG(xml.etree)
tsc / tsx / vitest / npm 包均不使用无 package.json、无 npm install
  1. 用错 python3 会静默降级——S 层仿真依赖 numpy / pandas,系统 3.9.6 没有这些库,import pandas 直接报 ModuleNotFoundError。命令一律写全路径 /opt/homebrew/bin/python3。
  2. NODE_PATH 对 ESM import 无效——W 层脚本里必须 createRequire(import.meta.url) 之后再 require("playwright"),不能写 import { chromium } from "playwright"。

环境自检脚本 sim/ch00_env_check.py 与卷一、卷二、卷三共用,见卷一的附录 A 。

四、六层可运行性口径与复现命令

每一层都有固定的复现命令。<NN> 换成章号(如 o09),命令均在 pebble/ 下执行:

层含义命令
S 仿真Python 读 config 跑仿真,产出 out/*.json / .csv / .svg`for f in sim/o_*.py; do /opt/homebrew/bin/python3 “$f” –config config/ –out out/
P 内核TypeScript 内核 + node --test 断言node --test 'core/test/o<NN>_*.test.ts'
W 网页单文件 HTML 构建 + Playwright 无头验收node web/build_o09_calculator.mjs && NODE_PATH=/opt/homebrew/lib/node_modules node web/verify_o09_calculator.mjs web/dist/o09.html
D 静态config 结构、账户层级、资质清单、合同条款、SOP 模板/opt/homebrew/bin/python3 sim/ch00_validate_config.py --config config/ --schema config/ch00_schema.json
X 不可复现真实投放后台、真实版号结果、真实结算单、真实合同无命令——正文每处已标注
B 基准*_bench.py 软告警基准`for f in sim/o*_bench.py; do /opt/homebrew/bin/python3 “$f” –config config/ –out out/

为什么 S 层要写 for f in …; do …; done 而不是 python3 sim/o09_*.py:shell 会把通配符展开成多个文件名传给同一个 python3,只有第一个脚本真的执行,其余被当成参数吃掉。逐个跑或用 for 循环才安全;node --test 自己展开 glob,不受影响。

五、全书公式与参数索引

下面按章列出核心公式与关键参数名。参数名一律取自对应 config/o<NN>_*.json 的键,金额一律用整数(分)、比例用整数千分比,全程地板除、无浮点——这是同一份 config 在 Python 仿真、TS 内核、浏览器原型三处能逐位一致的前提。

  • 第 1 章 投放后台与账户结构:账户日预算 accountDailyBudgetFen 按计划 / 广告组分摊,各 groups[].sharePermille 之和恒为 1000;minAdGroupBudgetFen 是广告组预算下限。
  • 第 2 章 出价与放量:o02_bid_choice.json 的 modes 给出同一 budgetFen 下 oCPM / CPA / ROI 三种出价在 minInstalls 约束下的安装量;放量按 safeStepPermille 爬坡,超限触发 resetPenaltyPermille(学习期重置惩罚)。
  • 第 3 章 素材工业化:CTR 按 baseCtrPermille × (decayNum / decayDen)^day 衰减,跌破 thresholdPermille 即进入 replacementCycles 轮替换;测试矩阵用 z2ThresholdX100 做两比例检验阈值。
  • 第 4 章 移动归因:真实安装 trueInstalls 经 optInPermille(授权率)、idfaMatchPermille、skanCoveragePermille 逐层折损;o04_recon.json 的 tolerancePermille 是渠道对账容差,o04_window.json 的 clickWindowMinutes / viewWindowMinutes 定义归因窗口。
  • 第 5 章 广告聚合平台:瀑布流按 o05_waterfall.json 里各网络的 ecpmCent 降序取;o05_mediation.json 用 weights 做广告源加权;o05_adapters.json 的 capKeys 定义各网络能力键。
  • 第 6 章 广告位配置与调优:o06_ecpm.json 的 floorCandidatesCent 扫描底价,按 tiers / groups 分层;o06_funnel.json 的 thresholdsPermille 是填充率 / 展示率的诊断线。
  • 第 7 章 内购与支付通道:订单回调按 callbackFirstSuccessPermille → callbackRetrySuccessPermille 重试至 retryMax,duplicatePermille 是重复回调比例;o07_recon.json 的 platformCutPermille 是平台抽成。
  • 第 8 章 变现合规:o08_adcopy.json 的 rules[].severity 决定广告文案能否过审,replacements 给替换词;o08_minorad.json 的 adEveryMin 与每局次数上限共同约束未成年人的广告暴露。
  • 第 9 章 渠道与联运:o09_settlement.json 的 sharePermille 是渠道分成,当月净收入 netMonthlyFen = grossFen × (1000 − sharePermille) ÷ 1000(地板除),账期 settlementLagMonths 决定峰值资金占用 peakGapFen。
  • 第 10 章 代理发行与商务:o10_decision.json 用 platformNetPermille 与 selfUaFen 比较自发行与代理发行的净收入;o10_clauses.json 的 riskWeightPermille 加权条款风险,超 reviewThresholdPermille 必须复核。
  • 第 11 章 版号与资质办理:o11_license_graph.json 的 licenses / obtained 构成前置依赖图;o11_reverse.json 由 target 日期倒排,含 jitterDays 抖动与 bufferDefault 缓冲。
  • 第 12 章 结算与财务:o12_settlement.json 的 periodDays 与各 platforms[].settleDays 决定到账节奏;o12_cashflow.json 的 reservePermille 是留存准备金比例,reserveReleaseDays 是释放周期。
  • 第 13 章 运营机制与协作:o13_capacity.json 由 demand 与 outsourcers[].capacityPerWeek 求产能缺口;o13_routing.json 按 severities / eventTypes 把事件路由到 owners。
  • 第 14 章 从投放到变现的完整账:单用户账为 LTV − CAC,o14_unit.json 的 variableCostPerUserFen 是单用户变动成本;o14_decision.json 用 stopFloorPermille / scaleFloorPermille / maxScalePermille 判定止损或加投。
  • 第 15 章 案例与失败模式:o15_failure.json 按 domains 归集失败场景;o15_matrix.json 的 decisions 是关键决策对照表;o15_retro.json 用 variableCostPerInstallFen 复盘。
  • 第 16 章 操盘收官:o16_recon.json 是全链路对账锚点——IAA 加 IAP 收入扣 withholdPermille 预扣,再减 variableCostPerDauFen 与 fixedCostFen,得经营净额;o16_delivery.json 的 readyThresholdPermille 是交付就绪线。

六、S 层清点脚本:把这份索引重新跑出来

第二节的对照表与「实测规模」不是手抄的,而是下面这个脚本扫出来的。它读 config/o17_manifest.json,按目录与章号清点 pebble/ 下属于卷四的文件,产出 out/o17_manifest.json / .csv / .svg——改一个目录或加一节,重跑一次就能拿到新的索引。

{
  "version": 1,
  "game": "merge-island",
  "observedAt": "2026-10",
  "note": "卷四工具链清点参数。口径是「工作区可复现」——扫描 pebble/ 下 o<NN>_ 前缀的文件,按章与目录分层统计,不是任何平台后台的实测。prefix=工具链文件名前缀(卷四 o、卷五 g);chapters=章节数;dirs=参与清点的目录;各目录里的 o<NN>_ 文件按文件名前两位数字归章。web/dist 是 build 脚本的产物,单列一档。全整数,无浮点,无随机。",
  "prefix": "o",
  "chapters": 16,
  "dirs": ["config", "sim", "core/src", "core/test", "web", "web/dist", "out"]
}
#!/usr/bin/env python3
# file: sim/o17_manifest.py
# tier: S
# verify: /opt/homebrew/bin/python3 sim/o17_manifest.py --config config/ --out out/
"""清点卷四工具链:扫描 pebble/ 下属于本卷的文件,按章与目录分层统计,
产出 out/o17_manifest.json / .csv / .svg。纯标准库,全整数,无随机。

命名口径两套:config / sim / core 是 `<前缀><章号>_…`(前缀在首位),
web 层是 `build_<前缀><章号>_…` / `verify_<前缀><章号>_…`(前缀在第二位),
两套都要认,否则 web 层会被清点成 0。
"""
import argparse
import json
import os
import xml.etree.ElementTree as ET


def load_json(path):
    with open(path, encoding='utf-8') as fh:
        return json.load(fh)


def line_count(path):
    try:
        with open(path, encoding='utf-8') as fh:
            return fh.read().count('\n') + 1
    except (UnicodeDecodeError, OSError):
        return 0


def chapter_of(fname, prefix):
    """返回该文件归属的两位章号;不属于本卷工具链则返回 None。"""
    for pre in (prefix, f"build_{prefix}", f"verify_{prefix}"):
        if fname.startswith(pre):
            head = fname[len(pre):len(pre) + 2]
            if len(head) == 2 and head.isdigit():
                return head
    return None


def write_svg(path, rows, peak):
    h, w, pad = 220, 40 + 30 * len(rows), 24
    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, 'text', {'x': '8', 'y': '16', 'font-size': '11'}).text = \
        'files per chapter (o* tools)'
    for i, r in enumerate(rows):
        x = pad + i * 30
        bh = 0 if peak == 0 else (160 * r['files']) // peak
        ET.SubElement(root, 'rect', {
            'x': str(x), 'y': str(190 - bh), 'width': '20', 'height': str(bh),
            'fill': '#4a7',
        })
        ET.SubElement(root, 'text', {
            'x': str(x + 10), 'y': '205', 'font-size': '9', 'text-anchor': 'middle',
        }).text = r['chapter']
    ET.ElementTree(root).write(path, encoding='utf-8', xml_declaration=True)


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

    cfg = load_json(os.path.join(args.config, 'o17_manifest.json'))
    root = os.path.dirname(os.path.abspath(args.config.rstrip('/')))
    prefix = cfg['prefix']
    rows = []
    total_files = total_lines = 0
    for ch in range(1, cfg['chapters'] + 1):
        ch_id = f'{ch:02d}'
        by_dir = {}
        ch_files = ch_lines = 0
        for d in cfg['dirs']:
            dpath = os.path.join(root, d)
            n = 0
            if os.path.isdir(dpath):
                for fname in sorted(os.listdir(dpath)):
                    if chapter_of(fname, prefix) == ch_id:
                        n += 1
                        ch_lines += line_count(os.path.join(dpath, fname))
            by_dir[d] = n
            ch_files += n
        rows.append({'chapter': ch_id, 'files': ch_files, 'lines': ch_lines, 'byDir': by_dir})
        total_files += ch_files
        total_lines += ch_lines

    digest = ','.join(f"{r['chapter']}:{r['files']}:{r['lines']}" for r in rows)
    digest += f';files={total_files},lines={total_lines}'
    peak = max((r['files'] for r in rows), default=0)

    os.makedirs(args.out, exist_ok=True)
    with open(os.path.join(args.out, 'o17_manifest.csv'), 'w', encoding='utf-8') as fh:
        fh.write('chapter,files,lines\n')
        for r in rows:
            fh.write(f"{r['chapter']},{r['files']},{r['lines']}\n")
    write_svg(os.path.join(args.out, 'o17_manifest.svg'), rows, peak)
    out = {
        'version': cfg['version'], 'game': cfg['game'], 'observedAt': cfg['observedAt'],
        'rows': rows, 'totalFiles': total_files, 'totalLines': total_lines,
        'peakFiles': peak, 'digest': digest,
    }
    with open(os.path.join(args.out, 'o17_manifest.json'), 'w', encoding='utf-8') as fh:
        json.dump(out, fh, ensure_ascii=False, indent=2)
    print(json.dumps({'totalFiles': total_files, 'totalLines': total_lines,
                      'peakFiles': peak, 'digest': digest}, ensure_ascii=False))


if __name__ == '__main__':
    main()

在本机跑得到 files=311, lines=17940,逐章件数从第 16 章的 16 件到第 5 章的 22 件。注意 chapter_of() 认的是 o / build_o / verify_o 三个前缀——web 层文件名是 build_o09_calculator.mjs,前缀不在首位,只认 o 会把 web/ 清点成 0。

七、一键复现与排错顺序

复现任意一节的标准动作:把该节的 file: 块还原到 pebble/<file>(工作区已就绪),再跑它 verify: 里那条命令。建议的整卷复现顺序是:

  1. 环境自检:/opt/homebrew/bin/python3 sim/ch00_env_check.py,确认解释器、numpy / pandas 与 Node ≥ 26 都在。
  2. S 层:for f in sim/o*.py; do /opt/homebrew/bin/python3 "$f" --config config/ --out out/ || exit 1; done,跑通全部仿真并落 out/。
  3. P 层:node --test 'core/test/o*.test.ts',全部内核断言通过。
  4. W 层:node web/build_o09_calculator.mjs,再 NODE_PATH=/opt/homebrew/lib/node_modules node web/verify_o09_calculator.mjs web/dist/o09.html,无头验收分成与结算计算器。
  5. B 层:for f in sim/o*_bench.py; do /opt/homebrew/bin/python3 "$f" --config config/ --out out/ || exit 1; done,看软告警基准。

数字对不上时的排错顺序:

  1. 解释器对吗? 用了系统 3.9.6 会在 import pandas 处报 ModuleNotFoundError,看起来像「代码错了」,其实是解释器错了。
  2. node 版本对吗? 低于 26 就没有原生 TypeScript,.ts 直接跑不起来。
  3. glob 加引号了吗? node --test 'core/test/o04_*.test.ts' 的引号不能省,否则 shell 先展开、再被 node --test 当多个参数处理。node --test <目录> 不会发现 .ts,必须传 glob。
  4. 前缀对吗? 卷四是 o<NN>_,不是卷一的 ch<NN>_、卷二的 e<NN>_ 或卷三的 l<NN>_。写错前缀会匹配不到文件(空跑)或误匹配别的卷。
  5. web 层的前缀在哪? 是 build_o<NN>_ / verify_o<NN>_,前缀在第二位——按 o<NN>_ 去 web/ 里找会一个都找不到。
  6. Playwright 复用对了吗? 未用 createRequire 复用全局包时无头浏览器起不来,截图会是空白——这是环境问题,不是渲染错。
  7. 配置被改过吗? 这正是「一份 config 三种消费」该有的行为——改 config/o09_settlement.json 里的分成或账期,sim/、core/、web/ 三处结果都会跟着变。
  8. 是不是「同机相对量级」? 正文标了这句话的地方,绝对值依赖机器,只有相对关系可移植。
  9. 还不对,那就是书错了。 请告诉我具体的节号与数字。

环境口径与实跑命令另见卷一的附录 A 、卷二的附录 C 与卷三的附录 D 。回到卷四目录页 。

继续阅读

探索更多技术文章

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

全部文章 返回首页

「minigame」更多文章

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