Sokoban Box Pushing Game — Free HTML CSS JS Snippet

Sokoban Box Pushing Game · Games · Plain HTML, CSS & JS · Live preview

What's included

Features

Levels authored as plain-text grids using standard Sokoban notation (#, ., $, @, *, +)
Full push-physics collision chain: rejects pushes into walls or other crates before mutating state
Delta-based undo stack stores only the player move and any crate push per turn, not full board snapshots
checkWin() via Set membership — every crate key must also be a target key, order-independent
Automatic progression to the next bundled level a short delay after clearing the current one
Both keyboard arrow-key controls and an on-screen directional pad for touch devices
Distinct visual states for crate-on-floor versus crate-on-target for at-a-glance progress
Reset button reloads the current level from its original text grid at any time

About this UI Snippet

Sokoban Box Pushing Game — Grid Puzzle with Push-Only Physics and Undo History

Screenshot of the Sokoban Box Pushing Game snippet rendered live

Sokoban ("warehouse keeper" in Japanese) is a classic grid-based puzzle: push crates onto target squares by walking into them, one at a time, in a warehouse full of walls. The core constraint that makes it a genuine puzzle rather than a maze is that crates can only ever be pushed, never pulled — a bad push can permanently trap a crate against a wall. This snippet implements a complete playable version in vanilla JavaScript: level parsing from plain text grids, push physics with full collision checking, an undo stack, and automatic progression through multiple bundled levels.

Levels as plain text grids

Each entry in LEVELS is an array of equal-purpose strings using standard Sokoban notation: # for a wall, a space for open floor, . for a target square, $ for a crate, * for a crate already on a target, @ for the player, and + for the player standing on a target. parseLevel() walks every character of every row once and sorts each symbol into a Setwalls, targets, or boxes — keyed by a "r,c" string, plus the player's starting { r, c } position. Using plain text for level data means new levels can be authored by just typing a grid, with no separate level editor or JSON schema required.

Push physics: the collision chain

attemptMove(dir) is the entire physics engine. It first checks whether the player's destination cell is a wall — if so, the move is silently rejected. If the destination holds a crate, it computes where *that* crate would land one more step in the same direction, and rejects the whole move if that landing cell is a wall or already holds another crate (isWall(br, bc) || isBox(br, bc)) — this is what prevents pushing a crate into another crate or into a wall. Only after both checks pass does the function actually mutate state: the crate's key moves from its old position to its new one in the boxes Set, and the player advances into the crate's old cell.

An undo stack instead of a full board-state history

Rather than snapshotting the entire board after every move, history stores only the minimal delta needed to reverse one step: the player's previous position, plus pushedBoxFrom/pushedBoxTo keys if a crate moved on that turn (both null if it didn't). undo() pops the most recent entry, restores the player position, and — if a crate was involved — deletes it from its new key and re-adds it at its old key. This keeps undo cheap regardless of board size and trivially supports unlimited undo depth since every entry is small and independent.

Win detection and auto-advance

checkWin() runs after every completed move and checks whether every key currently in the boxes Set also exists in the targets Set — the moment that's true, every crate is correctly placed regardless of which specific crate sits on which specific target. On a win, the game shows the final move count and, after a short delay, automatically advances to the next bundled level via levelIndex++ and a fresh loadLevel() call, or stays on the final level if none remain.

Build with AI

Build, Understand, Optimize, and Extend It With AI

Paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain exactly how attemptMove() chains its wall and crate collision checks before mutating any state, and why the undo stack only needs to store a small delta per move rather than a full board snapshot. It's also a strong candidate for extension — ask the assistant to add a level editor that lets you paint walls, crates, and targets directly on the grid and export the resulting text-grid level string, add a move-counter-based star rating per level, or implement a breadth-first-search solvability checker that validates a hand-authored level can actually be won before it ships.

Prompt to recreate it

Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:

text
Build a Sokoban-style box-pushing puzzle game in plain HTML, CSS, and JavaScript — no libraries, no backend.

Requirements:
- Represent each level as a plain-text grid using standard Sokoban notation: a wall character, a floor character (or space), a target character, a crate character, a crate-on-target character, and player-start characters for on-floor and on-target. Parse this text grid into wall, target, and crate coordinate sets plus a starting player position.
- Support arrow-key movement (with a focusable board element) and an on-screen directional pad for touch devices, both routed through one shared move function.
- Implement full push physics: moving into an empty floor cell just moves the player; moving into a crate must check whether the cell one further step in the same direction is free (not a wall and not another crate) before allowing the push, and must reject the entire move (player included) if that check fails — crates can only ever be pushed, never pulled.
- Implement an undo stack that stores, for each completed move, only the player's previous position and (if a crate was pushed) that crate's previous and new position — not a full board snapshot — and can reverse any number of moves in sequence.
- Detect a win the moment every crate coordinate is also a target coordinate, regardless of which specific crate ended up on which target, and show a completion message with the total move count.
- Bundle at least three levels of increasing difficulty as separate text grids in an array, and automatically advance to the next level a short moment after the current one is cleared.
- Add a Reset button that reloads the current level from its original text grid, discarding all progress and undo history for that attempt.

Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.

Source Code

<div class="sk-app">
  <div class="sk-header">
    <h2>Sokoban</h2>
    <div class="stats">
      <div class="stat"><span class="stat-label">Level</span><span class="stat-val" id="sk-level">1/3</span></div>
      <div class="stat"><span class="stat-label">Moves</span><span class="stat-val" id="sk-moves">0</span></div>
    </div>
  </div>

  <p class="sk-goal">Push every crate onto a target. Use arrow keys or the on-screen pad. You can push but never pull.</p>

  <div class="sk-board" id="sk-board" tabindex="0"></div>

  <p class="sk-feedback" id="sk-feedback">Click the board, then use arrow keys.</p>

  <div class="sk-pad" id="sk-pad">
    <button class="pad-btn pad-up" data-dir="up" aria-label="Up">▲</button>
    <button class="pad-btn pad-left" data-dir="left" aria-label="Left">◀</button>
    <button class="pad-btn pad-down" data-dir="down" aria-label="Down">▼</button>
    <button class="pad-btn pad-right" data-dir="right" aria-label="Right">▶</button>
  </div>

  <div class="sk-actions">
    <button class="ghost-btn" id="sk-undo">Undo</button>
    <button class="ghost-btn" id="sk-reset">Reset level</button>
  </div>
</div>

Step by step

How to Use

  1. 1
    Click the board to focus itThe board is a focusable div; clicking it (or an on-screen pad button) lets arrow-key input register via the keydown listener.
  2. 2
    Push crates onto targetsMove with arrow keys or the on-screen directional pad. Walking into a crate pushes it one cell further in the same direction, if that cell is open.
  3. 3
    Watch for dead-end pushesA push is rejected outright if the crate would land on a wall or another crate — attemptMove() checks this before allowing the move at all, so you cannot lock a crate in place by accident from the push itself.
  4. 4
    Undo a bad moveClick Undo to pop the last entry off the history stack, restoring the player position and reversing any crate push from that turn.
  5. 5
    Clear the levelcheckWin() watches whether every crate key also exists in the targets set. Getting every crate onto a target completes the level and auto-advances to the next one.
  6. 6
    Add your own levelsAppend a new grid of equal-length strings to the LEVELS array using #, space, ., $, *, @, and + — no additional configuration is needed.

Real-world uses

Common Use Cases

Spatial-reasoning puzzle collections
A genuinely different mechanic from pathfinding games like the maze runner game — Sokoban rewards planning a full push sequence in advance, since bad pushes can be irreversible without undo.
Teaching grid-based collision detection
The chained wall/crate collision check in attemptMove() is a compact, readable example of validating a multi-object move before committing any state change.
Reference implementation of delta-based undo
Storing only the minimal reversible change per action, instead of full state snapshots, is a broadly useful pattern for any undo/redo system beyond games.
Level-based puzzle widget for a games hub
The plain-text level format makes it trivial to bundle dozens of hand-authored levels without a level editor, ideal for a puzzle-of-the-day style feature.
Grid-cell state visualization reference
The layered cell classes (floor, target, box, box-on-target, player) demonstrate a clean way to represent multiple overlapping states per grid cell in CSS.

Got questions?

Frequently Asked Questions

No. The player can only walk into a crate to push it one cell further in the same direction; there is no pull mechanic, matching the classic Sokoban rule set. This means a poorly planned push can trap a crate against a wall or corner with no way to free it except undo.

attemptMove() computes the crate's would-be landing cell one step past its current position and checks isWall(br, bc) || isBox(br, bc) before allowing the move. If either is true, the entire move (including the player's own step) is rejected and neither the player nor the crate moves.

Each history entry stores only the player's previous position and, if a crate was pushed that turn, its previous and new key. undo() reverses exactly that delta: it restores the player position and moves the pushed crate's key back to its previous position in the boxes Set.

checkWin() checks that every key in the boxes Set also exists in the targets Set, using [...boxes].every(b => targets.has(b)). This is true regardless of which specific crate ends up on which specific target, as long as every crate is on some target.

Append a new array of equal-length strings to the LEVELS array using # for walls, space for floor, . for targets, $ for crates, * for a crate already on a target, and @ (or + for on a target) for the player start position. No other code changes are required.

checkWin() calls loadLevel() again with an incremented levelIndex inside a short setTimeout after showing the "Level cleared" message, so players can chain through the bundled level set without clicking a manual next-level button — it simply stays on the final level if none remain.