【ゲーム】JavaScript:39 迷路ゲーム

 「🌀 迷路ゲーム 🌀」は、ランダム生成された全3ステージの迷路をプレイヤー(オレンジ色のセル)で攻略し、各ステージのゴール(緑色のセル)を目指すタイムアタック型ゲームです。
矢印キーだけのシンプル操作ながら、ランダム壁配置による毎回異なるルート探索が楽しめます。

遊び方・操作方法

  1. タイトル画面で 「スタート ▶️」 をクリック
  2. ゲーム画面に切り替わり、ステージ1の迷路が表示
  3. 矢印キー(↑↓←→)でオレンジのプレイヤーセルを上下左右に移動
  4. 迷路のゴール(緑セル)に到達すると次ステージへ自動移行
  5. 全3ステージをクリアするとタイム計測終了・終了画面へ

ルール

  • 壁セル(黒) は通過不可。プレイヤーセルは壁を避けて移動
  • 道セル(白) のみ移動可能
  • 1ステージあたり必ずスタート(1,1)からゴール(18,18)まで到達可能な迷路を自動生成
  • 全3ステージクリアまでの累計タイムを競う
  • クリアタイムの上位5位をブラウザの LocalStorage に保存

🎮ゲームプレイ

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

39 迷路ゲーム

素材のダウンロード

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

maze_title.pngmaze_bg.png

ゲーム画面イメージ

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

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>🌀 迷路ゲーム 🌀</title>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <style>
    /* ================= 全体リセット ================= */
    * { box-sizing: border-box; margin: 0; padding: 0; }
    body {
      background: url('maze_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: 700px; max-width: 95%;
      box-shadow: 0 4px 8px rgba(0,0,0,0.3);
      z-index: 10;
    }

    /* ================ タイトル画面 ================ */
    #title-screen h1 {
      font-size: 2em; text-align: center; margin-bottom: 10px;
    }
    #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: #007bff;
      border: none; border-radius: 5px;
      cursor: pointer; transition: background-color .2s, transform .1s;
    }
    .btn:hover { background-color: #0056b3; transform: translateY(-2px); }
    .btn:active { transform: translateY(0); }

    /* ================ ゲーム画面 ================ */
    #game-screen {
      width: 700px; max-width: 95%;
      margin: 0 auto; padding-top: 40px;
      text-align: center;
    }
    #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);
      display: inline-block;
    }
    #status-message {
      margin-bottom: 10px; padding: 8px;
      background: rgba(0,0,0,0.7); color: #fff;
      border-radius: 5px; font-size: 1em;
    }
    .maze-grid {
      display: grid;
      grid-template-columns: repeat(20, 30px);
      grid-template-rows: repeat(20, 30px);
      gap: 2px; background: #000; padding: 2px;
    }
    .cell {
      width: 30px; height: 30px; background: #fff;
    }
    .cell.wall  { background: #333; }
    .cell.player { background: #ff5722; }
    .cell.goal   { background: #4caf50; }

    /* ================ 終了画面 ================ */
    #end-screen { text-align: center; }
    #time-message { font-size: 1.3em; margin: 10px 0; }
    #ranking {
      text-align: center; margin: 20px 0;
    }
    #ranking strong {
      display: block; font-size: 1.2em; 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;
    }
    #end-back-btn {
      display: block; margin: 20px auto 0;
    }
  </style>
</head>
<body>
  <!-- タイトル画面 -->
  <div id="title-screen" class="overlay">
    <img src="maze_title.png" alt="迷路ゲーム タイトル" class="title-image">
    <h1>🌀 迷路ゲーム 🌀</h1>
    <div class="instructions-title">📖 遊び方・ルール</div>
    <div class="instructions">
      <p>・矢印キーでプレイヤー(オレンジ)を操作します。</p>
      <p>・壁(黒)を避けてゴール(緑)を目指してください。</p>
      <p>・全3つの迷路をクリアするとゲームクリア!</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/3)</div>
      <div id="maze" class="maze-grid"></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 ROWS = 20, COLS = 20;
  const LEVELS = 3;
  let mazes = [], level = 0;
  let playerPos, startTime;
  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');
  const mazeEl      = document.getElementById('maze');
  const statusEl    = document.getElementById('status-message');
  const timeMsgEl   = document.getElementById('time-message');
  const rankingList = document.getElementById('ranking-list');

  /**
   * タイトル画面表示
   */
  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');
    // 3つの迷路を生成
    mazes = [];
    for (let i = 0; i < LEVELS; i++) {
      mazes.push(generateMaze());
    }
    level = 0;
    startTime = performance.now();
    startLevel();
  }

  /**
   * 各レベル開始
   */
  function startLevel() {
    // 初期位置とゴール
    playerPos = { x:1, y:1 };
    renderMaze();
    statusEl.textContent = 
      `レベル ${level+1} / ${LEVELS} へようこそ`;
    // 少し待って案内
    setTimeout(() => {
      statusEl.textContent = 
        `矢印キーで移動してください (${level+1}/${LEVELS})`;
      document.addEventListener('keydown', onKeyDown);
    }, 500);
  }

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

  /**
   * ランダム迷路生成 (border walls + ランダム内壁 調整)
   */
  function generateMaze() {
    // 0=道,1=壁
    let m = Array.from({length:ROWS}, () => Array(COLS).fill(0));
    // 周囲を壁
    for (let y=0; y<ROWS; y++) {
      for (let x=0; x<COLS; x++) {
        if (y===0||y===ROWS-1||x===0||x===COLS-1) m[y][x]=1;
        else m[y][x] = Math.random() < 0.25 ? 1 : 0;
      }
    }
    // 開始/ゴールは道
    m[1][1] = 0;
    m[ROWS-2][COLS-2] = 0;
    // 通路確保
    if (!isReachable(m)) return generateMaze();
    return m;
  }

  /**
   * ゴール到達可能か BFS
   */
  function isReachable(m) {
    let visited = Array.from({length:ROWS}, ()=>Array(COLS).fill(false));
    let q = [{x:1,y:1}]; visited[1][1]=true;
    while (q.length) {
      let {x,y} = q.shift();
      if (x===COLS-2 && y===ROWS-2) return true;
      [[1,0],[-1,0],[0,1],[0,-1]].forEach(([dx,dy])=>{
        let nx=x+dx, ny=y+dy;
        if (nx>0&&nx< COLS-1 && ny>0&&ny< ROWS-1 &&
            !visited[ny][nx] && m[ny][nx]===0) {
          visited[ny][nx]=true;
          q.push({x:nx,y:ny});
        }
      });
    }
    return false;
  }

  /**
   * 迷路描画
   */
  function renderMaze() {
    mazeEl.innerHTML='';
    const m = mazes[level];
    for (let y=0; y<ROWS; y++){
      for (let x=0; x<COLS; x++){
        const cell = document.createElement('div');
        cell.classList.add('cell');
        if (m[y][x]===1) cell.classList.add('wall');
        if (x===playerPos.x && y===playerPos.y) cell.classList.add('player');
        if (x===COLS-2 && y===ROWS-2) cell.classList.add('goal');
        mazeEl.appendChild(cell);
      }
    }
  }

  /**
   * 矢印キー押下
   */
  function onKeyDown(e) {
    let nx=playerPos.x, ny=playerPos.y;
    if (e.key==='ArrowUp')    ny--;
    else if (e.key==='ArrowDown') ny++;
    else if (e.key==='ArrowLeft') nx--;
    else if (e.key==='ArrowRight') nx++;
    else return;
    const m = mazes[level];
    if (m[ny][nx]===0 || (nx===COLS-2 && ny===ROWS-2)) {
      playerPos={x:nx,y:ny};
      renderMaze();
      if (nx===COLS-2 && ny===ROWS-2) {
        document.removeEventListener('keydown', onKeyDown);
        statusEl.textContent = `レベル ${level+1} クリア!`;
        level++;
        if (level<LEVELS) {
          setTimeout(() => startLevel(), 1000);
        } else {
          const elapsed = (performance.now()-startTime)/1000;
          setTimeout(()=>showEnd(elapsed), 500);
        }
      }
    }
  }

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

  /**
   * ランキング描画
   */
  function renderRanking() {
    rankingList.innerHTML='';
    const ranking = JSON.parse(localStorage.getItem('maze_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>

アルゴリズムの流れ

ステップ処理内容
1タイトル画面 → 「スタート」クリックで showGame() を実行
2showGame():レベル用に3つの迷路を生成し startLevel()
3startLevel():プレイヤー初期位置設定・描画・キーイベント登録
4矢印キー押下 → onKeyDown():衝突判定後に移動・再描画
5ゴール到達 → レベルクリア or 全レベルクリア判定
6クリア → 経過時間を計測し showEnd() で表示・ランキング登録

関数の詳細解説

関数名役割
generateMaze()周囲を壁にし、内部はランダム壁配置 → 通路確保できるまで再生成
isReachable(m)BFS でスタート→ゴール間の到達可能性を確認
renderMaze()<div class="cell"> を動的生成し、壁・プレイヤー・ゴールを描画
onKeyDown(e)矢印キーに応じて次セル判定→移動→クリア判定
updateRanking(t)LocalStorage からタイムを読み込み、ソート&上位5位保存
renderRanking()保存されたランキングを読み込み、HTMLでリスト表示

改造のポイント

  • 迷路形状の変更:壁の出現確率や範囲チェックを調整して難易度変更
  • ステージ数・サイズLEVELSROWS/COLS を変更し、短い/長い迷路に対応
  • 視覚演出:CSS アニメーションでプレイヤー移動時にスムーズなトランジションを追加
  • タイマーモード:ステージごとのクリアタイム&合計タイムの同時表示
  • ダッシュ機能:Shiftキー長押しで一時的に高速移動(通路のみ)を実装

シンプルながら毎回異なる体験ができる迷路ゲーム。ぜひコードを読み解き、自分好みに改造してみてください!