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.
This commit is contained in:
tiennm99 committed 2026-04-11 21:58:00 +07:00
1 parent a61d5a2410
commit 1d2fff6bf8
27 files changed
+1866 -881

No files matched your search

+36
View File
@@ -0,0 +1,36 @@
# Code Standards
## Language & Toolchain
- ES modules, modern JS (no TypeScript).
- Phaser 3.88+.
- Vite as build tool. Dev: `npm run dev`. Prod: `npm run build`.
## Naming
- Files: **kebab-case** with descriptive names (`board-renderer.js`, `progress-store.js`).
- Classes: PascalCase (`BoardModel`, `BoardRenderer`).
- Functions and variables: camelCase.
- Constants: UPPER_SNAKE for module-level tuning knobs (`KEY_REPEAT_MS`, `PER_PAGE`).
## File size
- Code files must stay under 200 lines of code.
- Pure-data files (levels, palettes) are exempt.
## Architecture rules
- **Scenes** own lifecycle + layout; they delegate drawing to renderers and logic to models.
- **Core** (`core/`) is pure JS — no Phaser imports. Anything that can be unit-tested without a canvas lives here.
- **UI** (`ui/`) is Phaser-specific but scene-agnostic — reusable widgets and renderers.
- No new dependencies without updating this doc.
## Style
- Prefer composition over inheritance.
- Fail loudly during development (console.error on unexpected state), fail gracefully at runtime (try/catch around level parsing, progress store).
- Comments: short, explain *why*, not *what*. File headers give a one-sentence purpose.
## Git / commits
- Conventional commits (`feat:`, `fix:`, `refactor:`, `docs:`, `chore:`).
- Never commit dotenv, keys, or build artifacts.
- Run `npm run build-nolog` before pushing to catch compile errors.
## Testing strategy (current)
No automated tests yet. Manual smoke test: load menu → play level 1 → complete → verify progress saved in localStorage → reload page → verify completion persists.
Future: unit tests for `level-parser.js` and `board-model.js` (both Phaser-free).
+43
View File
@@ -0,0 +1,43 @@
# Codebase Summary
## Layout
```
src/
├── main.js # Entry: boots Phaser on #game-container
└── game/
├── main.js # Phaser Game config + scene registration
├── data/
│ └── microban-levels.js # 100 XSB level strings
├── core/
│ ├── level-parser.js # XSB text → {walls, targets, boxes, player, floors}
│ ├── board-model.js # Pure game state + move/undo/win logic
│ ├── progress-store.js # localStorage persistence (completed + best moves)
│ └── theme.js # Nord color palette, fonts, tile-size helper
├── ui/
│ ├── button-factory.js # Reusable rounded button with hover/press
│ └── board-renderer.js # Draws the board + animates moves
└── scenes/
├── menu-scene.js # Title, play, progress counter, keybinds
├── level-scene.js # Paginated 5×4 level grid (5 pages × 20)
└── game-scene.js # Gameplay: input, HUD, win overlay
public/
├── style.css # Page background + container shadow
├── favicon.png
└── assets/ # bg.png, logo.png (reserved for future use)
```
## Data flow
1. `main.js` creates the Phaser Game with Menu / Level / Game scenes.
2. `level-scene` writes `registry.currentLevel` on click, starts `GameScene`.
3. `game-scene` reads the level index, calls `parseLevel()` on the XSB string, builds a `BoardModel`, hands it to `BoardRenderer`.
4. Input → `BoardModel.tryMove()` → `BoardRenderer.animateMove()` → win check → `progressStore.recordCompletion()`.
## Key design choices
- **XSB at runtime**: levels are stored as the standard Sokoban text format; the parser flood-fills interior floor tiles so the renderer only paints inside the puzzle shape.
- **Pure `BoardModel`**: move/undo/win logic has zero Phaser dependencies — trivial to unit-test later.
- **No Arcade physics**: the original version wired colliders but moved sprites by tween; the physics system did nothing. Removed.
- **One button factory**: every scene goes through `createButton()` so hover/press feels identical everywhere.
## File size budget
Every file under 200 LOC per development rules. Data file (`microban-levels.js`) exceeds that because it's pure data, not logic.
+30
View File
@@ -0,0 +1,30 @@
# Development Roadmap
## Phase 1 — Core game (complete)
- Phaser + Vite scaffolding.
- Menu / Level / Game scenes.
- Arrow-key movement, box pushing, target detection.
## Phase 2 — Overhaul (complete, 2026-04-11)
- Replace 3 hand-crafted levels with 100 Microban levels.
- Modularize: core / ui / scenes split, every file <200 LOC.
- BoardModel with undo + move counter.
- Paginated level select (5 pages × 20 levels).
- Nord theme + rounded button factory + animated board renderer.
- WASD support, U/Z undo, R restart, Esc to menu.
- localStorage progress (completed + best move count).
- Drop dead Arcade physics and broken shutdown code.
- Docs folder + README refresh.
## Phase 3 — Polish (planned)
- Sound effects (step, push, win).
- Player facing direction indicator.
- Level category tabs (Easy / Medium / Hard) derived from puzzle size or move count.
- Touch controls (swipe) for mobile.
- Unit tests for `level-parser` and `board-model`.
## Phase 4 — Stretch (ideas)
- Additional level packs (Sasquatch, Sokogen).
- Custom level importer (paste XSB text).
- Replay / move playback.
- Per-level leaderboards (local only).
+32
View File
@@ -0,0 +1,32 @@
# Project Changelog
## 2026-04-11 — Overhaul
### Added
- **100 Microban levels** (David W. Skinner) as the new stock level set, replacing the 3 hand-crafted puzzles. Stored as XSB text in `src/game/data/microban-levels.js`.
- **XSB level parser** (`core/level-parser.js`) with flood-fill floor detection.
- **BoardModel** (`core/board-model.js`) — pure state, undo history, win detection.
- **Progress store** (`core/progress-store.js`) — localStorage persistence of completion + best move count, fails gracefully if unavailable.
- **BoardRenderer** (`ui/board-renderer.js`) — floor/wall/target/box/player drawing with animated moves.
- **Button factory** (`ui/button-factory.js`) — one rounded-button implementation shared by every scene.
- **Nord theme** (`core/theme.js`) — palette, fonts, and responsive tile-size helper.
- **Paginated level select**: 5×4 grid × 5 pages, shows ✓ + best move count per completed level.
- **WASD controls**, **U/Z undo**, **R restart**, **Esc to menu**, move counter, win overlay with NEXT / LEVELS buttons.
- `docs/` — PDR, codebase summary, code standards, system architecture, roadmap, this changelog.
### Changed
- Scenes renamed to kebab-case (`menu-scene.js`, `level-scene.js`, `game-scene.js`) and rebuilt against the new core/ui modules.
- `main.js` now imports a shared theme and registers only the three new scenes.
- `public/style.css` now uses a radial gradient background and rounds the game container.
- `index.html` title updated.
### Removed
- `MenuScene.js`, `LevelScene.js`, `MainScene.js` (old PascalCase versions).
- `public/assets/levels/level1.json`, `level2.json`, `level3.json`.
- Arcade Physics setup inside `MainScene` — it was configured but never actually drove movement (moves were all tween-based on a grid). Colliders, `physics.world.shutdown()` call, and the hand-rolled `shutdown/destroy` lifecycle overrides are gone.
- Unused `import game from '../main.js'` in the old `MainScene`.
- Hardcoded `totalLevels: 3` and 3-element `levelCompleted` array in the game registry — replaced with dynamic lookup via `progressStore`.
### Fixed
- Scene restart no longer leaves dangling event listeners (Phaser handles this itself; the previous manual `removeAllListeners` was both redundant and fragile).
- Level select now scales to 100 puzzles via pagination instead of a single-row layout.
+28
View File
@@ -0,0 +1,28 @@
# Sokoban — Product Development Record
## What it is
Browser-based Sokoban puzzle game. 100 solvable Microban levels (David W. Skinner, freely distributable), Phaser 3 + Vite, deployed to GitHub Pages.
## Goals
- Clean, beginner-friendly Sokoban that runs in any modern browser with no install.
- 100 curated puzzles, progressing from easy to moderately tricky.
- Keyboard-first UX with undo, restart, move counter, persistent progress.
- Small, well-organized codebase (<200 LOC per file) that's easy to extend.
## Non-goals
- Custom level editor (puzzles are fixed, shipped with the build).
- Networked play / leaderboards.
- Sound / music (kept out to stay lightweight; may add later).
## Target audience
Casual puzzle fans, Sokoban beginners. Microban set chosen specifically because it's designed for beginners and illustrates one concept per puzzle.
## Success criteria
- All 100 levels playable end-to-end.
- Progress (completion + best moves) persists across sessions.
- Build output fits in the default Vite budget.
- Runs smoothly on desktop browsers at 1024×768 canvas (scaled responsively).
## Credits
- Puzzles: Microban by David W. Skinner (April 2000). Used per the collection's free-distribution terms with credit.
- Engine: Phaser 3.
+57
View File
@@ -0,0 +1,57 @@
# System Architecture
## High-level
Single-page static site. No backend. Phaser 3 runs the game loop inside a `<canvas>` element. Progress persists in `localStorage`.
```
index.html ──▶ src/main.js ──▶ src/game/main.js (Phaser Game)
│
├── MenuScene
├── LevelScene ── registry: currentLevel
└── GameScene
│
├── parseLevel(XSB) ── core/level-parser.js
├── BoardModel ── core/board-model.js
├── BoardRenderer ── ui/board-renderer.js
└── progressStore ── core/progress-store.js
│
└── localStorage
```
## Scene lifecycle
1. **MenuScene** — title, play, progress, hints.
2. **LevelScene** — paginated 5×4 grid, reads completion/best-moves from `progressStore`. On click: writes `currentLevel` to the Phaser registry and starts `GameScene`.
3. **GameScene** — `init` resets local state → `create` parses the level, builds `BoardModel`, instantiates `BoardRenderer`, wires input, builds HUD. `update()` handles key polling with a repeat gate (`KEY_REPEAT_MS = 130`).
## Input → state → render
```
key event ─▶ GameScene.update ─▶ BoardModel.tryMove(dx,dy)
│
└── returns true on legal move
│
├── renderer.animateMove()
├── moveLabel.setText()
└── if BoardModel.isSolved()
└── onWin() ─▶ progressStore.recordCompletion()
```
## Level data
- Stored as XSB strings in `src/game/data/microban-levels.js`.
- XSB symbols: `#` wall, ` ` floor, `.` target, `$` box, `*` box-on-target, `@` player, `+` player-on-target.
- Parser flood-fills from the player position to compute the interior floor set. Everything outside the flood is treated as exterior (not rendered, not walkable).
## Persistence schema
`localStorage['sokoban-progress-v1']`:
```json
{
"completed": { "0": true, "3": true, ... },
"bestMoves": { "0": 14, "3": 27, ... }
}
```
Keys are level indices (0-based). `getCompletedCount()` returns the size of `completed`.
## Responsive rendering
`computeTileSize(levelW, levelH, viewportW, viewportH)` picks the largest tile size (capped at 64px) that fits the level with margin, so small puzzles display large and the two huge Microban levels (#154, #155 — not shipped) would still fit.
## Deployment
Static build via `vite build --config vite/config.prod.mjs`, output pushed to GitHub Pages via the repo's CI. No server-side components.