タイピングゲームのローマ字判定の作り方|shi/si・「ん」・「っ」に対応する仕組み【JavaScript】
タイピングゲームを作るとき、一番むずかしいのがローマ字の判定です。
「し」は shi でも si でも打てるし、「ん」は n 1回で打つ人も nn と2回打つ人もいます。お手本どおりの打ち方しか受け付けないと、遊ぶ人はストレスがたまってしまいます。
この記事では、僕が AI の Claude と一緒に作ったカカポを育てる地名タイピングのローマ字判定のしくみを、実際のコードで解説します。
まずは試してみよう
下の入力欄に、ローマ字で打ってみてください。shi と si、kanntann と kantann のように、打ち方を変えても正しく判定されます。(日本語入力はオフにしてください)
ローマ字判定がむずかしい4つの理由
- 1文字に打ち方が何通りもある:し(shi / si / ci)、つ(tsu / tu)、ふ(fu / hu)など
- 2文字でひとまとまりの音がある:「しゃ」は sha / sya のほかに、shi + xya(し+小さいゃ)でも打てる
- 小さい「っ」は、次の文字で打ち方が変わる:「きっぷ」なら kippu(p を重ねる)、「まっちゃ」なら maccha や matcha
- 「ん」は、次の文字を見ないと決まらない:「かんた」は kanta でも kannta でもいい。でも「こんにちは」の「ん」は nn と打たないと、「こにちは」と区別がつかない
特にやっかいなのが 4 の「ん」です。n を1回打った時点では、「ん」を打ち終わったのか、「な行」を打っている途中なのか、まだわかりません。
考え方:「今の位置から打てる候補」だけを考える
言葉全体の打ち方を全部書き出そうとすると、組み合わせが多すぎて大変です。「おしゃまんべ」だけでも何十通りもあります。
そこで、「今打っている位置から始まる打ち方の候補」だけを考えて、1キーずつしぼり込んでいく方法をとりました。
- 今の位置から打てる候補を全部出す(例:「しゃ」の位置なら sha / sya / shi / si / ci)
- キーが押されるたびに、打った文字で始まる候補だけを残す
- 候補がぴったり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 と打ったときの動き
| 押したキー | 確定した文字 | 打っている途中 | 何が起きたか |
|---|---|---|---|
k | k | 「か」の候補 ka にしぼり込み中 | |
a | ka | 「か」を確定 | |
n | ka | n | 「ん」の候補は n / nn。まだ決められない |
t | kan | t | t は「ん」の続きではない → 「ん」を n で確定して、t を「た」の1文字目として判定 |
a | kanta | 「た」を確定。クリア! |
お手本の表示も、打ち方に合わせて変える
もうひとつの工夫が、画面に出すお手本です。「し」を s、y と打った人には、お手本を shi から sya に切り替えて表示します。上のデモで「おしゃまんべ」を選んで osy と打つと、残りのお手本が amanbe に変わるのがわかります。
これは、今打っている途中の文字だけ「打ったキーで始まる候補」をお手本に使い、それ以降は表の先頭の打ち方を使う、というしくみで実現しています。
AIと一緒に作ってみて
このローマ字判定も、AIの Claude に相談しながら作りました。
AIにコードを書いてもらうと、動くものはすぐにできます。でも、細かい修正を自分でしようとすると、仕組みがわかっていないと手が止まってしまいます。こうして仕組みを言葉で説明できるようにしておくと、「ここを直したい」と思ったときに、どこをさわればいいのかがわかるようになります。
AIと一緒にゲームを作った体験は、プログラミング経験ゼロの僕が、AI(Claude)と1日でブラウザゲームを作れた話に書いています。もっとシンプルなゲームから始めたい人は、HTMLとJavaScriptだけでクイズゲームを作る方法もどうぞ。
まとめ
- ローマ字判定は、「今の位置から打てる候補」を出して、1キーずつしぼり込むと作りやすい
- 拗音・小さい「っ」は、1文字の打ち方を組み合わせて候補を作る
- 「ん」は、次のキーを見てから確定することで、n 1回でも nn でも打てる
- お手本の表示も、遊ぶ人の打ち方に合わせて変えると、ぐっと遊びやすくなる
コードの全体は GitHub で公開しています。