マガジン

30分で作るタイピング練習:一文字ずつ判定する

遊びをつくる

執筆にClaudeを使用しています。

週末の明るい机に置かれた、一つの仕組みで遊べる小さな完成済みゲーム。
記事のテーマをもとに制作したAI生成の写真風イメージ。

タイピング練習は、画面に出た文字を打つだけの単純なゲームに見えます。しかし作ってみると、中心にあるのは「いま何文字目を待っているのか」を一つの数字で持ち、その数字と画面の見た目を常に対応させることだとわかります。この対応が崩れると、正しく打ったのに進まない、間違えたのに色が変わるといった不具合がすぐに出ます。 この記事では、外部ライブラリも画像も使わず、HTML1ファイルだけで「お題を出し、一文字ずつ判定し、正しいときだけ進む」タイピング練習を作ります。ひらがなのお題を表示し、ローマ字で打って答える形です。時間の目安は、コードを写して動かすまでが約10分、判定を読んで書き換えるのが約10分、拡張と投稿準備が約10分です。個人差があるので、あくまで目安として使ってください。

この記事の目次
  1. 1. 先に決める三つの状態
  2. 2. 手順:まず全体を動かす
  3. 3. 手順:判定の流れを一行ずつ読む
  4. 4. 手順:入力と表示の対応を自分で確かめる
  5. 5. 手順:拡張を三つ、優先順に足す
  6. 6. アソバースに出す前に確認すること
  7. 次の行動

1. 先に決める三つの状態

コードを書く前に、ゲームが覚えておくことを紙に書き出します。今回は次の三つで足ります。

  • お題の番号(何問目か)
  • 現在位置(そのお題の何文字目を待っているか)
  • ミスの回数

画面はこの三つから毎回描き直します。「文字の色を変える」という操作を直接積み重ねず、状態を更新してから、状態をもとに全体を描画します。この順序にしておくと、入力と表示がずれる余地が小さくなります。

表示は、お題の各文字を span で囲み、位置に応じて三種類に分けます。

  • 位置より前の文字は「打ち終わり」(緑)
  • 位置にある文字は「いま待っている文字」(黄色の背景)
  • 位置より後の文字は「まだ」(灰色)

判定のルールも一つだけです。押されたキーが「お題の現在位置の文字」と一致したら位置を1つ進め、一致しなければ位置は動かさずミスを数えます。間違えても先に進めず、正しい文字で進む仕組みがこの記事の核です。

2. 手順:まず全体を動かす

テキストエディタで新しいファイルを作り、index.html という名前で保存します。アソバースに投稿する場合も、入口は直下の index.html なので、最初からこの名前にしておくと後で困りません。次のコードをそのまま貼り付けてください。

html
<!doctype html>
<html lang="ja"><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>タイピング練習</title>
<style>
body{font-family:sans-serif;text-align:center;margin-top:12vh}
#text{font-size:2.4rem;font-family:monospace;letter-spacing:.1em}
.done{color:#2a8}
.cur{background:#fd6}
.rest{color:#888}
.miss{animation:s .15s}
@keyframes s{50%{transform:translateX(6px)}}
@media(prefers-reduced-motion:reduce){.miss{animation:none;outline:2px solid #c33}}
</style>
<p id="ja"></p>
<p id="text"></p>
<p id="info"></p>
<script>
const words=[["ねこ","neko"],["さくら","sakura"],["ほし","hoshi"],["うみ","umi"]];
let n=0,pos=0,miss=0,start=0,hit=0;
const $=id=>document.getElementById(id);
function render(){
  const t=words[n][1];
  $("ja").textContent=words[n][0];
  $("text").innerHTML=[...t].map((c,i)=>
    `<span class="${i<pos?"done":i==pos?"cur":"rest"}">${c}</span>`).join("");
  $("info").textContent=`${n+1} / ${words.length} 問 ミス ${miss}`;
}
addEventListener("keydown",e=>{
  if(n>=words.length)return;
  if(e.isComposing||e.ctrlKey||e.metaKey||e.altKey||e.key.length!==1)return;
  e.preventDefault();
  if(!start)start=performance.now();
  const t=words[n][1];
  if(e.key===t[pos]){
    pos++;hit++;
    if(pos===t.length){n++;pos=0;if(n===words.length)return finish();}
  }else{
    miss++;
    const el=$("text");
    el.classList.remove("miss");void el.offsetWidth;el.classList.add("miss");
  }
  render();
});
function finish(){
  const sec=((performance.now()-start)/1000).toFixed(1);
  const acc=Math.round(hit/(hit+miss)*100);
  $("ja").textContent="おわり";
  $("text").textContent=`${sec} 秒`;
  $("info").textContent=`正確さ ${acc}% (クリックでもう一度)`;
  addEventListener("click",()=>location.reload(),{once:true});
}
render();
</script>

ブラウザで index.html を開き、半角英数の状態で「neko」と打ってみてください。「n」を打つと先頭が緑になり、黄色の背景が次の「e」へ移れば、基本の対応はできています。

3. 手順:判定の流れを一行ずつ読む

動いたら、keydown の中身を上から順に確かめます。ここを理解しておくと、後の改造で迷いません。

  1. 入力を選別する。 e.isComposing(日本語入力の変換中かどうか)、Ctrl・Command・Altとの同時押し、そして e.key.length!==1 の場合は何もせず戻ります。e.key は押されたキーを表す文字列で、文字キーなら1文字、ShiftやEnterなら "Shift" や "Enter" のような長い名前になります。1文字のものだけを通す、という絞り込みです。
  2. 既定の動作を止める。 e.preventDefault() は、ブラウザ本来の反応を止めます。これがないと、スペースキーでページがスクロールするなど、遊びの邪魔になる動きが出ます。
  3. 計測を始める。 start がまだ0なら、最初の有効な入力の時刻を記録します。ページを開いた時刻ではなく、打ち始めた時刻を基準にするためです。
  4. 一致を比べる。 e.key===t[pos] が核心の一行です。t はお題のローマ字、pos は現在位置なので、「いま待っている文字」と押された文字を比べています。
  5. 一致なら進める。 pos と、正解数を数える hit を1つ増やします。位置がお題の長さに達したら、次のお題へ進めて位置を0に戻します。最後のお題なら finish を呼びます。
  6. 不一致ならミスを数える。 位置は変えません。揺れの演出は、miss クラスを一度外して付け直すことで再生しています。間に挟んだ void el.offsetWidth は、ブラウザに「いったん現在のレイアウトを計算しろ」と促す書き方です。これがないと、連続でミスしたときにアニメーションが再生されないことがあります。
  7. 最後に描画する。 render() は状態だけを見て画面を作り直します。判定の途中で色を直接いじる処理はどこにもありません。

この構造のおかげで、「判定で状態を変える」と「状態から画面を作る」が分かれています。表示の不具合を探すときは render を、進まない不具合を探すときは keydown の比較部分を見れば足ります。

4. 手順:入力と表示の対応を自分で確かめる

作った後は、次の項目を一つずつ試して、挙動を自分の目で確かめてください。結果はあなたの環境で決まります。ここでは確認の観点だけを挙げます。

  • 正しい文字だけで進むか。 お題が「neko」のとき、「n」の次にわざと「x」を打ちます。黄色の背景が「e」に残ったまま動かず、ミスの数だけが増えれば意図どおりです。
  • ミスの後に正しい文字で再開できるか。 「x」の後に「e」を打つと、続きから進むことを確かめます。
  • お題の切り替わりで位置が戻るか。 「neko」を打ち切った直後、次のお題「sakura」の先頭が黄色になっているかを見ます。前のお題の位置が残っていると、「s」を打っても進みません。
  • 最後のお題の後に入力が無視されるか。 終了画面でキーを叩いても、画面がエラーで壊れないことを確認します。ブラウザの開発者ツールのコンソールにエラーが出ていないかも見ておくと確実です。
  • 日本語入力がオンのとき。 全角入力のまま打つと、変換中の入力は無視されて何も進みません。遊ぶ人にとっては「壊れている」ように見えるので、画面に「半角英数で入力してください」の一言を添えると親切です。
  • CapsLockがオンのとき。 大文字で入力されるため、小文字のお題とは一致せずミスになります。仕様として許すのか、e.key.toLowerCase() で比べて許容するのかを、自分で決めてください。

見るべき失敗は「進まない」と「進みすぎる」の二種類です。前者は比較の条件が厳しすぎる場合、後者は選別の条件が緩すぎる場合に起きます。

5. 手順:拡張を三つ、優先順に足す

基本形が動いたら、体感の変わる順に足します。一度に全部入れず、一つ足すごとに動作を確かめてください。

お題を増やす

words の配列に、["ひかり","hikari"] のような組を足すだけです。最初は、打つ手の動きが違う文字(母音だけの語、子音が続く語)を混ぜると、練習として意味が出ます。

表記ゆれを許す

ローマ字には「し」を shi とも si とも打てる、といった揺れがあります。今のコードは1通りしか認めないため、人によっては正しく打っているのに進みません。直すときは、お題を「1つの文字列」ではなく「各かなの候補の一覧」として持つ形に変えます。たとえば ["し", ["shi","si"]] のように持ち、入力が候補のどれかの先頭と一致するあいだは受け付け、候補が1つに絞れたらそれを使い切るまで進める、という考え方です。状態に「確定した候補」が加わるので、実装は一段重くなります。残り時間に余裕があるときの課題にしてください。

制限時間かミス上限を足す

遊びとして緊張感を出すなら、全体の制限時間か、ミス回数の上限を決めます。ここは設計の選択です。

  • 制限なし(今の形)、向く人:入門者、練習目的、気をつける点:終わりが見えにくく、緊張感は出にくい
  • 全体の制限時間、向く人:速さを競いたい人、気をつける点:時間切れの判定を、入力とは別のタイマーで持つ必要がある
  • ミス上限、向く人:正確さを重視したい人、気をつける点:上限に達した時点の画面を別に用意する

どれを選んでも、「状態を更新してから描画する」の順序は変わりません。

6. アソバースに出す前に確認すること

このコードは外部通信もサーバーも使わず、1ファイルで動くので、アソバースの投稿条件と相性がよい形です。投稿前に次を確認してください。

  1. 入口のファイル名が、直下の index.html になっているか。ZIPで上げる場合も、ZIPの直下に置きます。素材を足すときは相対パスで指定します。
  2. サイズは、アップロード全体で20MiB以内です。このコードなら十分余裕があります。
  3. サーバー用のコードやサービスワーカーは対象外なので、入れないでください。
  4. 投稿は「入力→表示の確認→公開」の3段階です。表示の確認で、実際にキー入力が通るかを見てください。ゲームはsandbox付きのiframeの中で動くので、画面をクリックしてフォーカスを渡してから打ち始める必要が出る場合があります。「クリックして開始」の案内を入れておくと確実です。
  5. 公開にはメールアドレスの確認が必要です。

最高記録の扱いにも注意してください。アップロード作品の中で使う localStorage(ブラウザの保存領域)は一時メモリーの互換処理で、記録が長く残る保存先としては期待できません。記録を残したいときは、アップロード作品に自動で追加されるSDKとクラウドセーブ(作品ごとにJSONで128KiBまで)を使う方法があります。使い方は現在の案内で確認してください。保存の仕組みを入れない間は、「記録はこの画面を開いている間だけ」と画面に書いておくと誤解を防げます。

「動きを抑える」設定に配慮するなら、最初のコードに入れた prefers-reduced-motion の指定が役立ちます。ミスの揺れを消し、代わりに枠線で知らせる書き方です。

次の行動

まず index.html を保存し、「neko」を打って、緑と黄色の移り方を確かめてください。次に、第4節のチェック項目を一つずつ試して、自分の環境での挙動を記録します。余裕があれば、お題を自分の好きな言葉に差し替えて、アソバースの投稿画面の「表示の確認」で動作を見てください。