remove completed plan and report files

Plans for Svelte 5 rewrite and gameplay improvements have been
fully implemented — removing the now-obsolete plan phases and
research report.
This commit is contained in:
tiennm99 committed 2026-04-14 11:43:03 +07:00
1 parent 7233310662
commit 286edc3c1d
13 files changed
-1196

No files matched your search

@@ -1,50 +0,0 @@
# Phase 1: Scaffold Svelte 5 Project
**Priority:** Critical | **Status:** pending | **Effort:** Small
## Overview
Set up a fresh Svelte 5 + Vite project structure alongside existing code. Replace Phaser dependency with Svelte.
## Steps
1. Remove Phaser and related deps from `package.json`
2. Install Svelte 5 + vite-plugin-svelte
```bash
npm install svelte && npm install -D @sveltejs/vite-plugin-svelte
```
3. Replace Vite config files (`vite/config.dev.mjs`, `vite/config.prod.mjs`) with single `vite.config.js` using svelte plugin
4. Create directory structure:
```
src/lib/ # Pure JS game logic
src/lib/locales/ # Locale JSON files
src/lib/levels/ # Level data
src/lib/game/ # Game logic classes
src/components/ # Reusable Svelte components
src/scenes/ # Scene components
src/styles/ # CSS
```
5. Create `src/main.js` — mount Svelte app to `#app`
6. Create minimal `src/App.svelte` — renders "Hello NNTV" placeholder
7. Create `src/styles/theme.css` — port color/font constants from `theme.js` as CSS variables
8. Update `index.html` — remove Phaser-specific meta, keep basic structure
9. Update `package.json` scripts to use new vite config
10. Verify `npm run dev` serves the placeholder app
## Files to Create
- `vite.config.js`
- `src/main.js` (replace existing)
- `src/App.svelte`
- `src/styles/theme.css`
## Files to Delete
- `vite/config.dev.mjs`
- `vite/config.prod.mjs`
- `vite/` directory
- `log.js`
- `src/game/theme.js`
## Success Criteria
- `npm run dev` opens browser with "Hello NNTV"
- `npm run build` produces working dist/
- Zero Phaser imports remain
- CSS variables available globally
@@ -1,63 +0,0 @@
# Phase 2: Port Pure Game Logic
**Priority:** Critical | **Status:** pending | **Effort:** Medium
## Overview
Move all framework-agnostic game logic into `src/lib/`. Strip Phaser imports. Separate data/logic from rendering.
## Steps
1. **Copy as-is** (just move files):
- `locales/en.json` → `src/lib/locales/en.json`
- `locales/vi.json` → `src/lib/locales/vi.json`
- `levels/Levels.js` → `src/lib/levels/levels.js`
- `progress.js` → `src/lib/progress.js`
- `localization.js` → `src/lib/localization.js`
2. **Port TurnManager** → `src/lib/game/turn-manager.js`
- Remove `import Phaser` (line 1)
- Remove `this.scene` references
- Make it accept callbacks instead of calling scene methods directly
- Constructor takes `{ onLevelComplete, onDetected, getPlayerPos, getGrid, getGuards, getLightSystem }`
3. **Port GridSystem** → `src/lib/game/grid-system.js`
- Keep: grid data model, isValidPosition, setWall/setGoal/setLight, gridToPixel, pixelToGrid
- Remove: `scene.add.graphics()`, `render()` method, all Phaser graphics calls
- Merge LightingSystem's `clearAllLight()` into GridSystem
- Add `getCellState(row, col)` returning `{ isWall, isGoal, isLight }`
4. **Port Guards** → `src/lib/game/guards.js`
- Keep: all guard logic (updateLight, onTurnChange, patrol path, rotation, blinking)
- Remove: `createSprite()`, `update()` (sprite positioning), `scene.add.circle/line`
- Remove: `destroy()` sprite cleanup (no sprites to destroy)
- Each guard exposes: `{ row, col, color, type, direction, isOn }` for rendering
5. **Port Player** → `src/lib/game/player.js`
- Keep: `moveTo()`, `move()`, `isInLitCell()`, `isAtGoal()`
- Remove: `createSprite()`, `update()` (sprite), tweens
- Pure data: `{ row, col }` position tracking
6. **Port LevelManager** → `src/lib/game/level-manager.js`
- Rewrite as a pure function: `loadLevel(levelId, grid)` → returns `{ player, guards, isFinalLevel }`
- No scene references
- Creates guard instances, sets up grid walls/goals
## Files to Create
- `src/lib/game/grid-system.js`
- `src/lib/game/guards.js`
- `src/lib/game/player.js`
- `src/lib/game/turn-manager.js`
- `src/lib/game/level-manager.js`
- `src/lib/localization.js`
- `src/lib/progress.js`
- `src/lib/locales/en.json`
- `src/lib/locales/vi.json`
- `src/lib/levels/levels.js`
## Files to Delete (after all phases complete)
- Entire `src/game/` directory
## Success Criteria
- All game logic files import zero Phaser code
- Guard logic can be tested independently (no DOM/canvas needed)
- `loadLevel()` returns a complete game state object
@@ -1,87 +0,0 @@
# Phase 3: Build Game Board + Sprite Components
**Priority:** Critical | **Status:** pending | **Effort:** Medium
## Overview
Create Svelte components that render the game grid, player, and guards using HTML/CSS instead of Phaser canvas.
## Steps
1. **Button.svelte** — reusable styled button
- Props: `text`, `onclick`, `small` (boolean for compact variant)
- Styled with CSS variables from theme.css
- Hover/active states via CSS
2. **GameBoard.svelte** — renders the grid
- Props: `grid` (2D array of cell states), `rows`, `cols`, `cellSize`
- CSS Grid layout: `grid-template-columns: repeat(cols, cellSize)`
- Each cell is a `<div>` with class based on state: `.wall`, `.goal`, `.lit`, `.empty`
- Click handler on cells for tap-to-move (emits `oncellclick` with row, col)
- Cell colors from CSS variables
3. **PlayerSprite.svelte** — renders the player
- Props: `row`, `col`, `cellSize`, `gridOffset`
- Positioned absolutely over the grid
- CSS `transition: top 100ms, left 100ms` for smooth movement (replaces Phaser tweens)
- Black circle via `border-radius: 50%`
4. **GuardSprite.svelte** — renders a guard
- Props: `guard` object `{ row, col, type, direction, isOn }`
- Props: `cellSize`, `gridOffset`
- Positioned absolutely over the grid
- Circle colored by type (CSS class: `.guard-static`, `.guard-rotating`, etc.)
- Direction indicator for rotating/patrolling: rotated CSS triangle or line
- Blinking guard dim state: reduced opacity via class toggle
5. **GameHud.svelte** — top HUD bar
- Props: `lives`, `level`, `turns`
- Events: `onpause`, `onmenu`
- Fixed position, not part of game board
- Uses Button.svelte for pause/menu
6. **DetectionPopup.svelte** — detection overlay
- Props: `visible`
- Events: `onplayagain`
- Dark overlay with centered message + button
- Uses `transition:fade`
7. **PauseMenu.svelte** — pause overlay
- Props: `visible`
- Events: `onresume`, `onrestart`, `onmainmenu`
- Dark overlay with 3 buttons
- Uses `transition:fade`
## Rendering Strategy
```
┌─ GameBoard.svelte (CSS Grid) ─────────┐
│ ┌──┐ ┌──┐ ┌──┐ ┌──┐ ┌──┐ ┌──┐ │
│ │ │ │ │ │ │ │ │ │ │ │ │ ... │
│ └──┘ └──┘ └──┘ └──┘ └──┘ └──┘ │
│ (each cell = div with bg color) │
│ │
│ PlayerSprite (absolute positioned) │
│ GuardSprite × N (absolute positioned)│
└────────────────────────────────────────┘
```
## CSS Class Map
| Game State | CSS Class | Color Variable |
|---|---|---|
| Empty cell | `.cell` | `--grid-empty` |
| Wall | `.cell.wall` | `--grid-wall` |
| Goal | `.cell.goal` | `--grid-goal` |
| Lit | `.cell.lit` | `--grid-lit` |
| Static guard | `.guard.static` | `--guard-static` |
| Rotating guard | `.guard.rotating` | `--guard-rotating` |
| Blinking guard (on) | `.guard.blinking` | `--guard-blinking` |
| Blinking guard (off) | `.guard.blinking.off` | `--guard-blinking-off` |
| Patrolling guard | `.guard.patrolling` | `--guard-patrolling` |
## Success Criteria
- GameBoard renders correct grid for any level (6x6 through 10x10)
- Player circle moves smoothly with CSS transitions
- Guards display correct colors and direction indicators
- All overlays (detection, pause) fade in/out
- Components accept plain JS data — no Phaser objects
@@ -1,85 +0,0 @@
# Phase 4: Build Scene Components
**Priority:** High | **Status:** pending | **Effort:** Medium
## Overview
Replace all Phaser Scene classes with Svelte components. App.svelte acts as the scene router using reactive state + transitions.
## Steps
1. **App.svelte** — scene router
- Module-level `$state` for `currentScene` and `sceneData`
- `{#if currentScene === 'MainMenu'}` blocks for each scene
- Each block wrapped in `transition:fade={{ duration: 300 }}`
- Export `navigate(scene, data)` function for child components
- Initialize with `currentScene = 'MainMenu'`
2. **MainMenu.svelte**
- Starfield background (random positioned dots via `{#each}`)
- Title, ninja icon, 4 buttons (Start, Level Select, Guide, Settings)
- Buttons call `navigate('StoryIntro')` etc.
3. **StoryIntro.svelte**
- Dark background, scrolling story text
- CSS animation `@keyframes scroll` moves text upward
- Skip button → navigates to LevelIntro
- Auto-advance after scroll completes
4. **LevelIntro.svelte**
- Props via sceneData: `{ level, lives }`
- Shows level number, name (from levels.js), story text (from localization)
- Continue button → navigates to Game
5. **LevelSelect.svelte**
- Grid of 12 level buttons (CSS Grid, 4 columns)
- Reads progress from `getProgress()`
- Locked/unlocked/completed states via CSS classes
- Checkmark on completed levels
- Click → navigate to LevelIntro with level data
6. **GameOver.svelte**
- Props via sceneData: `{ level, isLastLevel }`
- Different message for final level vs normal game over
- Try Again + Main Menu buttons
7. **Settings.svelte**
- Language toggle (EN/VI)
- Calls `setLanguage()` from localization.js
- Re-renders text reactively via `$derived` on language
8. **Guide.svelte**
- Scrollable content area (native CSS `overflow-y: auto`)
- Sections: objectives, controls, enemy types
- Back button
## Scene Navigation Pattern
```svelte
<!-- App.svelte -->
<script>
let currentScene = $state('MainMenu');
let sceneData = $state({});
function navigate(scene, data = {}) {
sceneData = data;
currentScene = scene;
}
</script>
{#if currentScene === 'MainMenu'}
<div transition:fade>
<MainMenu {navigate} />
</div>
{:else if currentScene === 'Game'}
<div transition:fade>
<Game {navigate} level={sceneData.level} lives={sceneData.lives} />
</div>
{/if}
```
## Success Criteria
- All 8 scenes render correctly
- Scene transitions fade smoothly
- Language switching updates all visible text
- Level select shows correct progress state
- Story intro scrolls and auto-advances
@@ -1,96 +0,0 @@
# Phase 5: Wire Game Scene + Input
**Priority:** Critical | **Status:** pending | **Effort:** Large
## Overview
The main Game.svelte scene — connects game logic (grid, guards, turns, player) to the visual components and handles keyboard/click input.
## Steps
1. **Game.svelte** — main game orchestrator
- `$state` for: grid, player, guards, lives, level, turns, isPaused, detected, isFinalLevel
- `onMount`: call `loadLevel()` to get initial state, set up keyboard listener
- `onDestroy`: clean up keyboard listener
- Render: GameBoard + PlayerSprite + GuardSprites + GameHud + overlays
2. **Keyboard input**
```js
function onKeyDown(e) {
if (isPaused || !inputEnabled) return;
const dirMap = {
ArrowUp: 'up', ArrowDown: 'down', ArrowLeft: 'left', ArrowRight: 'right',
w: 'up', s: 'down', a: 'left', d: 'right',
};
const dir = dirMap[e.key];
if (dir) handleMove(dir);
}
```
- Bind to `window` via `svelte:window` directive: `<svelte:window onkeydown={onKeyDown} />`
3. **Click-to-move input**
- GameBoard emits `oncellclick(row, col)`
- Game.svelte checks if cell is adjacent to player, calls `handleMove()`
4. **Turn flow** (handleMove function)
```
1. player.move(direction)
2. If moved successfully:
a. Check if player at goal → handleLevelComplete()
b. clearAllLight()
c. Each guard.onTurnChange()
d. grid.render state update (triggers Svelte reactivity)
e. Check if player in lit cell → showDetection()
f. turns++
```
5. **Level 12 special mechanic**
- On each move, check `isFinalLevel && Manhattan distance to goal <= 2`
- If triggered: set ALL non-wall cells to `isLight = true`
- Show princess detected message
- This looks like normal gameplay — player won't know it's impossible until they experience it
6. **Detection flow**
- Set `detected = true` → shows DetectionPopup
- On "Play Again": `lives--`, if lives > 0 reload level, else navigate to GameOver
7. **Level complete flow**
- Call `completeLevel()` from progress.js
- Brief CSS flash animation on board
- Navigate to LevelIntro for next level (or GameOver for final)
8. **Pause flow**
- Toggle `isPaused` → shows PauseMenu
- Resume / Restart / Main Menu actions
## Reactive State Shape
```js
// In Game.svelte
let grid = $state(null); // GridSystem instance
let player = $state(null); // Player instance
let guards = $state([]); // Guard instances
let lives = $state(3);
let currentLevel = $state(1);
let turns = $state(0);
let isPaused = $state(false);
let detected = $state(false);
let isFinalLevel = $state(false);
// Derived for rendering
let cells = $derived(grid ? grid.getAllCells() : []);
let playerPos = $derived(player ? { row: player.row, col: player.col } : null);
let guardData = $derived(guards.map(g => ({
row: g.row, col: g.col, type: g.constructor.name,
direction: g.direction, isOn: g.isOn,
})));
```
## Success Criteria
- Arrow keys / WASD move player through grid
- Click adjacent cell moves player
- Guards update correctly each turn (static lit, rotating beam, blinking toggle, patrol movement)
- Detection popup shows when stepping on lit cell
- Level completes when reaching goal
- Level 12 lights up entire map near goal
- Pause menu works
- Lives decrement and game over triggers at 0
@@ -1,60 +0,0 @@
# Phase 6: Polish + Verify
**Priority:** High | **Status:** pending | **Effort:** Small
## Overview
Final pass — visual polish, cleanup old Phaser code, update docs, verify all 12 levels.
## Steps
1. **Delete old Phaser code**
- Remove entire `src/game/` directory
- Remove `vite/` directory
- Remove `log.js`
- Remove `public/style.css` (styles now in Svelte components + theme.css)
- Remove `phaser` from package.json dependencies
- Remove `terser` from devDependencies (Vite handles minification)
- Run `npm install` to clean lockfile
2. **Visual polish**
- Verify dark theme renders correctly on all scenes
- Test responsive scaling (Scale.FIT equivalent via CSS `max-width` + `aspect-ratio`)
- Ensure grid is centered on all level sizes (6x6 through 10x10)
- Test direction indicators on rotating/patrolling guards
- Verify blinking guard color toggle
3. **Test all 12 levels**
- Level 1: move freely, reach goal
- Levels 2-5: static guard lit cells block correct positions
- Level 6: rotating guard beam sweeps and blocks cells
- Levels 7-8: mixed guard interactions
- Level 9: blinking guard toggles on/off
- Level 10: patrolling guard moves along path
- Level 11: all 4 guard types simultaneously
- Level 12: approach goal → entire map lights up (unbeatable)
4. **Test game flows**
- Lives system: lose 3 lives → game over screen
- Level progression: complete level → intro → next level
- Level select: locked/unlocked/completed states correct
- Progress persistence: refresh browser, progress retained
- Language switch: EN ↔ VI updates all text
- Pause menu: resume, restart, main menu all work
5. **Update docs**
- Update `docs/system-architecture.md` — Svelte 5 architecture
- Update `docs/codebase-summary.md` — new file listing
- Update `docs/code-standards.md` — Svelte conventions
- Update `README.md` — remove Phaser references, add Svelte info
6. **Build verification**
- `npm run build` produces working dist/
- Serve dist/ locally, test full game
- Check bundle size (target: <50KB app code, down from ~1MB with Phaser)
## Success Criteria
- Zero Phaser code remains
- All 12 levels playable with correct guard behavior
- Level 12 secret mechanic works
- Bundle size < 50KB (excluding assets)
- Docs updated to reflect new stack
-124
View File
@@ -1,124 +0,0 @@
---
title: Rewrite NNTV from Phaser 3 to Svelte 5
status: ready
created: 2026-04-12
branch: main
blockedBy: []
blocks: []
---
# Rewrite NNTV: Phaser 3 → Svelte 5 + Vite
## Overview
Replace Phaser 3 game engine (~1MB) with plain Svelte 5 + Vite for a turn-based grid puzzle game that only uses rectangles, circles, and text. All game logic (guards, turns, levels, lighting) ports as-is. Only the rendering/UI layer changes.
## Architecture: Before → After
```
BEFORE (Phaser 3) AFTER (Svelte 5)
───────────────── ────────────────
Phaser.Game config → App.svelte (scene router)
Phaser.Scene classes → Svelte components per scene
scene.add.circle/rect/text → HTML/CSS elements
scene.input.keyboard → window keydown events
scene.tweens.add → CSS transitions
this.scene.start() → reactive scene state
camera.fadeIn/fadeOut → svelte/transition (fade)
Phaser physics (unused) → DELETE
```
## File Migration Map
| Current File | Action | New Location |
|---|---|---|
| `locales/en.json`, `vi.json` | PORT AS-IS | `src/lib/locales/` |
| `levels/Levels.js` | PORT AS-IS | `src/lib/levels/levels.js` |
| `progress.js` | PORT AS-IS | `src/lib/progress.js` |
| `localization.js` | PORT AS-IS | `src/lib/localization.js` |
| `TurnManager.js` | PORT (remove Phaser import) | `src/lib/game/turn-manager.js` |
| `GridSystem.js` | SPLIT: data logic → pure JS, rendering → Svelte | `src/lib/game/grid-system.js` + `GameBoard.svelte` |
| `Guard.js` | SPLIT: logic → pure JS, rendering → Svelte | `src/lib/game/guards.js` + `GuardSprite.svelte` |
| `Player.js` | SPLIT: logic → pure JS, rendering → Svelte | `src/lib/game/player.js` + `PlayerSprite.svelte` |
| `LightingSystem.js` | MERGE into grid-system.js | `src/lib/game/grid-system.js` |
| `LevelManager.js` | REWRITE as pure function | `src/lib/game/level-manager.js` |
| `theme.js` | REWRITE as CSS variables | `src/styles/theme.css` |
| All scenes/*.js | REWRITE as Svelte components | `src/scenes/*.svelte` |
| `main.js` | REWRITE | `src/main.js` + `src/App.svelte` |
| `LevelTester.js` | DELETE | — |
## New Project Structure
```
src/
├── main.js # Mount Svelte app
├── App.svelte # Scene router + transitions
├── styles/
│ └── theme.css # CSS variables (colors, fonts)
├── lib/
│ ├── localization.js # getText, setLanguage (ported)
│ ├── progress.js # localStorage persistence (ported)
│ ├── locales/
│ │ ├── en.json # English (ported)
│ │ └── vi.json # Vietnamese (ported)
│ ├── levels/
│ │ └── levels.js # Level data (ported)
│ └── game/
│ ├── grid-system.js # Grid data model + lighting (pure JS)
│ ├── guards.js # Guard classes, logic only (pure JS)
│ ├── player.js # Player position + movement (pure JS)
│ ├── turn-manager.js # Turn logic (pure JS)
│ └── level-manager.js # Level loading (pure JS)
├── components/
│ ├── GameBoard.svelte # CSS Grid rendering of cells
│ ├── PlayerSprite.svelte # Player circle element
│ ├── GuardSprite.svelte # Guard circle + direction indicator
│ ├── GameHud.svelte # Lives, level, turns, pause/menu buttons
│ ├── DetectionPopup.svelte # "You've been detected" overlay
│ ├── PauseMenu.svelte # Pause overlay
│ └── Button.svelte # Reusable styled button
└── scenes/
├── MainMenu.svelte
├── StoryIntro.svelte
├── LevelSelect.svelte
├── LevelIntro.svelte
├── Game.svelte # Main game scene
├── GameOver.svelte
├── Settings.svelte
└── Guide.svelte
```
## Key Design Decisions
1. **Plain Svelte + Vite** (not SvelteKit) — no routing needed for SPA game
2. **Svelte 5 runes** (`$state`, `$derived`, `$effect`) — reactive game state
3. **CSS Grid** for game board — native, responsive, zero dependencies
4. **CSS transitions** for player movement — replace Phaser tweens
5. **`svelte/transition`** for scene switching — fade between screens
6. **Pure JS game logic** — all guard/turn/level logic stays framework-agnostic
7. **Game state as module-level `$state`** in `.svelte.js` files — shared across components
## Phases
| # | Phase | Status | File |
|---|-------|--------|------|
| 1 | Scaffold Svelte 5 project | pending | [phase-01](phase-01-scaffold-project.md) |
| 2 | Port pure game logic | pending | [phase-02](phase-02-port-game-logic.md) |
| 3 | Build game board + sprites | pending | [phase-03](phase-03-game-board-components.md) |
| 4 | Build scene components | pending | [phase-04](phase-04-scene-components.md) |
| 5 | Wire game scene + input | pending | [phase-05](phase-05-game-scene-input.md) |
| 6 | Polish + verify | pending | [phase-06](phase-06-polish-verify.md) |
## Risk Assessment
| Risk | Impact | Mitigation |
|---|---|---|
| Grid reactivity perf on 10x10 | Low | Svelte fine-grained updates; only changed cells re-render |
| CSS transition timing mismatch with game turns | Medium | Use fixed duration, test each guard type |
| Level 12 special mechanic (lightUpEntireMap) | Low | Direct DOM class toggle on all cells |
## Dependencies
- Svelte 5 (latest)
- Vite 6.x (already used)
- No other runtime dependencies
@@ -1,54 +0,0 @@
## Phase 1: Wait Action
**Priority:** P1 | **Status:** Pending | **Effort:** 15min
### Overview
Spacebar (and tap-on-player for mobile) skips the player's move but still advances guards.
Creates timing puzzles — player must wait for a safe window.
### Data Flow
1. User presses Space / taps player cell
2. `handleMove` is NOT called; instead new `handleWait()` fires
3. `handleWait` calls `turnManager.nextTurn(grid, player, guards)` without moving player
4. Same detection/completion checks run
5. `renderVersion++` triggers re-render
### Files to Modify
**`src/scenes/Game.svelte`** (input handler only)
### Implementation Steps
- [ ] 1. In `onKeyDown`, add `' '` (Space) key check before direction map lookup
- [ ] 2. When Space detected, call `handleWait()` and `e.preventDefault()`
- [ ] 3. Create `handleWait()` function:
```js
function handleWait() {
if (!player || !grid) return;
const result = turnManager.nextTurn(grid, player, guards);
if (isFinalLevel && checkFinalLevel()) { renderVersion++; return; }
renderVersion++;
if (result.levelComplete) handleLevelComplete();
else if (result.detected) detected = true;
}
```
- [ ] 4. In `onCellClick`, if clicked cell === player position, call `handleWait()`
### Edge Cases
- Wait on level 12 still triggers princess detection expansion (covered — same `checkFinalLevel` path)
- Wait when already at goal: `nextTurn` returns `levelComplete: true` (correct)
- Wait counts as a turn for move counter (correct — `turnManager.turnCount++` happens in `nextTurn`)
### Success Criteria
- Space key advances turn without moving player
- Guards rotate/patrol/blink as normal
- Turn counter increments
- Detection still works if guard light reaches player after wait
### Rollback
Revert single commit. No schema changes.
@@ -1,89 +0,0 @@
## Phase 2: Star Rating + Par Moves
**Priority:** P1 | **Status:** Pending | **Effort:** 2h
### Overview
Each level gets a `parMoves` value. On completion, show 1-3 star rating.
Persist best stars + best moves in localStorage. Display stars on level select.
### Data Flow
```
Level complete → turnManager.turnCount vs level.parMoves → star rating
→ completeLevel(levelNum, totalLevels, moves) → localStorage update
→ LevelSelect reads stars from progress
```
### Star Calculation
| Condition | Stars |
|-----------|-------|
| moves <= parMoves | 3 |
| moves <= parMoves + 3 | 2 |
| completed | 1 |
### Files to Modify
#### 1. `src/lib/levels/levels.js`
- [ ] Add `parMoves` field to each level definition
- [ ] Placeholder values (will be finalized in Phase 3 after redesign):
- L1: 10, L2: 12, L3: 14, L4: 14, L5: 14, L6: 16
- L7: 16, L8: 18, L9: 18, L10: 20, L11: 22, L12: 99 (unbeatable)
#### 2. `src/lib/progress.js`
- [ ] Extend schema: add `levelStars: {}` and `levelBestMoves: {}` to DEFAULT_PROGRESS
- [ ] `getProgress()`: merge missing keys for backwards compat
- [ ] Update `completeLevel(levelNum, totalLevels, moves, parMoves)`:
- Calculate stars
- Store best stars (only if better than existing)
- Store best moves (only if fewer than existing)
- Return `{ ...progress, stars }` so caller can display
#### 3. `src/scenes/Game.svelte`
- [ ] Import `LEVELS` to access `parMoves` for current level
- [ ] Pass `turnManager.turnCount` and `parMoves` to `completeLevel()`
- [ ] After level complete, show star rating overlay before navigating
- [ ] Add `LevelComplete` popup component (inline or separate small component)
- Shows 1-3 stars with animation
- Shows move count vs par
- "Next Level" button
#### 4. `src/scenes/LevelSelect.svelte`
- [ ] Read `progress.levelStars` in addition to `completedLevels`
- [ ] Replace checkmark with star display (1-3 stars shown as filled/empty)
- [ ] Show best move count under level number
#### 5. `src/components/GameHud.svelte`
- [ ] No changes needed — already shows turn count
### localStorage Schema (backwards compatible)
```js
// Old format still works — missing keys get defaults
{
maxLevel: 5,
completedLevels: [1, 2, 3, 4],
// NEW — absent in old saves, defaults to {}
levelStars: { "1": 3, "2": 2, "3": 1 },
levelBestMoves: { "1": 8, "2": 14, "3": 20 }
}
```
### Edge Cases
- Old localStorage without `levelStars`/`levelBestMoves` → defaults to `{}`
- Level 12 unbeatable → `parMoves: 99` never matters
- Player completes level with worse score → keep existing best
- `completeLevel` called with no moves param (old code path) → skip star calc
### Success Criteria
- Star rating appears on level complete
- Best stars persist in localStorage
- Level select shows stars per level
- Old saves don't break (backwards compat)
### Rollback
Revert commit. Old localStorage data unaffected (new keys simply disappear).
@@ -1,89 +0,0 @@
## Phase 3: Level Redesign + Chaser Guard
**Priority:** P1 | **Status:** Pending | **Effort:** 3h
### Overview
Redesign existing 12 levels for more challenge. Add **Chaser guard** type.
Finalize `parMoves` values after redesign. Level 12 remains unbeatable.
### New Guard Type: ChaserGuard
**Behavior:** Dormant until player enters Manhattan distance <= `detectionRadius` (default 3).
Once triggered, moves 1 cell toward player's **last known position** each turn.
Lights its own cell + adjacent cell in movement direction. Returns to start if reaches
last known position and player not visible.
**Why Chaser over Sentry:** Chaser creates dynamic panic — player must change route.
Sentry is just a fancy static guard (cone vision = static pattern). Chaser adds genuine
reactive gameplay with minimal code.
#### Implementation in `src/lib/game/guards.js`
- [ ] Add `ChaserGuard` class extending `Guard`:
```
Properties: detectionRadius, startRow, startCol, targetRow, targetCol,
isChasing, lastKnownRow, lastKnownCol
Methods:
- onTurnChange(allGuards, player): check distance to player
- If within radius and not blocked by wall: set isChasing, record lastKnownPos
- If chasing: move 1 step toward lastKnownPos (prefer row then col)
- If reached lastKnownPos and player not in radius: return to start
- updateLight(): light own cell + 1 cell in facing direction
```
- [ ] Key: `onTurnChange` needs player reference — update signature
#### Turn Manager Change (`src/lib/game/turn-manager.js`)
- [ ] Pass `player` to `guard.onTurnChange(guards, player)` — non-breaking since
existing guards ignore extra params
#### Level Manager Change (`src/lib/game/level-manager.js`)
- [ ] Add `case 'chaser'` to guard factory switch
#### Guard Sprite (`src/components/GuardSprite.svelte`)
- [ ] Add chaser visual (red-tinted sprite, or different emoji/icon)
- [ ] Show "alert" state when `isChasing` is true
### Level Redesign Principles
1. **Tighter corridors** — reduce open space, force specific paths
2. **More guards per level** — current levels are sparse
3. **Timing windows** — require waiting (Phase 1) for safe passage
4. **Chaser placement** — put chasers near obvious paths to force detours
5. **Level 12** — keep walls + guard placement, only verify light wave still covers all paths
### Level-by-Level Notes
- [ ] **L1-2** (tutorial): Keep simple but add 1 more wall each to reduce trivial paths
- [ ] **L3-4** (rotating intro): Add extra static guard, narrow corridors
- [ ] **L5-6** (blinking): Tighter timing — reduce safe windows from 2 turns to 1
- [ ] **L7-8** (patrolling): Add walls to force player into patrol paths, require waiting
- [ ] **L9** (decoy): Add chaser near the "obvious" short path — punishes rushing
- [ ] **L10** (mirrors): Add more mirror bounces, tighter spaces
- [ ] **L11** (throne room): Add chaser + extra patrolling guard
- [ ] **L12** (princess): DO NOT CHANGE core mechanic. May add 1-2 more guards in upper sections
- [ ] **Finalize parMoves** for each level after redesign (playtest manually)
### Edge Cases / Risks
| Risk | Likelihood | Impact | Mitigation |
|------|-----------|--------|------------|
| Level becomes unsolvable | Medium | High | Playtest each level; keep at least 1 valid path |
| Chaser pathfinding hits wall | Medium | Medium | Simple Manhattan step; skip move if blocked by wall |
| Level 12 becomes beatable | Low | High | Verify: expanding light wave from (9,9) covers all reachable cells within player's max possible steps |
| ChaserGuard makes levels too hard | Medium | Low | Tune detectionRadius per level (2-4 range) |
### Success Criteria
- All levels 1-11 are solvable but require thought
- At least 3 levels benefit from wait action
- Chaser guard creates noticeable tension when triggered
- Level 12 remains provably unbeatable
- parMoves set to optimal-path length for each level
### Rollback
Revert commit restores old level definitions and removes ChaserGuard class.
@@ -1,49 +0,0 @@
## Phase 4: Guard Vision Preview (Optional)
**Priority:** P3 | **Status:** Pending | **Effort:** 45min
### Overview
Show semi-transparent overlay of cells that will be lit NEXT turn.
Helps player plan without trial-and-error. Toggle via button or hotkey.
### Data Flow
1. Before rendering, simulate next turn's guard positions/lights on a **cloned grid**
2. Diff current lit cells vs next-turn lit cells
3. Render "preview" cells with distinct CSS class (e.g., `cell-preview` with 20% opacity warning color)
### Implementation
#### `src/lib/game/turn-manager.js`
- [ ] Add `previewNextTurn(grid, guards)` method:
- Deep-clone guard states (row, col, direction, isOn, currentPathIndex, isReversing)
- Create temporary grid clone
- Run `onTurnChange` on clones
- Return Set of `"row,col"` strings for next-turn lit cells
#### `src/scenes/Game.svelte`
- [ ] Add `showPreview` toggle state (default false)
- [ ] Bind `v` key or HUD button to toggle
- [ ] Compute `previewCells` derived from `turnManager.previewNextTurn()` when toggle on
- [ ] Pass `previewCells` to `GameBoard`
#### `src/components/GameBoard.svelte`
- [ ] Accept optional `previewCells` prop
- [ ] Add `cell-preview` CSS class to cells in preview set
### Edge Cases
- Chaser guard preview needs player position — pass current player pos to preview
- Level 12 expanding wave — preview should show next ring expansion
- Performance: cloning 5-10 guards per frame is trivial for grid sizes <= 10x10
### Success Criteria
- Toggle shows/hides next-turn danger zones
- Preview visually distinct from current lit cells (different opacity/color)
- No performance impact on turn processing
### Rollback
Revert commit. Preview is purely additive UI — no game logic changes.
@@ -1,57 +0,0 @@
---
title: "Gameplay Improvements: Wait Action, Star Rating, Level Redesign"
description: "Add wait mechanic, star/par rating system, and redesign levels for challenge"
status: done
priority: P1
effort: 6h
branch: main
tags: [gameplay, levels, ux]
created: 2026-04-13
---
# Gameplay Improvements Plan
## Phases
| # | Phase | Effort | Status | Files Modified |
|---|-------|--------|--------|----------------|
| 1 | [Wait Action](phase-01-wait-action.md) | 15min | Done | Game.svelte |
| 2 | [Star Rating + Par Moves](phase-02-star-rating.md) | 2h | Done | levels.js, progress.js, Game.svelte, LevelSelect.svelte, LevelCompletePopup.svelte |
| 3 | [Level Redesign + Chaser Guard](phase-03-level-redesign.md) | 3h | Done | levels.js, guards.js, level-manager.js, GuardSprite.svelte, theme.css |
| 4 | [Guard Vision Preview](phase-04-vision-preview.md) | 45min | Done | GameBoard.svelte, Game.svelte, turn-manager.js, GameHud.svelte |
## Dependency Graph
```
Phase 1 (Wait) ── no deps, standalone
Phase 2 (Stars) ── no deps, standalone
Phase 3 (Levels) ── after Phase 1 + 2 (par values depend on wait mechanic existing)
Phase 4 (Preview) ── after Phase 3 (needs final guard configs)
```
## File Ownership (no parallel conflicts)
- Phase 1: Game.svelte only (input handler)
- Phase 2: progress.js, LevelSelect.svelte, GameHud.svelte, + level complete flow in Game.svelte
- Phase 3: levels.js, guards.js, level-manager.js, GuardSprite.svelte
- Phase 4: GameBoard.svelte (new overlay layer)
## Key Constraints
- Level 12 MUST remain unbeatable (expanding light wave from goal covers all paths)
- No new npm dependencies
- All state stays in pure JS classes; Svelte 5 reactivity via renderVersion bump pattern
- localStorage schema must be backwards-compatible (existing `nntv-progress` key)
## Rollback
Each phase is a single commit. Revert commit to rollback. No data migrations needed
(localStorage schema only adds optional fields, old data still works).
## Success Criteria
1. Spacebar wait works, guards advance, player stays
2. Stars show on level complete + level select; persist across sessions
3. Levels require more thought; at least 3 levels need wait action to solve optimally
4. Level 12 still unbeatable (manual verification)
5. (Optional) Ghost overlay shows next-turn danger zones
@@ -1,293 +0,0 @@
# Svelte 5 Research: Grid Puzzle Game Rewrite
**Status:** Complete | **Focus:** Runes, setup, transitions, grid patterns, state management
**Date:** 2026-04-12 | **Scope:** Actionable patterns for turn-based grid game
---
## 1. Svelte 5 Runes Syntax — Reactive State
### Core Pattern: $state for Grid
Runes are compiler keywords (not functions), prefixed with `$`. Use `$state` to declare reactive cell data.
**Grid state pattern (array of cells):**
```javascript
let grid = $state([
{ id: 0, type: 'wall' },
{ id: 1, type: 'floor' },
// ... 16+ cells
]);
```
**For object-based grid:**
```javascript
let gridMap = $state(new Map([
['0,0', { type: 'wall', solved: false }],
['0,1', { type: 'puzzle', value: 5 }],
]));
```
### $derived — Computed Grid State
Auto-updates when dependencies change (memoized). Zero side-effects allowed.
```javascript
let solvedCount = $derived(grid.filter(c => c.solved).length);
let isWon = $derived(solvedCount === grid.length);
let gridDisplay = $derived(grid.map(c => c.type)); // safe memo
```
### $effect — Side Effects (Subscriptions, DOM, Analytics)
Runs after DOM update. Return cleanup function for teardown.
```javascript
$effect(() => {
console.log('Game state changed:', grid);
localStorage.setItem('game', JSON.stringify(grid));
});
$effect(() => {
return () => console.log('Component unmounting'); // cleanup
});
```
### Key Gotchas
- Avoid mutations inside `$derived`: `$derived(count++)` **fails**.
- `$state` works outside `.svelte` files (in `.svelte.js` / `.svelte.ts`).
- Runes replace old `let x = 0; $: x = x + 1` reactive declarations.
---
## 2. Svelte 5 + Vite Setup
### Recommended: Use `sv` CLI (SvelteKit)
```bash
npx sv create my-puzzle-game
# Select: Svelte, TypeScript, Vitest, Prettier
cd my-puzzle-game
npm run dev
```
Scaffold includes: Vite (pre-configured), TypeScript, dev server at `localhost:5173`.
### Alternative: Bare Svelte + Vite
```bash
npm create vite@latest my-game -- --template svelte-ts
npm install
npm run dev
```
Simpler but misses SvelteKit's routing/build optimizations. **For a static SPA game, either works.**
### Vite Config (if bare Svelte)
`vite.config.ts` auto-includes `vite-plugin-svelte`. No manual config needed for basic setup.
---
## 3. Scene Transitions (Fade/Fly/Slide)
### Pattern: Conditional Scenes
```svelte
<script>
import { fade, fly, slide } from 'svelte/transition';
let currentScene = $state('intro');
</script>
{#if currentScene === 'intro'}
<div transition:fade={{ duration: 300 }}>
<h1>Intro Scene</h1>
<button onclick={() => currentScene = 'game'}>Play</button>
</div>
{:else if currentScene === 'game'}
<div transition:fly={{ x: 100, duration: 400 }}>
<!-- Game board here -->
</div>
{:else if currentScene === 'win'}
<div transition:slide={{ axis: 'y', duration: 600 }}>
<h1>You Won!</h1>
</div>
{/if}
```
### Available Transitions
| Name | Use | Params |
|------|-----|--------|
| `fade` | Opacity 0→1 | `duration`, `delay`, `easing` |
| `fly` | Position + opacity | `x`, `y`, `duration` |
| `slide` | Axis-based slide | `axis` ('x'/'y'), `duration` |
| `scale`, `blur` | Also available | config object |
### Bidirectional
`transition:` works both in and out. Reverses smoothly mid-animation if toggled.
---
## 4. CSS Grid for Game Board
### Reactive Grid Layout
```svelte
<script>
let grid = $state(Array(16).fill(null).map((_, i) => ({
id: i,
row: Math.floor(i / 4),
col: i % 4,
})));
</script>
<div class="board">
{#each grid as cell (cell.id)}
<div class="cell" onclick={() => handleCellClick(cell.id)}>
{cell.id}
</div>
{/each}
</div>
<style>
.board {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 8px;
width: 320px;
}
.cell {
aspect-ratio: 1;
background: #e0e0e0;
border-radius: 4px;
cursor: pointer;
}
</style>
```
### Performance Note
Svelte's fine-grained reactivity means updating a single cell **doesn't re-render all others**. Safe for responsive grids (6x6, 8x8).
---
## 5. State Management: Stores vs. Runes
### For Game Progress + Localization: Use Runes in `.svelte.js`
**gameState.svelte.js:**
```javascript
export const game = $state({
level: 1,
moves: 0,
isWon: false,
grid: [],
});
export const i18n = $state({
lang: 'en',
strings: { title: 'Puzzle Game', play: 'Play' },
});
export function resetLevel() {
game.level = 1;
game.moves = 0;
game.isWon = false;
}
```
**App.svelte:**
```svelte
<script>
import { game, i18n } from './gameState.svelte.js';
</script>
<h1>{i18n.strings.title}</h1>
<p>Level {game.level} • Moves: {game.moves}</p>
<button onclick={() => i18n.lang = 'es'}>Español</button>
```
### When to Use Stores (Deprecated Pattern)
Only for **async data streams** (polling, WebSocket). Most games don't need this.
### Why Runes > Stores
- No boilerplate (no `writable()`, `.subscribe()`).
- Works in plain `.js` files (shared logic).
- Memoization via `$derived`.
---
## 6. SvelteKit vs. Plain Svelte for Static Game
### Recommendation: **Use Plain Svelte + Vite**
| Criteria | Plain Svelte | SvelteKit |
|----------|--------------|-----------|
| **Setup time** | 2 min | 3 min |
| **Routing** | Manual | File-based (unnecessary) |
| **Build output** | Single HTML/JS | Server capable |
| **Static deploy** | ✅ Simpler | ✅ Needs adapter-static |
| **DevTools** | Vite only | Vite + SvelteKit CLI |
| **Scaling** | Hits limits ~2-3 scenes | Extensible |
### For a Static SPA Game
```bash
npm create vite@latest my-game -- --template svelte-ts
```
This is **KISS**. No file-based routing overhead, no server code, direct control.
### Only Use SvelteKit If
- Game needs **server-side scoring/leaderboards** (then use SvelteKit backend).
- Game will expand to **10+ pages** (use file-based routing).
- Team prefers **unified framework** across multiple projects.
---
## Implementation Checklist
### Phase 1: Setup
- [ ] `npm create vite@latest` (svelte-ts template)
- [ ] Replace Vite's default App.svelte with game component
- [ ] Verify `npm run dev` opens at localhost:5173
### Phase 2: Grid & State
- [ ] Create `gameState.svelte.js` with `$state(grid)`
- [ ] Build `GameBoard.svelte` with CSS Grid + `{#each grid}`
- [ ] Wire `$derived(isWon)` from grid state
### Phase 3: Interactions
- [ ] Add cell click handlers → update grid state
- [ ] Bind to `$effect` for persistence (localStorage)
- [ ] Test fine-grained reactivity (click cell, only that cell re-renders)
### Phase 4: Transitions
- [ ] Import `fade`, `fly`, `slide` from `svelte/transition`
- [ ] Wrap scenes in conditional blocks with `transition:` directives
- [ ] Test bidirectional transitions (scene switches)
### Phase 5: Deploy
- [ ] `npm run build` → outputs `dist/` (static)
- [ ] Upload `dist/` to any static host (Vercel, GitHub Pages, Netlify)
---
## Trade-offs & Adoption Risk
| Risk | Mitigation |
|------|-----------|
| Svelte 5 is new (2024+) | Use LTS minor versions; runes are stable now |
| Team unfamiliar with runes | Runes are **simpler** than Vue/React hooks; quick ramp |
| Grid performance at scale | Fine-grained reactivity handles 100+ cells easily |
| Transitions can stutter | Use `duration: 200–400ms`; test on target devices |
---
## Sources
- [Svelte Runes Documentation](https://svelte.dev/docs/svelte/what-are-runes)
- [Svelte Transitions](https://svelte.dev/docs/svelte/svelte-transition)
- [Svelte Stores](https://svelte.dev/docs/svelte/stores)
- [SvelteKit Single-Page Apps](https://svelte.dev/docs/kit/single-page-apps)
- [Getting Started](https://svelte.dev/docs/svelte/getting-started)
- [Mainmatter: Runes and Global State](https://mainmatter.com/blog/2025/03/11/global-state-in-svelte-5/)
- [Frontend Masters: Game Board Grid](https://frontendmasters.com/courses/svelte-v2/game-board-grid/)
---
## Unresolved Questions
- **WebSocket multiplayer?** Not covered; requires SvelteKit backend or third-party service.
- **Mobile touch events?** Standard DOM events work; no Svelte-specific guidance needed.
- **Save file encryption?** Depends on backend; out of scope for static game.