feat: persist best score across page reloads (#10) - #11
Conversation
Add bestscore.js, a pure dependency-free localStorage-backed best-score store (loadBest/saveBest over injectable storage, tolerant of unavailable or throwing storage). Wire index.html to restore the best at boot and persist a new record on game over, so the 'Best' display survives a reload and a fresh load in the same browser (M2-R1). Cover it in the same PR: unit tests for the store and new M2-R1 cases in the dependency-free vm/DOM browser harness (fake localStorage shared across two boots to prove the reload path). Also add the M2 ROADMAP.md row for #9, which milestone-new left unpushed. Co-Authored-By: Claude Opus 4.8 <[email protected]>
There was a problem hiding this comment.
The documented fallback does not preserve an in-session best when storage is present but unusable. saveBest() recomputes current with loadBest(s) on every game over; if getItem() throws, that always returns 0. After a score of 6, restarting and ending with 2 therefore changes the displayed best from 6 to 2. This contradicts both the task plan and PR description ("degrades to an in-session best" / "keeps an in-session best"). Please retain the prior in-memory best across calls when storage access or writes fail, wire it through the controller as needed, and add a regression test covering a higher run followed by a weaker run with throwing/unavailable storage.
saveBest() recomputed the current best from storage on every game over; when
getItem() throws (private mode / blocked cookies) that collapsed to 0, so a
weaker run after a strong one lowered the displayed best (6 -> 2) and could
overwrite a good stored value with the smaller number. This contradicted the
task plan and PR description ("keeps an in-session best").
Thread the caller's in-memory best into saveBest() as `priorBest` and floor
the current best at max(stored, priorBest); the index.html controller passes
its displayed `best`. A weaker run is now a no-op when reads fail, a genuine
new high still wins.
Regression coverage:
- bestscore.test.js: higher-then-weaker with a throwing store and with no
store at all, plus a genuine new high after.
- browser.test.js: storage readable at boot (restores 6) then throwing — the
controller keeps the DOM best at 6 across a weaker run instead of dropping
to the re-read 0.
Addresses radiusred-checky[bot]'s changes-requested review on PR #11.
There was a problem hiding this comment.
The storage-failure regression is fixed and node --test passes all 33 tests on c9772ad. One plan-level correctness issue remains: loadBest() promises (in the task plan and its own comment) that garbage values read as 0, but parseInt accepts a numeric prefix. For example, a corrupted localStorage value of 42garbage currently restores 42. Please validate the entire stored string as a positive integer (and add a numeric-prefix garbage regression case) so malformed persisted data follows the recorded behavior.
parseInt accepted a numeric prefix, so a corrupted 'snake.bestScore' of '42garbage' restored 42 instead of the documented 0. loadBest now validates the whole stored string as a run of digits before trusting it, matching the recorded behaviour that malformed persisted data starts a browser at 0. Adds a numeric-prefix (and whitespace/decimal/hex) regression case.
saveBest() recomputed the current best from storage on every game over; when
getItem() throws (private mode / blocked cookies) that collapsed to 0, so a
weaker run after a strong one lowered the displayed best (6 -> 2) and could
overwrite a good stored value with the smaller number. This contradicted the
task plan and PR description ("keeps an in-session best").
Thread the caller's in-memory best into saveBest() as `priorBest` and floor
the current best at max(stored, priorBest); the index.html controller passes
its displayed `best`. A weaker run is now a no-op when reads fail, a genuine
new high still wins.
Regression coverage:
- bestscore.test.js: higher-then-weaker with a throwing store and with no
store at all, plus a genuine new high after.
- browser.test.js: storage readable at boot (restores 6) then throwing — the
controller keeps the DOM best at 6 across a weaker run instead of dropping
to the re-read 0.
Addresses radiusred-checky[bot]'s changes-requested review on PR #11.
What & why
Persist the "Best" score so it survives a page reload and a fresh load in the
same browser (M2-R1). Before this,
index.htmlheldbestin an in-memoryvariable that reset to 0 on every visit.
Closes #10.Changes
bestscore.js— a new pure, dependency-free classic script mirroringgame.js.loadBest(storage)/saveBest(score, storage)over an injectablestorage that defaults to the browser's
localStorage(keysnake.bestScore).It degrades gracefully: a missing/garbage/negative value reads as
0(neverNaN), and touching or writing an unavailable store (private mode, blockedcookies) is caught so the page still boots and keeps an in-session best.
index.html— loadsbestscore.jsbefore the inline controller,initialises
bestfromBestScore.loadBest()(a reload shows the stored bestbefore any play), and persists on game over via
BestScore.saveBest(...),which only overwrites a lower record.
tests/bestscore.test.js— unit coverage of the store: empty → 0, storedinteger read back, garbage/negative → 0, new high persisted, lower score does
not overwrite, throwing storage tolerated.
tests/browser.test.js— the dependency-free vm/DOM harness gains a fakelocalStorage; new M2-R1 cases prove the full delivery path: a fresh bootrestores a pre-seeded best before play, a new best is written on game over,
the best survives a simulated reload (second boot, same storage), and a weaker
run does not clobber a higher stored best.
ROADMAP.md— adds the M2 row linking to M2: Persistent best score & progressive difficulty #9 (the coordinator'smilestone newupdate was never pushed).Requirements satisfied
localStorageandsurvives a fresh load in the same browser.
Tests
node --test— 30 pass, 0 fail (unit + browser-integration, no dependencies,no build step).
Decisions
No ask-the-human points:
localStorageis named in the requirement; the keyname and graceful-degradation behaviour are routine implementation choices,
recorded here rather than as a separate gate.