当サイトはアフィリエイト広告(PR)を利用しています

タイピングゲームのローマ字判定の作り方|shi/si・「ん」・「っ」に対応する仕組み【JavaScript】

タイピングゲームのローマ字判定の作り方

タイピングゲームを作るとき、一番むずかしいのがローマ字の判定です。

「し」は shi でも si でも打てるし、「ん」は n 1回で打つ人も nn と2回打つ人もいます。お手本どおりの打ち方しか受け付けないと、遊ぶ人はストレスがたまってしまいます。

この記事では、僕が AI の Claude と一緒に作ったカカポを育てる地名タイピングのローマ字判定のしくみを、実際のコードで解説します。

まずは試してみよう

下の入力欄に、ローマ字で打ってみてください。shi と si、kanntann と kantann のように、打ち方を変えても正しく判定されます。(日本語入力はオフにしてください)

ローマ字判定がむずかしい4つの理由

  1. 1文字に打ち方が何通りもある:し(shi / si / ci)、つ(tsu / tu)、ふ(fu / hu)など
  2. 2文字でひとまとまりの音がある:「しゃ」は sha / sya のほかに、shi + xya(し+小さいゃ)でも打てる
  3. 小さい「っ」は、次の文字で打ち方が変わる:「きっぷ」なら kippu(p を重ねる)、「まっちゃ」なら maccha や matcha
  4. 「ん」は、次の文字を見ないと決まらない:「かんた」は kanta でも kannta でもいい。でも「こんにちは」の「ん」は nn と打たないと、「こにちは」と区別がつかない

特にやっかいなのが 4 の「ん」です。n を1回打った時点では、「ん」を打ち終わったのか、「な行」を打っている途中なのか、まだわかりません。

考え方:「今の位置から打てる候補」だけを考える

言葉全体の打ち方を全部書き出そうとすると、組み合わせが多すぎて大変です。「おしゃまんべ」だけでも何十通りもあります。

そこで、「今打っている位置から始まる打ち方の候補」だけを考えて、1キーずつしぼり込んでいく方法をとりました。

  1. 今の位置から打てる候補を全部出す(例:「しゃ」の位置なら sha / sya / shi / si / ci)
  2. キーが押されるたびに、打った文字で始まる候補だけを残す
  3. 候補がぴったり1つに決まったら、その文字を確定して次の位置に進む

ステップ1:ひらがな1文字の打ち方の表

まずは、ひらがな1文字ごとの打ち方を表にします。先頭に書いた打ち方が、画面に出すお手本になります。

// ひらがな1文字の打ち方(先頭に書いたものが、画面に表示するお手本になる)
const KANA = {
  "あ": ["a"], "い": ["i", "yi"], "う": ["u", "wu"], "え": ["e"], "お": ["o"],
  "か": ["ka", "ca"], "き": ["ki"], "く": ["ku", "cu", "qu"], "け": ["ke"], "こ": ["ko", "co"],
  "さ": ["sa"], "し": ["shi", "si", "ci"], "す": ["su"], "せ": ["se", "ce"], "そ": ["so"],
  "た": ["ta"], "ち": ["chi", "ti"], "つ": ["tsu", "tu"], "て": ["te"], "と": ["to"],
  // …(中略)…
  "ゃ": ["xya", "lya"], "ゅ": ["xyu", "lyu"], "ょ": ["xyo", "lyo"],
  "っ": ["xtu", "ltu", "xtsu", "ltsu"],
  "ん": ["nn", "xn"],
};

ステップ2:「きゃ」「しょ」などの拗音

「しゃ」のような2文字の音は、「最初の文字の子音(sh / sy)」と「小さいゃゅょの母音(a / u / o)」を組み合わせて作ります。len: 2 は「ひらがな2文字分を一度に打つ」という意味です。

// 「きゃ」「しょ」のような2文字でひとまとまりの音(拗音)
const YOON_HEAD = {
  "き": ["ky"], "ぎ": ["gy"], "し": ["sh", "sy"], "じ": ["j", "jy", "zy"],
  "ち": ["ch", "ty", "cy"], "ぢ": ["dy"], "に": ["ny"], "ひ": ["hy"],
  "び": ["by"], "ぴ": ["py"], "み": ["my"], "り": ["ry"],
};
const YOON_TAIL = { "ゃ": "a", "ゅ": "u", "ょ": "o" };

// chunksAt(kana, i) の中(i 番目から始まる打ち方の候補を list に集める)
// 拗音(きゃ・しょ など)
if (YOON_TAIL[next] && YOON_HEAD[c]) {
  YOON_HEAD[c].forEach((head) => {
    list.push({ len: 2, romaji: head + YOON_TAIL[next] });
  });
}

この候補とは別に、「し」1文字分の候補(shi / si / ci)も残しておきます。だから shixya(し+小さいゃ)と打っても正解になります。

ステップ3:小さい「っ」は次の子音を重ねる

「っ」の候補は、次の文字の打ち方の最初の1文字を重ねたものです。「っぷ」なら、「ぷ」の pu の p を重ねて ppu になります。

// 小さい「っ」は、次の文字の最初の子音を重ねる(「っぷ」→ ppu)
if (c === "っ" && i + 1 < kana.length) {
  chunksAt(kana, i + 1).forEach((chunk) => {
    const first = chunk.romaji[0];
    if (!"aiueon".includes(first)) {
      list.push({ len: 1 + chunk.len, romaji: first + chunk.romaji });
      if (chunk.romaji.startsWith("ch")) {
        list.push({ len: 1 + chunk.len, romaji: "t" + chunk.romaji }); // 「っち」→ tchi も OK
      }
    }
  });
}

「っち」だけは、cchi のほかに tchi と打つ人もいるので、両方を候補に入れています。もちろん xtu のように、小さい「っ」を単体で打つこともできます。

ステップ4:「ん」を n 1回で打てるのはいつ?

「ん」を n 1回で打っていいのは、次の文字が母音・や行・な行で始まらないときだけです。たとえば「かんい」を kani と打つと「かに」と区別できないので、この場合は nn が必要です。

// 「ん」は、次が母音・や行・な行でなければ「n」1回でも OK(言葉の最後は nn)
if (c === "ん" && i + 1 < kana.length) {
  const after = chunksAt(kana, i + 1);
  const canSingleN = after.every((chunk) => !"aiueoyn".includes(chunk.romaji[0]));
  if (canSingleN) {
    list.push({ len: 1, romaji: "n" });
  }
}

言葉の最後の「ん」も、次の文字がないので nn と打ちます。

ステップ5:1キーずつ判定する

ここまでで「候補を出す」部分ができました。あとは、キーが押されるたびに候補をしぼり込む「判定係」を作ります。

// キーを1つ受け取って、正しければ true、間違いなら false を返す
input(key) {
  if (this.isDone()) {
    return false;
  }

  const candidates = chunksAt(this.kana, this.pos);
  const next = this.buffer + key;
  const matching = candidates.filter((c) => c.romaji.startsWith(next));

  if (matching.length > 0) {
    this.buffer = next;
    const exact = matching.find((c) => c.romaji === next);
    const hasLonger = matching.some((c) => c.romaji.length > next.length);
    // ぴったり一致して、もっと長い候補がなければ確定する
    if (exact && !hasLonger) {
      this.commit(exact);
    }
    return true;
  }

  // 「n」だけ打って次の文字に進んだとき(例:kan + t)は、
  // 「ん」を n で確定してから、同じキーで次の文字を判定し直す
  const exactNow = candidates.find((c) => c.romaji === this.buffer);
  if (this.buffer !== "" && exactNow) {
    this.commit(exactNow);
    return this.input(key);
  }

  return false;
}

commit(chunk) {
  this.pos += chunk.len;
  this.doneRomaji += chunk.romaji;
  this.buffer = "";
}

ポイントは後半の部分です。n を1回打ったあとに t が来たら、「ん」を n で確定してから、同じ t で次の文字を判定し直します。次のキーを見てから「ん」を確定することで、n 1回でも nn でも打てるようにしています。

「かんた」を kanta と打ったときの動き

押したキー確定した文字打っている途中何が起きたか
kk「か」の候補 ka にしぼり込み中
aka「か」を確定
nkan「ん」の候補は n / nn。まだ決められない
tkantt は「ん」の続きではない → 「ん」を n で確定して、t を「た」の1文字目として判定
akanta「た」を確定。クリア!

お手本の表示も、打ち方に合わせて変える

もうひとつの工夫が、画面に出すお手本です。「し」を s、y と打った人には、お手本を shi から sya に切り替えて表示します。上のデモで「おしゃまんべ」を選んで osy と打つと、残りのお手本が amanbe に変わるのがわかります。

これは、今打っている途中の文字だけ「打ったキーで始まる候補」をお手本に使い、それ以降は表の先頭の打ち方を使う、というしくみで実現しています。

AIと一緒に作ってみて

このローマ字判定も、AIの Claude に相談しながら作りました。

AIにコードを書いてもらうと、動くものはすぐにできます。でも、細かい修正を自分でしようとすると、仕組みがわかっていないと手が止まってしまいます。こうして仕組みを言葉で説明できるようにしておくと、「ここを直したい」と思ったときに、どこをさわればいいのかがわかるようになります。

AIと一緒にゲームを作った体験は、プログラミング経験ゼロの僕が、AI(Claude)と1日でブラウザゲームを作れた話に書いています。もっとシンプルなゲームから始めたい人は、HTMLとJavaScriptだけでクイズゲームを作る方法もどうぞ。

まとめ

  • ローマ字判定は、「今の位置から打てる候補」を出して、1キーずつしぼり込むと作りやすい
  • 拗音・小さい「っ」は、1文字の打ち方を組み合わせて候補を作る
  • 「ん」は、次のキーを見てから確定することで、n 1回でも nn でも打てる
  • お手本の表示も、遊ぶ人の打ち方に合わせて変えると、ぐっと遊びやすくなる

コードの全体は GitHub で公開しています。

地名タイピングで遊んでみる