这一页解决一个问题:书里的代码怎么在我自己的机器上跑起来,跑出来的数字为什么可能和书上不完全一样。
一、本机实测环境
这套书的环境前提是实测过的,不是「一般来说」:
| 项 | 实测结果 | 说明 |
|---|---|---|
| Python(S 层) | 3.14.6,/opt/homebrew/bin/python3 | S 层仿真用这个 |
| numpy | 2.5.1 | 数组与确定性随机数 |
| pandas | 3.0.3 | CSV 与数据表 |
| matplotlib | 未安装 | 所以图表走纯 Python SVG |
| scipy | 未安装 | 拟合用 numpy 手写 |
| Python(系统) | 3.9.6,/usr/bin/python3 | 无 numpy/pandas,只跑最朴素的脚本 |
| Node | 26.5.0 | P 层内核,原生跑 TypeScript |
| tsc / tsx / vitest | 均未安装 | 所以不用它们 |
| Playwright | 1.59.1,Chromium 已缓存 | W 层无头验收 |
环境说明:在已经准备这些依赖的机器上无需再次安装。 其他机器必须先准备符合基线的运行时与依赖;本机的绝对路径不是跨平台要求。 Python 有 numpy 和 pandas 就够了,Node 直接跑 TypeScript。没有 package.json,没有 npm install,没有 tsconfig.json。
网页原型共用构建文件(本次补齐)
正文多个 web/build_*.mjs 引用了 web/pack.mjs,旧稿没有交付该文件。把下面完整文件写入工作区,之后再执行各章构建命令。它使用 Node 官方类型擦除 API,不需要额外包;但只处理章节显式列出的简单本地模块,不能当作通用 bundler,也不做类型检查。依赖文件必须先列在使用者前;别名导入、动态导入与模块作用域冲突需要显式处理。
// file: web/pack.mjs
// tier: W
// verify: node web/pack.mjs
// 最小静态拼接器;只支持正文显式列出的、无别名的本地模块依赖。
import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
import { dirname, resolve } from "node:path";
import { stripTypeScriptTypes } from "node:module";
export function bundle(root, files) {
const pieces = [];
for (const name of files) {
const path = resolve(root, name);
if (!path.startsWith(resolve(root) + "/")) throw new Error("path outside project");
let js = stripTypeScriptTypes(readFileSync(path, "utf8"), { mode: "strip" });
// Module imports are replaced by the preceding source in the explicit list.
js = js.replace(/^import\s+[\s\S]*?\sfrom\s+["'][^"']+["'];?\s*$/gm, "")
.replace(/^export\s*\{[^}]*\};?\s*$/gm, "")
.replace(/\bexport\s+(?=(?:function|class|const|let|var)\b)/g, "");
if (/^\s*(?:import|export)\b/m.test(js)) throw new Error("unsupported module syntax: " + name);
pieces.push(js);
}
return pieces.join("\n");
}
export function inlineConfig(value) {
return JSON.stringify(value).replace(/</g, "\\u003c").replace(/\u2028/g,"\\u2028").replace(/\u2029/g,"\\u2029");
}
function escape(value) {
return String(value).replace(/[&<>"']/g, c => ({"&":"&","<":"<",">":">",'"':""","'":"'"}[c]));
}
export function page({title, body, script, css=""}) {
// body/script/css are authored source, not untrusted user content.
return `<!doctype html><html lang="zh-CN"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${escape(title)}</title><style>
*{box-sizing:border-box}body{margin:0;padding:24px;background:#0b1220;color:#e2e8f0;font:16px system-ui;max-width:960px;margin-inline:auto}h1{font-size:26px}.sub{color:#94a3b8;margin-bottom:18px}.card{padding:16px;margin-block:12px;background:#172033;border-radius:12px}.row{display:flex;gap:12px;flex-wrap:wrap}.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:12px}.big{font-size:28px;font-weight:700}button{padding:10px 16px;border:0;border-radius:8px;background:#38bdf8;color:#0b1220;cursor:pointer}canvas{max-width:100%;display:block}input{max-width:100%}${css}</style><body>${body}<script>(() => {${script}\n})();</script></body></html>`;
}
export function writePage(path, html) {
mkdirSync(dirname(path), { recursive:true });
writeFileSync(path, html, "utf8");
return Buffer.byteLength(html, "utf8");
}
单独运行该文件仅检查可加载性;原型内容须运行对应构建脚本和浏览器验证。inlineConfig 会转义 <,避免配置字符串提前结束 script 元素;页面外壳把脚本包在函数作用域内,避免导出的 top 等名字与 Window 的内置只读属性冲突;需要给验收器访问的 API 必须显式挂到 globalThis。页面外壳只接收可信作者代码。真实平台宿主和审核结果仍属于 X 层。
二、两个 python3:区别与陷阱
这是本套最容易踩的坑。本机同时存在两个 python3,它们不是同一个东西:
| 路径 | 版本 | 带 numpy/pandas? | 用途 |
|---|---|---|---|
/opt/homebrew/bin/python3 | 3.14.6 | 是 | S 层仿真一律用它 |
/usr/bin/python3 | 3.9.6 | 否 | 只跑最朴素的脚本 |
陷阱在于:如果你在正文里写 python3 sim/xxx.py,而 shell 的 PATH 恰好先命中系统那个 3.9.6,脚本会在 import pandas 处报 ModuleNotFoundError,看起来像「代码错了」,其实是解释器错了。所以本套所有命令一律写全路径 /opt/homebrew/bin/python3。
顺带一提,/opt/homebrew/bin/python3 本身是一个符号链接,os.path.realpath() 会解析到 .../Cellar/python@3.14/3.14.6/.../python3.14。写全路径时用 /opt/homebrew/bin/python3 这个稳定入口即可,不要写 Cellar 里的真实路径——升级补丁版本时它就会变。
下面这段 S 层脚本会替你把两个运行时都确认一遍:
# 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())
本机实跑输出:
python 3.14.6 (/opt/homebrew/Cellar/python@3.14/3.14.6/Frameworks/Python.framework/Versions/3.14/bin/python3.14)
numpy 2.5.1
pandas 3.0.3
node v26.5.0
pebble 能力阶梯:S 仿真 / P 内核 / W 网页 / D 静态 / X 不可复现 / B 基准
OK 环境就绪
三、Node 26 原生跑 TypeScript
Node 26 内置 TypeScript 类型擦除(strip-only),.ts 文件可以直接 node x.ts,断言用内置的 node --test。但擦除器只认「可以整体删掉、不改变运行语义」的语法,所以下面这些一律禁用:
enum(要改用const对象 + 联合类型);namespace(要改用普通模块);- 构造函数参数属性(
constructor(private x: number)); - 装饰器。
这些语法需要在运行时保留元数据,strip-only 擦不掉,会直接报错。本套的 core/ 代码因此全部只用可擦除语法。
第二个坑:node --test 传目录不发现 .ts 文件。 node --test core/test/ 会报找不到模块;必须用 glob:
# snippet: P 层断言的正确写法(必须用 glob)
node --test 'core/test/*.test.ts'
本机实跑 node --test 'core/test/*.test.ts' 的结果是 5 个测试全部通过(pass 5 / fail 0),断言内容包括「5 级产出 = 20」「留存第 30 天 = 8」等——它们同时是 S 层与 W 层的对照基准。
四、Playwright 复用写法
W 层用 Playwright 无头打开单文件 HTML、采样 globalThis.__pebble、检查 canvas 真画了东西、截图存档。本机 Playwright 装在 /opt/homebrew/lib/node_modules,不在项目里。复用它的正确写法是 NODE_PATH + createRequire:
// snippet: web/verify.mjs 的 Playwright 复用写法
import { createRequire } from "node:module";
const require = createRequire(import.meta.url);
const { chromium } = require("playwright"); // 必须走 require
关键陷阱:NODE_PATH 对 ESM import 无效。 如果你写 import { chromium } from "playwright",即使设了 NODE_PATH=/opt/homebrew/lib/node_modules 也会失败——ESM 的模块解析不读 NODE_PATH。必须用 createRequire(import.meta.url) 之后 require("playwright")。运行命令:
# snippet: W 层验收命令
NODE_PATH=/opt/homebrew/lib/node_modules node web/verify.mjs web/dist/index.html
本机实跑输出(canvas 采样到 15926 个非背景像素,说明确实画上了东西):
{
"page": ".../pebble/web/dist/index.html",
"probeLevel": 5,
"outputAtProbe": 20,
"domText": "20",
"canvasPaintedPx": 15926,
"screenshot": ".../pebble/out/shots/prototype.png"
}
SMOKE3/4/5: PASS
五、pebble/ 目录结构与复现步骤
pebble/
config/ # 唯一真源(全整数,禁浮点)
economy.json ch00_budget.json ch00_schema.json …
sim/ # Python:读 config 跑仿真,产出 JSON + CSV + SVG
ch00_env_check.py ch00_constraint_budget.py ch00_validate_config.py …
core/ # TypeScript:读 config 跑逻辑内核
src/ economy.ts index.ts ch00_two_hosts.ts …
test/ economy.test.ts …
web/ # 单文件 HTML 原型与无头验收
pack.mjs build.mjs verify.mjs dist/index.html
out/ # 仿真与原型产物:JSON / CSV / SVG / 截图
复现任意一节的步骤都一样:把该节的 file: 块还原到 pebble/<file>(本套工作区已就绪),然后跑它 verify: 里的那条命令。 具体地:
# snippet: 一键复现三步
cd ~/.mgbook/pebble
/opt/homebrew/bin/python3 sim/ch00_env_check.py # 先自检环境
/opt/homebrew/bin/python3 sim/ch00_constraint_budget.py --config config/ --out out/
node --test 'core/test/*.test.ts'
六、数字对不上怎么办
按这个顺序排查:
- 配置被改过吗? 这正是「一份 config 三种消费」该有的行为。改
ch00_budget.json里的atlasAssetKB,首包能放的贴图数就会变,书上写的是某个具体值时的结果。 - 解释器对吗? 用了系统 3.9.6 会在
import pandas处失败,见第二节。 node版本对吗? 低于 26 就没有原生 TypeScript,.ts直接跑不起来。- 是不是「同机相对量级」? 正文标了「同机相对量级,绝对数值不可移植」的地方,绝对值本来就依赖机器,只有相对关系可移植。
- 还不对,那就是书错了。 请告诉我具体的节号和数字。
两条纪律复现时也要守:不编造平台后台数字(真实平台只作「公开可观察机制」引用并标注观察时点);所有数字来自本机实跑,不写「提升 N 倍」式的绝对话。
配置表的字段字典与完整 schema 见附录 B 。回到目录页 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。