読めない地名が、向かってくる
全国の地名が奥から迫ってくる。読みは伏せてあり、ローマ字で打ち切ると弾ける。近づくにつれて読みが1文字ずつ開くので待てば誰でも打てるが、開くほど点は減る。問われているのは打鍵の速さではなく、読めるかどうか。難読度は外部の漢字辞典を使わず、全国12万件の地名だけから連立方程式で解いて決めている
制作メモ
- AIに任せた
- Day018のローマ字判定エンジン(lib/romaji.js・204行)をそのまま流用する判断。かな汎用で isSupported() による足切りまで入っていたので、書き直しは一切していない。新しく書いたのは難読度の計算・ヒントの段階開示・奥行きの描画・地方ブロック・ライフ制の進行/『読みを隠す』と決めた時点で、読めない人が1文字も打てずに終わる問題が確定することの指摘。Day018で実測した『遅い人ほど消える寸前の皿に縛られ続ける死のループ』と同じ構造なので、同じ性質の手当て(ヒントの段階開示+降参)を実装前に入れた/得点を『開いた文字数が少ないほど高い』に置いた設計。速さで点を付けると、読みを隠した意味がなくなり Day018 と同じゲームになる/開き切っても2割は点が入るようにした判断。0点にすると、開き切ったあとに打つ理由が消えて『見ているだけ』になる/打ち間違えたときに『押してほしかったキー』を返さない実装。Day018はミスの瞬間にそれを出していたが、読みを伏せている最中に出すと答えが漏れる。記録だけ残して結果画面で使う。テストで固定した/難読度を外部の漢字辞典なしで出す2段構えの設計。第1段は『かなの総数=各漢字のかな数の和』という式を全国12万件ぶん立てて、漢字約2,250種の期待かな長を最小二乗(Jacobi反復)で解く。第2段はその期待長を使って漢字とかなを動的計画法で対応づけ、漢字ごとの読み出現頻度を数える。難読度は珍しさ(−log 出現確率)の平均+かな長の意外さ/対応づけを2周まわして精度を上げたこと(1周目は長さだけ、2周目は1周目の頻度表を費用に足して切り直す)/ユニットテスト24本(開示の単調性、接近が速い回でも着弾前に開き切ること、得点の単調減少、ライフの減り方、正解を見せてから次に進むこと、降参した地名の再出題、在庫を使い切ったあとの巡回、答えを漏らさないこと)/出題の難易度を式ではなく実測で決めたこと。全件から等間隔に採ると1問目が「板山=いたやま」級で退屈だったので、東北の在庫を順位ごとに並べて中身を目で確かめ、全国上位20%かつ「少なくとも1文字は珍しい当て方をしている」を条件にした/E2Eを7本追加したこと。とくに『打ち間違えても押してほしかったキーを画面に出さない』は、破れると遊びが成立しなくなるので固定した/画面幅320〜1280pxで地名のはみ出しを実測する検査を書き、字の大きさを「どの文字数でも枠幅の約8割」になる式に直したこと
- 自分でやった
- 『難しい地名が近づいてきて、打てたらはじけて次に行く』というゲームの指定と、参照作(漢字でGO!)の提示/読めないときの逃げ道を『ヒント段階開示+降参』にする選択/出題の舞台を地方ブロック(東北・関東など)で選べるようにする指定/郵便番号データのダウンロード実行(Claude側は許可が下りず実行できなかった)
- 失敗と修正
- 郵便番号データのダウンロードURLが間違っていた。WebFetchが返した絶対URL(/zipcode/dl/utf/zip/utf_ken_all.zip)をそのまま使ったが、これは移転前のパスで404を返す。しかも落ちてきた72KBのHTMLを zip だと思い込んだまま本人に実行を頼み、本人の環境でも同じHTMLが落ちた → 配布ページのリンクが相対パス(utf/zip/utf_ken_all.zip)で書かれていることを確認し、基準ディレクトリと結合して正しい絶対URLを組み立てた。あわせて、外部から取ったファイルは file かサイズで中身を確かめてから次に進む手順にした。72KBという明らかに小さいサイズが最初から出ていたのに見ていなかった
難読度の上位に『御幸町(みゆき)』のような地名が混ざった。漢字3文字に対して読みが3かなしかなく、読まれない『町』にも1かなが割り当てられて、実在しない読みが珍しい読みとして数えられていた。実在の難読ではなく対応づけの失敗 → 『町』『村』『島』で終わるのに読みがそれを含まない地名を除外した。除外後の上位は上八(こうじょう)・石原(いさ)・東風平(こちんだ)・大豆谷(まめざく)・月出里(すだち)と、実在の難読地名だけが並ぶようになった
地方ごとの読めなさを『上位200件の平均』で比べていたが、これは母数が大きい地方ほど有利になる統計だった。中部(母数25,083)が1位、四国(4,070)と北海道(4,815)が下位という、母数の順とほぼ同じ並びが出ていた → 母数に対する割合(上位1%)と中央値で比べるようにした。並びは入れ替わり、九州・沖縄が1位、北海道が最下位になった。統計の取り方を変えると順位が変わることそのものを、画面に出す予定
実機で打鍵が1つも入らなかった。盤面のどこかを押すと入力欄へフォーカスを戻す作りにしていたが、押した既定の動作でフォーカスが盤面側へ移り、直後に入力欄から外れていた → pointerdown で preventDefault してから focus するようにした。あわせて、それでも外れたときのために、打ち始めた時点で入力欄へ戻す受け皿を document 側に置いた
4文字の地名(南外若林)が画面幅を超えた。字の大きさを固定にしていたため。文字数で割る式に変えたら今度は幅が文字数によらず一定になり、枠の半分(179px / 356px)しか使わず小さくなった → 上限3つの min() にした。①1〜2文字が大きくなりすぎない上限 ②横幅640pxで頭打ちになる枠に合わせた上限 ③それより狭い画面のための実測幅に合わせた上限。320〜1280pxのどの幅でも枠幅の約78%を占め、はみ出さないことを測って確認した
一覧用スクショが、地名が弾けている最中を写していた。burst の演出は字を2.1倍に拡大するので、枠で切れて何の画面か分からなくなっていた。振り付けの待ち条件が「読みに伏せ字でない文字がある」だけで、打ち切った直後の全開示にも当たっていた → 「向かってきている最中(incoming)」かつ「半分まで近づいた」かつ「読みが一部だけ開いている」の3つを待つようにした
npm run precheck が NG を1件出した。出題データに、非公開にしている語と偶然一致する実在の地名が含まれていた。全国12万件もあれば当たる → ビルド時に .precheck-ng.txt を読んで、都道府県・市区町村・町域名のどれかが当たる地名を除外するようにした(48件)。NGリストはgitignoreされているので、手元に無い環境では素通しになり、そのときは precheck 側が止める
meta.json に screenshot を書き忘れ、shared-share の『リンクを貼ったときに中身が出る(OGPとcanonical)』が落ちた。og:image はビルドが meta.json から入れるので、ファイルを置くだけでは足りない → screenshot: screenshot.webp を足した。ビルドの出力が『スクショ 18』のままだったのが手がかりだった
E2Eを1本ずつ流すと通るのに、全体(306件)で流したときだけ『読みを打ち切ると弾けて点が入る』が落ちた。テスト側が盤面の開始後に出題データ465KBを取りに行っており、混んでいるときはその間に地名が手前まで来てしまう。次の地名に変わったあとで古い読みを打っていたので、点が入らなかった → 出題データとローマ字変換を開始前に読み込んでおき、盤面が出てからは通信なしで引くだけにした。1本あたり5.2秒→0.2秒になり、3回繰り返しても落ちない。アプリ側の不具合ではなくテストの競合だった
テストの競合をもう1つ踏んだ。盤面が見えた時点で地名を読み取っていたが、地名が入るのは最初の描画(rAF)が回ったあと。混んでいると読み取りのほうが先に来て、出題データの引き当てが空になり、そこから先が全部おかしくなっていた → 盤面が出るのを待つだけでなく、地名が入るまで待つようにした。引き当てが空だったときはその場で落ちるようにもした(空のまま進むと、原因から遠いところで落ちて読み解けない)。5回繰り返しても通る






















