From 209ad9103b812072f60884ba041b0f44db492216 Mon Sep 17 00:00:00 2001 From: tiennm99 Date: Thu, 3 Sep 2026 12:25:48 +0700 Subject: [PATCH] feat: add hand-designed bonus levels after the Microban set Bonus layouts live in their own module so the license boundary stays clean: Microban levels remain under Skinner's redistribution grant, while the hand-designed ones fall under the repository's main LICENSE. A new levels module concatenates both sets into one index space, so progress-store keys and level numbering continue unbroken. Level select tags bonus entries so they read as distinct from the stock set. --- LICENSE-LEVELS.md | 8 +++++--- docs/codebase-summary.md | 6 ++++-- src/lib/data/bonus-levels.js | 29 +++++++++++++++++++++++++++++ src/lib/data/levels.js | 15 +++++++++++++++ src/views/GameView.svelte | 6 +++--- src/views/LevelSelectView.svelte | 22 ++++++++++++++++++---- src/views/MenuView.svelte | 4 ++-- 7 files changed, 76 insertions(+), 14 deletions(-) create mode 100644 src/lib/data/bonus-levels.js create mode 100644 src/lib/data/levels.js diff --git a/LICENSE-LEVELS.md b/LICENSE-LEVELS.md index 0089962..f01289e 100644 --- a/LICENSE-LEVELS.md +++ b/LICENSE-LEVELS.md @@ -1,6 +1,6 @@ # Level Data License -The puzzle layouts shipped in [`src/game/data/microban-levels.js`](src/game/data/microban-levels.js) are **not** covered by the repository's main [`LICENSE`](LICENSE). They are governed by the terms below. +The puzzle layouts shipped in [`src/lib/data/microban-levels.js`](src/lib/data/microban-levels.js) are **not** covered by the repository's main [`LICENSE`](LICENSE). They are governed by the terms below. ## Microban @@ -12,13 +12,15 @@ The puzzle layouts shipped in [`src/game/data/microban-levels.js`](src/game/data The set is distributed by David W. Skinner with permission to **freely redistribute** provided the layouts remain **properly credited** to him. This project relies on that grant. The credit is preserved in: -- The file header of `src/game/data/microban-levels.js`. +- The file header of `src/lib/data/microban-levels.js`. - The main menu of the game (bottom-of-screen attribution). - `README.md` and `docs/project-overview-pdr.md`. ## Scope -This file applies **only to the puzzle layouts**. The parser, renderer, UI, progression system, and every other file in this repository are original work and are covered by the main `LICENSE`. +This file applies **only to the Microban puzzle layouts**. The parser, renderer, UI, progression system, and every other file in this repository are original work and are covered by the main `LICENSE`. + +The bonus layouts in [`src/lib/data/bonus-levels.js`](src/lib/data/bonus-levels.js) are **not** Skinner's work. They are hand-designed for this project and fall under the main `LICENSE`. If you fork or redistribute this project: diff --git a/docs/codebase-summary.md b/docs/codebase-summary.md index 19a4001..d34218c 100644 --- a/docs/codebase-summary.md +++ b/docs/codebase-summary.md @@ -13,7 +13,9 @@ src/ │ │ ├── progress-store.js # localStorage persistence (completed + best moves) │ │ └── haptics.js # navigator.vibrate wrapper (10ms on box push, 60ms on win) │ └── data/ -│ └── microban-levels.js # 155 XSB level strings (Microban, D. W. Skinner) +│ ├── microban-levels.js # 155 XSB level strings (Microban, D. W. Skinner) +│ ├── bonus-levels.js # Hand-designed XSB levels (project's own, main LICENSE) +│ └── levels.js # Microban + bonus concatenated; single index space └── views/ ├── MenuView.svelte # Title, play, progress, hints ├── LevelSelectView.svelte # Paginated 5×4 grid (20/page × 8 pages) @@ -38,7 +40,7 @@ public/ 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`, `haptics.js`, and `microban-levels.js` contain zero Svelte — they could be lifted into any other stack. +- **Framework-agnostic core.** `level-parser.js`, `board-model.js`, `progress-store.js`, `haptics.js`, `microban-levels.js`, `bonus-levels.js`, and `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. Desktop & mobile share the same component; `touch-action: none` blocks browser gestures on all devices. - **Mobile controls opt-in.** `MobileControls.svelte` is hidden on desktop via `@media (pointer: coarse)`, so no UA sniffing or JS feature detection — CSS media queries handle the split. diff --git a/src/lib/data/bonus-levels.js b/src/lib/data/bonus-levels.js new file mode 100644 index 0000000..6300fd4 --- /dev/null +++ b/src/lib/data/bonus-levels.js @@ -0,0 +1,29 @@ +/** + * Hand-designed bonus puzzles for this project. + * + * Unlike `microban-levels.js` (David W. Skinner's set, used under his + * redistribution grant), these layouts are original work by the project + * author and are covered by the repository's main LICENSE. + * + * Format: XSB + * # wall + * (space) floor + * . target + * $ box + * * box on target + * @ player + * + player on target + * + * Every layout must be sealed (walls fully enclose the reachable floor) + * and solvable. Levels appear after the Microban set in level select. + */ +export const BONUS_LEVELS = [ +// Compass — four boxes pushed outward onto the cardinal points. +`####### +#. . .# +# $$$ # +#.$@$.# +# $$$ # +#. . .# +#######` +]; diff --git a/src/lib/data/levels.js b/src/lib/data/levels.js new file mode 100644 index 0000000..20bf067 --- /dev/null +++ b/src/lib/data/levels.js @@ -0,0 +1,15 @@ +/** + * The playable level list: the Microban set followed by the project's + * own bonus puzzles. Views import LEVELS from here so a single index + * space (and therefore a single progress-store key space) spans both sets. + */ +import { MICROBAN_LEVELS } from './microban-levels.js'; +import { BONUS_LEVELS } from './bonus-levels.js'; + +export const LEVELS = [...MICROBAN_LEVELS, ...BONUS_LEVELS]; + +/** Number of Microban levels; bonus levels start at this index. */ +export const MICROBAN_COUNT = MICROBAN_LEVELS.length; + +/** True when the given level index is one of the hand-designed bonus levels. */ +export const isBonusLevel = (index) => index >= MICROBAN_COUNT; diff --git a/src/views/GameView.svelte b/src/views/GameView.svelte index d1ead13..4c2ac8a 100644 --- a/src/views/GameView.svelte +++ b/src/views/GameView.svelte @@ -11,14 +11,14 @@ import { BoardModel } from '../lib/core/board-model.js'; import { progressStore } from '../lib/core/progress-store.js'; import { pulse } from '../lib/core/haptics.js'; - import { MICROBAN_LEVELS } from '../lib/data/microban-levels.js'; + import { LEVELS } from '../lib/data/levels.js'; let { levelIndex, onMenu, onLevels, onNext } = $props(); // --- Level setup (runs once because GameView is keyed on levelIndex in App) --- function buildLevel() { try { - const lv = parseLevel(MICROBAN_LEVELS[levelIndex]); + const lv = parseLevel(LEVELS[levelIndex]); if (!lv.player) throw new Error('Level has no player tile'); return { level: lv, model: new BoardModel(lv), error: null }; } catch (err) { @@ -162,7 +162,7 @@ }; }); - const hasNext = $derived(levelIndex + 1 < MICROBAN_LEVELS.length); + const hasNext = $derived(levelIndex + 1 < LEVELS.length); let donateOpen = $state(false); diff --git a/src/views/LevelSelectView.svelte b/src/views/LevelSelectView.svelte index 404e3dc..0e099bf 100644 --- a/src/views/LevelSelectView.svelte +++ b/src/views/LevelSelectView.svelte @@ -5,12 +5,12 @@