【ゲーム】JavaScript:17 パックンマン

 「パックンマン」は、プレイヤーが😄(パックンマン)を操作して迷路内のドット(●)をすべて食べつくし、おばけ(👻🧟💀)に捕まらないようにクリアを目指すシンプルなアクションゲームです。ステージは全3面で、ステージが上がるごとにおばけの数が増え、難易度が上昇します。

遊び方・操作方法

  • 操作キー:キーボードの矢印キー(↑↓←→)でパックンマンを上下左右に移動
  • スタート:「▶️ スタート」ボタンをクリックするとゲーム開始
  • ライフ:初期ライフは2(😄😄)。おばけに捕まるとライフが1つ減り、ライフが0になるとゲームオーバー
  • ステージクリア:迷路内のすべてのドットを食べるとステージクリア

ルール

  1. ドットを食べる:通過したマスにあるドット(●)を食べるとスコア+10
  2. おばけを回避:おばけと同じマスに入るとライフが1減少
  3. 残りライフがある場合:捕まれても同ステージを最初から再スタート
  4. ライフ0でゲームオーバー:タイトル画面に戻る
  5. 全3ステージクリア:クリアメッセージを表示してタイトルへ戻る

🎮ゲームプレイ

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

17 パックンマン

素材のダウンロード

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

pac_man_bg.pngpac_man_title.png

ゲーム画面イメージ

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

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>パックンマン</title>
  <style>
    /* 画面全体スクロール無効 */
    html, body {
      margin: 0;
      padding: 0;
      overflow: hidden;
      width: 100%;
      height: 100%;
    }
    body {
      background: url('pac_man_bg.png') no-repeat center center fixed;
      background-size: cover;
      font-family: Arial, sans-serif;
      color: #fff;
      text-align: center;
    }
    button {
      cursor: pointer;
      border: none;
      border-radius: 5px;
    }

    /* タイトル画面 */
    #titleScreen {
      position: absolute;
      top: 0; left: 0; right: 0; bottom: 0;
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      background: rgba(0,0,0,0.6);
    }
    #titleScreen img {
      max-width: 80%;
      height: auto;
    }
    #titleScreen h1 {
      font-size: 3rem;
      margin: 1rem 0;
    }
    #titleScreen p {
      font-size: 1.2rem;
      max-width: 60%;
      line-height: 1.5;
    }
    #startButton {
      margin-top: 2rem;
      padding: 0.8rem 2rem;
      font-size: 1.2rem;
      background: #ff0;
      color: #000;
      font-weight: bold;
    }

    /* ゲーム画面コンテナ */
    #gameContainer {
      display: none;
      position: relative;
      width: 100%;
      height: 100vh;
    }

    /* マップラッパー */
    #mapWrapper {
      position: relative;
      width: 600px;    /* 20 cols × 30px */
      height: 480px;   /* 16 rows × 30px */
      margin: 2rem auto 0;
    }

    /* キャンバス(マップ) */
    #gameCanvas {
      display: block;
      width: 600px;
      height: 480px;
      background: rgba(0,0,0,0.5);
    }

    /* メッセージエリア:マップ中央 */
    #messageArea {
      position: absolute;
      top: 50%; left: 50%;
      transform: translate(-50%, -50%);
      background: rgba(0,0,0,0.8);
      padding: 1.2rem 1.5rem;
      border-radius: 10px;
      font-size: 1.2rem;
      display: none;
      max-width: 80%;
      line-height: 1.5;
      z-index: 10;
    }
    #messageArea button {
      margin-top: 1rem;
      padding: 0.6rem 1.2rem;
      background: #ff0;
      color: #000;
      font-weight: bold;
    }

    /* HUD:マップ下部に表示 */
    #hud {
      background: rgba(0,0,0,0.7);
      padding: 12px 16px;
      border-radius: 5px;
      font-size: 1.6rem;
      display: flex;
      gap: 32px;
      justify-content: center;
      width: max-content;
      margin: 1rem auto;
    }
  </style>
</head>
<body>

  <!-- タイトル画面 -->
  <div id="titleScreen">
    <img src="pac_man_title.png" alt="パックンマン タイトル">
    <h1>🎮 パックンマンへようこそ 🎮</h1>
    <p>👻 おばけを避けながら、すべてのドットを食べ尽くそう!</p>
    <button id="startButton">▶️ スタート</button>
  </div>

  <!-- ゲーム画面 -->
  <div id="gameContainer">
    <div id="mapWrapper">
      <canvas id="gameCanvas"></canvas>
      <div id="messageArea"></div>
    </div>
    <div id="hud">
      <div>ライフ: <span id="lives"></span></div>
      <div>ステージ: <span id="stage"></span></div>
      <div>スコア: <span id="score"></span></div>
    </div>
  </div>

  <script>
    // 要素取得
    const titleScreen   = document.getElementById('titleScreen');
    const startButton   = document.getElementById('startButton');
    const gameContainer = document.getElementById('gameContainer');
    const canvas        = document.getElementById('gameCanvas');
    const ctx           = canvas.getContext('2d');
    const messageArea   = document.getElementById('messageArea');
    const hudLives      = document.getElementById('lives');
    const hudStage      = document.getElementById('stage');
    const hudScore      = document.getElementById('score');

    // マップ設定
    const tileSize = 30, rows = 16, cols = 20;
    canvas.width = cols * tileSize;
    canvas.height = rows * tileSize;

    // ゲーム状態
    let maze, player, enemies, lives, currentStage, score, inMessage, gameLoopId;

    // 1ステージ用マップテンプレート
    const originalMaze = [
      [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
      [1,0,0,0,0,0,1,0,0,0,0,0,0,1,0,0,0,0,0,1],
      [1,0,0,0,0,0,1,0,0,0,0,0,0,1,0,0,0,0,0,1],
      [1,0,1,1,1,0,1,0,1,1,1,1,0,1,0,1,1,1,0,1],
      [1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1],
      [1,0,1,0,1,0,1,1,1,0,0,1,1,1,0,1,0,1,0,1],
      [1,0,1,0,1,0,1,0,0,0,0,0,0,1,0,1,0,1,0,1],
      [1,0,0,0,1,0,1,0,0,0,0,0,0,1,0,1,0,0,0,1],
      [1,0,0,0,1,0,1,0,0,0,0,0,0,1,0,1,0,0,0,1],
      [1,0,1,0,1,0,1,0,0,0,0,0,0,1,0,1,0,1,0,1],
      [1,0,1,0,1,0,1,1,1,1,1,1,1,1,0,1,0,1,0,1],
      [1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1],
      [1,0,1,1,1,0,1,0,1,1,1,1,0,1,0,1,1,1,0,1],
      [1,0,0,0,0,0,1,0,0,0,0,0,0,1,0,0,0,0,0,1],
      [1,0,0,0,0,0,1,0,0,0,0,0,0,1,0,0,0,0,0,1],
      [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
    ];

    // 全モンスター候補
    const allEnemies = [
      { x: 18, y: 2,  emoji: '👻' },
      { x: 3,  y: 14, emoji: '🧟' },
      { x: 10, y: 8,  emoji: '💀' }
    ];

    // ゲーム開始
    startButton.addEventListener('click', () => {
      titleScreen.style.display   = 'none';
      gameContainer.style.display = 'block';
      initGame();
      // 敵移動+描画ループ (600ms間隔でモンスターさらにゆっくり)
      gameLoopId = setInterval(gameLoop, 600);
    });

    function showTitle() {
      clearInterval(gameLoopId);
      gameContainer.style.display = 'none';
      titleScreen.style.display   = 'flex';
    }

    // 初期化
    function initGame() {
      lives        = 2;
      currentStage = 1;
      score        = 0;
      inMessage    = false;
      resetStage();
    }

    // ステージ設定/リセット
    function resetStage() {
      inMessage = false;
      maze      = originalMaze.map(r => r.slice());
      player    = { x: 1, y: 2 };
      // 現ステージ分のモンスターを配置
      enemies = allEnemies.slice(0, currentStage).map(e => ({ ...e }));
      updateHUD();
      drawAll();
    }

    // HUD更新
    function updateHUD() {
      hudLives.textContent = '😄'.repeat(lives);
      hudStage.textContent = currentStage;
      hudScore.textContent = score;
    }

    // メインループ
    function gameLoop() {
      if (inMessage) return;
      moveEnemies();
      checkCollision();
      drawAll();
      checkStageClear();
    }

    // 描画
    function drawAll() {
      ctx.clearRect(0, 0, canvas.width, canvas.height);
      drawMaze();
      drawPlayer();
      drawEnemies();
    }
    function drawMaze() {
      for (let y = 0; y < rows; y++) {
        for (let x = 0; x < cols; x++) {
          if (maze[y][x] === 1) {
            ctx.fillStyle = 'blue';
            ctx.fillRect(x * tileSize, y * tileSize, tileSize, tileSize);
          } else if (maze[y][x] === 0) {
            ctx.fillStyle = 'yellow';
            ctx.beginPath();
            ctx.arc(
              x * tileSize + tileSize / 2,
              y * tileSize + tileSize / 2,
              5, 0, Math.PI * 2
            );
            ctx.fill();
          }
        }
      }
    }
    function drawPlayer() {
      ctx.font = `${tileSize}px Arial`;
      ctx.textAlign = 'center';
      ctx.textBaseline = 'middle';
      ctx.fillText(
        '😄',
        player.x * tileSize + tileSize / 2,
        player.y * tileSize + tileSize / 2
      );
    }
    function drawEnemies() {
      enemies.forEach(e => {
        ctx.font = `${tileSize}px Arial`;
        ctx.textAlign = 'center';
        ctx.textBaseline = 'middle';
        ctx.fillText(
          e.emoji,
          e.x * tileSize + tileSize / 2,
          e.y * tileSize + tileSize / 2
        );
      });
    }

    // プレイヤー移動
    document.addEventListener('keydown', e => {
      if (['ArrowUp','ArrowDown','ArrowLeft','ArrowRight'].includes(e.key)) {
        e.preventDefault();
      }
      if (inMessage) return;
      let dx = 0, dy = 0;
      if (e.key === 'ArrowUp')    dy = -1;
      if (e.key === 'ArrowDown')  dy =  1;
      if (e.key === 'ArrowLeft')  dx = -1;
      if (e.key === 'ArrowRight') dx =  1;
      if (dx || dy) movePlayer(dx, dy);
    });
    function movePlayer(dx, dy) {
      const nx = player.x + dx, ny = player.y + dy;
      if (maze[ny] && maze[ny][nx] !== 1) {
        player.x = nx; player.y = ny;
        if (maze[ny][nx] === 0) {
          maze[ny][nx] = 2;
          score += 10;
          hudScore.textContent = score;
        }
      }
      drawAll();
    }

    // 敵移動
    function moveEnemies() {
      const dirs = [{dx:0,dy:-1},{dx:0,dy:1},{dx:-1,dy:0},{dx:1,dy:0}];
      enemies.forEach(e => {
        let best = null, minD = Infinity;
        dirs.forEach(d => {
          const nx = e.x + d.dx, ny = e.y + d.dy;
          if (maze[ny] && maze[ny][nx] !== 1) {
            const dist = Math.abs(nx - player.x) + Math.abs(ny - player.y);
            if (dist < minD) { minD = dist; best = d; }
          }
        });
        if (best) {
          e.x += best.dx;
          e.y += best.dy;
        }
      });
    }

    // 衝突判定
    function checkCollision() {
      for (let e of enemies) {
        if (e.x === player.x && e.y === player.y) {
          handleCaught();
          break;
        }
      }
    }
    function handleCaught() {
      inMessage = true;
      lives--;
      updateHUD();
      if (lives > 0) {
        showTemporaryMessage(
          `💥 捕まった! 残りライフ: ${lives}`, 
          3000, 
          () => resetStage()
        );
      } else {
        showReturnMessage('💀 ゲームオーバー 💀');
      }
    }

    // ステージクリア判定
    function checkStageClear() {
      if (!maze.some(row => row.includes(0))) {
        inMessage = true;
        if (currentStage < 3) {
          showTemporaryMessage(
            `🎉 ステージ${currentStage}クリア! 次へ…`, 
            5000, 
            () => {
              currentStage++;
              resetStage();
            }
          );
        } else {
          showReturnMessage('🏆 全ステージクリア! おめでとう! 🏆');
        }
      }
    }

    // メッセージ表示
    function showTemporaryMessage(text, duration, cb) {
      messageArea.innerHTML = `<p>${text}</p>`;
      messageArea.style.display = 'block';
      setTimeout(() => {
        messageArea.style.display = 'none';
        cb && cb();
      }, duration);
    }
    function showReturnMessage(text) {
      messageArea.innerHTML =
        `<p>${text}</p><button id="returnBtn">タイトル画面へ戻る</button>`;
      messageArea.style.display = 'block';
      document.getElementById('returnBtn').addEventListener('click', () => {
        messageArea.style.display = 'none';
        showTitle();
      });
    }
  </script>
</body>
</html>

アルゴリズムの流れ

ステップ処理内容主な関数・命令
1. 初期化ライフ・ステージ・スコアをリセットし、ステージのマップを生成initGame()resetStage()
2. メインループ一定間隔(600ms)でおばけを動かし、衝突判定・描画・クリア判定gameLoop()
3. 描画迷路・プレイヤー・おばけをキャンバスに描画drawMaze(), drawPlayer(), drawEnemies()
4. プレイヤー移動矢印キーで移動可能判定 → ドットを食べる(スコア加算)movePlayer(dx,dy)
5. おばけ移動四方の移動先を評価し、プレイヤーへの距離が最小となる方向へ移動moveEnemies()
6. 衝突判定プレイヤーとおばけが同座標か判定checkCollision()handleCaught()
7. ステージクリア判定ドットが残っているか判定 → クリア/全ステージクリア処理checkStageClear()

関数の詳しい解説

関数名説明
initGame()ゲーム開始時の初期設定。ライフ・ステージ・スコアを初期化し、最初のステージをセット
resetStage()マップをコピーしてプレイヤー・おばけを再配置し、HUDを更新
updateHUD()ライフ・ステージ・スコアを画面下部のHUDに反映
gameLoop()メインループ。おばけ移動・衝突判定・描画・クリア判定を順に実行
drawMaze()maze 配列を走査し、壁(1)を青、ドット(0)を黄色の円で描画
drawPlayer()プレイヤー絵文字(😄)を現在座標に描画
drawEnemies()各おばけの絵文字をそれぞれの座標に描画
movePlayer(dx,dy)移動先が通行可能(壁でない)ならプレイヤー座標を更新し、ドットならスコア+■
moveEnemies()四方向の移動先を評価し、プレイヤーとの距離が最小となる方向へおばけを移動
checkCollision()プレイヤーとおばけが同じ座標かチェックし、捕まった場合に handleCaught() を呼び出し。
handleCaught()ライフ減少、再スタート or ゲームオーバーメッセージ表示
checkStageClear()マップ内にドットが残存しているかチェックし、次ステージ or クリア完了メッセージを表示
showTemporaryMessage()一時メッセージを表示し、一定時間後に非表示→コールバック実行
showReturnMessage()終了メッセージ+「タイトルへ戻る」ボタンを表示

改造のポイント

  • タイルサイズの変更tileSize を変えてマップの見た目を拡大/縮小
  • ステージ数追加allEnemies 配列と if (currentStage < 3) の条件を調整して、4面以上を追加可能
  • おばけの動き多様化moveEnemies() にランダム要素や追跡アルゴリズム(A* など)を導入
  • パワーアップアイテム:マップに特殊アイテムを配置し、取得時におばけを一定時間無力化
  • BGM/SE導入Audio オブジェクトで効果音やBGMを再生
  • スコアランキング機能localStorage へスコアを保存し、ランキング画面を表示
  • 難易度調整setInterval(gameLoop, 600) の間隔を短くしてスピードアップ