【ゲーム】JavaScript:52 倉庫番


 「📦倉庫番(PushBox)」ゲームは、荷物(箱)を押してゴール(🎯)に運ぶパズルゲームです。プレイヤー(🧑‍🔧)は箱を押して移動できますが、「箱は引けない・複数同時に押せない」という制約があります。シンプルなルールながら頭を使う奥深いロジックパズルです。

遊び方と操作方法

  • 移動操作:キーボードの矢印キー(↑↓←→)またはWASDキーでプレイヤーを動かします。
  • 箱を押す:箱の隣に移動し、箱の方向に進むと箱を1マスだけ押せます。
  • リタイア:画面右上の「リタイア」ボタンでタイトル画面に戻れます。
  • クリア条件:すべての箱が緑色のゴール(🎯)に乗るとクリア!

ルール

  • 箱(📦)を押してゴール(🎯)へ運ぶ。
  • 1回に1つだけ箱を押せる。
  • 箱は引けません。押すだけ。
  • 箱をゴールに運ぶと色が変化します。
  • すべての箱がゴール上に乗ればクリアです。

🎮ゲームプレイ

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

52 倉庫番

素材のダウンロード

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

warehouse-keeper_title.pngwarehouse-keeper_bg.png

ゲーム画面イメージ

プログラム全文(warehouse-keeper.html)

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>📦 倉庫番(PushBox)ゲーム 📦</title>
  <style>
    html, body {
      width: 100vw; height: 100vh;
      margin: 0; padding: 0;
      background: url('warehouse-keeper_bg.png') no-repeat center center fixed;
      background-size: cover;
      font-family: 'Yu Gothic', 'Meiryo', sans-serif;
      overflow: auto;
    }
    body {
      display: flex; justify-content: center; align-items: center;
      min-height: 100vh;
    }
    #container {
      width: 800px;
      background: rgba(30, 32, 40, 0.95);
      border-radius: 18px;
      box-shadow: 0 0 16px 4px #222a;
      padding: 32px 0 32px 0;
      margin: 20px 0;
      position: relative;
      z-index: 10;
    }
    .center { text-align: center; }
    .title-img { display: block; margin: 0 auto 18px auto; max-width:600px; max-height:180px; width:auto; height:auto; }
    .button {
      display: inline-block;
      padding: 10px 30px;
      background: #f6c94d;
      color: #322; font-size: 1.25rem; font-weight: bold;
      border: none; border-radius: 8px;
      box-shadow: 0 2px 8px #1118;
      cursor: pointer;
      margin: 12px auto 0 auto;
      transition: filter 0.2s;
    }
    .button:hover { filter: brightness(1.1); }
    .main-title {
      font-size: 2.1rem;
      font-weight: bold;
      color: #ffe46c;
      letter-spacing: .1em;
      margin-bottom: 12px;
      text-shadow: 0 2px 8px #322;
    }
    .rule-title {
      font-size: 1.4rem;
      font-weight: bold;
      color: #fafafa;
      margin: 32px 0 8px 0;
      text-align: center;
      text-shadow: 0 2px 8px #222;
    }
    .rule-desc {
      background: rgba(60,60,60,0.8);
      border-radius: 8px;
      padding: 10px 32px;
      font-size: 1.08rem;
      color: #fff;
      text-align: left;
      margin: 0 auto 24px auto;
      line-height: 1.8;
      width: 600px;
      text-shadow: 0 1px 4px #000b;
    }
    .msg-box {
      background: rgba(40, 60, 180, 0.85);
      color: #fff;
      border-radius: 8px;
      font-size: 1.5rem;
      font-weight: bold;
      padding: 24px 0;
      text-align: center;
      margin: 24px auto 0 auto;
      width: 480px;
      box-shadow: 0 0 16px #2228;
      text-shadow: 0 2px 8px #000;
      z-index: 100;
    }
    #game-board {
      display: grid;
      grid-template-rows: repeat(9, 42px);    /* 9行 */
      grid-template-columns: repeat(14, 42px); /* 14列 */
      justify-content: center;
      background: #252834dd;
      padding: 14px;
      margin: 0 auto 16px auto;
      border-radius: 12px;
      box-shadow: 0 4px 24px #000b;
      width: min-content;
    }
    .cell {
      width: 38px; height: 38px;
      display: flex; align-items: center; justify-content: center;
      font-size: 1.6rem;
      border-radius: 6px;
      background: #3e4356;
      border: 1px solid #626478;
      box-shadow: 0 1px 4px #0004;
      transition: background 0.1s;
    }
    .wall { background: #888a9a; }
    .goal { background: #3da65c; }
    .box { background: #b58444; }
    .box.goal { background: #e2d90c; color: #715b00; }
    .player { background: #5688db; }
    #retire-btn-area {
      position: absolute;
      top: 30px;
      right: 32px;
      z-index: 11;
    }
    #retire-btn-area .button {
      padding: 6px 20px;
      font-size: 1.07rem;
      margin: 0;
    }
    #stage-select-area {
      display: flex; gap: 24px; justify-content: center; margin: 14px auto 0 auto;
    }
    .stage-btn {
      background: #82dc7e; color: #232; font-weight: bold;
      border-radius: 8px; padding: 8px 28px;
      border: 2px solid #6c6;
      font-size: 1.12rem;
      margin-top: 0; margin-bottom: 0;
      cursor: pointer;
      box-shadow: 0 2px 8px #1116;
      transition: filter 0.2s;
    }
    .stage-btn:hover { filter: brightness(1.13); }
    #end-buttons {
      display: flex; justify-content: center; margin-top: 20px;
    }
    @media (max-width: 960px) {
      #container { width: 99vw; min-width: 0; }
      #game-board { max-width: 96vw; overflow-x: auto; }
      .rule-desc { width: 94vw;}
    }
  </style>
</head>
<body>
  <div id="container"></div>
  <script>
    // ==== 14×9マスでクリアできる練習マップ3面 ====
    // 0:床 1:壁 2:箱 3:ゴール 4:プレイヤー 5:箱+ゴール 6:プレイヤー+ゴール

    // --- ステージ1: 箱1個 ---
    const STAGE_LIST = [
      [
        [1,1,1,1,1,1,1,1,1,1,1,1,1,1],
        [1,0,0,0,0,0,0,0,0,0,0,0,3,1],
        [1,0,1,1,1,1,1,1,1,1,1,1,0,1],
        [1,0,0,0,0,0,2,0,0,0,0,0,0,1],
        [1,1,1,1,1,1,0,1,1,1,1,1,0,1],
        [1,0,0,0,0,0,0,0,0,0,0,0,0,1],
        [1,0,1,1,1,1,1,1,1,1,1,1,0,1],
        [1,4,0,0,0,0,0,0,0,0,0,0,0,1],
        [1,1,1,1,1,1,1,1,1,1,1,1,1,1],
      ],
      // --- ステージ2: 箱2個 ---
      [
        [1,1,1,1,1,1,1,1,1,1,1,1,1,1],
        [1,0,0,0,0,0,0,0,0,0,0,3,0,1],
        [1,0,1,1,1,0,1,1,1,1,0,1,0,1],
        [1,0,2,0,0,0,1,1,1,1,0,2,0,1],
        [1,1,1,1,1,0,1,1,1,1,0,1,0,1],
        [1,0,0,0,0,0,0,0,0,0,0,1,3,1],
        [1,0,1,1,1,1,1,1,1,1,1,1,0,1],
        [1,4,0,0,0,0,0,0,0,0,0,0,0,1],
        [1,1,1,1,1,1,1,1,1,1,1,1,1,1],
      ],
      // --- ステージ3: 箱3個 ---
      [
        [1,1,1,1,1,1,1,1,1,1,1,1,1,1],
        [1,0,0,0,0,3,0,0,0,0,3,0,0,1],
        [1,0,1,1,1,0,1,0,1,1,1,1,0,1],
        [1,0,2,0,0,0,0,0,2,0,0,2,0,1],
        [1,1,1,1,1,0,1,1,1,1,0,1,0,1],
        [1,0,0,0,0,0,0,0,0,0,0,1,3,1],
        [1,0,1,1,1,1,1,1,1,1,1,1,0,1],
        [1,4,0,0,0,0,0,0,0,0,0,0,0,1],
        [1,1,1,1,1,1,1,1,1,1,1,1,1,1],
      ]
    ];

    // ==== グローバル変数 ====
    let stageIndex = 0;
    let board;
    let playerPos;
    let gameState = "title";
    const container = document.getElementById('container');

    // ==== 盤面初期化 ====
    function initBoard(idx) {
      board = STAGE_LIST[idx].map(row => [...row]);
      playerPos = null;
      for (let y = 0; y < board.length; y++) {
        for (let x = 0; x < board[0].length; x++) {
          if (board[y][x] === 4 || board[y][x] === 6) {
            playerPos = {x, y};
          }
        }
      }
      // 万一プレイヤーいなければ、床に設置
      if (!playerPos) {
        for (let y = 0; y < board.length; y++) {
          for (let x = 0; x < board[0].length; x++) {
            if (board[y][x] === 0) {
              board[y][x] = 4;
              playerPos = {x, y};
              return;
            }
          }
        }
      }
    }

    // ==== タイトル画面 ====
    function showTitle() {
      gameState = "title";
      let html = `
        <div class="main-title center">📦 倉庫番(PushBox)ゲーム 📦</div>
        <img src="warehouse-keeper_title.png" class="title-img">
        <div class="rule-title">📝 ゲームのルール 📝</div>
        <div class="rule-desc">
          ・荷物(箱)をすべて緑のゴールへ<strong>押して</strong>運ぶパズルゲームです。<br>
          ・荷物は<strong>1つずつ</strong>しか押せません。<br>
          ・荷物を<strong>引く</strong>ことはできません。<br>
          ・荷物がゴールに載ると色が変わります。<br>
          ・すべての荷物をゴールへ運べばクリアです。<br>
          ・キーボードの「↑↓←→」または「WASD」キーで操作します。<br>
          <span style="color:#e2d90c">頭を使ってすべての荷物をゴールへ運びましょう!</span>
        </div>
        <div id="stage-select-area">
          <button class="stage-btn" onclick="startGame(0)">ステージ 1</button>
          <button class="stage-btn" onclick="startGame(1)">ステージ 2</button>
          <button class="stage-btn" onclick="startGame(2)">ステージ 3</button>
        </div>
      `;
      container.innerHTML = html;
    }

    // ==== ゲーム画面描画 ====
    function renderGame() {
      let html = `<div class="main-title center" style="margin-bottom:12px;">📦 倉庫番 - ステージ${stageIndex + 1} 📦</div>`;
      // リタイアボタン(右上固定)
      html += `<div id="retire-btn-area"><button class="button" onclick="showTitle()">リタイア</button></div>`;
      // 盤面
      html += `<div id="game-board">`;
      for (let y = 0; y < board.length; y++) {
        for (let x = 0; x < board[0].length; x++) {
          let cellClass = "cell";
          let symbol = "";
          switch (board[y][x]) {
            case 1: cellClass += " wall"; symbol = "🧱"; break;
            case 0: cellClass += "";     symbol = "";     break;
            case 2: cellClass += " box"; symbol = "📦"; break;
            case 3: cellClass += " goal"; symbol = "🎯"; break;
            case 4: cellClass += " player"; symbol = "🧑‍🔧"; break;
            case 5: cellClass += " box goal"; symbol = "📦"; break;
            case 6: cellClass += " player goal"; symbol = "🧑‍🔧"; break;
          }
          html += `<div class="${cellClass}">${symbol}</div>`;
        }
      }
      html += `</div>`;
      // 操作説明
      html += `
        <div class="center" style="margin-top:8px;">
          <span style="background:rgba(0,0,0,0.6);color:#ffe46c;padding:6px 18px;border-radius:6px;font-size:1.1rem;">
            「↑↓←→」または「WASD」キーで移動。荷物は押すだけでOK!
          </span>
        </div>
      `;
      container.innerHTML = html;
    }

    // ==== ゲーム開始 ====
    function startGame(idx) {
      stageIndex = idx;
      initBoard(stageIndex);
      gameState = "play";
      renderGame();
    }

    // ==== ゲームクリア判定 ====
    function isClear() {
      for (let y = 0; y < board.length; y++) {
        for (let x = 0; x < board[0].length; x++) {
          if (board[y][x] === 2) return false; // 箱が残っていれば未クリア
          if (board[y][x] === 3) return false; // ゴール上に箱がないなら未クリア
        }
      }
      return true;
    }

    // ==== プレイヤーの移動処理 ====
    function move(dx, dy) {
      if (gameState !== "play") return;
      const {x, y} = playerPos;
      const nx = x + dx, ny = y + dy;
      const nnx = x + dx * 2, nny = y + dy * 2;
      // 範囲外
      if (ny < 0 || ny >= board.length || nx < 0 || nx >= board[0].length) return;
      const dest = board[ny][nx];
      // 壁やプレイヤー禁止
      if (dest === 1 || dest === 4 || dest === 6) return;
      // 箱または箱+ゴールを押す場合
      if (dest === 2 || dest === 5) {
        // その1マス先が床orゴールなら押せる
        if (nny < 0 || nny >= board.length || nnx < 0 || nnx >= board[0].length) return;
        const beyond = board[nny][nnx];
        if (beyond === 0 || beyond === 3) {
          board[nny][nnx] = (beyond === 0) ? 2 : 5;
          board[ny][nx] = (dest === 5) ? 3 : 0;
        } else {
          return;
        }
      }
      // プレイヤー移動
      if (board[y][x] === 4) board[y][x] = 0;
      if (board[y][x] === 6) board[y][x] = 3;
      if (board[ny][nx] === 3) board[ny][nx] = 6;
      else board[ny][nx] = 4;
      playerPos = {x: nx, y: ny};
      renderGame();
      // クリア判定
      if (isClear()) {
        setTimeout(showClear, 300);
      }
    }

    // ==== クリア画面 ====
    function showClear() {
      gameState = "clear";
      let html = `
        <div class="main-title center" style="margin-bottom:14px;">🎉 ステージクリア! 🎉</div>
        <div class="msg-box" style="background:rgba(60,180,80,0.95);">
          全ての荷物をゴールに運びました!<br>おめでとうございます!
        </div>
        <div id="end-buttons">
          <button class="button" onclick="showTitle()">タイトル画面に戻る</button>
        </div>
      `;
      container.innerHTML = html;
    }

    // ==== キー操作(矢印&wasd対応) ====
    document.addEventListener("keydown", function(e) {
      if (gameState !== "play") return;
      switch (e.key) {
        case "ArrowUp": case "w": case "W": move(0, -1); break;
        case "ArrowDown": case "s": case "S": move(0, 1); break;
        case "ArrowLeft": case "a": case "A": move(-1, 0); break;
        case "ArrowRight": case "d": case "D": move(1, 0); break;
      }
    });

    // ==== 初期表示 ====
    showTitle();

    // ==== ウィンドウリサイズ対応 ====
    window.addEventListener('resize', () => {
      container.style.minHeight = (window.innerHeight - 40) + 'px';
    });
  </script>
</body>
</html>

アルゴリズムの流れ

ステップ内容
1. 初期表示タイトル画面を表示(ルール説明・画像・ステージ選択ボタン)
2. ゲーム開始選択されたステージの盤面を初期化
3. ゲーム画面盤面描画・リタイアボタン描画・操作説明
4. 入力処理矢印キー/WASDキーによるプレイヤーの移動・箱の押し出し処理
5. 描画更新プレイヤーや箱の位置、盤面の状態を再描画
6. クリア判定すべての箱がゴール上にあるかチェック
7. ゲームクリアクリア画面を表示(タイトルに戻るボタン)

主要関数・命令の解説

関数名・変数概要
showTitle()タイトル画面、ルール、画像、ステージ選択を表示
startGame(idx)選んだステージを初期化し、プレイ画面に遷移
initBoard(idx)盤面配列・プレイヤー座標を初期化
renderGame()盤面の状態をHTMLで再描画
move(dx, dy)プレイヤーと箱の移動処理(方向指定)
isClear()クリア判定:すべての箱がゴール上ならtrue
showClear()クリア画面を描画
document.addEventListener("keydown", ...)キーボード操作のイベント監視

関数の処理内容

関数名主な処理内容
showTitleタイトル・画像・ルール・ステージ選択を描画
startGame盤面初期化・プレイ画面描画
initBoard盤面配列(2次元配列)の初期化・プレイヤー座標特定
renderGame盤面上のマス状態ごとにHTMLを生成・描画
moveプレイヤーの移動・箱を押す処理(衝突判定・盤面配列の状態更新・再描画)
isClear全ての箱(2,5)がゴール上(5)のみならtrue
showClearクリア時のメッセージ・ボタン描画

改造のポイント

ステージ追加・難易度調整

  • STAGE_LIST 配列に新しい盤面を追加するだけでステージ追加が可能。
  • マスサイズや盤面サイズも調整できます。

操作性の向上

  • Undo(1手戻し)機能やリトライボタンを追加すれば快適性UP。
  • プレイヤー・箱のアニメーション追加で演出強化。

見た目・演出の工夫

  • 箱やゴール、壁などを画像アイコンでリッチにカスタマイズ。
  • 効果音やBGMを加えるとレトロな雰囲気を楽しめます。

アドバイス

 本プログラムはシンプルな構造なので、JavaScript初心者でもカスタマイズしやすいのが特徴です。盤面データ(2次元配列)を編集してオリジナルステージを作るのがおすすめです。
Undo機能や手数カウントなどパズルゲームらしい機能追加にも挑戦してみてください!

自分だけの“倉庫番”パズルを作って、みんなで遊ぼう!