JavaScriptでタイピングゲームを自作する方法【コード全文公開】

Javascrypt

Web制作やプログラミング学習の題材として人気が高い「タイピングゲーム」。この記事では、HTML・CSS・JavaScriptだけで動く日本語タイピングゲーム「カナウチ」を実際に作った手順を、コード全文つきで解説します。ライブラリ不要、1つのHTMLファイルにコピペするだけで今すぐ動かせます。

「JavaScript タイピングゲーム 作り方」で検索して来た方にも、これから初めてゲームっぽいUIを作ってみたい方にも役立つ内容になっています。

この記事でわかること

  • タイピングゲームに必要な機能の全体像
  • 単語データ・タイマー・判定ロジックの実装方法
  • スコアやコンボ、WPM(打鍵速度)の計算方法
  • そのままコピペで動く完成コード

完成イメージ

今回作るのは、画面に表示された漢字・ひらがなの単語を見て、対応するローマ字を60秒間タイピングしていくゲームです。

  • 単語ごとに漢字表記・かな読み・ローマ字を表示
  • 打った文字をリアルタイムでハイライト
  • ミスをすると画面が揺れてコンボがリセット
  • スコア・コンボ・WPM・残り時間を常時表示
  • 次に押すキーを仮想キーボード上でハイライト
  • 制限時間終了でリザルト画面(スコア/WPM/正確率)を表示

必要な前提知識

  • HTMLの基本タグが読める
  • CSSでレイアウトを組んだ経験がある
  • JavaScriptのaddEventListenersetIntervalが何となくわかる

これくらいのレベルがあれば、コードを読みながら十分に理解できる内容です。フレームワークやビルドツールは一切使わないので、HTMLファイルをブラウザで開くだけで動作確認できます。

全体構成:タイピングゲームに必要な要素

タイピングゲームは分解すると、次の要素の組み合わせでできています。

  1. 出題データ:単語(漢字・かな・ローマ字)のリスト
  2. 表示処理:今出題している単語を画面に反映する
  3. 入力判定:ユーザーが打った文字が正しいか1文字ずつ比較する
  4. タイマー:制限時間をカウントダウンする
  5. スコアリング:正解数・コンボ・WPM・正確率を計算する
  6. 画面状態管理:開始前・プレイ中・結果表示の3状態を切り替える

この6つを押さえておくと、実装の見通しが一気によくなります。

Step1. 単語データを持たせる

まず出題する単語データを配列で用意します。今回は日本語学習でも使いやすいよう、漢字・かな・ローマ字の3セットを1単語ごとに持たせました。

javascript

const WORDS = [
  { kanji:"今日", kana:"きょう", romaji:"kyou" },
  { kanji:"ありがとう", kana:"ありがとう", romaji:"arigatou" },
  { kanji:"桜", kana:"さくら", romaji:"sakura" },
  // ...以下、単語を追加していくだけ
];

このデータ形式にしておくと、あとから単語を増やすのも、英単語版・四字熟語版に差し替えるのも配列を書き換えるだけで対応できます。

Step2. 出題する単語を画面に反映する

出題中の単語をcurrentWordという変数で管理し、ローマ字部分は「打ち終えた文字」「今のカーソル位置」「まだ打っていない文字」の3種類に分けて色分け表示します。

javascript

function renderRomaji(){
  const r = currentWord.romaji;
  let html = '';
  for(let i=0;i<r.length;i++){
    if(i < typed.length){
      html += `<span class="done">${r[i]}</span>`;
    } else if(i === typed.length){
      html += `<span class="pending cursor">${r[i]}</span>`;
    } else {
      html += `<span class="pending">${r[i]}</span>`;
    }
  }
  romajiLine.innerHTML = html;
}

こうすることで「どこまで打てたか」が視覚的にひと目でわかり、タイピングゲームらしい手応えが生まれます。

Step3. 入力を1文字ずつ判定する

タイピングゲームの心臓部です。ユーザーが押した文字と、正解であるローマ字の「今打つべき1文字」を比較します。

javascript

function handleChar(ch){
  if(state !== 'playing') return;
  const expected = currentWord.romaji[typed.length];
  if(!expected) return;
  totalKeys++;
  if(ch === expected){
    typed += ch;
    correctKeys++;
    combo++;
    maxCombo = Math.max(maxCombo, combo);
    score += 10 + Math.min(combo, 20); // コンボが伸びるほどボーナス増加
    renderRomaji();
    if(typed.length === currentWord.romaji.length){
      setTimeout(nextWord, 90); // 1単語打ち切ったら次の単語へ
    }
  } else {
    combo = 0; // ミスしたらコンボだけリセットして継続
  }
}

ポイントは、ミスをしてもゲームを止めないことです。コンボが0に戻るだけで、そのまま入力を続けられるようにすると、プレイのテンポが崩れません。

入力そのものは、画面外に置いた非表示の<input>要素で受け取っています。モバイル端末でもソフトウェアキーボードが立ち上がるようにするための工夫です。

javascript

typeInput.addEventListener('input', (e) => {
  const val = e.target.value;
  const ch = val[val.length - 1]?.toLowerCase();
  if(/^[a-z]$/.test(ch)) handleChar(ch);
  typeInput.value = '';
});

Step4. 制限時間をカウントダウンする

setIntervalで1秒ごとに残り時間を減らし、0になったらゲームを終了します。

javascript

function tick(){
  timeLeft--;
  statTime.textContent = timeLeft;
  if(timeLeft <= 0) endGame();
}

timer = setInterval(tick, 1000);

残り10秒を切ったら文字色を赤くするなど、CSSクラスの切り替えだけで緊張感を演出できます。

Step5. スコア・WPM・正確率を計算する

タイピングゲームの評価でよく使われる指標が「WPM(Words Per Minute)」です。一般的には5文字を1単語とみなす慣例があるため、次の式で計算します。

javascript

const mins = (DURATION - timeLeft) / 60;
const wpm = mins > 0 ? Math.round((correctKeys / 5) / mins) : 0;

正確率は「正しく打てた文字数 ÷ 打鍵した総文字数」で算出します。

javascript

const acc = totalKeys > 0 ? Math.round((correctKeys / totalKeys) * 100) : 100;

これらをリザルト画面にまとめて表示すれば、プレイヤーは自分の実力を数値で振り返ることができます。

Step6. 状態管理でゲームの流れを作る

最後に、state変数で「開始前(idle)」「プレイ中(playing)」「結果表示(result)」の3状態を切り替えます。状態ごとに表示するオーバーレイをCSSのdisplayで出し分けるだけなので、実装はシンプルです。

javascript

let state = 'idle';

function startGame(){
  state = 'playing';
  timeLeft = DURATION;
  score = 0; combo = 0; correctKeys = 0; totalKeys = 0;
  nextWord();
  timer = setInterval(tick, 1000);
}

この3状態管理は、タイピングゲームに限らずクイズアプリやミニゲーム全般に応用できる考え方なので、覚えておくと便利です。

完成コード(コピペでそのまま動きます)

ここまで解説した内容を1つのHTMLファイルにまとめた完成版です。typing-game.htmlとして保存し、ブラウザで開くだけで動作します。フォントはGoogle Fontsから読み込んでいるため、インターネット接続が必要です。

html

<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>カナウチ — タイピングゲーム</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Shippori+Mincho:wght@500;800&family=Zen+Kaku+Gothic+New:wght@400;500;700&family=JetBrains+Mono:wght@400;500;700&display=swap" rel="stylesheet">
<style>
  :root{
    --bg: #1c1a17;
    --panel: #262320;
    --panel-raised: #2f2b26;
    --ink: #ede6d8;
    --muted: #948a78;
    --amber: #d9a441;
    --amber-dim: #7a6535;
    --good: #6b9b6f;
    --bad: #c1554d;
    --line: #3a352e;
    --radius: 6px;
  }
  @media (prefers-reduced-motion: reduce){
    *{ animation-duration: 0.001ms !important; animation-iteration-count: 1 !important; transition-duration: 0.001ms !important; }
  }
  *{ box-sizing: border-box; }
  html,body{
    margin:0; padding:0; min-height:100vh;
    background:
      radial-gradient(ellipse 900px 500px at 50% -10%, #2a2620 0%, transparent 60%),
      var(--bg);
    color: var(--ink);
    font-family: 'Zen Kaku Gothic New', sans-serif;
    -webkit-font-smoothing: antialiased;
  }
  body{
    display:flex; flex-direction:column; align-items:center;
    min-height:100vh; padding: 28px 16px 48px;
  }
  .ribbon{
    width:100%; max-width: 720px; height: 6px; border-radius: 3px; margin-bottom: 22px;
    background: repeating-linear-gradient(
      90deg,
      var(--bad) 0px, var(--bad) 26px,
      #1a1a1a 26px, #1a1a1a 30px,
      var(--ink) 30px, var(--ink) 56px,
      #1a1a1a 56px, #1a1a1a 60px
    );
    opacity: 0.85;
  }
  header{ width:100%; max-width: 720px; margin-bottom: 22px; }
  .eyebrow{
    font-family:'JetBrains Mono', monospace; font-size: 11px; letter-spacing: 0.28em;
    color: var(--amber); text-transform: uppercase; margin: 0 0 6px;
  }
  h1{
    font-family:'Shippori Mincho', serif; font-weight: 800; font-size: clamp(28px, 5vw, 40px);
    margin: 0; letter-spacing: 0.04em;
  }
  h1 span{ color: var(--amber); }
  .sub{ color: var(--muted); font-size: 13px; margin-top: 6px; }
  main{ width:100%; max-width: 720px; }
  .stats{
    display:grid; grid-template-columns: repeat(4, 1fr); gap: 10px;
    margin-bottom: 18px;
  }
  .stat{
    background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius);
    padding: 10px 12px;
  }
  .stat-label{
    font-family:'JetBrains Mono', monospace; font-size: 10px; letter-spacing: 0.16em;
    color: var(--muted); text-transform: uppercase; margin-bottom: 4px;
  }
  .stat-value{
    font-family:'JetBrains Mono', monospace; font-weight: 700; font-size: 22px;
    font-variant-numeric: tabular-nums; color: var(--ink);
  }
  .stat-value.flip{ animation: flip 0.28s ease; }
  @keyframes flip{
    0%{ transform: rotateX(0deg); }
    45%{ transform: rotateX(70deg); color: var(--amber); }
    100%{ transform: rotateX(0deg); }
  }
  .stat.time.warn .stat-value{ color: var(--bad); }
  .stage{
    position: relative;
    background: var(--panel);
    border: 1px solid var(--line);
    border-radius: 10px;
    min-height: 260px;
    display:flex; align-items:center; justify-content:center;
    padding: 32px 20px;
    overflow: hidden;
    margin-bottom: 18px;
  }
  .stage::before{
    content:""; position:absolute; top:0; left:0; right:0; height: 3px;
    background: linear-gradient(90deg, var(--bad), var(--amber));
  }
  .word-block{ text-align:center; width:100%; }
  .kana{ color: var(--muted); font-size: 15px; letter-spacing: 0.08em; margin-bottom: 6px; }
  .kanji{
    font-family:'Shippori Mincho', serif; font-weight: 800; font-size: clamp(30px, 6vw, 46px);
    margin-bottom: 18px; color: var(--ink);
  }
  .romaji{
    font-family:'JetBrains Mono', monospace; font-size: clamp(20px, 4vw, 28px);
    letter-spacing: 0.05em;
  }
  .romaji .done{ color: var(--good); }
  .romaji .pending{ color: var(--muted); opacity: 0.55; }
  .romaji .cursor{
    border-bottom: 3px solid var(--amber);
    animation: blink 1s step-end infinite;
  }
  @keyframes blink{ 50%{ border-color: transparent; } }
  .shake{ animation: shake 0.24s; }
  @keyframes shake{
    25%{ transform: translateX(-6px); }
    75%{ transform: translateX(6px); }
  }
  .overlay{
    position:absolute; inset:0; background: rgba(28,26,23,0.96);
    display:flex; flex-direction:column; align-items:center; justify-content:center;
    text-align:center; padding: 24px; gap: 14px;
  }
  .overlay h2{
    font-family:'Shippori Mincho', serif; font-size: 26px; margin:0; font-weight: 800;
  }
  .overlay p{ color: var(--muted); font-size: 13px; margin: 0; max-width: 380px; line-height: 1.7; }
  .result-grid{
    display:grid; grid-template-columns: repeat(3,1fr); gap: 10px; width: 100%; max-width: 380px; margin-top: 4px;
  }
  .result-grid .stat-label{ text-align:center; }
  .result-grid .stat-value{ text-align:center; font-size: 20px; }
  button.primary{
    font-family:'JetBrains Mono', monospace; font-weight: 700; font-size: 13px; letter-spacing: 0.1em;
    text-transform: uppercase; color: #1c1a17; background: var(--amber);
    border: none; border-radius: var(--radius); padding: 12px 28px; cursor: pointer;
    transition: transform 0.12s ease, background 0.12s ease;
  }
  button.primary:hover{ background: #e6b657; transform: translateY(-1px); }
  button.primary:active{ transform: translateY(0px); }
  button.primary:focus-visible{ outline: 2px solid var(--ink); outline-offset: 2px; }
  .keyboard{ margin-top: 20px; display:flex; flex-direction:column; align-items:center; gap: 6px; }
  .krow{ display:flex; gap: 6px; }
  .key{
    font-family:'JetBrains Mono', monospace; font-size: 12px; color: var(--muted);
    background: var(--panel-raised); border: 1px solid var(--line); border-radius: 4px;
    width: 30px; height: 30px; display:flex; align-items:center; justify-content:center;
    text-transform: uppercase; transition: all 0.1s ease;
  }
  .key.active{
    background: var(--amber); color: #1c1a17; border-color: var(--amber);
    box-shadow: 0 0 0 3px rgba(217,164,65,0.25);
  }
  #typeInput{
    position:absolute; opacity:0; width:1px; height:1px; pointer-events:none;
  }
  footer{ margin-top: 24px; color: var(--muted); font-size: 11px; font-family:'JetBrains Mono', monospace; }
</style>
</head>
<body>

<div class="ribbon"></div>

<header>
  <p class="eyebrow">JP TYPING DRILL</p>
  <h1>カナウチ<span>。</span></h1>
  <p class="sub">表示されたローマ字を60秒間、できるだけ正確に打ち込もう。</p>
</header>

<main>
  <div class="stats">
    <div class="stat time">
      <div class="stat-label">Time</div>
      <div class="stat-value" id="statTime">60</div>
    </div>
    <div class="stat">
      <div class="stat-label">Score</div>
      <div class="stat-value" id="statScore">0</div>
    </div>
    <div class="stat">
      <div class="stat-label">Combo</div>
      <div class="stat-value" id="statCombo">0</div>
    </div>
    <div class="stat">
      <div class="stat-label">WPM</div>
      <div class="stat-value" id="statWpm">0</div>
    </div>
  </div>

  <div class="stage" id="stage">
    <div class="word-block" id="wordBlock">
      <div class="kana" id="kanaLine">&nbsp;</div>
      <div class="kanji" id="kanjiLine">&nbsp;</div>
      <div class="romaji" id="romajiLine">&nbsp;</div>
    </div>

    <div class="overlay" id="startOverlay">
      <p class="eyebrow" style="margin:0;">READY?</p>
      <h2>キーを押してスタート</h2>
      <p>単語の下に出るローマ字をそのまま打ちます。ミスしてもコンボが途切れるだけ、そのまま続けてOK。</p>
      <button class="primary" id="startBtn">はじめる</button>
    </div>

    <div class="overlay" id="resultOverlay" style="display:none;">
      <p class="eyebrow" style="margin:0;">TIME UP</p>
      <h2 id="resultHeadline">お疲れさま</h2>
      <div class="result-grid">
        <div class="stat">
          <div class="stat-label">Score</div>
          <div class="stat-value" id="resScore">0</div>
        </div>
        <div class="stat">
          <div class="stat-label">WPM</div>
          <div class="stat-value" id="resWpm">0</div>
        </div>
        <div class="stat">
          <div class="stat-label">正確率</div>
          <div class="stat-value" id="resAcc">0%</div>
        </div>
      </div>
      <button class="primary" id="retryBtn">もう一度</button>
    </div>
  </div>

  <input type="text" id="typeInput" autocomplete="off" autocapitalize="off" spellcheck="false">

  <div class="keyboard" id="keyboard">
    <div class="krow" data-row="qwertyuiop"></div>
    <div class="krow" data-row="asdfghjkl"></div>
    <div class="krow" data-row="zxcvbnm"></div>
  </div>
</main>

<footer>CLICK STAGE OR PRESS ANY KEY TO FOCUS</footer>

<script>
(function(){
  const WORDS = [
    { kanji:"今日", kana:"きょう", romaji:"kyou" },
    { kanji:"ありがとう", kana:"ありがとう", romaji:"arigatou" },
    { kanji:"こんにちは", kana:"こんにちは", romaji:"konnichiha" },
    { kanji:"桜", kana:"さくら", romaji:"sakura" },
    { kanji:"日本", kana:"にほん", romaji:"nihon" },
    { kanji:"楽しい", kana:"たのしい", romaji:"tanoshii" },
    { kanji:"音楽", kana:"おんがく", romaji:"ongaku" },
    { kanji:"友達", kana:"ともだち", romaji:"tomodachi" },
    { kanji:"元気", kana:"げんき", romaji:"genki" },
    { kanji:"世界", kana:"せかい", romaji:"sekai" },
    { kanji:"机", kana:"つくえ", romaji:"tsukue" },
    { kanji:"電車", kana:"でんしゃ", romaji:"densha" },
    { kanji:"りんご", kana:"りんご", romaji:"ringo" },
    { kanji:"夏休み", kana:"なつやすみ", romaji:"natsuyasumi" },
    { kanji:"夢", kana:"ゆめ", romaji:"yume" },
    { kanji:"星", kana:"ほし", romaji:"hoshi" },
    { kanji:"海", kana:"うみ", romaji:"umi" },
    { kanji:"山", kana:"やま", romaji:"yama" },
    { kanji:"家族", kana:"かぞく", romaji:"kazoku" },
    { kanji:"仕事", kana:"しごと", romaji:"shigoto" },
    { kanji:"勉強", kana:"べんきょう", romaji:"benkyou" },
    { kanji:"おいしい", kana:"おいしい", romaji:"oishii" },
    { kanji:"早い", kana:"はやい", romaji:"hayai" },
    { kanji:"きれい", kana:"きれい", romaji:"kirei" },
    { kanji:"写真", kana:"しゃしん", romaji:"shashin" },
    { kanji:"電話", kana:"でんわ", romaji:"denwa" },
    { kanji:"車", kana:"くるま", romaji:"kuruma" },
    { kanji:"学校", kana:"がっこう", romaji:"gakkou" },
    { kanji:"先生", kana:"せんせい", romaji:"sensei" },
    { kanji:"図書館", kana:"としょかん", romaji:"toshokan" }
  ];

  const $ = id => document.getElementById(id);
  const stage = $('stage'), startOverlay = $('startOverlay'), resultOverlay = $('resultOverlay');
  const kanaLine = $('kanaLine'), kanjiLine = $('kanjiLine'), romajiLine = $('romajiLine');
  const statTime = $('statTime'), statScore = $('statScore'), statCombo = $('statCombo'), statWpm = $('statWpm');
  const typeInput = $('typeInput');
  const keyboard = $('keyboard');

  const DURATION = 60;
  let state = 'idle';
  let timeLeft = DURATION;
  let timer = null;
  let score = 0, combo = 0, maxCombo = 0;
  let correctKeys = 0, totalKeys = 0;
  let currentWord = null, typed = '';
  let lastWordIndex = -1;

  keyboard.querySelectorAll('.krow').forEach(row => {
    const letters = row.dataset.row.split('');
    letters.forEach(l => {
      const k = document.createElement('div');
      k.className = 'key';
      k.dataset.key = l;
      k.textContent = l;
      row.appendChild(k);
    });
  });
  const keyEls = {};
  keyboard.querySelectorAll('.key').forEach(k => keyEls[k.dataset.key] = k);

  function setActiveKey(char){
    Object.values(keyEls).forEach(k => k.classList.remove('active'));
    if(char && keyEls[char]) keyEls[char].classList.add('active');
  }

  function pickWord(){
    let idx;
    do { idx = Math.floor(Math.random() * WORDS.length); } while(idx === lastWordIndex && WORDS.length > 1);
    lastWordIndex = idx;
    return WORDS[idx];
  }

  function renderWord(){
    kanaLine.textContent = currentWord.kana;
    kanjiLine.textContent = currentWord.kanji;
    renderRomaji();
    setActiveKey(currentWord.romaji[typed.length]);
  }

  function renderRomaji(){
    const r = currentWord.romaji;
    let html = '';
    for(let i=0;i<r.length;i++){
      if(i < typed.length){
        html += `<span class="done">${r[i]}</span>`;
      } else if(i === typed.length){
        html += `<span class="pending cursor">${r[i]}</span>`;
      } else {
        html += `<span class="pending">${r[i]}</span>`;
      }
    }
    romajiLine.innerHTML = html;
  }

  function flashStat(el){
    el.classList.remove('flip');
    void el.offsetWidth;
    el.classList.add('flip');
  }

  function updateStats(){
    statScore.textContent = score;
    statCombo.textContent = combo;
    const mins = (DURATION - timeLeft) / 60;
    const wpm = mins > 0 ? Math.round((correctKeys / 5) / mins) : 0;
    statWpm.textContent = wpm;
  }

  function nextWord(){
    currentWord = pickWord();
    typed = '';
    renderWord();
  }

  function startGame(){
    state = 'playing';
    timeLeft = DURATION;
    score = 0; combo = 0; maxCombo = 0; correctKeys = 0; totalKeys = 0;
    statTime.textContent = timeLeft;
    statTime.parentElement.classList.remove('warn');
    startOverlay.style.display = 'none';
    resultOverlay.style.display = 'none';
    nextWord();
    updateStats();
    typeInput.value = '';
    typeInput.focus();
    timer = setInterval(tick, 1000);
  }

  function tick(){
    timeLeft--;
    statTime.textContent = timeLeft;
    flashStat(statTime);
    if(timeLeft <= 10) statTime.parentElement.classList.add('warn');
    if(timeLeft <= 0){
      endGame();
    }
  }

  function endGame(){
    state = 'result';
    clearInterval(timer);
    setActiveKey(null);
    const mins = DURATION / 60;
    const wpm = Math.round((correctKeys / 5) / mins);
    const acc = totalKeys > 0 ? Math.round((correctKeys / totalKeys) * 100) : 100;
    $('resScore').textContent = score;
    $('resWpm').textContent = wpm;
    $('resAcc').textContent = acc + '%';
    $('resultHeadline').textContent =
      wpm >= 40 ? 'すごい速さ!' : wpm >= 20 ? 'なかなかの腕前' : 'お疲れさま';
    resultOverlay.style.display = 'flex';
  }

  function handleChar(ch){
    if(state !== 'playing') return;
    const expected = currentWord.romaji[typed.length];
    if(!expected) return;
    totalKeys++;
    if(ch === expected){
      typed += ch;
      correctKeys++;
      combo++;
      maxCombo = Math.max(maxCombo, combo);
      const bonus = 10 + Math.min(combo, 20);
      score += bonus;
      flashStat(statScore);
      renderRomaji();
      setActiveKey(currentWord.romaji[typed.length]);
      if(typed.length === currentWord.romaji.length){
        setTimeout(nextWord, 90);
      }
    } else {
      combo = 0;
      $('wordBlock').classList.remove('shake');
      void $('wordBlock').offsetWidth;
      $('wordBlock').classList.add('shake');
    }
    updateStats();
  }

  typeInput.addEventListener('input', (e) => {
    const val = e.target.value;
    if(val.length === 0) return;
    const ch = val[val.length - 1].toLowerCase();
    if(/^[a-z]$/.test(ch)) handleChar(ch);
    typeInput.value = '';
  });

  stage.addEventListener('click', () => typeInput.focus());
  document.addEventListener('keydown', (e) => {
    if(state === 'idle' && e.key.length === 1){ startGame(); return; }
    if(state !== 'result') typeInput.focus();
  });

  $('startBtn').addEventListener('click', startGame);
  $('retryBtn').addEventListener('click', startGame);

  currentWord = pickWord();
  renderWord();
})();
</script>

</body>
</html>

カスタマイズ方法

このゲームは配列とちょっとした定数を変えるだけで、いろいろな方向にアレンジできます。

  • 単語を増やす/差し替えるWORDS配列にオブジェクトを追加するだけ。四字熟語や英単語、専門用語リストにすれば別ジャンルの学習ゲームになります。
  • 制限時間を変えるDURATIONの値(秒)を変更するだけ。30秒のスピード版や180秒のロング版も簡単に作れます。
  • 難易度を調整する:コンボボーナスの上限(Math.min(combo, 20)20)を変えると、コンボを稼ぐ楽しさを強化・抑制できます。
  • ローマ字表記を増やす:現状は1単語につき1通りのローマ字しか正解になりません(例:「し」はshiのみ)。siのような別表記も許容したい場合は、判定部分を配列比較に拡張するのがおすすめです。

よくある質問

Q. ライブラリやフレームワークは必要ですか? A. 不要です。素のHTML・CSS・JavaScriptのみで動作します。Google Fontsの読み込みだけインターネット接続が必要です。

Q. スマートフォンでも遊べますか? A. 遊べます。画面外に配置した<input>要素がソフトウェアキーボードを呼び出す仕組みになっています。

Q. コードを自分のブログやポートフォリオで公開してもいいですか? A. 学習目的・改変・再配布は自由に行っていただけます。オリジナルのゲームとして紹介する際は、単語データやデザインを自分なりにアレンジすると、より自分の作品として見せやすくなります。

まとめ

タイピングゲームは、①データ設計 ②表示処理 ③入力判定 ④タイマー ⑤スコアリング ⑥状態管理という6つの要素に分解すると、決して複雑な実装ではないことがわかります。今回のコードをベースに、単語データやデザインを差し替えるだけでも、オリジナルのタイピングゲームとして十分に公開できる完成度になります。

ぜひ自分だけの単語リストやテーマでカスタマイズして、ブログやポートフォリオで公開してみてください。

コメント

タイトルとURLをコピーしました