タイピングゲームは自体は難しくない、日本語が難しい
夏休みの自由研究として、javascriptでタイピングゲームを作ろうと思いましたが、意外なところでつまいづいたという話。
基本的にやることは3つです。
1.問題を表示する
2.基本的にaddeventlistnerでキャッチした入力をhtmlのdivのidめがけて放り込んで、一致したら正解。
3.次の問題へ
しかしここからが沼です。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>タイピングゲーム</title>
</head>
<body>
<h1>タイピングゲーム</h1>
<!-- 現在表示されている単語 -->
<div id="word">hello</div>
<!-- ユーザーの進捗状況を表示 -->
<div id="progress"></div>
<!-- ユーザーのスコアを表示 -->
<div id="score">スコア: 0</div>
<script>
// ゲームで使用する単語のリスト
const words = ['apple', 'banana', 'peach', 'melon', 'grape'];
// 現在の単語をランダムに選択
let currentWord = words[Math.floor(Math.random() * words.length)];
// ユーザーの入力を管理する変数
let userInput = '';
// ユーザーのスコアを管理する変数
let score = 0;
// 現在の単語を表示する
document.getElementById('word').textContent = currentWord;
// 進捗状況(プログレスバー)を更新する関数
function updateProgressBar() {
// 現在のユーザー入力に応じてアンダーバーの長さが変化
const progress = currentWord.split('').map((char, index) =>
userInput[index] === char ? char : '_'
).join('') + '_'.repeat(currentWord.length - userInput.length);
// バーを表示
document.getElementById('progress').textContent = progress;
}
// スコアを更新する関数
function updateScore() {
userInput = ''; // ユーザーの入力をリセット
currentWord = words[Math.floor(Math.random() * words.length)]; // 新しい単語をランダムに選択
// 新しい単語を表示
document.getElementById('word').textContent = currentWord;
// 進捗を更新
updateProgressBar();
}
// キー入力に対するイベントリスナー
document.addEventListener('keydown', (event) => {
const key = event.key.toLowerCase(); // 入力されたキーを小文字に変換
if (/^[a-zA-Z]$/.test(key)) { // 入力がアルファベットであるか確認
// 現在のユーザー入力が正しいか確認
if (currentWord.startsWith(userInput + key)) {
userInput += key; // ユーザーの入力を更新
// ユーザーの入力が単語と一致する場合
if (userInput === currentWord) {
score++; // スコアを増加
// スコアを表示
document.getElementById('score').textContent = `スコア: ${score}`;
// ゲームをリセット
updateScore();
}
// 進捗を更新
updateProgressBar();
}
}
});
// 初期状態の進捗表示
updateProgressBar();
</script>
</body>
</html>
日本語の入力を判定する
日本語の入力をするにあたって、読み(つまり、ひらがな若しくはカタカナ)をローマ字アルファベットの一覧に対応させて、正誤判定すればよいのではないかと考えました。ところが…
ひらがなのローマ字変換は難しい
突然ですが、みなさんはローマ字変換表を何も見ずに「完璧」に作成できますか?
作る前は私も容易いと思っていました。
const hiraganaToRomajiMap = {
// 通常のひらがな
'あ': ['a'], 'い': ['i'], 'う': ['u'], 'え': ['e'], 'お': ['o'],
'か': ['ka', 'ca'], 'き': ['ki'], 'く': ['ku', 'cu', 'qu'], 'け': ['ke'], 'こ': ['ko', 'co'],
'さ': ['sa'], 'し': ['shi', 'si'], 'す': ['su'], 'せ': ['se', 'ce'], 'そ': ['so'],
'た': ['ta'], 'ち': ['chi', 'ti'], 'つ': ['tsu', 'tu'], 'て': ['te'], 'と': ['to'],
'な': ['na'], 'に': ['ni'], 'ぬ': ['nu'], 'ね': ['ne'], 'の': ['no'],
'は': ['ha'], 'ひ': ['hi'], 'ふ': ['fu', 'hu'], 'へ': ['he'], 'ほ': ['ho'],
'ま': ['ma'], 'み': ['mi'], 'む': ['mu'], 'め': ['me'], 'も': ['mo'],
'や': ['ya'], 'ゆ': ['yu'], 'よ': ['yo'],
'ら': ['ra'], 'り': ['ri'], 'る': ['ru'], 'れ': ['re'], 'ろ': ['ro'],
'わ': ['wa'], 'を': ['wo'], 'ん': ['n','nn'],
// 小文字のひらがな
'ぁ': ['la', 'xa'], 'ぃ': ['li', 'xi'],
'ぅ': ['lu','xu'], 'ぇ': ['le','xe'], 'ぉ': ['lo','xo'],
'ゃ': ['lya','xya'],'ゅ': ['lyu','xyu'],'ょ': ['lyo','xyo'],
'っ': ['ltu','xtu'],
// 濁音を含む音
'が': ['ga'], 'ぎ': ['gi'], 'ぐ': ['gu'], 'げ': ['ge'], 'ご': ['go'],
'ざ': ['za'], 'じ': ['ji', 'zi'], 'ず': ['zu'], 'ぜ': ['ze'], 'ぞ': ['zo'],
'だ': ['da'], 'ぢ': ['ji', 'di'], 'づ': ['du'], 'で': ['de'], 'ど': ['do'],
'ば': ['ba'], 'び': ['bi'], 'ぶ': ['bu'], 'べ': ['be'], 'ぼ': ['bo'],
'ぱ': ['pa'], 'ぴ': ['pi'], 'ぷ': ['pu'], 'ぺ': ['pe'], 'ぽ': ['po'],
'ゔ': ['vu'],
'ゔぁ': ['va'], 'ゔぃ': ['vi'], 'ゔぇ': ['ve'], 'ゔぉ': ['vo'],
// 拗音
'きゃ': ['kya'], 'きぃ': ['kyi'], 'きゅ': ['kyu'], 'きぇ': ['kye'], 'きょ': ['kyo'],
'くぁ': ['qa'], 'くぃ': ['qi'], 'くぇ': ['qe'], 'くぉ': ['qo'],
'しゃ': ['sha', 'sya'], 'しゅ': ['shu', 'syu'], 'しぇ': ['she', 'sye'], 'しょ': ['sho', 'syo'],
'ちゃ': ['cha', 'cya'], 'ちゅ': ['chu', 'cyu'], 'ちぇ': ['che', 'cye'], 'ちょ': ['cho', 'cyo'],
'てゃ': ['tha'], 'てぃ': ['thi'], 'てゅ': ['thu'], 'てぇ': ['the'], 'てょ': ['tho'],
'にゃ': ['nya'], 'にゅ': ['nyu'], 'にぇ': ['nye'], 'にょ': ['nyo'],
'ひゃ': ['hya'], 'ひゅ': ['hyu'], 'ひぇ': ['hye'], 'ひょ': ['hyo'],
'ふぁ': ['fa'], 'ふぃ': ['fi'], 'ふぇ': ['fe'], 'ふぉ': ['fo'],
'みゃ': ['mya'], 'みぃ': ['myi'], 'みゅ': ['myu'], 'みぇ': ['mye'], 'みょ': ['myo'],
'いぇ': ['ye'],
'りゃ': ['rya'], 'りゅ': ['ryu'], 'りぇ': ['rye'], 'りょ': ['ryo'],
// 濁音を含む拗音
'ぎゃ': ['gya'], 'ぎゅ': ['gyu'], 'ぎょ': ['gyo'],
'じゃ': ['ja', 'zya'], 'じゅ': ['ju', 'zyu'], 'じょ': ['jo', 'zyo'],
'ぢゃ': ['zya'], 'ぢゅ': ['zyu'], 'ぢょ': ['zyo'],
'でゃ': ['dya', 'dha'], 'でぃ': ['dyi', 'dhi'], 'でゅ': ['dyu', 'dhu'], 'でぇ': ['dhe'], 'でょ': ['dyo', 'dho'],
'びゃ': ['bya'], 'びゅ': ['byu'], 'びょ': ['byo'],
'ぴゃ': ['pya'], 'ぴゅ': ['pyu'], 'ぴょ': ['pyo'],
// 半濁音を含む拗音
'ぴゃ': ['pya'], 'ぴゅ': ['pyu'], 'ぴょ': ['pyo'],
};
そう、問題は「正解パターンがいくつも存在する」のです。
この時点で、「あ、自分には無理だな」と判断しましたが、せっかくなのでもう少し考えてみようと思いました。
「ちゃんちゃんこ」の正解パターンを考えよう
「ちゃんちゃんこ」を日本語入力したときの正解パターンをかんがえます。
場合の数ですね。
まず、「ちゃ」の入力候補ですが、「tya」「cha」「tilya」「tixla」「chilya」「chixta」この時点で6通りあります。
次に「ん」の入力候補ですが、「n」「nn」2通りあります。
次にまた「ちゃ」(6通り)、その次にまた「ん」(2通り)です。
※注 mogamoga1337さんより「xn」もあるとご指摘頂きました。
最後に「こ」の入力候補は、「ko」「co」(2通り)です。
つまり、「ちゃんちゃんこ」の入力パターンは6×2×6×2×2=288通りになります(多分もっとある)。
そもそも同じ人間が1回目の「ちゃん」と2回目の「ちゃん」は「フツーに考えて」同じように入力するハズですし、「tixla」「chilya」「chixta」なんて誰が使うんだよ?というツッコミもあるかもしれませんが、入力可能なものを「動かない仕様」にするのはどうか?と考えてしまうのはqiitaとか見ている皆さんなら解ると思います。
問題の初期化時に1題ぶんの正解パターンを配列に組み込み、正誤判定させようと思いましたが、288通りもあるのであれば、どのみちこれは現実的でないなと思いました。
拗音・促音・撥音を考慮する
拗音「ちゃ」「にゅ」「うぃ」は基本的に2文字で1つ(1音節)と認識しているという感覚です。ローマ字入力をする際に、この1音節ごとに入力するか、小さい音を別に入力するかの選択肢があるというところが難しくなる原因ですね。
さらに促音「っ」は、次の文字列の子音連続を判定する場合と、「っ」だけを入力する場合があります。
撥音「ん」は「n」「nn」の2通りの入力があり、その次に「な行」が来るとnが3連続します。
このあたりのところをどうすればいいのか?はアルゴリズムを考える良い訓練だなあと思いました。
参考になりそうサイト
参考になりそうなサイトはこのあたりかな、と思います。
ライブラリもあるようなので、解決は難しくなさそうです。
まとめ
中途半端になってしまいましたが、問題提起だけなのでここまでとします。
夏休みはもう少しあるので、時間があればがんばってみたいと思います。