feat(web): persist game state across page refreshes with resume dialog
Build and Deploy / build-and-push (push) Successful in 2m54s
Android Release / build-apk (push) Successful in 4m38s

- solitaire_wasm: add SolitaireGame::serialize() and from_saved() so JS
  can round-trip the full GameState through localStorage as JSON
- game.js: save {gameState, elapsedSecs, drawThree} to localStorage
  (key: fs_game_save) on every render(); clear the save on win
- game.js: on bootstrap, check for a saved game and show a resume
  dialog if one exists; Resume restores state + timer, New Game discards
  the save and starts fresh with a random seed
- game.html: add #resume-overlay markup (same pattern as win-overlay)
- game.css: add styles for the resume dialog and its secondary button

localStorage failures (private-browsing quota) are silently ignored so
they never block gameplay.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
funman300
2026-05-19 15:38:07 -07:00
parent 76cf41e7a9
commit 90eb5fd207
4 changed files with 185 additions and 6 deletions
+93 -6
View File
@@ -69,6 +69,34 @@ function preloadTheme(theme) {
preloadTheme("classic");
preloadTheme("dark");
// ── Persistence ──────────────────────────────────────────────────────────────
const LS_SAVE_KEY = "fs_game_save";
function saveState() {
if (!game) return;
try {
const gameState = game.serialize();
if (typeof gameState !== "string") return;
localStorage.setItem(LS_SAVE_KEY, JSON.stringify({ gameState, elapsedSecs, drawThree }));
} catch (e) {
// localStorage may be unavailable (private browsing quota, etc.) — never block gameplay.
console.warn("fs: save failed", e);
}
}
function clearSave() {
try { localStorage.removeItem(LS_SAVE_KEY); } catch { /* ignore */ }
}
function loadSave() {
try {
const raw = localStorage.getItem(LS_SAVE_KEY);
if (!raw) return null;
const save = JSON.parse(raw);
return save?.gameState ? save : null;
} catch { return null; }
}
// ── State ────────────────────────────────────────────────────────────────────
let game = null;
let snap = null; // last rendered GameSnapshot
@@ -138,16 +166,72 @@ async function bootstrap() {
await init();
syncThemeButton();
const params = new URLSearchParams(window.location.search);
const urlSeed = params.has("seed") ? Number(params.get("seed")) : randomSeed();
drawThree = params.has("draw3");
chkDraw3.checked = drawThree;
buildSlots();
scaleBoard();
window.addEventListener("resize", scaleBoard);
startGame(urlSeed);
attachHandlers();
const saved = loadSave();
if (saved) {
showResumeDialog(saved);
} else {
const params = new URLSearchParams(window.location.search);
const urlSeed = params.has("seed") ? Number(params.get("seed")) : randomSeed();
drawThree = params.has("draw3");
chkDraw3.checked = drawThree;
startGame(urlSeed);
}
}
function showResumeDialog(saved) {
const overlay = document.getElementById("resume-overlay");
if (overlay) overlay.classList.remove("hidden");
document.getElementById("btn-resume").onclick = () => {
if (overlay) overlay.classList.add("hidden");
resumeGame(saved);
};
document.getElementById("btn-resume-new").onclick = () => {
clearSave();
if (overlay) overlay.classList.add("hidden");
drawThree = false;
chkDraw3.checked = false;
startGame(randomSeed());
};
}
function resumeGame(saved) {
let restored;
try {
restored = SolitaireGame.from_saved(saved.gameState);
} catch (e) {
console.warn("fs: restore failed, starting new game", e);
clearSave();
startGame(randomSeed());
return;
}
game = restored;
drawThree = !!saved.drawThree;
elapsedSecs = saved.elapsedSecs || 0;
chkDraw3.checked = drawThree;
const displaySeed = Math.round(game.seed());
hudSeed.textContent = `seed ${displaySeed}`;
winOverlay.classList.add("hidden");
cardEls.clear();
board.querySelectorAll(".card, .recycle-label").forEach(el => el.remove());
const url = new URL(window.location);
url.searchParams.set("seed", displaySeed);
if (drawThree) url.searchParams.set("draw3", "");
else url.searchParams.delete("draw3");
history.replaceState(null, "", url);
const s = game.state();
snap = s;
render(s);
if (!s.is_won) startTimer();
}
function randomSeed() {
@@ -304,9 +388,12 @@ function render(s) {
acTimer = setInterval(doAutoCompleteStep, 380);
}
if (s.is_won) {
clearSave();
stopTimer();
if (acTimer) { clearInterval(acTimer); acTimer = null; }
showWin(s);
} else {
saveState();
}
}