diff --git a/index.html b/index.html
index dbf4b90..74d53b5 100644
--- a/index.html
+++ b/index.html
@@ -1755,19 +1755,23 @@
}
// --- seed with the example problem ---
- addCity({name: "0", type: "hub", renown: 2});
- addCity({name: "1", type: "foundry", renown: 2, vat_steel: 1, vat_brass: 1, vat_electrum: 1});
- addCity({name: "2", type: "hub", renown: 2});
- addCity({name: "3", type: "foundry", renown: 2});
- addCity({name: "4", type: "monument", renown: 2});
- addAgent({kind: "Planner"});
- addTerms({
- "renown": 0,
- "luxuries": 1,
- "steel": 2,
- "brass": 1,
- "electrum": 2
- })
+ // Only used for a fresh tab; a tab reload/restore refills the form from
+ // sessionStorage instead (see restoreSession below).
+ function seedDefault() {
+ addCity({name: "0", type: "hub", renown: 2});
+ addCity({name: "1", type: "foundry", renown: 2, vat_steel: 1, vat_brass: 1, vat_electrum: 1});
+ addCity({name: "2", type: "hub", renown: 2});
+ addCity({name: "3", type: "foundry", renown: 2});
+ addCity({name: "4", type: "monument", renown: 2});
+ addAgent({kind: "Planner"});
+ addTerms({
+ "renown": 0,
+ "luxuries": 1,
+ "steel": 2,
+ "brass": 1,
+ "electrum": 2
+ });
+ }
// --- deep links ---
// /?solve= loads one solve; /?solves=,,… loads an arbitrary
@@ -1785,13 +1789,103 @@
document.getElementById("output").prepend(card);
pending.set(token, {token, card, n, confirmed: true});
}
+
+ // --- session persistence -------------------------------------------
+ // The browser natively restores plain form controls (the Game inputs,
+ // start-resource numbers, tradeable checkboxes) on a reload / "reopen
+ // closed tab", but not the SPA-built cards (cities, agents, terms, …) nor
+ // the queue of solve cards under #output — those are recreated by script
+ // each load. We mirror both into sessionStorage (per-tab, and preserved
+ // across tab restore) so the whole page comes back as it was left.
+ const SESSION_KEY = "dws_session";
+
+ // Tokens of every solve card currently on the page, top-to-bottom.
+ function currentTokens() {
+ return [...document.querySelectorAll("#output [data-token]")]
+ .map(e => e.dataset.token);
+ }
+
+ function saveSession() {
+ // buildProblem() samples log terms and can throw on a half-typed
+ // expression; if so, keep whatever problem we last stored so a
+ // transiently-invalid form doesn't wipe the saved inputs.
+ let problem = null, maxTime;
+ try {
+ problem = buildProblem();
+ maxTime = +document.getElementById("time").value;
+ } catch (e) {/* keep previously-saved problem */}
+ let prev = {};
+ try {prev = JSON.parse(sessionStorage.getItem(SESSION_KEY) || "{}");}
+ catch (e) {/* ignore corrupt/absent state */}
+ const data = {
+ problem: problem || prev.problem || null,
+ maxTime: problem ? maxTime : prev.maxTime,
+ tokens: currentTokens(),
+ solutionCount,
+ };
+ try {sessionStorage.setItem(SESSION_KEY, JSON.stringify(data));}
+ catch (e) {/* storage may be full/unavailable; state just won't persist */}
+ }
+
+ // Debounced: coalesce bursts of edits (and card add/remove mutations)
+ // into a single write.
+ let saveTimer = null;
+ function scheduleSave() {
+ clearTimeout(saveTimer);
+ saveTimer = setTimeout(saveSession, 300);
+ }
+
+ // Refill the form and re-open the solve queue from a prior session.
+ // Returns true if there was a session to restore.
+ function restoreSession() {
+ let data;
+ try {data = JSON.parse(sessionStorage.getItem(SESSION_KEY) || "null");}
+ catch (e) {return false;}
+ if (!data) return false;
+ if (data.problem) applyProblem(data.problem, data.maxTime);
+ const tokens = data.tokens || [];
+ // Reserve a card per token and let the shared /job_status stream
+ // resolve each (rendering a finished solve fetched from the server,
+ // or tracking one still queued/running). Load in reverse so the
+ // first token ends up on top, mirroring how they were prepended.
+ for (const token of tokens.slice().reverse()) loadSharedSolve(token);
+ if (tokens.length) syncStream();
+ // Keep future solve numbering above anything from the old session.
+ if (typeof data.solutionCount === "number")
+ solutionCount = Math.max(solutionCount, data.solutionCount);
+ return true;
+ }
+
+ // --- startup ---
+ const restored = restoreSession();
+ if (!restored) seedDefault();
+
+ // Deep links still work on top of a restored session; skip any token the
+ // restored queue already holds so it isn't duplicated.
const params = new URLSearchParams(location.search);
const sharedTokens = (params.get("solves") || params.get("solve") || "")
.split(",").map(s => s.trim()).filter(Boolean);
+ const present = new Set(currentTokens());
+ const newShared = sharedTokens.filter(t => !present.has(t));
// Load in reverse so the first id ends up on top (each load prepends).
- for (const token of sharedTokens.slice().reverse()) loadSharedSolve(token);
- if (sharedTokens.length) syncStream();
+ for (const token of newShared.slice().reverse()) loadSharedSolve(token);
+ if (newShared.length) syncStream();
updateNotifyUi();
+
+ // Wire autosave only after the initial load has settled, so the seed /
+ // restore / deep-link work above doesn't churn through it. Edits to any
+ // control save; card add/remove and queue changes save via observers.
+ document.addEventListener("input", scheduleSave);
+ document.addEventListener("change", scheduleSave);
+ const cardObserver = new MutationObserver(scheduleSave);
+ for (const id of ["cities", "agents", "terms", "constraints",
+ "conversions", "optional_conversions"])
+ cardObserver.observe(document.getElementById(id), {childList: true});
+ // The solve queue: card add/remove plus the data-token that appears when a
+ // pending card is replaced by its rendered solution.
+ new MutationObserver(scheduleSave).observe(document.getElementById("output"),
+ {childList: true, subtree: true, attributes: true, attributeFilter: ["data-token"]});
+ saveSession();