mirror of
https://github.com/tiennm99/ai-coding-workflow-labs.git
synced 2026-09-18 22:22:31 +00:00
Game state saved after every move, restored on page load. Best score persisted separately. New Game clears game state but preserves best score. Schema validation prevents crashes from corrupted localStorage. 49 tests passing (11 new storage tests).
6.7 KiB
6.7 KiB
Story 2.2: Game State Persistence
Status: done
Story
As a player, I want my in-progress game to survive a page refresh, so that I never lose my progress unexpectedly.
Acceptance Criteria
- After a valid move, the full game state is saved to localStorage under the
gameStatekey as JSON:{ grid, score, won, keepPlaying } - The best score is saved separately under the
bestScorekey as a single number - On app load with a saved
gameStatein localStorage, the game restores the saved grid, score, won, and keepPlaying values, and the best score is restored from thebestScorekey (silent restore, no loading indicator) - On app load with no saved game or corrupted localStorage data, a fresh game starts with 2 random tiles and score 0 (silent recovery, no error shown)
- The
storage.jsmodule catchesJSON.parseerrors silently and returns null (triggering fresh game start) - When the player clicks New Game, the
gameStatekey in localStorage is cleared and thebestScorekey is preserved
Tasks / Subtasks
-
Task 1: Create storage.js module (AC: #1, #2, #3, #4, #5)
- Create
src/lib/storage.jswith functions:saveGameState(state),loadGameState(),saveBestScore(score),loadBestScore() saveGameStateserializes{ grid, score, won, keepPlaying }to localStorage keygameStateloadGameStateparses localStoragegameState— returns parsed object or null on failure/missingsaveBestScoresaves a number to localStorage keybestScoreloadBestScoreparses localStoragebestScore— returns number or 0 on failure/missing- Wrap all
JSON.parsecalls in try/catch, return null/0 on error
- Create
-
Task 2: Integrate storage into App.svelte initialization (AC: #3, #4)
- Import storage functions into App.svelte
- On init: attempt
loadGameState()— if non-null, use as initial gameState; otherwiseinitGame() - On init: attempt
loadBestScore()— use as initial bestScore value - Ensure bestScore from storage is at least as high as restored game score
-
Task 3: Save state after every move and score change (AC: #1, #2)
- Add
$effectto save gameState to localStorage whenever gameState changes - Update existing bestScore
$effectto also callsaveBestScore(bestScore)when bestScore updates
- Add
-
Task 4: Handle New Game clearing gameState (AC: #6)
- In
handleNewGame(), call storage function to clear gameState from localStorage - Verify bestScore key is NOT cleared on New Game
- In
-
Task 5: Write tests for storage.js (AC: #5)
- Test saveGameState/loadGameState round-trip
- Test saveBestScore/loadBestScore round-trip
- Test loadGameState returns null for corrupted data
- Test loadBestScore returns 0 for missing/corrupted data
- Test loadGameState returns null when key is missing
-
Task 6: Run full test suite
- All 48 tests pass (38 existing + 10 new storage tests)
Dev Notes
Architecture Compliance
- File location:
src/lib/storage.js— pure JS module, ZERO Svelte imports, ZERO DOM access beyond localStorage - Exports:
saveGameState,loadGameState,saveBestScore,loadBestScore,clearGameState - Error handling: Wrap
JSON.parsein try/catch — on failure, return null (gameState) or 0 (bestScore). No user-facing errors. - localStorage keys:
gameState(JSON object),bestScore(JSON number) - No new dependencies — uses built-in localStorage API only
Critical Anti-Patterns (DO NOT)
- DO NOT store bestScore inside the gameState localStorage key — they are separate keys
- DO NOT show error messages to the user on corrupted localStorage — silent recovery
- DO NOT import Svelte in storage.js — it's a pure JS module
- DO NOT use sessionStorage — use localStorage for cross-session persistence
- DO NOT save the entire App component state — only save the canonical game state shape
localStorage Schema
// Key: "gameState"
{ grid: number[][], score: number, won: boolean, keepPlaying: boolean }
// Key: "bestScore"
number
Save/Load Pattern in App.svelte
import { saveGameState, loadGameState, saveBestScore, loadBestScore, clearGameState } from './lib/storage.js';
// Initialize from localStorage or fresh
const savedState = loadGameState();
let gameState = $state(savedState || initGame());
let bestScore = $state(Math.max(loadBestScore(), savedState?.score || 0));
// Save on every state change
$effect(() => { saveGameState(gameState); });
$effect(() => {
if (gameState.score > bestScore) bestScore = gameState.score;
saveBestScore(bestScore);
});
// New Game: clear saved state, keep bestScore
function handleNewGame() {
clearGameState();
gameState = initGame();
}
Previous Story Intelligence
From Story 2.1:
bestScoreis a separate$state(0)in App.svelte — NOT part of gameState$effectwatchesgameState.scoreand updatesbestScorewhen exceededhandleNewGame()only resetsgameState = initGame()— bestScore survives- ScoreBoard receives
scoreandbestScoreprops — no changes needed - 38 tests passing in
src/lib/game-logic.test.js
Testing Strategy
- Mock localStorage using Vitest's built-in support or a simple mock object
- Test storage.js in isolation — no Svelte needed
- Test file:
src/lib/storage.test.js(co-located with module)
References
- [Source: _bmad-output/planning-artifacts/epics.md#Story 2.2]
- [Source: _bmad-output/planning-artifacts/architecture.md#Data Architecture - localStorage Schema]
- [Source: _bmad-output/planning-artifacts/architecture.md#Implementation Patterns - Process Patterns]
Dev Agent Record
Agent Model Used
Claude Opus 4.6 (1M context)
Debug Log References
Completion Notes List
- Created
src/lib/storage.jswith 5 exports: saveGameState, loadGameState, clearGameState, saveBestScore, loadBestScore - All JSON.parse wrapped in try/catch — corrupted data returns null/0 silently
- saveGameState destructures only canonical fields (grid, score, won, keepPlaying) — no extra data leaks
- App.svelte initializes from localStorage on load, falls back to initGame() if missing/corrupted
- bestScore initialized as max of stored bestScore and restored game score
- Two $effects: one saves gameState on every change, one tracks+saves bestScore
- handleNewGame calls clearGameState() — removes gameState key but preserves bestScore
- 10 new tests in storage.test.js with localStorage mock, 48 total tests passing
File List
- src/lib/storage.js (new — localStorage persistence module)
- src/lib/storage.test.js (new — 10 tests for storage module)
- src/App.svelte (modified — integrated storage save/load/clear)