【ゲーム】JavaScript:38 アタック25

 「アタック25」は1~25までの数字タイルをランダム配置し、1から順番にタイルをクリックして消していくシンプルながら反射神経と集中力を問うゲームです。最後の25をクリックするとクリアとなり、クリアタイムを計測してランキングに登録します。

遊び方・操作方法

  1. タイトル画面で「スタート ▶️」をクリック
  2. ゲーム画面に切り替わり、1~25のタイルがシャッフルされた盤面が表示されます
  3. 「1」のタイルをクリックすると消え、続いて「2」「3」と順にクリック
  4. 最後の「25」をクリックするとクリアとなり、クリアタイムが表示されます
  5. 終了画面で上位5位のランキングを確認のうえ、タイトルに戻ることができます

ルール

  • タイルはクリックのみで、ドラッグや移動はできません
  • 1→2→…→25 の順に正しくクリックしないと無効クリック扱い
  • 開始から最後のタイルを消すまでのタイムを計測
  • クリアタイム上位5位をLocalStorageに保存

🎮ゲームプレイ

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

38 アタック25

素材のダウンロード

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

attack25_title.pngattack25_bg.png

ゲーム画面イメージ

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

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>🎯 アタック25 🎯</title>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <style>
    /* ================= 全体リセット ================= */
    * {
      box-sizing: border-box;
      margin: 0;
      padding: 0;
    }
    body {
      background: url('attack25_bg.png') no-repeat center center fixed;
      background-size: cover;
      font-family: 'Arial', sans-serif;
      height: 100vh;
      user-select: none;
      position: relative;
      color: #333;
    }
    .hidden { display: none; }

    /* ================ オーバーレイ共通 ================ */
    .overlay {
      position: absolute;
      top: 50%; left: 50%;
      transform: translate(-50%, -50%);
      background: rgba(255,255,255,0.95);
      padding: 20px;
      border-radius: 10px;
      width: 600px;          /* 横幅600px */
      max-width: 90%;
      box-shadow: 0 4px 8px rgba(0,0,0,0.3);
      z-index: 10;
    }

    /* ================ タイトル画面 ================ */
    #title-screen h1 {
      font-size: 2em;
      margin-bottom: 10px;
      text-align: center;
    }
    #title-screen .title-image {
      display: block;
      margin: 0 auto 10px;
      max-width: 80%;
      height: auto;
    }
    #title-screen .instructions-title {
      text-align: center;
      font-weight: bold;
      font-size: 1.1em;
      margin-top: 10px;
    }
    #title-screen .instructions {
      text-align: left;
      margin: 10px 0;
      line-height: 1.4;
      font-size: 0.95em;
    }
    #title-screen .btn {
      display: block;
      margin: 20px auto 0;
    }

    /* ================ ボタン共通 ================ */
    .btn {
      padding: 10px 20px;
      font-size: 1em;
      font-weight: bold;
      color: #fff;
      background-color: #28a745;
      border: none;
      border-radius: 5px;
      cursor: pointer;
      transition: background-color .2s, transform .1s;
    }
    .btn:hover {
      background-color: #218838;
      transform: translateY(-2px);
    }
    .btn:active {
      transform: translateY(0);
    }

    /* ================ ゲーム画面 ================ */
    #game-screen {
      width: 600px;          /* 横幅600px */
      max-width: 90%;
      margin: 0 auto;
      padding-top: 40px;
    }
    #game-screen .game-container {
      background: rgba(255,255,255,0.85);
      padding: 20px;
      border-radius: 10px;
      box-shadow: 0 4px 8px rgba(0,0,0,0.2);
      text-align: center;
    }
    /* ステータスメッセージ */
    #status-message {
      margin-bottom: 10px;
      padding: 8px;
      background: rgba(0,0,0,0.7);
      color: #fff;
      border-radius: 5px;
      font-size: 1em;
    }
    /* 5×5 ボード */
    .board {
      display: grid;
      grid-template-columns: repeat(5, 1fr);
      grid-gap: 5px;
      margin-bottom: 10px;
    }
    .tile {
      background: #007bff;
      color: #fff;
      font-size: 1.2em;
      font-weight: bold;
      display: flex;
      align-items: center;
      justify-content: center;
      height: 60px;
      border-radius: 5px;
      cursor: pointer;
      transition: opacity .3s, transform .2s;
    }
    .tile.hidden {
      opacity: 0;
      transform: scale(0.5);
      pointer-events: none;
    }

    /* ================ 終了画面 ================ */
    #end-screen {
      text-align: center;
    }
    #time-message {
      font-size: 1.3em;
      margin: 10px 0;
    }
    #ranking {
      margin: 20px 0;
      text-align: center;       /* 中央寄せ */
    }
    #ranking strong {
      font-size: 1.2em;
      display: block;
      margin-bottom: 10px;
    }
    #ranking-list {
      list-style: none;
      padding: 0;
      margin: 0 auto;
      display: inline-block;    /* 中央寄せ */
    }
    #ranking-list li {
      font-size: 1.2em;         /* 表示を大きく */
      margin: 5px 0;
      line-height: 1.4;
    }
    #end-back-btn {
      display: block;
      margin: 20px auto 0;
    }
  </style>
</head>
<body>
  <!-- タイトル画面 -->
  <div id="title-screen" class="overlay">
    <img src="attack25_title.png" alt="アタック25 タイトル" class="title-image">
    <h1>🎯 アタック25 🎯</h1>
    <div class="instructions-title">📖 遊び方・ルール</div>
    <div class="instructions">
      <p>・1〜25 の数字パネルがランダムに配置されます。</p>
      <p>・1 のパネルから順にクリックし、正解のパネルは消えます。</p>
      <p>・最後の 25 をクリックするとクリア!</p>
      <p>・開始から終了までの時間を計測し、上位5位のタイムを保存します。</p>
    </div>
    <button id="start-btn" class="btn">スタート ▶️</button>
  </div>

  <!-- ゲーム画面 -->
  <div id="game-screen" class="hidden">
    <div class="game-container">
      <div id="status-message">1 のパネルをクリックしてください</div>
      <div id="board" class="board"></div>
    </div>
  </div>

  <!-- 終了画面 -->
  <div id="end-screen" class="overlay hidden">
    <h1>🏁 クリア! 🏁</h1>
    <p id="time-message"></p>
    <div id="ranking">
      <strong>🏆 ランキング (上位5位)</strong>
      <ol id="ranking-list"></ol>
    </div>
    <button id="end-back-btn" class="btn">タイトル画面に戻る ↩️</button>
  </div>

  <script>
  // ================= 定数 =================
  const TOTAL = 25;                         // パネル総数
  let sequence = [];                        // 1..25 の順番
  let nextNumber = 1;                       // 次にクリックすべき数字
  let startTime = 0;                        // 開始時刻 ms

  // DOM 要素
  const boardEl      = document.getElementById('board');
  const statusEl     = document.getElementById('status-message');
  const timeMsgEl    = document.getElementById('time-message');
  const rankingList  = document.getElementById('ranking-list');
  const titleScreen  = document.getElementById('title-screen');
  const gameScreen   = document.getElementById('game-screen');
  const endScreen    = document.getElementById('end-screen');
  const startBtn     = document.getElementById('start-btn');
  const endBackBtn   = document.getElementById('end-back-btn');

  /**
   * タイトル画面表示
   */
  function showTitle() {
    titleScreen.classList.remove('hidden');
    gameScreen.classList.add('hidden');
    endScreen.classList.add('hidden');
  }

  /**
   * ゲーム画面開始
   */
  function showGame() {
    titleScreen.classList.add('hidden');
    gameScreen.classList.remove('hidden');
    endScreen.classList.add('hidden');
    initGame();
  }

  /**
   * 終了画面表示
   */
  function showEnd(elapsed) {
    gameScreen.classList.add('hidden');
    endScreen.classList.remove('hidden');
    timeMsgEl.textContent = `クリアタイム: ${elapsed.toFixed(2)} 秒`;
    updateRanking(elapsed);
    renderRanking();
  }

  /**
   * ゲーム初期化
   */
  function initGame() {
    sequence = Array.from({length: TOTAL}, (_, i) => i + 1);
    shuffleArray(sequence);
    nextNumber = 1;
    statusEl.textContent = `1 のパネルをクリックしてください`;
    renderBoard();
    startTime = performance.now();
  }

  /**
   * 配列シャッフル(Fisher–Yates)
   */
  function shuffleArray(arr) {
    for (let i = arr.length - 1; i > 0; i--) {
      const j = Math.floor(Math.random() * (i + 1));
      [arr[i], arr[j]] = [arr[j], arr[i]];
    }
  }

  /**
   * ボード描画
   */
  function renderBoard() {
    boardEl.innerHTML = '';
    sequence.forEach(num => {
      const tile = document.createElement('div');
      tile.classList.add('tile');
      tile.textContent = num;
      tile.dataset.num = num;
      tile.addEventListener('click', onTileClick);
      boardEl.appendChild(tile);
    });
  }

  /**
   * タイルクリック処理
   */
  function onTileClick(e) {
    const clicked = Number(e.currentTarget.dataset.num);
    if (clicked !== nextNumber) return;
    e.currentTarget.classList.add('hidden');
    nextNumber++;
    if (nextNumber <= TOTAL) {
      statusEl.textContent = `${nextNumber} のパネルをクリックしてください`;
    } else {
      const elapsed = (performance.now() - startTime) / 1000;
      statusEl.textContent = `🎉 クリア! 🎉`;
      setTimeout(() => showEnd(elapsed), 500);
    }
  }

  /**
   * ランキング更新
   */
  function updateRanking(time) {
    let ranking = JSON.parse(localStorage.getItem('attack25_ranking') || '[]');
    ranking.push(time);
    ranking.sort((a,b) => a - b);
    ranking = ranking.slice(0,5);
    localStorage.setItem('attack25_ranking', JSON.stringify(ranking));
  }

  /**
   * ランキング描画
   */
  function renderRanking() {
    rankingList.innerHTML = '';
    const ranking = JSON.parse(localStorage.getItem('attack25_ranking') || '[]');
    ranking.forEach((t, i) => {
      const li = document.createElement('li');
      li.textContent = `${i+1}位: ${t.toFixed(2)} 秒`;
      rankingList.appendChild(li);
    });
  }

  // ===== イベント登録 =====
  startBtn.addEventListener('click', showGame);
  endBackBtn.addEventListener('click', showTitle);

  // 初期表示
  document.addEventListener('DOMContentLoaded', showTitle);
  </script>
</body>
</html>

アルゴリズムの流れ

ステップ処理
1showTitle() でタイトル画面を表示
2「スタート」クリック→showGame()initGame()
3initGame():1〜25を配列に用意し、shuffleArray()でシャッフル
4renderBoard():DOMにタイルを描画
5タイルクリック→onTileClick():正解なら消して次へ
625 まで消したら経過時間計算→showEnd()に渡す
7showEnd():終了画面を表示→updateRanking()renderRanking()

関数の詳細解説

関数名説明
initGame()ゲーム開始時に配列の初期化、シャッフル、DOM描画、タイマー開始
shuffleArray(arr)Fisher–Yates アルゴリズムで配列をランダムにシャッフル
renderBoard()#board 要素内に数字タイルを動的生成しイベントリスナーを設定
onTileClick(e)クリックしたタイルの数字を判定。正解なら非表示にし、クリア判定
updateRanking(time)LocalStorage から既存ランキング取得→新タイム追加→上位5位に整形保存
renderRanking()保存されたランキングを読み出し、<ol> に表示

改造のポイント

  • 難易度調整TOTAL を 16 や 36 に変更して盤面サイズを変える
  • タイマー表示:クリアタイムをリアルタイム表示するウォッチ機能を追加
  • テーマ変更:タイルに数字ではなく画像やアイコンを使い「絵合わせ版」に
  • スコア要素:正解ミス数やクリック回数をカウントしてペナルティ導入
  • SNS共有:クリアタイムをツイートできるボタンを設置してトーナメント感UP

シンプルだからこそ奥が深い「アタック25」。ぜひ自由に改造して、自分だけの楽しいルールや演出を加えてみてください!