0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

辞書(JMdict)とAIエージェントで、答えが1つに決まる漢字パズルを何枚でも作る

0
Posted at

漢字パズルのプリントを、答え付きで何枚でも作れるページを公開しました。サーバーはなく、GitHub Pages の静的なページだけで動いています。

(このページは入口で、実際にプリントを作る画面は mazeru.html?mode=print です。)

作れるのは2種類です。真ん中の□に1字入れて4つの熟語を作る「和同開珎(わどうかいちん)」と、7つの漢字で熟語をいくつ作れるかを競う「7字でつくる」。1枚目が問題、2枚目が答えで、A4 で印刷できます。

この記事では、作り方を順に書きます。

  • 電子辞書 JMdict を「答えに使える熟語」の表にする(Python)
  • 和同開珎の答えが1つに決まることを、索引の積集合で確かめる(JavaScript)
  • 同じプリントを、アドレスだけでもう一度出せるようにする
  • 作っている途中で見つけた、辞書の分類の不具合と直し方

実装と検証の大半は、AIエージェント(Claude Code)に任せました。最後の節に、人が決めたことと任せたことの分け方を書いています。

全体の形

JMdict_e.gz, kanjidic2.xml.gz(EDRDG, CC BY-SA 4.0)
   │  scripts/kanji/build_dict.py(手元で1回)
   ▼
data/kanji/words.json   熟語 12,048語 [語, 読み, 学年, 質]
data/kanji/kanji.json   常用漢字 2,136字 → 学年
   │  ブラウザで読み込む
   ▼
assets/kanji-core.js    問題を作る・確かめる(画面に依存しない関数だけ)
mazeru.html             選ぶ画面と、印刷用の紙

辞書から表を作るのは手元の Python で1回だけ。問題は、ブラウザがその場で作ります。問題を前もって作り置きしないので、何枚刷っても尽きません。

1. 辞書を「答えに使える熟語」の表にする

JMdict には漢字だけの見出しが大量にあり、そのまま答えにすると、ふつうの人が知らない語ばかりの問題になります。そこで、次の条件をすべて満たす語だけを「答え」にしました。

  • 漢字だけで2〜4字、全部の字が常用漢字
  • 頻度の印(news1, ichi1, spec1/2, gai1, nf01〜24)が付いている
  • 下品・差別・性的・配慮が必要の印(vulgar など)が付いていない。むごい印象の字を含まない
PRI = re.compile(r"^(news1|ichi1|spec1|spec2|gai1|nf(0[1-9]|1\d|2[0-4]))$")
# 頻度の印があり、まれな表記(BAD_KEINF: rarely used kanji form など)でない見出しだけ
is_common = any(PRI.match(p) for p in pri) and not any(t in x for x in kinf for t in BAD_KEINF)

下品・差別などの印(BAD_MISC: vulgar, derogatory など)は、これとは別に、見出しごと除外しています。

この条件を通った「よく使う熟語」が12,048語、「辞書にはあるが答えにしない語」が85,536語、除外した語が919語です。

よく使う熟語には、もう1段階の印(質)を付けています。0 は新聞の上位や基本語彙の印がある語(7,225語)、1 はそれ以外の頻度の印だけの語(4,394語)、2 は古語・略語・固有名詞らしい語など、問題の答えには出さない語(429語)です。プリントの小学校・中学の設定は、質0の語だけで問題を作ります。除外語のリストは公開リポジトリに平文で置きたくなかったので、SHA-256 のハッシュでだけ持っています。

自作の集計は、知っている答えで必ず確かめるようにしています。

assert idx["火山"][1:3] == ["かざん", 1]
assert idx["図書館"][1:3] == ["としょかん", 3]       # 館は小3
assert idx["一石二鳥"][1:3] == ["いっせきにちょう", 2]

2. 和同開珎の答えが1つに決まることを確かめる

和同開珎は、上と左の字が□の前に、右と下の字が□の後ろにつきます。上が「真」、左が「今」、右が「至」、下が「季」なら、□は「夏」(真夏・今夏・夏至・夏季)。

和同開珎の答えの例。上と左の字は□の前に、右と下の字は□の後ろにつく

困るのは、答えが2つ以上ある問題です。上「東」、左「南」、右「風」、下「部」だと、「北」でも「西」でも4つの熟語がそろいます。印刷して配ったあとで「西でも合ってるよね?」となると、答えのページの意味がなくなります。

答えが1つかを確かめずに作ると、どれくらい起きるかを測りました。よく使う2字の熟語(質0〜1)から、中央の字と、それにつながる4字をでたらめに選んで2万問作ると、答えが2つ以上になったのは138問、0.7%でした。少なく見えますが、9問を互いに無関係とみなすと、1枚9問のうちどれかに別解が混ざる紙は約6%です。週に1枚配るなら、4か月に1回ほど当たります。

そこで、2字の熟語から「この字の後ろにつく字」「この字の前につく字」の索引を作り、4方向の集合の積を取ります。

function makeTwoIndex(rows, filter) {
  var after = new Map(), before = new Map();
  for (var i = 0; i < rows.length; i++) {
    var r = rows[i], w = r[0];
    if (w.length !== 2 || w[0] === w[1]) continue;
    if (filter && !filter(r)) continue;
    if (!after.has(w[0])) after.set(w[0], new Set());
    after.get(w[0]).add(w[1]);
    if (!before.has(w[1])) before.set(w[1], new Set());
    before.get(w[1]).add(w[0]);
  }
  return { after: after, before: before };
}
/** 上・左の字(□の前)と右・下の字(□の後ろ)から、□に入る字の候補をすべて返す。 */
function wadouCandidates(two, up, left, right, down) {
  var A = two.after.get(up), B = two.after.get(left), C = two.before.get(right), D = two.before.get(down);
  if (!A || !B || !C || !D) return [];
  var out = [];
  A.forEach(function (x) { if (B.has(x) && C.has(x) && D.has(x)) out.push(x); });
  return out.sort();
}
/** 問題が1字だけに決まるか(common 全体で確かめる)。 */
function wadouUnique(twoAll, p) {
  var c = wadouCandidates(twoAll, p.up, p.left, p.right, p.down);
  return c.length === 1 && c[0] === p.x;
}

ポイントは、索引を2つ使い分けることです。問題を作るときは、学年に合う字だけの索引(小学低学年なら小1・2の字の語だけ)から4字を選びます。答えが1つかを確かめるときは、学年でも質でも絞らない、よく使う熟語12,048語全体の索引を使います。絞った索引で確かめると、「小2の字では1つだけど、中学の字まで入れると別解がある」問題をすり抜けさせてしまうからです。

「辞書にはあるが答えにしない」85,536語までは入れていません。ほとんど誰も知らない語で別解が成り立つたびに問題を捨てると、良い問題まで減ってしまうからです。その分、別解が残る余地があります(最後の節に書きました)。

関数そのものも、別の書き方で検算しています。テストでは、4つの難しさで作った144問(難しさごとに3つの種×12問)について、□に常用漢字2,136字を1字ずつ全部当てはめ、4つの熟語がそろう字が本当に1つだけかを数えています。索引を使った版と、総当たりの版の答えが一致するかを見るためです。

3. 同じプリントを、アドレスだけでもう一度出す

「先週の問題で復習したい」に応えるため、問題は種(seed)から決まる乱数で作っています。Math.random は種を渡せないので、文字列の種を FNV-1a でハッシュして、mulberry32 という小さな乱数に渡しています。種はアドレスに入るので、辞書を作り直さない限り、ブックマークすれば同じ紙がもう一度出ます。

function rng(seed) {
  var a = (typeof seed === 'number' ? seed : hashSeed(seed)) >>> 0;
  return function () {
    a = (a + 0x6D2B79F5) >>> 0;
    var t = a;
    t = Math.imul(t ^ (t >>> 15), t | 1);
    t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
  };
}
// 例: mazeru.html?mode=print&fmt=a&lv=high&n=9&size=l&seed=k3x9q2

印刷は CSS だけで切り替えています。画面には操作パネルと縮小した紙を出し、印刷のときは紙だけを A4 で1枚ずつ出します。

@page{size:A4 portrait;margin:0}
@media print{
  body>*:not(#print-root){display:none!important}
  #print-root{display:block!important}
  #print-root .sheet{transform:none!important;break-after:page;page-break-after:always}
  #print-root .sheet:last-child{break-after:auto;page-break-after:auto}
}

4. 途中で見つけた不具合:「社会」「先生」が答えにならない

同じ辞書で毎日1問の漢字パズルも出しています。その作業中、「社会」「先生」「大学」「電話」のような語が、「ふだん使わない言葉」に分類されていることに気づきました。

原因は、古語や略語の印の見方でした。JMdict の印は意味(sense)ごとに付きます。「社会」には「社会科の略」という意味があり、そこに略語の印があります。最初のコードは、語のどれか1つの意味に印があれば、その語全体を外していました。

# 前: 語のどこかに古語・略語の印があれば外す
obsc = any(any(t in m for t in OBSCURE_MISC) for m in misc)
# 後: 全部の意味がそうなときだけ外す(1つだけなら出す。例: 社会・先生・大学)
obsc = bool(senses) and all(any(any(t in (m.text or "") for t in OBSCURE_MISC)
                                for m in sn.findall("misc")) for sn in senses)

直すと、389語が答えに戻りました。電話・警察・普通・時代・野菜・郵便・大丈夫など、外れていたのがおかしい語ばかりです。逆に、地名や社名、子ども向けのパズルに出したくない語も十数語戻ってきたので、それらは個別に外しました。

もう1つ考えたのは、すでに公開している毎日の問題の扱いです。辞書を作り直すと、問題の盤そのものが変わりかねません。そこで、盤(字の並び)はそのままにして、次の日以降の答えだけを作り直す処理を足しました。作り直すと、数字や日付の字だけでできた語(二十日・三人など)が答えの25%をこえてしまう日が2日あったので、その2日だけは答えを据え置いています。今後同じ失敗をしないように、確かめる語の中に「社会」「先生」「大学」も加えました。

for w in ("社会", "先生", "大学"):   # 略語などの印が一部の意味だけにある語は答えにする
    assert idx[w][3] < 2, (w, idx[w])

人が決めたこと、AIエージェントに任せたこと

コードを書き、テストを回し、画面を撮って確かめる作業の大半は Claude Code が進めました。人が決めたのは次のことです。

  • 何のためのページか(介護レク・授業・家庭学習でそのまま配れるプリント)
  • 答えにしてよい語の線引き(子どもや高齢の方が使う前提で、何を外すか)
  • 公開してよいか

任せるときに効いたのは、「知っている答えで assert する」を最初に決めておいたことです。上の「社会」の不具合も、確かめる語を増やしていく途中で見つかりました。

assert だけでは拾えないものは、出てきた答えを人が読んで見つけています。たとえば「火口」は、同じ字の見出しが2つあり、ふだんの読み「かこう」でない方の読みが採られていました。これは直す表(READ_FIX)を足して直しています。逆に、エージェントの数字で人の仕様を変えたこともあります。毎日の問題を「中央の字を必ず使う」決まりで作ろうとしたところ、エージェントが辞書で総当たりして「12語以上の盤が作れる中央の字は6字しかない」と示したので、中央の字は必須にしないことにしました。

残っていること

答えが1つに決まるかを確かめているのは、辞書のよく使う熟語の範囲です。古い言葉まで入れれば、別解のある問題が残っているかもしれません。プリントのページには「作者にひとこと」の欄を付けたので、おかしな問題が届いたら、辞書の線引きを見直す材料にします。

同じ辞書を使って、画面で遊ぶ漢字のゲーム「役札」も作りました。手札の漢字で熟語を作り、点×倍で目標をこえるローグライトです。

熟語のデータ(data/kanji/)は、元の辞書と同じ CC BY-SA 4.0 で配っています。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?