倉庫番

倉庫番 hi0a.com

view source

JavaScript

const TILE_SIZE = 40;
const WIDTH = 10;
const HEIGHT = 10;
const TILE_EMPTY = 0;
const TILE_WALL = 1;
const TILE_BOX = 2;
const TILE_PLAYER = 3;
const TILE_OBSTACLE = 4;

let level = [];
let goalX = 0;
let goalY = 0;
let playerX = 0;
let playerY = 0;

// === 安全なマップ生成 ===
function generateSafeMap() {
  let tries = 0;
  while (tries < 50) {
    const map = generateSolvableMap();
    // ゴールの周囲に空きがあるかチェック
    const dirs = [[0,-1],[1,0],[0,1],[-1,0]];
    let pushable = false;
    for (const [dx, dy] of dirs) {
      const nx = goalX + dx;
      const ny = goalY + dy;
      if (
        ny >= 0 && ny < HEIGHT &&
        nx >= 0 && nx < WIDTH &&
        map[ny][nx] === TILE_EMPTY
      ) {
        pushable = true;
        break;
      }
    }
    if (pushable) return map;
    tries++;
  }
  console.warn("マップ生成に失敗しました(リトライ制限)");
  return generateSolvableMap();
}

// === マップ生成 ===
function generateSolvableMap() {
  let map = Array.from({ length: HEIGHT }, () => Array(WIDTH).fill(TILE_EMPTY));

  // 壁
  for (let x = 0; x < WIDTH; x++) {
    map[0][x] = TILE_WALL;
    map[HEIGHT - 1][x] = TILE_WALL;
  }
  for (let y = 0; y < HEIGHT; y++) {
    map[y][0] = TILE_WALL;
    map[y][WIDTH - 1] = TILE_WALL;
  }

  // ゴール位置(壁から安全な距離に)
  goalX = Math.floor(Math.random() * (WIDTH - 4)) + 2;
  goalY = Math.floor(Math.random() * (HEIGHT - 4)) + 2;

  // 逆操作で箱とプレイヤーを動かす
  let boxX = goalX;
  let boxY = goalY;
  let px = boxX;
  let py = boxY + 1;

  const pathMap = new Set();
  pathMap.add(`${boxX},${boxY}`);
  pathMap.add(`${px},${py}`);

  for (let i = 0; i < 10; i++) {
    const dirs = [[0,-1],[1,0],[0,1],[-1,0]];
    const [dx, dy] = dirs[Math.floor(Math.random() * dirs.length)];
    const nbx = boxX + dx;
    const nby = boxY + dy;
    const npx = boxX;
    const npy = boxY;

    if (
      nbx >= 1 && nbx < WIDTH - 1 &&
      nby >= 1 && nby < HEIGHT - 1
    ) {
      boxX = nbx;
      boxY = nby;
      px = npx;
      py = npy;
      pathMap.add(`${boxX},${boxY}`);
      pathMap.add(`${px},${py}`);
    }
  }

  map[boxY][boxX] = TILE_BOX;
  map[py][px] = TILE_PLAYER;
  playerX = px;
  playerY = py;

  // 障害物(安全地帯を除く)
  let placed = 0;
  while (placed < 16) {
    const ox = Math.floor(Math.random() * (WIDTH - 2)) + 1;
    const oy = Math.floor(Math.random() * (HEIGHT - 2)) + 1;
    const key = `${ox},${oy}`;
    if (
      map[oy][ox] === TILE_EMPTY &&
      !pathMap.has(key) &&
      !(ox === goalX && oy === goalY)
    ) {
      map[oy][ox] = TILE_OBSTACLE;
      placed++;
    }
  }

  return map;
}

// === 描画 ===
function draw() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);

  // 背景(壁・障害物)
  for (let y = 0; y < HEIGHT; y++) {
    for (let x = 0; x < WIDTH; x++) {
      const tile = level[y][x];
      if (tile === TILE_WALL) {
        ctx.fillStyle = "black";
        ctx.fillRect(x * TILE_SIZE, y * TILE_SIZE, TILE_SIZE, TILE_SIZE);
      } else if (tile === TILE_OBSTACLE) {
        ctx.fillStyle = "#222222";
        ctx.fillRect(x * TILE_SIZE, y * TILE_SIZE, TILE_SIZE, TILE_SIZE);
      }
    }
  }

  // ゴール(★は常に表示)
  ctx.fillStyle = "orange";
  ctx.font = "20px Arial";
  ctx.textAlign = "center";
  ctx.textBaseline = "middle";
  ctx.fillText("★", goalX * TILE_SIZE + TILE_SIZE / 2, goalY * TILE_SIZE + TILE_SIZE / 2);

  // オブジェクト(箱・プレイヤー)
  for (let y = 0; y < HEIGHT; y++) {
    for (let x = 0; x < WIDTH; x++) {
      const tile = level[y][x];
      if (tile === TILE_BOX) {
        ctx.fillStyle = "brown";
        ctx.fillRect(x * TILE_SIZE + 4, y * TILE_SIZE + 4, TILE_SIZE - 8, TILE_SIZE - 8);
      } else if (tile === TILE_PLAYER) {
        ctx.fillStyle = "blue";
        ctx.beginPath();
        ctx.arc(x * TILE_SIZE + TILE_SIZE / 2, y * TILE_SIZE + TILE_SIZE / 2, TILE_SIZE / 3, 0, 2 * Math.PI);
        ctx.fill();
      }
    }
  }
}

// === 衝突判定 ===
function isBlocked(x, y) {
  const tile = level[y][x];
  return tile === TILE_WALL || tile === TILE_OBSTACLE;
}

// === プレイヤー移動 ===
function move(dx, dy) {
  const tx = playerX + dx;
  const ty = playerY + dy;

  if (isBlocked(tx, ty)) return;

  if (level[ty][tx] === TILE_BOX) {
    const bx = tx + dx;
    const by = ty + dy;
    if (
      bx < 0 || bx >= WIDTH || by < 0 || by >= HEIGHT ||
      isBlocked(bx, by) || level[by][bx] === TILE_BOX
    ) return;

    level[ty][tx] = TILE_EMPTY;
    level[by][bx] = TILE_BOX;
  }

  level[playerY][playerX] = TILE_EMPTY;
  level[ty][tx] = TILE_PLAYER;
  playerX = tx;
  playerY = ty;

  draw();

  // クリア判定 → 自動で次のマップ
  if (level[goalY][goalX] === TILE_BOX) {
    setTimeout(() => {
      level = generateSafeMap();
      draw();
    }, 500);
  }
}

// === キー入力 ===
document.addEventListener("keydown", (e) => {
  if (["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"].includes(e.key)) {
    e.preventDefault();
  }
  switch (e.key) {
    case "ArrowUp": move(0, -1); break;
    case "ArrowDown": move(0, 1); break;
    case "ArrowLeft": move(-1, 0); break;
    case "ArrowRight": move(1, 0); break;
  }
});

// === 初期化 ===
const canvas = document.getElementById("gameCanvas");
const ctx = canvas.getContext("2d");
level = generateSafeMap();
draw();

CSS

HTML

ページのソースを表示 : Ctrl+U , DevTools : F12

view-source:https://hi0a.com/game/game-soukoban/

ABOUT

hi0a.com 「ひまあそび」は無料で遊べるミニゲームや便利ツールを公開しています。

プログラミング言語の動作デモやWEBデザイン、ソースコード、フロントエンド等の開発者のための技術を公開しています。

必要な機能の関数をコピペ利用したり勉強に活用できます。

プログラムの動作サンプル結果は画面上部に出力表示されています。

環境:最新のブラウザ GoogleChrome / Windows / Android / iPhone 等の端末で動作確認しています。

画像素材や音素材は半分自作でフリー素材配布サイトも利用しています。LINK参照。

仕様変更、API廃止等、実験途中で動かないページもあります。