
调换后的顺序
- AI 设计网格:模型不写字,只输出黑格坐标。要求 180° 对称、黑格占 40~60%,并避开条纹、长梯子和孤立的岛。
- 网格验证与校正:不相信模型给自己打的分,由程序来检查。检查是否对称、白格是否全部相连、每个白格是否都属于两个字以上的单词、最长的格子是否超出限制、四个字以上的格子是否足够。如果不符合,就截断过长的行、填掉过短的行、打通堵住的地方来校正。
- 填入单词:从匹配候选最少的格子开始填,卡住时就回退换别的单词。如果 AI 网格不行,就使用预先画好的网格(每种尺寸 8 个);还不行的话,就生成随机网格。
- AI 出题:只把网格上已确定的单词列表交给模型。如果它加了列表中没有的单词或改了拼写,就丢弃结果,题目中也不能原样出现正确答案。
模型是在 Mac 的 Ollama 上运行的 gemma4。因为是通过兼容 OpenAI 的地址调用,所以只要修改设置就能使用其他模型。
用常识和时事代替释义
只问单词意思的题目太简单了。所以我们加入了这样的问答式题目。
- 说过“远看是喜剧,近看是悲剧”的人(卓别林)
- 在海洋石灰岩地带形成的巨大蓝色洞穴(蓝洞)
- 旧金山名胜、这座桥的名字(金门)
- 通过分析梦来讲述无意识的心理学家(弗洛伊德)
也加入了时事题,比如冰川崩塌引发大洪水的国家、美国墨西哥加拿大联合举办的世界杯、米兰-科尔蒂纳冬奥会等。每次生成网格时,都会读取谷歌新闻的头条,并从维基百科的随机条目中再获取一些单词。
词库中目前有 1,102 个单词。
- 长度:两个字 568 个,三个字 291 个,四个字 168 个,五个字 74 个
- 领域:日常 810 个,科学 86 个,历史 60 个,技术 49 个,古语 34 个,文化 30 个,地理 21 个,时事 12 个
- 问答式题目:147 个
- 按字幕频率排名,把单词分为常用词、普通词、罕见词等级。
在一台设备上最近出现过的 180 个单词会被记下来,在下一局中排除。这是因为之前出现过同样的单词接连出现的问题。
难度
- 简单:8×8,开始时公开 18% 的白格,答案最多 5 个字
- 普通:10×10,公开 8%,最多 6 个字
- 困难:12×12,不公开,最多 8 个字
每种难度的横向、纵向题目数和中间交叉数的最小值也各不相同。困难要求横向 8 道、纵向 6 道、中间交叉 6 处以上。
中途退出也能继续
最初大厅里有一个“继续”按钮。在解困难网格的途中选了简单,就会出现“继续”,搞不清是新网格还是接着解的网格。所以我们改变了大厅的概念。
- 大厅里只创建新游戏。
- 如果有正在解的网格,就不停留在大厅,直接进入游戏房间。
- 在游戏房间里按“大厅”或“新游戏”时,会询问是否放弃当前题目。
接下来的问题是记录会消失。做完最后一次输入、关掉窗口后再进入大厅,正在解的网格就从数据库中被删除了。原来是在进入大厅时清理进行中的网格时把它删掉了。现在只要输入过一格的网格都会作为“中途保存”留下来,只删除什么都没输入的网格。游戏房间里也有保存按钮,从记录列表中重新打开就能接着解。
手机画面
在手机上,虚拟键盘遮住了半个屏幕。题目也用 A1 这样的坐标来显示,很难找。所以我们做了修改。
- 点击网格上的编号,先显示横向题目,用“横向/纵向”标签切换。
- 不是一个字一个字地输入,而是输入整个答案后按回车,横向题目就横着填,纵向题目就竖着填。
- 点击格子时键盘不会马上弹出,可以先看网格。
这一阶段的实际代码
这是生成新网格的 API。网格 → 单词 → 题目的顺序一目了然。
app/api/puzzle/route.ts
// 1. 모델이 글자 없는 판 구조를 먼저 제안한다.
const patternCandidates = await generateAiPatternCandidates(difficulty);
// 2. 판의 슬롯 길이가 정해진 뒤 후보 단어를 모아 실제로 채운다.
const dynamicBank = await loadDynamicBank(difficulty, avoidWords, { useLlm: false });
let puzzle: Puzzle | null = null;
let patternSource: "ai" | "curated" = "curated";
// AI의 판 초안을 목표 모양으로 삼고, 연결형 배치기가 실제 단어를 고른다.
for (const rows of patternCandidates) {
puzzle = generatePuzzleFromRows(difficulty, rows, {
avoidWords,
extraBank: dynamicBank.entries,
});
if (puzzle) {
patternSource = "ai";
break;
}
}
if (!puzzle) {
puzzle = generatePuzzle(difficulty, {
avoidWords,
extraBank: dynamicBank.entries,
});
}
if (!puzzleMeets(puzzle, difficulty)) {
puzzle = generatePuzzle(difficulty, { extraBank: dynamicBank.entries });
}
if (!puzzleMeets(puzzle, difficulty)) {
puzzle = generatePuzzle(difficulty);
}
// 3. 최종 선택된 정답만 모델에 전달해 문제를 쓴다.
const selected = puzzle.across
.concat(puzzle.down)
.map(({ word, clue }) => ({ word, clue }));
const generatedClues = await generateCluesForWords(difficulty, selected);
puzzle = applyGeneratedClues(puzzle, generatedClues);这是退回大厅时整理网格的部分。空网格删除,输入过的网格作为中途保存留下。
lib/db.ts
for (const row of rows) {
if (isEmptyBoard(row)) {
d.prepare(`DELETE FROM moves WHERE code = ?`).run(row.code);
d.prepare(`DELETE FROM games WHERE code = ?`).run(row.code);
continue;
}
d.prepare(
`UPDATE games
SET status = 'abandoned', end_text = COALESCE(NULLIF(end_text, ''), '중간 저장'), updated_at = ?
WHERE code = ?`,
).run(now, row.code);
}下一篇是“3. 用统计重新审视出题过程与问题”。
COMMENTS · CHAPTER 02
先做网格,再填单词,最后出题 评论 0
还没有评论。阅读时有疑问或想补充的内容,欢迎留言。
发表评论前,请用 Google 开始并登记昵称和邮箱。
用 Google 开始