第01章 - 数思工坊项目概述与设计理念
1.1 这是什么:一间「思维工具使用场」
不是刷题,是锻造思维工具。
《数思工坊》(Math Mind Forge)面向 5–18 岁,副标题是「从模式到边界的思维闯关」。它的自我定位不是题库、不是练习册,而是一间思维工具使用场:玩家在可操作、可失败、可修正的情境里,反复调用六件工具,构造出满足约束的结构、规则、模型或论证,才能推进。
这一定位把「通关」的定义整个换掉了。通关条件不是「答对」,而是工具产物通过校验、约束被满足、证据链完整。所以界面上没有「提交答案」按钮,只有「完成」「重来」这类动作;玩家不是先想好再点选,而是调工具、看后果、改一处、再看后果。
三条设计红线在工程上被翻译成了可执行、可被机器检查的规则(出自 docs/00-方案总览.md §1):
| 原始红线 | 工程化定义 | 怎么被机器检查 |
|---|---|---|
| 关卡不是题组 | 每关声明必须使用的工具产物;一件产物都没有时不能过关 | 运行时门禁 + 反刷题审计 A1 |
| 难度不沿计算量上升 | 难度只有四个轴:结构复杂度、表征抽象度、约束耦合度、迁移距离 | 关卡 schema 的 difficulty 四轴(各 1–5);审计拒绝 number-size |
| 反馈不指向对错 | 反馈必须绑定到结构化失败类(规则失效 / 结构断裂 / 模型缺变量 / 论证缺口) | 每关失败类必须有反馈文案与负例测试 |
第三条最容易被写成口号,这个项目把它落成了数据结构:失败不是一句「错了」,而是一份 Verdict,里面写明 failure.class(断在哪一类)、causes(指向哪个格子/哪一步/哪条约束)、missingEvidence(还缺哪一步证据)。玩家看到的是「你的规则在第 3 类地图会撞墙」这种语言,而不是分数。
玩家侧的核心循环是这样的(docs/00-方案总览.md §3):
进入工坊 → 读懂情境(观察) → 选一件/几件工具
→ 构造/调整(规则、摆放、参数、采样、反例)
→ 点「完成」 → 看结构性后果(哪里断了/哪个指标没到/哪个前提失败)
→ 修改 → 再运行 → 通过三道门
→ 得到印章 + 思维轨迹 + 徽章进度
仓库是开源的(MIT):znlgis/mathmindforge。想先玩再读代码,可以直接打开在线版:数思工坊在线试玩,20 关全部可玩,进度只存本机。
1.2 六件工具与六个模板
「六件思维工具」不是六个按钮,而是六套有状态、有产物、可校验的编辑器。每件工具都必须回答三个问题:玩家在这里做什么操作、产出什么结构、这个结构能被谁校验。
| 工具 | id | 产物(artifact) | 判定在检查什么 |
|---|---|---|---|
| 模式卡 | pattern |
PatternRule(重复单元 + 周期 + 变换) |
规则能否补全缺口、换表征后是否仍成立 |
| 规则卡 | rule |
程序 AST + 执行轨迹 | 程序可执行、能收齐、在变体地图上仍然成立 |
| 构造块 | construct |
摆放集合 / 统计条 / 图与课表 | 覆盖、不重叠、对称、连通、预算等硬约束 |
| 参数旋钮 | knob |
参数向量 + 运行指标 | 预算内指标是否达标、换种子是否仍稳 |
| 数据采集器 | probe |
数据集 + 样本量 + 结论 | 样本是否充分、结论是否超出证据 |
| 反例探测器 | counter |
反例实例 + 失效前提 | 这个例子是否真的推翻了规则/贪心 |
工具与模板是多对多:一个模板可以开放多件工具,一件工具也能出现在多个模板里;工具的实现只写一次,判定逻辑放在纯逻辑包里(第 07 章展开工具台,第 08–12 章逐个模板展开)。六个模板决定「画布长什么样、工具有哪些、判定怎么做」,内容 JSON 只填「这一关的具体参数」:
| 模板 | id | 画布 | 默认工具 | 判定核心 | 对应主线 |
|---|---|---|---|---|---|
| 模式 | pattern |
序列带 / 墙 / 积木路 | pattern + construct | 规则补全 + 换表征迁移 | 结构 |
| 规则 | rule |
网格地图 | rule + counter | 程序执行 + 变体泛化 | 算法 |
| 构造 | construct |
网格 / 等距板 / 表格图 | construct + counter | 约束满足 + 多解 + 不可能证书 | 结构/算法/建模 |
| 模拟 | simulate |
简化城镇 / 队列 | knob + probe | 预算内指标 + 换种子稳健 | 建模 |
| 实验 | experiment |
对局 / 数据面板(可无画布) | probe(按关卡加 knob / construct / rule) | 大样本比例 + 证据充分 | 数据/算法 |
| 论证 | argue |
结构 / 图表(可无画布) | counter + probe + 论证板 | 证书可检验 + 边界解释 | 边界 |
模板的第一个工程要求是数据完备:同一模板要能承载玩家编辑器(v1.1)生成的新关卡,判定与求解只依赖 levelScript,不为某一关写分支。这也是为什么内容体检要在构建期把每关的 levelScript 过一遍模板自己的 zod schema。
第二道保险是审计。requiredToolArtifacts 在 2026-11 的交互简化轮之后被明确为内容自检字段——它保证「声明的必用产物确实有工具产得出」,运行时不再据此做第二道门;真正守住「必须用工具」的是运行时判定顺序与 audit:levels 的 A1:直接 dispatch({ type: 'submit' }) 而无任何产物,必须被拒并且带 missingEvidence。
1.3 20 关的全景:5 学段 × 4 主线
内容层的所有 id 只有一个事实来源:packages/content/src/ids.ts。它刻意不含 zod、不含任何内容 JSON,外壳、工具、运行时都从这里取常量,避免把 zod 拖进首屏或单测:
export const BANDS = ['k', 'g13', 'g46', 'g79', 'g1012'] as const;
export const THREADS = ['structure', 'algorithm', 'modeling', 'data'] as const;
export const TOOL_IDS = ['pattern', 'rule', 'construct', 'knob', 'probe', 'counter'] as const;
export const TEMPLATE_IDS = ['pattern', 'rule', 'construct', 'simulate', 'experiment', 'argue'] as const;
export const STAMP_IDS = ['constraint', 'simple', 'transfer', 'argument'] as const;
export const EVIDENCE_PHASES = [
'observe', 'hypothesize', 'construct', 'run', 'result', 'revise', 'generalize', 'argue',
] as const;
export const VARIANT_AXES = ['structure', 'representation', 'constraint', 'transfer'] as const;
首发 20 关就是这张 5 × 4 的矩阵,每格一关(括号内是主模板):
| 学段 | 结构与模式 | 算法与构造 | 建模与优化 | 数据与系统 |
|---|---|---|---|---|
| 幼儿园 K(5–6 岁) | 模式修补匠(pattern) | 规则小火车(rule) | 公平分点心(construct) | 分类小站(experiment) |
| 小学低段 G1–3 | 对称补墙(pattern) | 规则工程师·路线(rule) | 公平分装(construct) | 天气记录站(experiment) |
| 小学高段 G4–6 | 铺砌挑战(construct) | 最少枚数支付(construct) | 排队模型(simulate) | 不变量侦探(argue) |
| 初中 G7–9 | 路网连通(construct) | 课程表调度(construct) | 城市调度员(simulate) | 抽样调查(experiment) |
| 高中 G10–12 | 结构不变量(argue) | 算法边界关(argue) | 胜率实验室(experiment) | 统计推断站(experiment) |
模板分布:pattern 2、rule 2、construct 6、simulate 2、experiment 5、argue 3,六件工具全部被真实使用。注意 construct 占了 6 关——它不是「拼图」,而是「在约束下把结构摆出来」这一原语在不同题材里的复用:分点心、分装、铺砌、支付、路网、课表。
关卡 id 的命名规则是 <学段>-<主线>-<特征>,例如 k-structure-pattern-path、g46-algorithm-payment。ids.ts 里其实有两层清单:
/** 20 关目标矩阵:内容层唯一事实来源 */
export const LEVEL_IDS = [ /* 20 条 */ ] as const;
/** M0 只发布这一关;M1 扩到 6 关、M2 扩到 20 关 */
export const SHIPPED_LEVEL_IDS = [
'k-structure-pattern-path',
'k-data-tally',
// …(按星图推荐顺序排列,共 20 条)
] as const;
export type ShippedLevelId = (typeof SHIPPED_LEVEL_IDS)[number];
保留 SHIPPED_LEVEL_IDS 这层间接,是为了「迟到的关卡」:内容没写完时把它从清单里摘掉即可,loadLevel 的 loader 表、registry 的 Record<ShippedLevelId, LevelLoader>、摘要表会立刻少一条而 typecheck 报错——不会出现「文件在但路由 404」的半截状态。现在 20 关全部在册,apps/site/src/registry.ts 里就是 20 条动态 import。
1.4 三条架构铁律
docs/02-技术架构.md §2 列了十条架构原则,其中前三条是不可违背的,后面所有包划分、构建配置、审计脚本都是它们的推论。这三条也是本书反复要回到的主线。
铁律一:逻辑与渲染分离。 packages/toolkit(判定与求解)、packages/evidence、packages/progress、所有 *.logic.ts 都不 import Phaser / Vue / DOM。这样判定、求解、证据聚合都能在 Node 里跑,审计脚本才能对 20 关做自动判定。eslint.config.js 直接把它写成错误:
{
files: ['packages/toolkit/src/**/*.ts', 'packages/templates/src/**/*.logic.ts'],
rules: {
'no-restricted-properties': ['error',
{ object: 'Math', property: 'random', message: '纯逻辑层禁止 Math.random,请注入种子。' },
{ object: 'Date', property: 'now', message: '纯逻辑层禁止 Date.now,请注入 now。' },
],
'no-restricted-imports': ['error', { paths: [
{ name: 'phaser', message: '纯逻辑层禁止 Phaser。' },
{ name: 'vue', message: '纯逻辑层禁止 Vue。' },
]}],
},
},
铁律二:平台出口唯一。 存储、音频、语音、文件、生命周期一律经 PlatformServices。游戏/工具代码里出现 localStorage / fetch / indexedDB 视为 bug,由 lint 的 no-restricted-globals 拦下;platform 包内部才允许碰这些。接口本身只有八个字段(packages/game-kit/src/services.ts):
/** 唯一平台出口:游戏/工具代码不直接碰 localStorage / fetch / IndexedDB */
export interface PlatformServices {
storage: ProgressStore;
evidenceStore: EvidenceStore;
audio: AudioBus;
files: FileBus;
preferences: Preferences;
lifecycle: LifecycleBus;
telemetry: Telemetry;
now(): number;
}
铁律三:动作驱动,同一路径。 所有交互(点、拖、调滑杆、采样、提交)都变成 LevelAction,由纯 LevelRuntime 处理;画布与工具台只是两个视图,都只读同一份 RunSnapshot。最关键的一条纪律是:自动通关(autoplay)内部逐条 dispatch 黄金解的动作序列,不允许另写「自动通关分支」。所以 e2e 与审计跑的是玩家那条路,而不是一条平行实现。
这三条合起来解释了一个设计取舍:为什么规则编辑器、数据表、论证板要做成 Vue 的 ToolDock,而不是画在 Phaser 画布里。因为这几样东西必须可读、可键盘操作、可响应式;而画布负责空间与时间。双视图的细节在第 13 章。
1.5 仓库结构总览
mathmindforge/
├─ apps/
│ └─ site/ # Vue 3 外壳:工坊大厅、星图、关卡宿主、日志、徽章、报告
│ # (desktop/ 与 mobile/ 尚未创建:本机无 Rust / Android SDK)
├─ packages/
│ ├─ game-kit/ # 契约:LevelModule / Context / Runtime / Action / Verdict / Save
│ ├─ content/ # ids / 摘要 / 详情 / loadLevel / localize / full(仅构建期)
│ ├─ toolkit/ # 纯逻辑:模式规则、种子 RNG、统计(Wilson 区间等)
│ ├─ toolkit-ui/ # Vue:ToolDock、六件工具面板、论证板
│ ├─ canvas-kit/ # Phaser 裁剪版接入 + BaseScene + 主题 + 绘制助手
│ ├─ templates/ # 六个模板:schema / logic / runtime / scene / layout / verifier
│ ├─ evidence/ # 证据聚合、四维画像、徽章规则、轨迹、报告与本地化
│ ├─ progress/ # 存档、迁移、ProgressManager、日志
│ ├─ platform/ # Web 实现:IndexedDB / 音频 / TTS / 文件 / 偏好 / 生命周期
│ └─ i18n/ # 界面文案(zh 常驻 + en 表)
├─ games/forge/ # 20 个关卡入口(一关一个动态 import 子路径)
├─ e2e/ # Playwright
├─ scripts/ # verify / content-check / audit-levels / check-size / check-packaging
├─ docs/ # 方案、规格、实施记录、审查记录
└─ 根配置文件(workspace / tsconfig / eslint / vite / vitest / playwright)
三个要点值得先记住:
apps/site是唯一产品入口。 它不是「网页版」,而是产品本身;将来 Tauri/Capacitor 只是加载同一份apps/site/dist。games/forge是一个玩法包承载 20 关。 每关一个模块文件、一个子路径导出(@mmf/game-forge/k-structure-pattern-path),由外壳逐条动态 import,一关一个 chunk。包级出口刻意是空的,理由写在源码里:
/**
* @mmf/game-forge 的包级出口。
*
* 关卡入口是**子路径导出**(./k-structure-pattern-path 等,见 package.json exports),
* 由 apps/site/src/registry.ts 逐条动态 import —— 这样每关是一个独立 chunk,
* 外壳的 registry 才不会被 20 关的代码拖进首屏。
*
* 这里刻意不 re-export 任何关卡模块:一句 export * from './levels/...'
* 就会让所有关卡(连同 Phaser 画布场景)进入同一个 chunk,docs/02 §7.1 的
* "外壳不 import Phaser" 与体积门禁都会失守。
*/
export {};
- 契约包
game-kit不依赖 Vue / Phaser / zod。 它只定义类型与常量(常量从@mmf/content/idsre-export,避免双份事实)。谁都可以依赖它,它不依赖业务。
包的职责边界与依赖方向、exports 直连 .ts 源码的解析方式,是第 02 章的内容。
1.6 一个关卡的完整生命周期
一关从「星图上的卡片」到「完成面板」,走的是一条固定的五段路。这里用真实代码把顺序钉死,第 04 章再展开运行时内部,第 05 章展开外壳。
① 关卡模块是一个「纯运行时 + 可选画布」的组合。 20 个关卡模块都长这样,只有 manifest 不同:
// games/forge/src/levels/k-structure-pattern-path.ts
import type { LevelManifest } from '@mmf/game-kit';
import { definePatternLevel } from '@mmf/templates/pattern';
import { DESIGN } from '@mmf/templates/layout';
const manifest: LevelManifest = {
id: 'k-structure-pattern-path',
band: 'k',
thread: 'structure',
template: 'pattern',
designSize: DESIGN,
estimatedMinutes: 5,
hasCanvas: true,
};
export default definePatternLevel(manifest);
definePatternLevel 返回的就是 LevelModule:createRuntime(detail, opts) 造纯运行时,mount(ctx, runtime) 才去创建 Phaser 游戏。无画布关卡(例如纯数据实验)的 manifest 里 hasCanvas: false,mount 是空实现,chunk 里可以完全没有 Phaser。
② 宿主按顺序装配。 WorkshopView 的启动函数是这条链路的唯一权威:
async function start(): Promise<void> {
// 先 await 读档:深链秒通关时,旧快照不能覆盖刚写入的完成状态
await progress.init();
const loader = registry[props.levelId];
if (!loader) throw new Error('关卡没有登记:' + props.levelId);
const loaded = (await loader()).default; // ① 关卡 chunk(可能含 Phaser)
module.value = loaded;
// hasCanvas 的画布容器由 v-if 渲染:等这一拍 DOM 出来,ctx.canvasHost 才不会为 null
await nextTick();
const loadedDetail = await loadLevel(props.levelId); // ② 详情 chunk(懒加载)
if (!loadedDetail) throw new Error('关卡内容没有加载成功:' + props.levelId);
detail.value = loadedDetail;
const runtimeRef = loaded.createRuntime(loadedDetail, {
seed: seedFor(props.levelId, progress.save?.profile.id ?? 'local-default'),
now: platform.now,
locale: 'zh-CN',
reducedMotion,
autoplay: debugOptions?.autoplay,
onEvidence: (draft) => { /* 入队,2 秒批量落盘 */ },
});
runtime.value = runtimeRef;
unsubscribe = runtimeRef.subscribe((snapshot) => { liveSnapshot.value = snapshot; });
const ctx = createLevelContext({ /* container / canvasHost / manifest / services … */ });
await loaded.mount(ctx, runtimeRef); // ③ 画布挂载
recordDebugEvent('level:ready:' + props.levelId);
}
顺序里填过三个坑,值得单独记一笔:先读档再挂载(否则深链秒通关的状态会被旧快照覆盖)、先 nextTick 再取 canvasHost(画布容器由 v-if 渲染,早一拍拿到的是 null)、mount 之后再报 level:ready(e2e 用这个事件判「可以操作了」)。
③ 工具台按内容渲染,不依赖关卡模块导出 UI。 ToolDock 只吃 detail.tools(ToolSpec[])与同一个 runtime,因此「这关开放哪几件工具」是内容的事,不是代码的事。
④ 提交走 dispatch({ type: 'submit' }),拿回一个 Verdict。 通过则进完成面板;不通过则失败横幅只显示「刚刚那次提交」的结果——玩家一动工具,旧提示立刻消失,避免提示粘住误导。
⑤ 完成才落盘。 completeNow() 先 await progress.completeLevel(...),再 refreshBadges(),最后才发 level:complete 事件。事件必须在写盘之后:e2e 用 level:complete 当「存档已就绪」的信号,事件早于写盘会让第二个页面读到旧档(这条在双页用例里真的踩过)。
同一路径的验证方式是 autoplay:?autoplay=1 时宿主调用 runtime.autoplay(),而它内部就是逐条 dispatch 黄金解的动作序列,最后补一个 submit。审计脚本用的也是这条路径,所以「autoplay 能过」与「玩家点得过去」在结构上是同一件事(第 04 章详述,第 18 章讲门禁)。
1.7 与 CHAGS、BambooBuddy 的传承关系
这个项目的技术底子来自同一作者的两个前作,但复用与不复用被划得很清楚。docs/00-方案总览.md §2.6 把这张表列成两半:
| 沿用(被验证过的工程纪律,来自 BambooBuddy) | 有意不同(来自本作的玩法判断) |
|---|---|
| pnpm workspace + TS strict + Vite + Vue + Pinia + Tailwind | 不做宠物养成与亲密度经济;成长感来自工具库、徽章、星图与证据链 |
| Phaser 只出现在关卡 chunk,外壳首屏不含引擎 | 不做每日任务/连续天数压力;改为「推荐下一关 + 自由工坊 + 每周回顾」 |
| 内容 JSON + zod、构建期体检、摘要/详情拆两半 | 不做固定竖屏的纯 Phaser 页面;改为 Canvas + ToolDock 双面板 |
| 逻辑与渲染分离、纯逻辑可脱离浏览器单测 | 不做星星评分;完成结果只有「通过了哪些门 + 得到哪些印章」 |
PlatformServices 是唯一平台出口 |
不做「点选即通关」;工具产物与约束满足才是判定对象 |
每关可无头直达、?autoplay=1 自动通关 |
不做刷题式关卡量;v1.0 收敛到 20 关、每关 2–3 个变体 |
Vitest + Playwright + 体积门禁 + 一条 pnpm verify |
不做正确率/排名/计分(报告里也不出现) |
与 CHAGS(历史典故游戏合集)的关系更偏「方法论」:CHAGS 用「一个模板 = 一种交互原语」的思路,把 53 部游戏压到 26 个模板 id 上,证明的是「模板 + 内容 JSON + 约 20% 定制」这条路能规模化生产。数思工坊继承了这个结构,但换了分组依据:CHAGS 的模板按交互原语分(拖拽、瞄准、分类、路径……),数思工坊的模板按教学结构分(pattern / rule / construct / simulate / experiment / argue)——因为这里要判定的不是「有没有玩完」,而是「产物是否满足约束、是否泛化、是否有边界」。
由此带来两处必须重写的东西:
- 判定必须能在 Node 里跑。 CHAGS 的判定可以贴着场景写,因为闸门主要是「玩到了没」;数思工坊的判定要对 20 关做自动审计(A1/A1b/A2–A4/A6/A8),所以模板的
logic.ts必须是纯函数,validate(detail, variant, artifact)与actionsFor(detail, variant, artifact)成对出现:前者给审计快速验产物,后者给 autoplay 走真实动作链,两者共用同一套约束检查函数,避免判定漂移。 - 证据是一等公民。 CHAGS 的存档记的是过关与星级;数思工坊记的是
EvidenceEvent(阶段 / 工具 / 动作 / 产物指纹 / 结果 / 失败类 / 修订序号),再聚合成四维画像、9 枚徽章、思维日志与家长报告。这套东西必须可复现,于是引出了第 04 章的确定性纪律:注入种子、注入时钟,纯逻辑层禁止Math.random/Date.now。
一个诚实的边界:CHAGS 那 53 部游戏是「一局 1–2 分钟」的短体验,而数思工坊一关 3–10 分钟、要求构造与修正,内容成本明显更高。这正是为什么 v1.0 只做 20 关而不是原方案的 120–200 关——把「少而深」从口号变成唯一可执行的内容预算。
1.8 本章要点
- 数思工坊是思维工具使用场,不是题库;通关靠工具产物、约束满足与证据链,不靠答对。
- 六件工具(pattern / rule / construct / knob / probe / counter)各有产物与校验器;六个模板(pattern / rule / construct / simulate / experiment / argue)决定画布与判定;两者多对多,工具实现只写一次。
- 20 关 = 5 学段 × 4 主线;
LEVEL_IDS是目标矩阵,SHIPPED_LEVEL_IDS是真正上线的子集,后者驱动 registry 与 typecheck。 - 三条铁律:逻辑与渲染分离、平台出口唯一、动作驱动同一路径(autoplay 与真实操作共用
dispatch)。 - 一关的生命周期是固定五段:读档 → 加载关卡 chunk 与详情 chunk →
createRuntime→mount画布并接上同一个 runtime →submit拿Verdict→ 写盘后才发完成事件。 - 与 CHAGS 共享「模板 + 内容 JSON」的生产方式,与 BambooBuddy 共享工程纪律;但双视图、动作驱动运行时、证据链与三道门是本作新增的部分。