【ゲーム】JavaScript:60 ストップウォッチチャレンジ

 「🕒 ストップウォッチチャレンジ 🕒」は、指定された秒数ぴったりでストップウォッチを止めるタイミング精度を競う3ラウンド制のゲームです。各ラウンドごとにランダムな目標秒数(5~11秒)が表示され、スタートボタンで計測を開始し、体感で「ストップ」ボタンを押して指定秒にどれだけ近づけるかを試します。差分に応じて得点が入り、合計スコアを競います。

遊び方・操作方法

  1. タイトル画面で「スタート」をクリック。
  2. ゲーム画面に目標秒数(例:7秒)とストップウォッチが表示されます。
  3. 「スタート」ボタンを押すと計測開始。タイマーが動き始めます。
  4. 体感で指定秒数を測りながら、「ストップ」ボタンを押して計測を止めます。
  5. 記録(経過秒)と目標との差分が表示され、得点が加算されます。
  6. 全3ラウンド終了後、各ラウンドのデータと合計スコアが表示され、タイトルに戻ります。

ルール

  • ラウンド数:3回チャレンジ
  • 目標秒数:各ラウンドで5~11秒の整数から重複なくランダム選出
  • 制限時間:なし(ストップボタンを押すまで無制限に計測)
  • 得点計算:差分が
    ・0.00~0.069秒 → 100点(パーフェクト)
    ・0.070~0.199秒 → 80点
    ・0.200~0.399秒 → 60点
    ・0.400~0.699秒 → 40点
    ・0.700秒以上 → 0点
  • 終了:3ラウンド終了時に集計画面へ。タイトル画面に戻る。

🎮ゲームプレイ

以下のリンク先から実際にプレイできます。

60 ストップウォッチチャレンジ

素材のダウンロード

以下のリンクから使用する素材をダウンロードできます。

stopwatch_challenge_title.pngstopwatch_challenge_bg.png

ゲーム画面イメージ

プログラム全文(stopwatch_challenge.html)

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>🕒 ストップウォッチチャレンジ 🕒</title>
  <style>
    html, body {
      margin: 0; padding: 0;
      width: 100vw; height: 100vh;
      font-family: 'Yu Gothic', 'Meiryo', sans-serif;
      background: url('stopwatch_challenge_bg.png') no-repeat center center fixed;
      background-size: cover;
    }
    body {
      width: 100vw;
      height: 100vh;
      overflow: auto;
    }
    .container {
      width: 1000px;
      margin: 40px auto;
      background: rgba(255,255,255,0.94);
      border-radius: 22px;
      box-shadow: 0 4px 24px rgba(0,0,0,0.13);
      padding-bottom: 32px;
      min-height: 600px;
      position: relative;
    }
    .title-img {
      display: block;
      margin: 20px auto 18px auto;
      width: 400px;
      max-width: 80%;
      height: auto;
    }
    h1 {
      text-align: center;
      font-size: 2.1em;
      margin: 0.6em 0 0.15em 0;
      color: #1976d2;
      text-shadow: 1px 1px 7px #fff;
      letter-spacing: 0.04em;
    }
    .rule-section {
      background: rgba(235,245,255,0.98);
      border-radius: 14px;
      margin: 28px 32px 14px 32px;
      padding: 16px 24px;
      box-shadow: 0 2px 8px #4d90fe22;
    }
    .rule-title {
      text-align: center;
      font-weight: bold;
      font-size: 1.3em;
      margin-bottom: 10px;
      color: #1976d2;
    }
    .rule-text {
      text-align: left;
      font-size: 1.09em;
      line-height: 1.65;
      color: #174078;
      letter-spacing: 0.01em;
    }
    .btn {
      display: block;
      margin: 28px auto 0 auto;
      padding: 14px 50px;
      font-size: 1.2em;
      border: none;
      border-radius: 28px;
      background: linear-gradient(90deg, #87c9fa, #1565c0);
      color: #fff;
      font-weight: bold;
      box-shadow: 0 2px 8px #1976d266;
      cursor: pointer;
      transition: background 0.2s;
    }
    .btn:hover { background: linear-gradient(90deg, #b7e4fe, #1976d2); }
    .game-area {
      width: 900px;
      margin: 40px auto 0 auto;
      background: rgba(240,248,255,0.96);
      border-radius: 16px;
      box-shadow: 0 1px 10px #1565c055;
      display: flex;
      flex-direction: column;
      align-items: center;
      min-height: 260px;
      padding: 24px 0 36px 0;
      user-select: none;
    }
    .status-bar {
      text-align: center;
      margin: 18px auto 0 auto;
      font-size: 1.12em;
      font-weight: bold;
      background: rgba(255,255,220,0.96);
      color: #1976d2;
      width: 380px;
      border-radius: 12px;
      box-shadow: 0 1px 7px #1976d244;
      padding: 10px;
      letter-spacing: 0.05em;
    }
    .timer {
      font-size: 3.3em;
      color: #1976d2;
      background: #fff;
      border-radius: 20px;
      padding: 18px 56px;
      margin: 38px 0 26px 0;
      box-shadow: 0 2px 14px #1976d222;
      border: 3px solid #2196f3;
      text-align: center;
      letter-spacing: 0.04em;
      min-width: 320px;
      font-family: 'Consolas', 'Menlo', 'Yu Gothic', 'Meiryo', sans-serif;
    }
    .target-sec {
      font-size: 1.8em;
      color: #e53935;
      margin-bottom: 18px;
      background: #fffbe8;
      padding: 7px 25px;
      border-radius: 12px;
      border: 2px dashed #fbc02d;
      display: inline-block;
    }
    .result {
      font-size: 1.4em;
      margin-top: 24px;
      margin-bottom: 8px;
      color: #388e3c;
      text-align: center;
    }
    .message-box {
      text-align: center;
      background: rgba(235,245,255,0.98);
      font-size: 2em;
      color: #1976d2;
      font-weight: bold;
      border-radius: 16px;
      box-shadow: 0 4px 24px #1976d233;
      width: 90%;
      max-width: 850px;
      margin: 40px auto 0 auto;
      padding: 44px 12px;
      position: relative;
      z-index: 2;
    }
    .center-btn { margin: 24px auto 0 auto; }
    .score-table {
      margin: 0 auto 18px auto;
      border-collapse: collapse;
      font-size: 1.05em;
      width: 95%;
    }
    .score-table th, .score-table td {
      border: 1px solid #bbdefb;
      padding: 6px 12px;
      background: #e3f2fd;
      text-align: center;
    }
    .score-table th {
      background: #bbdefb;
      color: #1976d2;
      font-size: 0.7em;
    }
    .score-table .small-label {
      font-size: 0.89em;
      color: #333;
      font-weight: normal;
    }
    .score-table .round-label {
      font-size: 0.75em;
      color: #1976d2;
      font-weight: normal;
    }
    @media (max-width: 1100px) {
      .container, .game-area { width: 98vw !important; min-width: 0; }
      .game-area { padding-left:0; padding-right:0; }
      .timer { min-width: 0; font-size:2em; padding: 10px 10vw;}
      .message-box { width: 99vw; max-width: 99vw; }
      .score-table { width: 98vw; }
    }
  </style>
</head>
<body>
  <div class="container" id="main-container"></div>
  <script>
    // ========================
    // ストップウォッチチャレンジ
    // ========================
    let round = 0;
    let totalScore = 0;
    let targetSecs = [];
    let startTime = 0;
    let intervalId = null;
    let isRunning = false;
    let resultTimes = [];
    let resultDiffs = [];
    const ROUND_MAX = 3; // 3回挑戦

    // 指定秒リストをランダム作成
    function makeTargets() {
      let arr = [];
      let used = {};
      while(arr.length < ROUND_MAX) {
        let sec = Math.floor(Math.random()*7)+5; // 5~11秒
        if (!used[sec]) {
          arr.push(sec);
          used[sec] = true;
        }
      }
      return arr;
    }

    // タイトル画面
    function showTitleScreen() {
      clearInterval(intervalId);
      document.getElementById('main-container').innerHTML = `
        <h1>🕒 ストップウォッチチャレンジ 🕒</h1>
        <img src="stopwatch_challenge_title.png" class="title-img" alt="ストップウォッチチャレンジ タイトル">
        <div class="rule-section">
          <div class="rule-title">📝 遊び方・ルール 📝</div>
          <div class="rule-text">
            ・表示された秒数ぴったりを目指して、ストップウォッチを止めよう!<br>
            ・スタートボタンを押すと計測が始まります。<br>
            ・指定秒数(例:7秒)を体感で測ってストップボタンを押してください。<br>
            ・「近さ」に応じてスコアがつきます。全3回チャレンジの合計点を競おう!
          </div>
        </div>
        <button class="btn" id="start-btn">スタート</button>
      `;
      document.getElementById('start-btn').onclick = startGame;
    }

    // ゲーム開始
    function startGame() {
      round = 0;
      totalScore = 0;
      resultTimes = [];
      resultDiffs = [];
      targetSecs = makeTargets();
      showGameScreen();
    }

    // ゲーム画面表示
    function showGameScreen() {
      clearInterval(intervalId);
      isRunning = false;
      document.getElementById('main-container').innerHTML = `
        <h1>🕒 ストップウォッチチャレンジ 🕒</h1>
        <div class="game-area">
          <div class="target-sec" id="target-sec">目標:${targetSecs[round]} 秒</div>
          <div class="timer" id="timer">0.00</div>
          <button class="btn" id="action-btn">${isRunning ? "ストップ" : "スタート"}</button>
          <div class="result" id="result"></div>
          <div class="status-bar">ラウンド ${round+1} / ${ROUND_MAX}</div>
        </div>
      `;
      document.getElementById('action-btn').onclick = isRunning ? stopTimer : startTimer;
    }

    // 計測開始
    function startTimer() {
      if (isRunning) return;
      isRunning = true;
      document.getElementById('action-btn').textContent = "ストップ";
      document.getElementById('result').textContent = "";
      startTime = Date.now();
      intervalId = setInterval(()=>{
        const now = Date.now();
        const sec = ((now-startTime)/1000).toFixed(2);
        document.getElementById('timer').textContent = sec;
      }, 20);
      document.getElementById('action-btn').onclick = stopTimer;
    }

    // 計測ストップ
    function stopTimer() {
      if (!isRunning) return;
      isRunning = false;
      clearInterval(intervalId);
      const now = Date.now();
      const elapsed = ((now-startTime)/1000).toFixed(2);
      document.getElementById('timer').textContent = elapsed;
      const target = targetSecs[round];
      const diff = Math.abs(elapsed - target);
      resultTimes.push(elapsed);
      resultDiffs.push(diff);
      // スコア評価
      let score = 0;
      let evalMsg = "";
      if (diff < 0.07) {
        score = 100;
        evalMsg = "パーフェクト!";
      } else if (diff < 0.20) {
        score = 80;
        evalMsg = "すごい!";
      } else if (diff < 0.40) {
        score = 60;
        evalMsg = "なかなか!";
      } else if (diff < 0.70) {
        score = 40;
        evalMsg = "おしい!";
      } else {
        score = 0;
        evalMsg = "ドンマイ…";
      }
      totalScore += score;

      document.getElementById('result').textContent =
        `記録: ${elapsed} 秒 差: ${diff.toFixed(2)} 秒 → ${evalMsg} (+${score}点)`;

      round++;
      if (round < ROUND_MAX) {
        setTimeout(showGameScreen, 1800);
      } else {
        setTimeout(showEndScreen, 2300);
      }
    }

    // 終了画面
    function showEndScreen() {
      // 結果表作成
      let rows = "";
      for (let i = 0; i < ROUND_MAX; i++) {
        rows += `<tr>
          <th class="round-label">第${i+1}回</th>
          <td class="small-label">目標:${targetSecs[i]} 秒</td>
          <td class="small-label">記録:${resultTimes[i]} 秒</td>
          <td class="small-label">差:${resultDiffs[i].toFixed(2)} 秒</td>
        </tr>`;
      }
      document.getElementById('main-container').innerHTML = `
        <h1>🕒 ストップウォッチチャレンジ 🕒</h1>
        <img src="stopwatch_challenge_title.png" class="title-img" alt="ストップウォッチチャレンジ タイトル">
        <div class="message-box">
          <div style="font-size:1.1em;margin-bottom:18px;">全3ラウンドの記録</div>
          <table class="score-table">${rows}</table>
          <div style="font-size:1.3em;color:#1976d2;">
            合計スコア:<b>${totalScore} 点</b>
          </div>
        </div>
        <button class="btn center-btn" id="back-title-btn">タイトル画面に戻る</button>
      `;
      document.getElementById('back-title-btn').onclick = showTitleScreen;
    }

    // 初期表示
    showTitleScreen();
  </script>
</body>
</html>

アルゴリズムの流れ

手順処理内容
1showTitleScreen() でタイトル画面を描画
2「スタート」押下 → startGame()targetSecs(目標秒リスト)生成&初回ラウンド準備
3showGameScreen() でラウンド画面描画:目標秒/タイマー/ボタン/ラウンド表示
4「スタート」押下 → startTimer() で時刻を取得し setInterval で表示を更新
5「ストップ」押下 → stopTimer() で計測停止、経過秒を取得し差分を算出
6差分に応じて得点を計算、totalScoreresultTimesresultDiffs に記録
7ラウンド進行管理:3ラウンド未満なら次ラウンドへ、3ラウンド目終了後は showEndScreen() を実行
8showEndScreen() で各ラウンドの記録表と合計スコアを表示し、タイトル画面に戻る

関数の詳細

関数名機能概要詳細説明
makeTargets()目標秒リスト生成5~11秒の範囲から重複なく3つの整数秒をランダム選出し配列で返す
showTitleScreen()タイトル画面描画ルールと「スタート」ボタンを表示。既存のタイマーをクリア。
startGame()ゲーム開始処理roundtotalScore をリセットし、targetSecs を生成後、showGameScreen() を呼び出して第1ラウンドを開始
showGameScreen()ラウンド画面描画目標秒、タイマー表示、ボタン、結果欄、ラウンド表示をDOMに書き出し、ボタンのonclickを設定
startTimer()計測開始開始時刻を Date.now() で取得し、20ms間隔で経過時間を計算・表示
stopTimer()計測停止&結果処理clearInterval() で表示更新停止、経過秒と目標秒の差分を算出、スコア評価を行い結果を表示、次ラウンド or 終了判定
showEndScreen()結果画面描画全ラウンドの記録を表形式で表示し、合計スコアを表示。タイトル画面に戻るボタンを設置
shuffleArray(arr)(本プログラムでは未使用)Fisher–Yatesアルゴリズムで配列要素をランダムシャッフル

改造のポイント

  • ラウンド数・時間幅変更ROUND_MAXmakeTargets の秒幅を調整し、チャレンジ回数や目標秒レンジを自由にカスタマイズ可能。
  • UI演出強化:スタート時やストップ時にアニメーションやサウンドを追加し、体感的フィードバックを強化。
  • スコアボーナス:連続パーフェクト時のボーナス点や、最速ストップタイムにボーナスを付与すると戦略性アップ。
  • ランキング機能localStorage やサーバー連携でハイスコアを保存して、リーダーボードを実装。
  • マルチモード:一定秒以内に何回止められるかを競う「カウントモード」、一定回数止めるまでの平均誤差を競う「精度モード」など、多様なモードを追加。

アドバイス
まずはストップウォッチの動作とスコア計算を安定稼働させ、次にUI/UXの演出やサウンド追加でゲーム性を高めていきましょう。ランキングや複数モードを段階的に実装すると、飽きずに繰り返し遊んでもらえます!