【每日一次部署】制作了在1000种可能中“必定能锁定到一人”的推理游戏
【1日1デプロイ】1000通りで「必ず1人に絞れる」推理ゲームを作った


本次制作的内容
作为“1天1部署”的新作,我制作了一款短篇推理游戏沙沙!森林足迹侦探,玩家需要阅读证据,从3名候选人中找出1人。
线索有以下3种。
- 足迹:圆形、细长、分叉
- 携带物:果实、羽毛、钥匙
- 方角:川、丘、森
将“脚印细长”“没有朝向河流方向”等2~3张证据,与3位森林居民的特征进行比对。若能在30秒内解决全部3起事件,即完成调查。
即使选错也不会游戏结束。剩余时间会减少3秒,并会收到能够区分所选对象与正确答案者的 追加证据。没有让失败止于对答案,而是将其转化为下一次判断的材料。
玩法
- 阅读证据卡
- 比较3人的脚印、随身物品、方位
- 点击满足所有条件的1个人
- 3解决事件
在 PC 上也可以用数字键 1 / 2 / 3 进行选择。结果中会显示解决数、失误数、最大连续答对、剩余时间和等级。
今日的升级1:把证据做成函数而非文章
证据不仅准备为用于显示的文本,还准备为能够判定候选是否满足条件的函数。
const PREDICATES = [
{
id: "fp-slender",
text: "足あとは細長い",
test: (suspect) => suspect.footprint === "slender",
},
{
id: "not-river",
text: "川の方へは向かっていない",
test: (suspect) => suspect.direction !== "river",
},
];
同一个 test() 会用于事件生成、画面上的正误判定、追加证据的选择和自动测试。
如果分开编写显示用文本和判定逻辑,就会出现这样的偏差:“文本上说的是没有去河边,代码却把去了河边的候选定为正确答案”。通过把证据做成数据与函数的组合,就可以在整个流程中共享同一处的条件。
今日的升级2:完成随机题目后进行检查
如果只是随机选两张证据,可能会出现仍剩下两名候选答案的情况,反过来也可能只看第一张就能知道正确答案。
因此,我们从符合正确答案人物的证据中生成2~3张的排列,并且只采用满足以下所有条件的组合。
function isValidClueSequence(suspects, answer, clues) {
if (clues.length < 2 || clues.length > 3) return false;
if (!clues.every((clue) => clue.test(answer))) return false;
let previousCount = suspects.length;
for (let i = 0; i < clues.length; i += 1) {
const matches = matchingSuspects(suspects, clues.slice(0, i + 1));
if (matches.length > previousCount) return false;
if (i === 0 && matches.length < 2) return false;
previousCount = matches.length;
}
const final = matchingSuspects(suspects, clues);
return final.length === 1 && final[0].id === answer.id;
}
重要的是,不要止步于“因为是随机生成的,所以每次玩都会不同”,而是要在生成之后检查它是否是能玩的问题。
- 所有证据都与正确答案人物一致
- 仅凭第1张时仍会剩下2名以上候选人
- 添加证据后候选不会增加
- 最后只剩下1名答对者
- 每个答错者都有可区分的追加证据
如果生成失败,就丢弃该事件并创建另一个组合。我们还使用了从相同 seed 会生成相同事件的伪随机数,使其也能复现。
最初的实现是“仅凭第1张就能解开的题目”
步骤1的最初实现中也有唯一解的确认。但是,在从空数组开始添加证据时,只采用了使候选变成1人的证据。
也就是说,虽然满足了“最后只剩1个人”,但尽是仅凭第一张就决定答案的问题。并没有成为需要核对多条证据的推理游戏。
测试本身也有问题。
// 修正前
ok("single clue may be ambiguous", matches.length >= 1);
如果是符合正确答案者的证据,那么留下1个以上候选人就是理所当然的。在这个测试中,无法确认“是否在第一张时筛选过度”。
因此将条件改为 matches.length >= 2,并重新改造成调查证据排列的方式。在 seed 1000〜1999 的1000个事件中,验证唯一解、第一张的模糊性、追加证据、相同 seed 的可复现性。
OK: 1000 seeds
OK: capture
OK: penalty / lock / timeout
All tests passed.
测试的成功日志汇总为4行,只有失败时才详细输出 seed 和原因。即使进行大量确认,也做到不增加人需要阅读的信息。
今日的升级3:把时间和输入作为纯粹状态来测试
失误时,将剩余时间减少3秒,并把连续答对次数重置为0。另外,为了防止快速连按同一个候选项时失误被加算两次,在选择处理的最开始就锁定输入。
这些逻辑没有只写在画面操作里,而是拆分到了game-state.js中。
function applyMissPenalty(state) {
return {
...state,
mistakes: state.mistakes + 1,
penaltySec: state.penaltySec + 3,
streak: 0,
};
}
function remainingSeconds(startMs, nowMs, penaltySec) {
const raw = 30 - (nowMs - startMs) / 1000 - penaltySec;
return Math.max(0, Math.ceil(raw));
}
只要传入开始时刻、当前时刻和惩罚值,就能确定剩余时间,因此不必等30秒也能测试时间耗尽。输入锁也作为状态变化来验证,并固定了连打时第二次输入不被接受这一行为。
今天的设计:对齐比较的位置
舞台是放置在黄昏森林中的调查桌。使用了深绿、深棕、米白、红叶色,候选项则被画成博物画风格的森林居民。
优先便于推理,3人卡片的顺序是固定的。
- 足迹
- 随身物品
- 方角
如果每张卡片上信息的位置不同,那么在阅读内容之前,视线就得先寻找位置。通过把相同属性对齐到相同高度,使其可以横向比较。
否定证据不仅显示朱红色条带,还显示“否定”文字;正确与错误也通过“一致”“重新调查”的标记来传达。这是一种即使无法仅凭颜色辨别,也能了解进度的设计。
非工程师初学者用AI制作游戏的技巧
如果只是向AI请求“随机生成推理题”,就很容易混入偶然解不出来的问题或过于简单的问题。
首先,将完成后的题目应满足的 性质 写成文字。
请制作一个从3名候选人中确定1人的推理游戏。证据作为判定候选人的函数来管理。证据为2~3条,仅采用所有证据都与正确答案者一致、第1条证据后仍剩下2名以上候选人、使用全部证据后只留下1名正确答案者的问题。相同 seed 下请生成相同的问题。
此外,不仅要确认若干样本,还要给出批量检查的条件。
请在连续的1000个 seed 中确认:候选人有3名、特征不重复、第1条证据后仍剩下2人以上、最后只剩1名正确答案者、存在能区分各名错误答案者的追加证据。失败时请显示 seed 和原因。
在界面操作中也加入正常流程以外的检查。
不仅要确认正确解答,还要确认以下4条路径:从失误中借助追加证据恢复、对同一候选人高速连点、超时、从结果画面重试。
与其要求 AI“不要有 bug”,具体给出希望成立的条件以及容易出问题的操作,得到的成果物更容易确认。
公開
已 push 到 GitHub 的 main 分支,并通过“ロリポップ!デプロイナウ”公开上线。
与以往以反应速度和时机为主的游戏不同,这次是排列证据、排除条件、逐步逼近答案的游戏。每天都会公开一个小游戏或实用工具。
来源:zenn · 游戏开发社区 · zenn.dev