Files
sokoban/docs/codebase-summary.md
T
tiennm99 8a3d4b4a9d feat!: rewrite on Svelte 5, drop Phaser
Replace Phaser 3 with Svelte 5 as the rendering and UI layer. The
framework-agnostic core (level parser, board model, progress store,
microban level data) moves from src/game/core → src/lib/core with zero
code changes. Scenes and the hand-rolled button factory are gone; in
their place:

- src/App.svelte            root router (menu / levels / game)
- src/views/MenuView        title + play + progress + hints
- src/views/LevelSelectView paginated 5x4 grid with native <button>s
- src/views/GameView        owns BoardModel, handles input, HUD, win
- src/views/Board           purely presentational DOM renderer
- src/views/AppButton       shared themed wrapper for native <button>
- src/app.css               Nord palette ported to CSS variables

GameView uses a non-reactive BoardModel ref and syncs plain snapshot
fields (player, boxes, moves, won) into $state after every mutation —
Board consumes only plain props, so Svelte reactivity stays predictable
and the core class stays framework-agnostic. GameView is keyed on
levelIndex in App, so changing level remounts with fresh state.

Native <button> everywhere kills the click-hitbox class of bugs.
Animations are now CSS transform transitions (110ms) instead of tweens.

Bundle shrinks from ~1.5 MB Phaser to ~65 kB JS / 23 kB gzipped — about
60x smaller. Removed: phaser, terser, src/game, log.js (analytics
ping), phasermsg vite plugin, manual Phaser chunks, terser config,
public/style.css. Scripts simplified to dev/build.

Docs updated: codebase summary, architecture, code standards,
changelog, roadmap, README.
2026-04-12 00:50:46 +07:00

3.2 KiB
Raw Blame History

Codebase Summary

Layout

src/
├── main.js                         # Mounts App.svelte into #app
├── App.svelte                      # Root router: menu / levels / game
├── app.css                         # Nord palette (CSS variables) + resets
├── lib/
│   ├── 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)
│   └── data/
│       └── microban-levels.js      # 155 XSB level strings (Microban, D. W. Skinner)
└── views/
    ├── MenuView.svelte             # Title, play, progress, hints
    ├── LevelSelectView.svelte      # Paginated 5×4 grid (20/page × 8 pages)
    ├── GameView.svelte              # Board + HUD + win overlay + input
    ├── Board.svelte                # Presentational DOM board (div-per-tile)
    └── AppButton.svelte             # Shared themed button (wraps native <button>)

public/
├── style.css                       # Legacy file — theme now lives in src/app.css
├── favicon.png
└── assets/                         # bg.png, logo.png (unused, reserved)

Data flow

  1. src/main.js mounts App.svelte.
  2. App.svelte holds view and levelIndex state and renders one of three view components.
  3. MenuView → calls onPlay() → App switches to LevelSelectView.
  4. LevelSelectView reads completion + best-move data from progressStore, renders a paginated grid, calls onSelect(i) on click.
  5. GameView is keyed on levelIndex ({#key levelIndex} in App) so every level change remounts it with fresh state.
  6. Inside GameView: parseLevel(xsb) → new BoardModel(level), keyboard input calls model.tryMove() / model.undo(), after each mutation syncFromModel() reassigns the $state snapshots that Board reads as props.
  7. On win: progressStore.recordCompletion() + overlay with NEXT / LEVELS actions.

Key design choices

  • Framework-agnostic core. level-parser.js, board-model.js, progress-store.js, and microban-levels.js contain zero Svelte — they could be lifted into any other stack.
  • BoardModel as a non-reactive ref. Svelte reactivity is driven by plain $state snapshot fields (player, boxes, moves, won) that syncFromModel() reassigns after every mutation. Cleaner than making the class instance itself reactive.
  • Board is purely presentational. Takes plain props (walls, targets, floors, player, boxes, tileSize) so Svelte reactivity is predictable.
  • Animations via CSS. Box and player use transform: translate() with transition: transform 110ms ease. No JS animation loop.
  • Responsive tile sizing. GameView.computeTileSize() picks a tile size that fits the level inside the viewport, capped at 56 px.
  • Scoped CSS per component. Svelte SFCs keep markup, style, and logic co-located and isolate styles to the component that owns them.

File size

Every .js / .svelte file is under the 200-LOC budget, except microban-levels.js which is pure data (155 XSB strings) and exempt.