
変えた順序
- AIによる盤の設計: モデルは文字なしで黒マスの座標だけを出します。180°対称、黒マス40〜60%、縞模様・長いはしご・離れた島は避けるよう求めます。
- 盤の検証・補正: モデルが自分で付けた点数は信じず、プログラムが検査します。対称かどうか、白マスがすべてつながっているか、すべての白マスが2文字以上の単語に属しているか、最も長いマスが制限を超えていないか、4文字以上のマスが十分にあるかを見ます。ずれていれば、長い列を切り、短い列を埋め、塞がった所を開けて補正します。
- 単語の埋め込み: 合う候補が最も少ないマスから埋め、行き詰まったら戻って別の単語を入れます。AIの盤がうまくいかなければ、あらかじめ描いておいた盤(サイズごとに8個)を使い、それでもだめならランダムな盤を作ります。
- AIによる問題作成: 盤に確定した単語のリストだけをモデルに渡します。リストにない単語を足したり綴りを変えたりしたら捨て、問題に答えがそのまま入るのもいけません。
モデルは Mac の Ollama で動かしている gemma4 です。OpenAI 互換のアドレスで呼ぶので、設定を変えるだけで別のモデルも使えます。
意味の説明の代わりに常識と時事
単語の意味だけを問う問題は簡単すぎました。そこで、こんなクイズ形式の問題を入れました。
- 遠くから見れば喜劇、近くで見れば悲劇だと言った人(채플린、チャップリン)
- 海の石灰岩地帯にできる巨大な青い穴(블루홀、ブルーホール)
- サンフランシスコの名所であるこの橋の名前(금문、ゴールデンゲート)
- 夢を分析して無意識を語った心理学者(프로이트、フロイト)
時事問題も入れました。氷河が崩れて大洪水が起きた国、アメリカ・メキシコ・カナダが共同で開いたワールドカップ、ミラノ・コルティナ冬季オリンピックといった問題です。盤を作るたびに Google ニュースの見出しを読み、ウィキペディアのランダムな記事から単語をさらに取ってきます。
単語バンクには今1,102個あります。
- 長さ: 2文字568個、3文字291個、4文字168個、5文字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個以上です。
途中で抜けても続きから
最初の待合室には「続きから」ボタンがありました。むずかしいの盤を解いている途中でやさしいを選ぶと「続きから」が表示され、新しい盤なのか続きの盤なのか紛らわしくなっていました。そこで待合室の考え方を変えました。
- 待合室では新しいゲームだけを作ります。
- 解きかけの盤があれば、待合室にとどまらずにすぐゲームルームに入ります。
- ゲームルームで待合室や新しいゲームを押すと、今の問題をあきらめるかどうかを尋ねます。
次の問題は記録が消えることでした。最後の入力をしてウィンドウを閉じ、待合室に入ると、解いていた盤がDBから消えていました。待合室に入るときに進行中の盤を整理しながら消していたのです。今は1マスでも入力した盤は「途中保存」として残し、何も入力していない盤だけを消します。ゲームルームには保存ボタンもあり、記録の一覧から開き直せば続きから解けます。
スマートフォンの画面
スマートフォンでは、仮想キーパッドが画面の半分を覆っていました。問題も A1 のような座標で表示されていて探しにくくなっていました。そこで変えました。
- 盤の番号を押すとヨコの問題から表示され、ヨコ/タテのタブで切り替えます。
- 1文字ずつではなく答えをまとめて書いてエンターを押すと、ヨコの問題はヨコに、タテの問題はタテに埋まります。
- マスを押してもキーパッドがすぐには出ないので、先に盤を見られます。
この段階の実際のコード
新しい盤を作る 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で始める