diff --git a/README.md b/README.md index 6703fcb..be5dd52 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Sokoban -My from-scratch Sokoban game, built with Phaser 3 and Vite. The engine, UI, level parser, and progression system are all my own implementation. The puzzle layouts themselves come from David W. Skinner's freely distributable **Microban** set — I'm not reusing any of his code, only his level designs. +My from-scratch Sokoban game, built with **Svelte 5** and **Vite**. The engine, UI, level parser, and progression system are all my own implementation. The puzzle layouts themselves come from David W. Skinner's freely distributable **Microban** set — I'm not reusing any of his code, only his level designs. Play: [https://tiennm99.github.io/sokoban/](https://tiennm99.github.io/sokoban/) @@ -18,8 +18,6 @@ Play: [https://tiennm99.github.io/sokoban/](https://tiennm99.github.io/sokoban/) npm install npm run dev # dev server on http://localhost:8080 npm run build # production build -npm run dev-nolog # dev without the analytics ping -npm run build-nolog # build without the analytics ping ``` ## Project layout @@ -35,7 +33,7 @@ See [`docs/codebase-summary.md`](docs/codebase-summary.md). ## Credits - Puzzle layouts: **Microban** level set by David W. Skinner (April 2000). Freely distributable with credit. Original site: http://users.bentonrea.com/~sasquatch/sokoban/ - Game code: tiennm99 (with AI pair-programming from Claude). -- Engine: [Phaser 3](https://phaser.io/). +- Framework: [Svelte 5](https://svelte.dev/) on top of [Vite](https://vite.dev/). ## License - Source code: see [`LICENSE`](LICENSE). diff --git a/docs/code-standards.md b/docs/code-standards.md index bda595b..dccc78c 100644 --- a/docs/code-standards.md +++ b/docs/code-standards.md @@ -2,23 +2,26 @@ ## Language & Toolchain - ES modules, modern JS (no TypeScript). -- Phaser 3.88+. +- Svelte 5 with runes (`$state`, `$derived`, `$props`). - 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`). +- Plain JS files: **kebab-case** with descriptive names (`board-model.js`, `progress-store.js`). +- Svelte components: **PascalCase.svelte** per ecosystem convention (`MenuView.svelte`, `AppButton.svelte`). +- Classes: PascalCase (`BoardModel`). - Functions and variables: camelCase. -- Constants: UPPER_SNAKE for module-level tuning knobs (`KEY_REPEAT_MS`, `PER_PAGE`). +- Constants: UPPER_SNAKE for module-level tuning knobs (`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. +- **Views** (`src/views/*.svelte`) own layout + user interaction. Each screen is one component, kept under 200 LOC. +- **Core** (`src/lib/core/`) is pure JS — no Svelte imports. Anything that can be unit-tested without a DOM lives here (parser, board model, progress store). +- **Data** (`src/lib/data/`) is framework-agnostic static data. +- `Board.svelte` is purely presentational: plain props in, DOM out. It does not import or touch `BoardModel`. +- `GameView.svelte` owns the mutable `BoardModel` instance and calls `syncFromModel()` after every mutation to reassign the reactive `$state` snapshots that `Board` consumes. - No new dependencies without updating this doc. ## Style @@ -33,4 +36,4 @@ ## 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). +Future: unit tests for `level-parser.js` and `board-model.js` (both framework-free JS). diff --git a/docs/codebase-summary.md b/docs/codebase-summary.md index 0dad45d..d55fcbf 100644 --- a/docs/codebase-summary.md +++ b/docs/codebase-summary.md @@ -3,41 +3,45 @@ ## Layout ``` src/ -├── main.js # Entry: boots Phaser on #game-container -└── game/ - ├── main.js # Phaser Game config + scene registration - ├── data/ - │ └── microban-levels.js # 155 XSB level strings (full Microban set) - ├── 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 +├── 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 + + diff --git a/src/views/Board.svelte b/src/views/Board.svelte new file mode 100644 index 0000000..df4a2b3 --- /dev/null +++ b/src/views/Board.svelte @@ -0,0 +1,159 @@ + + + +
+ {#each floorCells as cell (cell.x + ',' + cell.y)} +
+ {/each} + + {#each targetCells as cell (cell.x + ',' + cell.y)} +
+ {/each} + + {#each wallCells as cell (cell.x + ',' + cell.y)} +
+ {/each} + + {#each boxes as box (box.id)} +
+ {/each} + +
+
+ + diff --git a/src/views/GameView.svelte b/src/views/GameView.svelte new file mode 100644 index 0000000..2378a56 --- /dev/null +++ b/src/views/GameView.svelte @@ -0,0 +1,253 @@ + + + + + +
+ {#if parseError} +

{parseError}

+ BACK TO MENU + {:else} +
+
+
LEVEL {levelIndex + 1}
+
+ Moves: {moves} + {#if best != null}  ·  Best: {best}{/if} +
+
+
+ UNDO + RESTART + LEVELS +
+
+ +
+ +
+ + {#if won} +
+
+

LEVEL COMPLETE!

+

Moves: {moves}

+
+ {#if hasNext} + NEXT LEVEL + {/if} + LEVELS +
+
+
+ {/if} + {/if} +
+ + diff --git a/src/views/LevelSelectView.svelte b/src/views/LevelSelectView.svelte new file mode 100644 index 0000000..136c395 --- /dev/null +++ b/src/views/LevelSelectView.svelte @@ -0,0 +1,179 @@ + + + + + +
+
+ < BACK +

SELECT LEVEL

+ {completedCount} / {total} +
+ +
+ {#each visibleLevels as level (level.index)} + + {/each} +
+ +
+ changePage(-1)} disabled={page === 0}>< + Page {page + 1} / {totalPages} + changePage(1)} disabled={page >= totalPages - 1}>> +
+
+ + diff --git a/src/views/MenuView.svelte b/src/views/MenuView.svelte new file mode 100644 index 0000000..e03a080 --- /dev/null +++ b/src/views/MenuView.svelte @@ -0,0 +1,43 @@ + + + + + + diff --git a/vite/config.dev.mjs b/vite/config.dev.mjs index 71e0cc8..cc69daa 100644 --- a/vite/config.dev.mjs +++ b/vite/config.dev.mjs @@ -1,16 +1,9 @@ import { defineConfig } from 'vite'; +import { svelte } from '@sveltejs/vite-plugin-svelte'; export default defineConfig({ base: './', - build: { - rollupOptions: { - output: { - manualChunks: { - phaser: ['phaser'] - } - } - }, - }, + plugins: [svelte()], server: { port: 8080 } diff --git a/vite/config.prod.mjs b/vite/config.prod.mjs index 7152b08..70fffe5 100644 --- a/vite/config.prod.mjs +++ b/vite/config.prod.mjs @@ -1,47 +1,7 @@ import { defineConfig } from 'vite'; - -const phasermsg = () => { - return { - name: 'phasermsg', - buildStart() { - process.stdout.write(`Building for production...\n`); - }, - buildEnd() { - const line = "---------------------------------------------------------"; - const msg = `❤️❤️❤️ Tell us about your game! - games@phaser.io ❤️❤️❤️`; - process.stdout.write(`${line}\n${msg}\n${line}\n`); - - process.stdout.write(`✨ Done ✨\n`); - } - } -} +import { svelte } from '@sveltejs/vite-plugin-svelte'; export default defineConfig({ base: '/sokoban/', - logLevel: 'warn', - build: { - rollupOptions: { - output: { - manualChunks: { - phaser: ['phaser'] - } - } - }, - minify: 'terser', - terserOptions: { - compress: { - passes: 2 - }, - mangle: true, - format: { - comments: false - } - } - }, - server: { - port: 8080 - }, - plugins: [ - phasermsg() - ] + plugins: [svelte()] });