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

《小游戏开发实战》卷五《跨平台发行与出海》附录:全书公式与参数索引、`g<章号>_` 工具链命名约定、六层可运行性口径与逐层复现命令,并给出可跑的 S 层清点脚本与 P 层索引校验内核。按章节提供仿真、逻辑内核与原型;当前试点阶段已有复核记录,完整项目的配置、共享模块和交付验收仍在补齐。

这一页解决一个问题:把卷五 16 章散落在正文里的配置键、复现命令与产物清单收进一处。它不重复正文的推导,只保证你想自己跑一遍时有唯一入口。

一、这份索引解决什么

卷五的可运行性主张只有一句话:一份 config,三种消费——同一份 pebble/config/ 里的整数配置,被 Python 仿真、TypeScript 内核与浏览器单文件原型同时读入。这句话要能被检验,就必须能回答三个问题:

哪些文件属于这一卷:本卷工具链文件名一律带 g<章号>_ 前缀,与前四卷的 ch / e / l / o 互不重叠。每个数字从哪来:正文里出现的每个参数都能追到 config/g<章号>_*.json 的一个键,且全为整数(金额用「分」,比例用「千分比」)。怎么复现:每一档有一条确定的命令行,跑完产出 JSON / CSV / SVG 或测试结果。

下面三节分别给出命名约定、环境前提与逐层复现命令;第六、七节给出两个可跑的索引脚本,把这份索引本身变成可验证的产物。

二、卷五工具链命名约定

本卷四种形态的命名如下(NN 为两位章号):

config/g03_matrix.json          每章一份或多份整数配置表(D 层)
sim/g03_build_matrix.py         Python 仿真脚本(S 层),一律用 /opt/homebrew/bin/python3
core/src/g03/platform.ts        TypeScript 内核模块(P 层),每章一个 gNN/ 目录
core/test/g03_matrix.test.ts    内核测试,glob 必须写全 g03_*.test.ts
web/build_g05_text_fit.mjs      浏览器原型构建脚本(W 层,前缀在第二位)
web/verify_g05_text_fit.mjs     无头验收脚本,产出 out/shots/g05_text_fit.png

注意 web/ 层前缀不在首位(build_g05_… / verify_g05_…),清点脚本必须同时认这两套写法,否则 W 层会被统计成 0。测试 glob 也必须写全章号:

# snippet: 五卷的测试 glob 前缀互不重叠,node --test 不会跨卷误匹配
node --test 'core/test/ch00_*.test.ts'   # 卷一
node --test 'core/test/e03_*.test.ts'    # 卷二
node --test 'core/test/l03_*.test.ts'    # 卷三
node --test 'core/test/o03_*.test.ts'    # 卷四
node --test 'core/test/g03_*.test.ts'    # 卷五

不要写成裸 g*:node --test 接收的是 glob 而非目录,economy 等共享文件名也以 e 开头,裸通配会跨卷误匹配。

三、环境速查

项值说明
Python/opt/homebrew/bin/python3(3.14.6)带 numpy / pandas / PIL;系统 /usr/bin/python3 是 3.9.6,不要用
Node26 及以上原生跑 TypeScript,但是 strip-only:禁 enum / namespace / 参数属性 / 装饰器
Playwright全局已装W 层验收用 NODE_PATH=/opt/homebrew/lib/node_modules
图表纯 Python xml.etree本机没有 matplotlib,SVG 一律手写
包管理无不建 package.json、不装 npm 包,原型与仿真均零依赖

两条必须记住的陷阱:python3 有两个,正文与脚本一律写全路径 /opt/homebrew/bin/python3;date 必须严格早于构建时刻,否则 Hugo 的 buildFuture = false 会静默跳过该页并把指向它的链接变成 REF_NOT_FOUND。

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

层含义复现命令产物
S 仿真Python 数值仿真/opt/homebrew/bin/python3 sim/g03_build_matrix.py --config config/ --out out/out/*.json + .csv + .svg
P 内核TypeScript 逻辑内核node --test 'core/test/g03_*.test.ts'测试结果(pass / fail)
W 网页单文件 HTML 原型node web/build_g05_text_fit.mjs → NODE_PATH=/opt/homebrew/lib/node_modules node web/verify_g05_text_fit.mjs web/dist/g05.htmlweb/dist/g05.html + 截图
D 静态配置表与清单node --test 'core/test/g03_*.test.ts' 中的 schema 用例校验结果
X 不可复现真实平台后台 / 审核 / 到账 / 税务结论无正文逐处标注
B 基准扰动下的稳定性与同章 S 脚本一同跑JSON + 软告警(不硬失败)

跑 S 层时不要用通配符:python3 sim/g03_*.py 只会执行第一个脚本,其余被当成参数吃掉。逐个跑,或用 for 循环:

# snippet: 逐章逐个跑 S 层,避免 shell 把通配符展开成多个文件名
for f in sim/g03_*.py; do /opt/homebrew/bin/python3 "$f" --config config/ --out out/ || exit 1; done

五、全书公式与参数索引

每个参数都能追到一个配置键;下表按章给出主配置与关键参数(全部为整数)。

  • 第 1–2 章 g01_platform_tiers.json(playRatePermille / tiers)、g01_share_terms.json(revenueMix)、g01_selection.json(budgetManDays / dimensions)、g02_sdk_matrix.json(formatKeys / weights)、g02_submission.json(materialKeys / rejectionReasons)、g02_platform_features.json(featureKeys / fallbackCost)。
  • 第 3–4 章 g03_channels.json(base / features)、g03_matrix.json(baseSizeKb / assetTiers)、g03_consistency.json(errorRollbackPermille / goldenSourceRev)、g04_adformats.json、g04_pure_iaa.json(sessionsPerDauPermille / publisherSharePermille)、g04_revenue_mix.json(totalDau / daysPerMonth)。
  • 第 5–6 章 g05_i18n.json(baseLocale / keyRules)、g05_langpack.json(chains / packBytes)、g05_textfit.json(containerWidthPx / baseWidthPx)、g06_taboo.json(rules)、g06_variants.json(markets)、g06_format.json(locales / fx / samples)。
  • 第 7–8 章 g07_pseudo.json(padCount / charWidthPx)、g07_qa.json(checks / results)、g07_cost.json(translateFenPerWord / reviewFenPerHour / layoutFenPerKey)、g08_lang_metric.json(minSampleNewUsers)、g08_event_creative.json(languages / events)、g08_minor_lang.json(budgetFen / horizonMonths)。
  • 第 9–10 章 g09_data_map.json(regimes / fields)、g09_kids_policy.json(ageBands / platformFlags)、g09_consent_transfer.json(purposes / transfers)、g10_att_link.json(attStates / skanFields)、g10_alt.json(seed / noisePermille)、g10_dp_agg.json(budget / minCount)。
  • 第 11–12 章 g11_payout_methods.json(methods)、g11_fx_fee.json(commissionPermille / pairs)、g11_tax_form.json(forms / withholding)、g12_content_rights.json(regions / contents)、g12_sanction_regions.json(tierPolicy)、g12_fraud.json(seed / cohorts / sweepThresholds)。
  • 第 13–14 章 g13_ua_caliber.json(spendFen / settleModels)、g13_creative_compliance.json(severity / regions)、g13_roi_stop_loss.json(seed / stopLoss)、g14_rank_structure.json(signals)、g14_competitor_localization.json(dimensionWeights)、g14_genre_region.json(affinityPermille)。
  • 第 15–16 章 g15_timeline.json(sharedTasks)、g15_landing.json(items)、g15_data_iter.json(seed / volatilityPermille)、g16_manifest.json(chapters)、g16_fullchain.json(netArpdauThresholdDeciFen)、g16_finale.json(readyThresholdPermille / chain / unknowns)。

三条硬规则贯穿全部配置:不用浮点(金额用「分」、比例用「千分比」);除法一律整数地板除;随机数必须可播种(core/ 内禁用 Math.random(),TS 侧幂运算用 BigInt)。

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

下面这段脚本扫描 pebble/ 下全部 g<章号>_ 文件,按章与目录分层统计,产出 out/g17_manifest.json / .csv / .svg。它同时是这份索引的自检工具:跑一遍就知道有没有哪一章的文件被漏算。

它只读一份配置 config/g17_manifest.json:prefix(卷五为 g)、chapters(16)与 dirs(config / sim / core/src / core/test / web / web/dist / out),全整数、无浮点、无随机。脚本 sim/g17_manifest.py(与工作区文件逐字节一致):

#!/usr/bin/env python3
# file: sim/g17_manifest.py
# tier: S
# verify: /opt/homebrew/bin/python3 sim/g17_manifest.py --config config/ --out out/
"""清点卷五工具链:扫描 pebble/ 下属于本卷的文件,按章与目录分层统计,
产出 out/g17_manifest.json / .csv / .svg。纯标准库,全整数,无随机。

三种命名形态都要认:config / sim / core/test 是 `<前缀><章号>_…`(前缀在首位),
web 层是 `build_<前缀><章号>_…` / `verify_<前缀><章号>_…`(前缀在第二位),
`core/src/` 下每章是一个 `<前缀><章号>/` 目录(里面的 .ts 文件不带前缀)。
目录按名归章并递归统计其中的常规文件,不把目录本身记成一条——卷四附录 E
那版脚本没有这一步,把目录当成文件计数、行数记 0,故两卷数字不可直接相加。
"""
import argparse
import json
import os
import xml.etree.ElementTree as ET


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 count_entry(dpath, fname):
    """把一个条目折算成 (文件数, 行数):目录按名归章并递归统计其中的常规文件。"""
    full = os.path.join(dpath, fname)
    if not os.path.isdir(full):
        return 1, line_count(full)
    n = lines = 0
    for sub in sorted(os.listdir(full)):
        subfull = os.path.join(full, sub)
        if os.path.isfile(subfull):
            n += 1
            lines += line_count(subfull)
    return n, lines


def write_svg(path, rows, peak):
    h, w = 220, 40 + 30 * len(rows)
    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 (g* tools)'
    for i, r in enumerate(rows):
        x = 24 + 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': '#47a'})
        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()

    with open(os.path.join(args.config, 'g17_manifest.json'), encoding='utf-8') as fh:
        cfg = json.load(fh)
    root = os.path.dirname(os.path.abspath(args.config.rstrip('/')))
    rows, total_files, total_lines = [], 0, 0
    for ch in range(1, cfg['chapters'] + 1):
        ch_id = f'{ch:02d}'
        by_dir, ch_files, ch_lines = {}, 0, 0
        for d in cfg['dirs']:
            dpath = os.path.join(root, d)
            n = 0
            for fname in sorted(os.listdir(dpath)) if os.path.isdir(dpath) else []:
                if chapter_of(fname, cfg['prefix']) == ch_id:
                    nf, nl = count_entry(dpath, fname)
                    n += nf
                    ch_lines += nl
            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, 'g17_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, 'g17_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, 'g17_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()

真跑输出(/opt/homebrew/bin/python3 sim/g17_manifest.py --config config/ --out out/):

01:17:1382,02:21:1486,03:21:1184,04:15:815,05:16:937,06:18:1235,07:17:1000,08:18:1357,09:21:1449,10:20:1346,11:11:610,12:21:1702,13:19:1268,14:17:1177,15:17:1099,16:18:2177;files=287,lines=20224

core/src/ 是三种命名形态里唯一按目录归章的:每章一个 gNN/ 目录,里面的 .ts 文件不带章号前缀。清点脚本按目录名归章并递归统计其中的常规文件,不把目录本身记成一条——卷四附录 E 那版脚本没有这一步,把目录当成文件计数、行数记 0;本版修掉了这个口径缺陷,故两卷的清点数字不可直接相加。

七、P 层索引校验内核:与 Python 侧逐字符对拍

清点逻辑在 TypeScript 侧重写了一遍(core/src/g17/index_audit.ts),用同一份配置扫同一个工作区,摘要必须与 Python 侧逐字符一致。这是「一份逻辑两种宿主」在附录里的最小落点。核心函数摘录:

// snippet: 索引校验内核的核心函数(完整文件见 core/src/g17/index_audit.ts)
/** 返回该文件名归属的两位章号;不属于本卷工具链则返回 null。 */
export function chapterOf(fname: string, prefix: string): string | null {
  for (const pre of [prefix, `build_${prefix}`, `verify_${prefix}`]) {
    if (fname.startsWith(pre)) {
      const head = fname.slice(pre.length, pre.length + 2);
      if (isTwoDigits(head)) return head;
    }
  }
  return null;
}

// …(类型声明、lineCount、countEntry 与逐章循环略;
//   lineCount 等价于 count('\n') + 1,countEntry 对 gNN/ 目录递归统计其中的 .ts;
//   完整文件见 core/src/g17/index_audit.ts)

  let digest = rows.map((r) => `${r.chapter}:${r.files}:${r.lines}`).join(",");
  digest += `;files=${totalFiles},lines=${totalLines}`;
  const peakFiles = rows.reduce((m, r) => (r.files > m ? r.files : m), 0);

验收命令与真实结果(node --test 的 6 个用例):

# snippet: 与 Python 侧清点逐字符对拍
node --test 'core/test/g17_index_audit.test.ts'
✔ 跨语言一致:TS 清点摘要 == Python 金标准   ✔ 章号齐全、无空章、合计自洽
✔ 峰值章等于逐章文件数的最大值   ✔ 归章认三套前缀;错前缀返回 null
✔ core/src 一档统计的是 .ts 文件而非目录本身   ✔ 行数口径等价于 count('\n') + 1
ℹ tests 6  ℹ pass 6  ℹ fail 0

八、一键复现与排错顺序

从零复现卷五全部产物,按这个顺序走:

  1. /opt/homebrew/bin/python3 sim/ch00_env_check.py —— 确认解释器、numpy / pandas 与 Node 版本。
  2. /opt/homebrew/bin/python3 sim/g17_manifest.py --config config/ --out out/ —— 清点工具链,确认文件数与行数落在预期区间。
  3. 逐章跑 S 层:for f in sim/g<NN>_*.py; do /opt/homebrew/bin/python3 "$f" --config config/ --out out/; done(不要用裸通配符)。
  4. node --test 'core/test/g<NN>_*.test.ts' —— P 层逐章跑;W 层两节用 build_g05 / verify_g05 与 build_g07 / verify_g07。
  5. node --test 'core/test/g17_index_audit.test.ts' —— 回到这份索引本身做一次跨语言对拍。

排错顺序(按「先怀疑自己的判据」排):

  1. 数字对不上:先确认跑的是本卷前缀。mg_run*.py 的 --book 是装饰性参数,只读单槽 out/blocks.json,先 mg_extract.py --book 跨平台发行与出海 覆盖,否则跑的是上一卷的块。
  2. W 层断言随机失败:mg_runweb.py 的截图断言扫全局 out/shots/*.png,每卷开工前清空该目录;无头 Chromium 冷启动首个页面读 canvas 像素可能恒空白,断言写 >= 1 px 并重试一次。
  3. P 层报 ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX:Node 26 是 strip-only,把 enum / namespace / 参数属性 / 装饰器换掉。
  4. 测试 glob 命中别的卷:写全 g<章号>_*.test.ts,不要写裸 g*。
  5. 链接 404:站内单段 /<topic>/ 是死链(普通文章在根级 /<slug>/,栏目页在 /posts/<topic>/);含中文的 slug 必须核对 public/ 产物,Hugo 会把路径里的 Latin 字符小写化。
  6. 构建报 REF_NOT_FOUND:检查 date 是否早于构建时刻;--quiet 会掩盖这类错误却仍 exit 0,必须用非 quiet 构建。

卷四的同类索引见附录 E:全书公式、参数与数据集索引 ;本卷的定位与分工见卷五目录 。

继续阅读

探索更多技术文章

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

全部文章 返回首页

「minigame」更多文章

  1. 9.3 同意管理与数据出境
  2. 9.2 COPPA 与儿童向游戏的限制
  3. 9.1 GDPR / CCPA 的工程落点