はじめに:倉庫番ゲームとは?
倉庫番ゲーム(Sokoban)は、1980年代に日本で生まれたクラシックなパズルゲームです。
プレイヤーがキャラクターを動かし、倉庫に置かれた箱をゴール地点まで押していくというシンプルなルール。
ですがシンプルでありながら、
- 状態管理
- 当たり判定
- 移動制御
といったプログラミングの要素が凝縮されており、学習教材としても優れています。
本記事では、中級者向けに HTML + CSS + JavaScript を使って、倉庫番ゲームを1から実装する手順を徹底解説します。
使用技術と学習ポイント
- HTML:ゲーム画面の枠組み(
<div>
または<canvas>
) - CSS:セルの見た目を整える(壁、箱、ゴールなどを色分け)
- JavaScript:キャラクター操作、当たり判定、勝敗条件を制御
本記事を通して学べるポイントは:
- 2次元配列によるマップ設計
- キーボード操作イベントの処理
- 当たり判定(壁や箱にぶつかったときの挙動)
- 状態変化(箱がゴールに乗ったら色を変える)
倉庫番ゲームの構成要素
- プレイヤー(キャラ)
- 自由に動ける
- 箱を押すことができる
- 箱
- プレイヤーが押すと動く
- ゴール地点に乗るとクリア条件にカウント
- 壁
- 通過不可
- ゴール
- 箱を置くべき場所
- マップ
- 上記要素を組み合わせて作成
- 配列で表現
HTMLとCSSで土台を作る
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>倉庫番ゲーム</title>
<style>
body { display: flex; justify-content: center; margin-top: 50px; }
#game { display: grid; grid-template-columns: repeat(8, 50px); }
.cell { width: 50px; height: 50px; border: 1px solid #ccc; }
.wall { background: #333; }
.player { background: #0000ff; }
.box { background: #a52a2a; }
.goal { background: #ffd700; }
.box-goal { background: #32cd32; }
</style>
</head>
<body>
<div id="game"></div>
<script src="script.js"></script>
</body>
</html>
#game
:display: grid
を設定することで、マップを格子状に配置します。.cell
:全てのマスに共通のサイズを定義します。.wall
,.player
,.box
など:それぞれの要素に色や枠線で視覚的な特徴を与えます。.box-on-goal
は、箱がゴールの上に乗った状態を表現するためのクラスです。
JavaScriptでマップを定義
ゲームのマップは、文字列の配列(2次元配列)で表現します。
各文字は、#
(壁)、(床)、$
(箱)、@
(プレイヤー)、.
(ゴール)に対応します。
また、ゴールの位置はゲームクリアの判定に使うため、別の配列に保存しておきます。
// マップの初期配置
let map = [
"########",
"# . #",
"# $ #",
"# @ #",
"########"
];
// ゴールの座標を記録しておく
const goals = [];
map.forEach((row, y) => {
row.split("").forEach((cell, x) => {
if (cell === ".") goals.push({ x, y });
});
});
map
は2次元配列の文字列版- JavaScriptのループ処理を使って、マップの各セルに対応する
<div>
要素を生成し、適切なクラスを付与してゲーム画面に描画します。
画面を描画する関数
render()
関数を作成し、map
配列の内容を元に画面を構築します。
この関数はゲームの状態が変化するたびに呼び出され、画面を最新の状態に更新します。
const game = document.getElementById("game");
function render() {
game.innerHTML = "";
map.forEach((row, y) => {
row.split("").forEach((cell, x) => {
const div = document.createElement("div");
div.classList.add("cell");
// ゴール位置は常に背景として描画
if (goals.some(g => g.x === x && g.y === y)) {
div.classList.add("goal");
}
if (cell === "#") div.classList.add("wall");
if (cell === "$") {
// 箱がゴール上にある場合は色を変える
if (goals.some(g => g.x === x && g.y === y)) {
div.classList.add("box-goal");
} else {
div.classList.add("box");
}
}
if (cell === "@") div.classList.add("player");
game.appendChild(div);
});
});
}
render();
map
配列を走査して画面を構築- これで 壁・ゴール・箱・プレイヤー が表示されます
プレイヤーの移動処理を実装する
キーボードの矢印キー入力に応じて、プレイヤーを動かすロジックを実装します。
document.addEventListener("keydown", e => {
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;
movePlayer(dx, dy);
});
movePlayer関数で当たり判定を処理
function movePlayer(dx, dy) {
let px, py;
map.forEach((row, y) => {
const x = row.indexOf("@");
if (x !== -1) { px = x; py = y; }
});
const target = map[py + dy][px + dx];
if (target === " " || target === ".") {
// プレイヤーが移動できる
map[py] = replaceChar(map[py], px, " ");
map[py + dy] = replaceChar(map[py + dy], px + dx, "@");
} else if (target === "$") {
// 箱を押す処理
const next = map[py + dy*2][px + dx*2];
if (next === " " || next === ".") {
map[py] = replaceChar(map[py], px, " ");
map[py + dy] = replaceChar(map[py + dy], px + dx, "@");
map[py + dy*2] = replaceChar(map[py + dy*2], px + dx*2, "$");
}
}
render();
checkClear();
}
function replaceChar(str, index, char) {
return str.substring(0, index) + char + str.substring(index + 1);
}
- 壁にぶつかったら何もしない
- 箱の先が空いていれば箱を押す
render()
関数を呼び出して、ゲームの状態が変更されるたびに画面を更新します。
勝利条件をチェックする
すべてのゴールマスに箱が置かれているかを判定する関数を実装します。
function checkClear() {
let clear = true;
goals.forEach(goal => {
// ゴール位置に箱があるかをチェック
if (map[goal.y][goal.x] !== "$") {
clear = false;
}
});
if (clear) {
setTimeout(() => {
alert("おめでとう!クリアです!");
}, 100); // 少し遅延させると描画後に表示される
}
}
- 全てのゴール
.
に箱が置かれればクリア - アラートで結果を表示
改良ポイント
- 複数ステージ対応
配列を切り替えて次のマップへ進める - 操作数カウント
「何手でクリアしたか」を表示 - UI改善
- リセットボタン
- ステージ選択メニュー
- スマホ対応
- タッチ操作で移動
完成動画/今回のまとめ
倉庫番ゲームを実装することで、
- 2次元配列での状態管理
- 当たり判定
- プレイヤーとオブジェクトの連動処理
といった中級者に必要なスキルを一通り学ぶことができました。
このシンプルなゲームをベースに、ステージを増やしたり、UIを整えたりすれば、オリジナルの「本格倉庫番ゲーム」に育てていけます。
プログラミング学習の一環として、ぜひ挑戦してみてください!
よくある質問(実装の落とし穴)
- 「ゴールの上から箱やプレイヤーが離れたら背景は?」
→ 背景はgoals
による “別レイヤ描画” なので自動で元通り。map 側に.
を戻す必要はなし。 - 「箱がゴール上かの見た目はどう変えてる?」
→render()
内でif (c === "$") { if (isGoal(x,y)) div.classList.add("box-goal") ... }
によって制御。 - 「当たり判定の境界チェックが煩雑にならない?」
→cellAt
が 範囲外 = 壁 を返す設計にしているので、呼び出し側はシンプルに書ける。 - 「描画前に alert が出て見た目が更新されない」
→setTimeout(..., 100)
の小遅延で先に描画を反映。UI/UX が良くなる。
発展課題(中級者〜)
- 複数ステージ:
rawMaps = [ [...], [...], ... ]
とcurrentStage
を持ち、checkClear()
で次へ。 - 操作回数/時間の計測:
moves++
、Date.now()
の差分でタイム。 - アンドゥ(1手戻す):操作履歴(プレイヤー位置、箱移動の有無)をスタックで保持。
- 最短手数探索:BFS/IDA* などの探索アルゴリズム適用(別記事の良ネタ)。
- 描画最適化:差分レンダリング(前フレームと比較して変更セルのみ差し替え)。
おすすめ本を紹介!

コメント