跳到正文
zenn · 游戏开发社区· 来源· タイピング寿司ラボ·· 10 小时前AI 评分38

在浏览器中运行的打字游戏的罗马字输入判定思路|如何接受「し=shi/si」「ん」「っ」

ブラウザで動くタイピングゲームのローマ字入力判定の考え方|「し=shi/si」「ん」「っ」をどう受け付けるか

正文 · 原文

はじめに

日本語タイピングゲームを作ると、最初にぶつかるのがローマ字の表記ゆれです。お題が「すし」なら sushi も susi も正解にしたいし、「かんた」は kanta でも kannta でもよい。一方で「かんい」を kani と打たれたら「かに」と区別がつきません。ここでは、こうした判定を1キーずつ行う考え方を、小さな JavaScript の例で整理します。

方針:かなを「単位」に分け、単位ごとに候補を持つ

  1. お題のかなを、拗音(「しゃ」など)を1単位として分割する
  2. 単位ごとに、受け付けるローマ字の候補リストを持つ
  3. 「ん」「っ」は次の単位を見て候補を増やす
  4. 入力中は「今の単位の入力バッファ」が、どれかの候補の先頭と一致していればOK

文字列全体のパターンを先に全部作る方法もありますが、組み合わせが爆発しやすいので、単位ごとに判定するほうが扱いやすくなります。

コード例

const TABLE = {
  'す':['su'], 'し':['shi','si','ci'], 'か':['ka','ca'], 'た':['ta'],
  'ほ':['ho'], 'い':['i'], 'や':['ya'], 'ん':['nn','xn'],
  'しゃ':['sha','sya'], 'っ':['xtu','ltu'],
};
const tokenize = s => s.match(/.[ゃゅょ]?/g); // 拗音は2文字で1単位

function candidates(units, i) {
  const [u, next] = [units[i], units[i + 1]];
  const list = [...TABLE[u]];
  const heads = next ? candidates(units, i + 1).map(r => r[0]) : [];
  if (u === 'ん' && next && heads.every(h => !'aiueoyn'.includes(h))) list.push('n');
  if (u === 'っ') list.push(...heads.filter(h => !'aiueon'.includes(h)));
  return [...new Set(list)];
}

class RomajiJudge {
  constructor(kana) { this.units = tokenize(kana); this.i = 0; this.buf = ''; }
  get done() { return this.i >= this.units.length; }
  input(key) {
    if (this.done) return false;
    const cands = candidates(this.units, this.i);
    const next = this.buf + key;
    if (cands.some(c => c.startsWith(next))) {
      this.buf = next;
      if (!cands.some(c => c !== next && c.startsWith(next))) { this.i++; this.buf = ''; }
      return true;
    }
    if (cands.includes(this.buf)) { this.i++; this.buf = ''; return this.input(key); }
    return false; // ミス
  }
}

ポイント解説

確定のタイミング

shi と si のように、候補の一方が他方の途中にならない場合は、完全一致した時点で確定できます。問題は「ん」の n と nn のように、短い候補が長い候補の先頭になっているケースです。n を打った時点では、ユーザーが nn を打つつもりかどうかわかりません。

そこで、完全一致しても長い候補が残っているあいだは確定を保留し、次のキーが長い候補に続かなければ、短い候補で確定してそのキーを次の単位に回すようにしています。input() の中で自分自身を再帰的に呼んでいるのはこのためです。

「ん」の n 1回を許す条件

次の単位の候補が母音・や行・な行で始まるときは、n 1回だと読みが変わってしまう(「かんい」と「かに」)ので、nn を必須にします。お題の末尾の「ん」も、次の単位がないので nn が必要です。

「っ」の子音重ね

「った」なら次の単位の先頭子音 t を重ねて tta と打てます。次の単位の候補の先頭文字を集めて、母音と n 以外を候補に追加しています。xtu / ltu の単独入力も残しておくと、打ち方の好みに対応できます。

キー入力の受け取り方

ブラウザでは keydown の e.key を使い、1文字のキーだけを input() に渡します。日本語 IME がオンのままだと変換中の文字が届くので、e.isComposing が true のときは無視して「半角英数に切り替えてください」と案内を出すと、問い合わせが減ります。大文字は toLowerCase() でそろえておきましょう。

動作確認

const run = (kana, keys) => {
  const j = new RomajiJudge(kana);
  return [...keys].every(k => j.input(k)) && j.done;
};
run('すし', 'suci');     // true
run('かんた', 'kanta');  // true
run('かんい', 'kani');   // false(nn が必要)
run('かった', 'katta');  // true

実際のゲームで足りないもの

このサンプルは考え方の確認用なので、実用にするには「ゃ・ゅ・ょ」を含む拗音のテーブルの網羅、tsu / tu のような候補の追加、お手本表示を入力に合わせて切り替える処理などが必要です。ちなみに、ブラウザで遊べる寿司打では、「し」を shi / si / ci のどれで打っても正解になり、打った文字に合わせてお手本も切り替わります。実装の参考に触ってみると、どこで確定しているかがよくわかります。

おわりに

ローマ字判定は「候補の集合」と「確定の保留」の2つを押さえると、ぐっと見通しがよくなります。まずは小さなテーブルで動かしてみて、遊ぶ人の打ち方に合わせて候補を育てていくのがおすすめです。

来源:zenn · 游戏开发社区 · zenn.dev