Files
sokoban/src/game/core/level-parser.js
T
tiennm99 1d2fff6bf8 feat: ship 100 Microban levels with overhauled UI and modular core
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.
2026-04-11 21:58:00 +07:00

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 };