2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

**たのしい**タイピング練習アプリ

2
Posted at

序

2026年7月〜8月にかけて行われていた記事投稿キャンペーン「さくらのAI Engine 3,000リクエスト使い切りチャレンジ」で、使い倒したで賞をいただきました。ありがとうございます。

副賞として「HHKB Studio 日本語配列/墨」をいただいて早速使っているのですが、これまでロープロファイルのキーボード(Logicool MX Mechanical Mini)を使っていた関係か、ただでさえ多かったタイプミスが増えた感じがしたので、初心に帰ってタイピングの練習をすることにしました。

世の中にはたくさんタイピング練習アプリがあります。私もこれまで寿司打!、e-typing、monkeytypeなどを触ってきましたが、Webで気軽に遊べるものに関しては、どちらかというとどれも競技性重視というか、真面目というか、実用的というかーーとにかくストイックなものが多い印象だったので、バイブコーディングで自分好みにチューニングしたタイピング練習アプリを作りました。

利用モデルはClaude Opus 5.5、デスクトップ版Claudeで指示して作りました。
いい時代になったといえばいいのか、明日の食い扶持を心配した方がいいのか、微妙な心境です。

DOPA TYPE

プレイ画面

派手なエフェクトと効果音、BGMを備えたたのしいタイピング練習Webアプリです。

時間内にどれだけタイプできるかを競う「スプリント」、既定の問題数をどれだけ早く打ち切れるかを競う「タイムアタック」、ただただキーボードを打ちたい時のための「エンドレス」の3つのモードで遊べます。

本体はHTML一枚(ライブラリなし)で、記録はlocalStorage、効果音とBGMはWeb Audio APIを使っています。ホスティングはGitHub Pages。ミスなくタイプするとコンボが伸びていき、それにつれて演出は派手になり、BGMにもエフェクトが追加されます。たのしい。

プレイ記録は画像でシェアできます。

dopatype-timed-1792158417889.png

差別化要素1

  • ネオン調の派手な演出
    • コンボや残り時間のオーバーレイは、タイピング練習アプリとしてはめちゃくちゃ邪魔です
      • でもあったほうがたのしいと思ったので採用
  • アガるBGMと、それとキー(Fmin)を合わせた効果音群
    • コンボが伸びるにつれてBGMにエフェクトが追加されていく気持ちよさ
    • ちなみにBGMもタイピング練習アプリとしてはめちゃくちゃ邪魔です
      • でも何回もスプリントをやってると、曲の位置で残り時間がおおよそわかるようになってくるので、邪魔なだけにはなってないと思います
  • 問題送りにEnterキーの打鍵を要求
    • 多くのアプリでは押す必要がなく、ホームポジションで素早く打つUXになっていることが多いですが、やっぱり(ッターン!)をやったほうがたのしいかなと
  • コンボボーナスがでかい=ミスタッチへのペナルティがでかい
  • たのしい(^p^)2

技術的なポイント

作ったのはAIですが、「どう動いているか」は把握しておきたかったので、面白かったところを2つ紹介します。

1. ローマ字入力の判定(入力ゆれへの対応)

日本語タイピングアプリでまず問題になるのが入力ゆれです。「し」はshiでもsiでもいいし、「っ」は子音を重ねてもxtuでもいい。「ん」は後ろに来る音によってn1回でいいときとnnが必要なときがあります。

お題ごとに正解の綴りを全パターン持たせるのは無理があるので、DOPA TYPEでは「今いるかな・・の位置で、打てるローマ字の候補」をその場で作り、入力済みの文字列と前方一致する候補で絞り込む方式にしています。

候補の作り方

基本はかな→ローマ字の対応表です。2文字のかな(きゃ、ふぁなど)を優先して引き、なければ1文字で引きます。

特別扱いが必要なのは「っ」と「ん」の2つだけです。

function cands(i) {
  const ch = s[i];
  let out = [];
  if (ch === 'っ') {
    // 次の音の候補の頭の子音を重ねる(「った」→ tta、「っち」→ cchi / tti / tchi)
    for (const c of base(i + 1)) {
      const h = c.r[0];
      if (/[a-z]/.test(h) && !'aiueon'.includes(h)) {
        out.push({ n: 1 + c.n, r: h + c.r });
        if (c.r.startsWith('ch')) out.push({ n: 1 + c.n, r: 't' + c.r });
      }
    }
    for (const r of ['xtu', 'ltu', 'xtsu', 'ltsu']) out.push({ n: 1, r });
  } else if (ch === 'ん') {
    // 次の音の候補が「すべて」母音・n・y 以外の子音で始まるときだけ n 1回を許す
    const nx = base(i + 1);
    const ok = nx.length > 0 && nx.every(c => /^[a-z]/.test(c.r) && !'aiueony'.includes(c.r[0]));
    if (ok) out.push({ n: 1, r: 'n' });
    for (const r of ['nn', 'xn', "n'"]) out.push({ n: 1, r });
  } else {
    out = base(i);
  }
  return out;
}

nは消費するかなの数、rはローマ字です。「っ」の候補は「っ+次の音」をまとめて1つの候補にしているので、nが2以上になります。

「ん」のルールは、Microsoft IMEの挙動に合わせました。kanka(かんか)は通り、zeninn(ぜんいん)は「ぜにん」になってしまうので通しません。

確定のタイミング

キーが押されたら、入力中の文字列 + 押されたキーに前方一致する候補を探します。

  • 候補が1つに決まって、しかも完全一致したら確定して次のかなへ進む
  • 候補はあるが決まらないなら、そのまま待つ
  • 候補がなくなったら、それまでの入力に完全一致する候補で確定してから、押されたキーを次のかなで評価し直す

3つ目がポイントで、n→kと打ったときにこれが活きてきます。nを打った時点では「ん」の候補nとnnのどちらにも一致するので決まりません。次にkが来るとnkに一致する候補がないので、nで「ん」を確定して、kを次の「か」として評価します。

// 候補が尽きたら、確定済みの短い候補で区切って次へ進める
if (st.buf) {
  const ex = cands(st.i).find(x => x.r === st.buf);
  if (ex) {
    const save = { ...st };
    st.typed += st.buf; st.i += ex.n; st.buf = '';
    if (this.key(c)) return true;
    Object.assign(st, save); // 次のキーも通らなければミス扱いにして元に戻す
  }
}

画面のローマ字ガイドは「入力済みの綴り+残りのおすすめ綴り」で毎回作り直しているので、siで打ち始めればガイドもsi前提の表示に切り替わります。

考えうる入力は人間側でも一通り試したつもりですが、違和感なく判定できてると思います。ただ、一部ストレスに感じられる挙動が残っているので、今後Backspaceを使えるようにすることも検討しようかなと思ってます。

  • たとえば、「ウィンドウ」を「windou」と打つところで、wではなくuで始めてしまうと、次がxiやliに固定されてしまいます。私は普段小文字をxiで出す習慣がなく、このケースでは1文字消してwiで打ち直すことが多いので、ノってる時にこれが起こると興が削がれるのです

お題1,287問の検証

お題はAIに 1,287問書いてもらいました。量が多いので、全問を「ガイドどおりに打ったら最後まで打ち切れるか」Node で機械的にチェックしています。読みにひらがな以外が混ざっていないか、重複がないかも同じスクリプトで見ています。

for (const [text, reading] of QUESTIONS) {
  const m = makeMatcher(reading);
  for (const c of m.fullDefault()) if (!m.key(c)) throw new Error(`打てない: ${text}`);
  if (!m.done()) throw new Error(`打ち切れない: ${text}`);
}

ただしこれで分かるのは「打てるかどうか」だけで、「読みが正しいかどうか」や「正しい言葉かどうか」は分かりません。実際、AI が自分で見直して修正した問題がいくつかありました。(たとえば、「ファイナルラウンド」を「ふぁいならるらうんど」と書いていたり、「温故創新」なる未知の四字熟語が入っているなど)

あと、これは議論の余地があるかもしれませんが「朝会で進捗を共有」が「ちょうかいで〜」になっていて、規範的には「ちょうかい」かもしれないけど、ビジネスの場では慣例的に「あさかい」と読まれることが多いと思ってます。なので、これは人の手で直しました3。

2. Web Audio APIで作るコンボ連動BGM

ここが一番やりたかったところです。やっぱり音楽があるとたのしくなります。

曲の設計

BGMは128BPMで作っています。128BPMの1小節は1.875秒なので、32小節でピッタリ60秒、96小節でぴったり180秒になります。これはスプリントモードの秒数に合わせた設計で、スプリントモードでタイムアップ時に曲がぶつ切りにならないように、最後に7秒ちょっとの余韻を持たせてます。

160BPM と 96BPM も検討しましたが、8小節1フレーズとしたとき、曲の展開が作りやすそうだし、スピード感がちょうどいいかなと思って128BPMに決めました。

アプリ側では次のように使い分けています。

  • スプリント:TIME UPの瞬間が曲の最後の一発になるように、余韻まで流しきる
  • タイムアタック / エンドレス:3分版を180秒ごとにループ
    • この2つは将来的に別の曲をあてるかもしれない

ぴったり3分でループさせる

<audio loop> はファイルの終わりでループするので、余韻ごとループしてしまいます。AudioBufferSourceNode の loopEnd を使えば 180秒で折り返せますが、余韻がぶつっと切れます。

そこで、AudioContext のタイマーで、次の周回の再生を 180秒ごとに先回りして予約しています。各周回は最後まで鳴らすので、前の周の余韻が次の周の頭に自然に重なります。

// 次の周回の頭が 2.5 秒以内に迫ったら予約しておく
this.timer = setInterval(() => {
  while (this.nextLoop - this.ctx.currentTime < 2.5) {
    this.source(buf, this.nextLoop, 0, BGM_LOOP_KEEP_TAIL ? 0 : this.nextLoop + BGM_LOOP_SEC);
    this.nextLoop += BGM_LOOP_SEC;
  }
}, 500);

setInterval 自体はずれますが、実際の再生開始は source.start(when) に渡した AudioContext の時刻で決まるので、継ぎ目はサンプル単位でそろいます。setTimeout で直接再生すると、タブがバックグラウンドになったときに簡単に数百ミリ秒ずれます。

カウントダウンもテンポに合わせる

開始時のカウントダウン「3, 2, 1, GO!」は 2拍間隔(0.9375秒)で表示し、GO! の瞬間に曲頭とタイマーの計測開始がそろうようにしています。画面の表示は setTimeout、BGM の開始は AudioContext の時刻で予約するので、両者の誤差は数ミリ秒です。

エフェクトのつなぎ方

コンボの段階ごとに、リバーブとディレイの送り量、オートフィルターの揺れ幅と速さを変えています。

コンボ リバーブ ディレイ ローパス
0〜 なし なし なし
30〜 16% なし なし
60〜 22% 16% なし
100〜(FEVER) 28% 20% 1小節で1往復(900Hz〜16kHz)
150〜 34% 24% 1小節で2往復
220〜 40% 30% 1拍で1往復

ミスするとエフェクトが全部外れ、さらに別のローパスで一瞬音がこもります。50コンボごとの区切りでは、ハイパスで音を吸い上げて次の拍で戻すエフェクトを入れてます。

ユーザー操作の前にデコードしておく

ブラウザは、ユーザーが操作するまでAudioContextでの再生を許可しません。一方、3分の曲のデコードには少し時間がかかります。そこで、デコードだけはOfflineAudioContextでページを開いた時点に済ませておき、AudioBufferを後から本番のAudioContextで使っています。

音声は Opus(.ogg)を基本にして、canPlayType('audio/ogg; codecs="opus"')が空を返す環境では AAC(.mp4)にフォールバックします。

効果音のキーを曲に合わせる

曲がFマイナーなので、効果音もすべてFマイナーの音に合わせました。周波数はMIDIノート番号から計算します。

const midi = n => 440 * Math.pow(2, (n - 69) / 12);
  • 打鍵音:Fマイナーペンタトニック(C Eb F Ab Bb)を、コンボに合わせてC5から駆け上がる
  • Enter の和音:コンボの段階に応じて Fm → Ab → Bbm → Db → Eb → Fm(1オクターブ上)と進む
  • 終了のファンファーレ:F Ab C Eb C Eb F で主音に終止

打鍵音がスケールの上を登っていくので、速く正確に打つほどフレーズっぽく聞こえます。これは作っていて一番たのしかった部分です。

人がやったこと

  • BGMの制作
  • キー・テンポ・小節数を曲側で決めて、アプリに合わせられるようにしたこと
  • 「Enter を押させる」「たのしさ優先で、競技性は二の次」といった方針
  • 聴いて、遊んで、気になったところを伝える

コードは1行も書いていませんが、「何を作るか」と「それが気持ちいいかどうか」の手綱は最後まで離さずにいられたと思います。

AIによって実装コストが限りなく下がっていく今、エンジニアのレゾンデートルは「何を」「何故」作るのかに収束しつつあり、そう遠くない未来には、そこに収束し切ってしまうのかもしれません。
その時自分は何をしているのかなぁ、などと考えつつ、それはそれとして、作りたいものが気軽に作れる楽しさを、今は享受している。
複雑な時代になってしまいました。

終わりに

演出やBGMも相まって、仕事前の眠気覚まし・指慣らしに「スプリント」の3分がちょうどよく、日課になりつつあります。
私はそもそもそんなにタイピングが速くも・正確でもないのですが、これを機にもうちょっとミスタッチ減らせたらいいなぁと思ったりしてます。

おまけ

BGMはAI生成ではなく、趣味の音楽制作で大量に溜め込んでいるサンプリング素材をコネコネして作りました4。サンキューVengeance、サンキューBlackOctopus

  1. 私は数多あるタイピング練習アプリを広く知っているわけではないので、これらの要素を持ったタイピング練習アプリがないとは言えません。 ↩

  2. 私にとって ↩

  3. 他に変な読みを見つけたらIssueで指摘してもらえると嬉しいです。 ↩

  4. アプリ本体よりこっちの方が時間かかりました。多分3時間ぐらい。 ↩

2
0
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?