mirror of
https://github.com/tiennm99/sokoban.git
synced 2026-09-12 04:20:59 +00:00
Replace the 3 hand-crafted JSON levels with the full Microban set (100 solvable puzzles by David W. Skinner) stored as XSB text and parsed at runtime. Rework the game into kebab-case modules under a 200-LOC budget: - core/level-parser: XSB parse with flood-fill interior detection - core/board-model: pure move/undo/win logic, Phaser-free - core/progress-store: localStorage persistence with graceful fallback - core/theme: Nord palette, fonts, responsive tile sizer - ui/button-factory: one rounded button impl with hover/press - ui/board-renderer: animated tile/wall/goal/box/player drawing - scenes: menu, paginated level select (5x4 grid, 5 pages), game Add WASD movement, U/Z undo, R restart, Esc to menu, live move counter, best-move tracking per level, win overlay with next/levels actions, and a radial-gradient CSS backdrop. Drop the dead Arcade Physics wiring, the broken manual shutdown/destroy code, the unused main.js self-import, and the hardcoded 3-level registry state. Add docs/ and refresh README.
90 lines
2.7 KiB
JavaScript
90 lines
2.7 KiB
JavaScript
/**
|
|
* XSB Sokoban level parser.
|
|
* Converts a raw XSB string into a structured level:
|
|
* { width, height, walls, targets, boxes, player, floors }
|
|
* Coordinates are {x, y} grid cells (0-indexed).
|
|
* `floors` is the set of reachable tiles inside the puzzle (flood-fill
|
|
* from the player), so the renderer only paints floor inside the level.
|
|
*/
|
|
|
|
const WALL = '#';
|
|
const FLOOR = ' ';
|
|
const TARGET = '.';
|
|
const BOX = '$';
|
|
const BOX_ON_TARGET = '*';
|
|
const PLAYER = '@';
|
|
const PLAYER_ON_TARGET = '+';
|
|
|
|
const key = (x, y) => `${x},${y}`;
|
|
|
|
function parseGrid(xsb) {
|
|
const lines = xsb.split('\n').filter(l => l.length > 0 && !l.startsWith(';'));
|
|
const width = lines.reduce((max, l) => Math.max(max, l.length), 0);
|
|
return { lines, width, height: lines.length };
|
|
}
|
|
|
|
function extractEntities(lines, width, height) {
|
|
const walls = new Set();
|
|
const targets = new Set();
|
|
const boxes = [];
|
|
let player = null;
|
|
|
|
for (let y = 0; y < height; y++) {
|
|
const row = lines[y];
|
|
for (let x = 0; x < width; x++) {
|
|
const ch = row[x] || ' ';
|
|
switch (ch) {
|
|
case WALL:
|
|
walls.add(key(x, y));
|
|
break;
|
|
case TARGET:
|
|
targets.add(key(x, y));
|
|
break;
|
|
case BOX:
|
|
boxes.push({ x, y });
|
|
break;
|
|
case BOX_ON_TARGET:
|
|
boxes.push({ x, y });
|
|
targets.add(key(x, y));
|
|
break;
|
|
case PLAYER:
|
|
player = { x, y };
|
|
break;
|
|
case PLAYER_ON_TARGET:
|
|
player = { x, y };
|
|
targets.add(key(x, y));
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return { walls, targets, boxes, player };
|
|
}
|
|
|
|
function floodFillFloors(player, walls, width, height) {
|
|
const floors = new Set();
|
|
if (!player) return floors;
|
|
const stack = [player];
|
|
while (stack.length) {
|
|
const { x, y } = stack.pop();
|
|
if (x < 0 || y < 0 || x >= width || y >= height) continue;
|
|
const k = key(x, y);
|
|
if (floors.has(k) || walls.has(k)) continue;
|
|
floors.add(k);
|
|
stack.push({ x: x + 1, y });
|
|
stack.push({ x: x - 1, y });
|
|
stack.push({ x, y: y + 1 });
|
|
stack.push({ x, y: y - 1 });
|
|
}
|
|
return floors;
|
|
}
|
|
|
|
export function parseLevel(xsb) {
|
|
const { lines, width, height } = parseGrid(xsb);
|
|
const { walls, targets, boxes, player } = extractEntities(lines, width, height);
|
|
const floors = floodFillFloors(player, walls, width, height);
|
|
return { width, height, walls, targets, boxes, player, floors };
|
|
}
|
|
|
|
export { key as cellKey };
|