跳转至

随机出的题,凭什么说答案唯一

算法实验室 TSP 复盘 · 第 3 篇。等高线反读这款游戏的题目没有题库:地形是程序生成的,题从地形上现采。这在体验上是个卖点(无限题量、3D 揭晓与出题同源),在正确性上是个坑:题是无限的,你没法人工审题。

现象

出题流程:seed 生成双层值噪声高度场 → 在场内随机选点 → 按题型出题(坡向 / 地貌 / 谁高)。三类题都可能采出"废题":

  • 坡向题采在近平台处——坡度小到"雨水往哪流"对人类判读没有意义。读图的人靠等高线疏密判断,疏密差异小于一根线宽时,这就是歧义题;
  • 地貌题采在地貌过渡带——分类器在这一点判山脊,偏移半格就判坡面。人类按教材判读,答"错"不服;
  • 谁高题采在高差不足一个等高距的两点——数线数不出确定答案。

题库模式不会有这个问题(人工审过),随机生成模式必须另想办法。

我一开始的错误判断

这一篇有一个记录完整的错误判断,链条是"验证 → 发现盲区 → 处置":

以为"合成场验证通过"等于"验证完成"。 地貌分类器(山顶/洼地/山脊/谷地/鞍部/坡面六分)开发时是在合成场上验证的。引擎级试玩阶段跑真实噪声场才发现:鞍部(SADDLE)判定在自然场里罕见——合成场覆盖了它,自然噪声场几乎不产生符合条件的采样点。当时的处置原话写进了试玩报告:

classifyLandform 对真实噪声场的 SADDLE 判定偏少(合成场验证过、自然场罕见)——可接受

"可接受"是一个记录在案的赌注:赌玩家在这套地形参数下很少踩到鞍部盲区。这个赌注之所以敢下,是因为发现它的途径(固定 seed 的引擎级试玩)本身可复跑——盲区大小是可观测的,不是悬而未决。

补测实验:把"罕见"变成数字(成稿时补做,可复跑:npx vitest run src/evidence-sweep.test.ts)。 上面的"可接受"当时只有定性判断,成稿时补跑了量化:50 个自然场 seed、每场全网格采样(3249 点),逐项数据落盘 data/landform-stats-contour.json。鞍部在自然场的平均占比 1.57%,逐 seed 范围 0.52%–2.40%——"罕见"坐实,而且是在每个 seed 上都罕见。完整分布:坡面 52.3%、山脊 19.7%、谷地 19.6%、洼地 3.5%、山顶 3.4%、鞍部 1.6%。两个附带发现对出题器更有直接价值:邻域同判门槛会拦下约 26.6% 的原始采样点——重试机制不是摆设,是日常主力;坡度门槛拦下约 9.9%。三类门槛的日常负载,现在有数了。

另一个容易混淆的层次,值得单独说清:等值线画得准 ≠ 题出得对。 marching squares 是成熟算法,等高线位置精确。但命题正确性根本不在几何精度层——等高线画得再准,采点采在歧义位置上,题就是废的。几何精度是算法层的旧问题,命题唯一性是生成层的全新问题。

排查与关键证据

quiz.ts 的头注释把最终原则写死了:

生成器从真实高度场采样:坡向题要求坡度足够陡;地貌题要求分类稳定;答案选项由 seed 洗牌——所有题的答案在给定高度场上可严格验证。

落地是三类生成时门槛 + 重试 + 兜底:

  • 坡向题:坡度 ≥ 0.12(米/米)。 用中心差分梯度算,低于门槛重选——对应人类判读规则"看等高线疏密"。
  • 地貌题:邻域同判。 单点 classifyLandform 之后,再判偏移半格的邻点,两处不同类就重选——"分类稳定"四个字对应人类判读规则"看弯曲方向"。
  • 谁高题:高差 ≥ 等高距(默认 10 m)。 高差不足一格,"数等高线层数"没有确定答案。
  • 每题最多尝试 60 个采样点;三类门槛全部失败退回坡向题兜底——坡向题一定可生成。原则:宁可出简单题,不出歧义题。

出题与验证同源是第二道保险。 3D 揭晓环节直接引用出题时的高度场(quizField 返回 state.heights),答案可由同一份数据严格复算——不存在"出题一套实现、判卷另一套实现"的打架空间。

门槛与人类判读规则一一对应是这套设计里最值得学的部分:每一条生成门槛都不是"数值洁癖",而是某条人类判读规则的机器化翻译——疏密(坡度门槛)、弯曲(邻域同判)、数线(高差门槛)。玩家答错时,复盘路径就是"你没用对那条判读规则",而不是"出题器抽风"。

根因

出题正确性不在算法层,在命题层。 机器判据(中心差分梯度、邻域分类、高差阈值)和人类判读规则(看疏密、看弯曲、数线)是两套体系;歧义题就是两套体系覆盖范围不一致处的边缘样本。生成时门槛的本质,是把人类判读规则翻译成机器可执行的可计算条件。

质量必须前移成生成时约束。 题是无限的,人工审题审不过来;审过的题也会因为地形参数变化失效。唯一可扩展的位置,是把"这题有唯一答案"变成生成路径上的可计算门槛。

合成场验证只覆盖"判据实现",不覆盖"判据与数据的联合分布"。 鞍部盲区不是分类器写错了,是自然噪声场里满足"两两对反"的采样点本来就稀少——验证集(合成场)和部署分布(自然场)不一致,是程序化生成特有的坑:你既造了数据,又消费了数据,两边的错位只有真实分布能暴露。

约束与取舍

  • 放弃人工题库。 换来无限题量、出题与 3D 揭晓同源(题库做不到后者);代价是永远存在漏网边缘题,且要接受分类器盲区(SADDLE 在自然场罕见——已记录在案、判定可接受)。
  • 放弃更精细的地貌分类器。 换来判据可解释——"看疏密与弯曲方向"是可教的规则,玩家答错能复盘;代价是自然场覆盖不全。
  • 接受题池偏科。 兜底是坡向题,门槛最严的地貌题产量最低——试玩报告里记录了实际构成(一卷 8 题:3 地貌 + 3 坡向 + 2 谁高,山脊出现略多)。这是"宁可简单不可歧义"的直接代价。

可复用检查清单

  1. 每种题型先写"该题在给定数据上唯一可验证"的判据,再写生成器——顺序不能反。
  2. 质量门槛用"邻域样本同判"类稳定性条件,不只看单点。
  3. 重试上限 + 兜底路径必须同时存在:宁可出简单题,不出歧义题,更不出死循环。
  4. 出题、揭晓、判卷必须同一数据源,禁止两套实现。
  5. 程序化生成同时扮演"数据生产者"和"数据消费者"——验证必须跑在真实参数分布上,合成场/合成用例只证明判据实现正确。

记录核对情况(发布前删除本节)

已按仓库记录落实:

  • 鞍部盲区的发现与"可接受"处置:docs/playtest-report.md #10 原话。
  • 门槛数值与兜底逻辑:src/algorithms/contour-reading/engine/quiz.ts(坡度 0.12、60 次尝试、高差 ≥ contourInterval)、docs/tuning-guide.md(等高距默认值)。
  • 一卷 8 题的实际构成:docs/playtest-report.md #10。

仍属重建、无记录支撑(发布前请按真实经历改写或删除):

  • "以为等值线画得准 = 题出得对"这一层在仓库里没有对应记录,现按设计分析表述(几何精度与命题唯一性是两层问题),未声称是个人经历。
  • 人工题库方案是否被真实评估过:评审文档只有结论性表述;若当时有成本估算,请补一句,没有可删去。

引用出处

  • src/algorithms/contour-reading/engine/quiz.ts(头注释、makeQuestion 三类门槛与兜底)
  • src/algorithms/contour-reading/engine/heightfield.ts(classifyLandform、gradientAt)
  • docs/playtest-report.md(#10)
  • docs/tuning-guide.md(#10 行)
  • src/evidence-sweep.test.ts(补测实验 C:50 seed 自然场地貌统计)
  • docs/articles/data/landform-stats-contour.json(分布数据 + 复现命令)