clone

a browser grid puzzle game where you push words around to rewrite the rules of each level

Separate index progress storage from map navigation

Dami committed 308450f
commit
308450f03f151b1502baa15e95c6760d74aab45c
parent
b772422c8c1aabda6e5054b1178a0577eb26a7ad
author
Dami <dami.etoile@gmail.com>
2 files changed, +86 -80
Msrc/scenes/map.js87++----------------------------
Asrc/scenes/progress.js79+++++++++++++++++++++++++++
M src/scenes/map.js +7 -80
@@ -5,90 +5,17 @@
55 * catalog's stable pack/level records into serpentine sheets and provides the66 * small amount of progress and cursor state the scene needs.77 */8-9-export const INDEX_COLUMNS = 5;10-export const INDEX_ROWS = 3;11-export const INDEX_NODES_PER_SHEET = INDEX_COLUMNS * INDEX_ROWS;12-export const PROGRESS_PREFIX = 'miau.index.progress/';13-14-const clone = value => typeof structuredClone === 'function'15- ? structuredClone(value)16- : JSON.parse(JSON.stringify(value));8+import { cloneData } from '../data/clone.js';9+export { PROGRESS_PREFIX, getProgressKey, createProgressStore } from './progress.js';1710 1811 function nonEmpty(value, label) {1912 if (typeof value !== 'string' || value.length === 0) throw new TypeError(`${label} must be a non-empty string`);2013 return value;2114 }2215 23-function progressKey(packId, levelId) {24- return `${nonEmpty(packId, 'packId')}/${nonEmpty(levelId, 'levelId')}`;25-}26-27-/** The storage key used for one proof. It is intentionally human-readable. */28-export function getProgressKey(packId, levelId) {29- return `${PROGRESS_PREFIX}${progressKey(packId, levelId)}`;30-}31-32-function storageWorks(storage) {33- return storage && typeof storage.getItem === 'function'34- && typeof storage.setItem === 'function' && typeof storage.removeItem === 'function';35-}36-37-/**38- * A localStorage backed solved set. A memory fallback keeps private browsing39- * and server side rendering usable without changing the scene API.40- */41-export function createProgressStore(storage) {42- let candidate = storage;43- if (candidate === undefined) {44- try { candidate = globalThis.localStorage; } catch { candidate = null; }45- }46- const backing = storageWorks(candidate) ? candidate : null;47- const memory = new Set();48- const read = key => {49- if (memory.has(key)) return true;50- if (!backing) return false;51- try {52- const solved = backing.getItem(key) === '1';53- if (solved) memory.add(key);54- return solved;55- } catch { return false; }56- };57- const write = (key, solved) => {58- if (solved) memory.add(key); else memory.delete(key);59- if (!backing) return;60- try {61- if (solved) backing.setItem(key, '1');62- else backing.removeItem(key);63- } catch { /* A full or blocked store still has the memory fallback. */ }64- };65- return Object.freeze({66- key: getProgressKey,67- isSolved(packId, levelId) { return read(getProgressKey(packId, levelId)); },68- get(packId, levelId) { return read(getProgressKey(packId, levelId)); },69- setSolved(packId, levelId, solved = true) {70- const key = getProgressKey(packId, levelId);71- write(key, Boolean(solved));72- return Boolean(solved);73- },74- markSolved(packId, levelId) { write(getProgressKey(packId, levelId), true); return true; },75- clear(packId, levelId) { write(getProgressKey(packId, levelId), false); },76- clearAll() {77- for (const key of [...memory]) write(key, false);78- if (!backing) return;79- try {80- const keys = [];81- for (let index = 0; index < backing.length; index++) {82- const key = backing.key(index);83- if (key?.startsWith(PROGRESS_PREFIX)) keys.push(key);84- }85- keys.forEach(key => backing.removeItem(key));86- } catch { /* Clearing memory remains safe when storage is unavailable. */ }87- },88- snapshot() { return [...memory].map(key => key.slice(PROGRESS_PREFIX.length)); },89- });90-}91-16+export const INDEX_COLUMNS = 5;17+export const INDEX_ROWS = 3;18+export const INDEX_NODES_PER_SHEET = INDEX_COLUMNS * INDEX_ROWS;9219 function levelEntries(source) {9320 if (Array.isArray(source)) return source;9421 if (source && typeof source.getLevels === 'function') return source.getLevels();
@@ -164,7 +91,7 @@ function makeSheet(group, sheetEntries, sheetIndex, globalSheetIndex, allSheetId
16491 column: coordinate.column,16592 x: coordinate.column,16693 y: coordinate.row,167- level: clone(entry.level),94+ level: cloneData(entry.level),16895 };16996 });17097 for (const node of nodes) {
@@ -318,7 +245,7 @@ export function createIndexNavigator(layout, options = {}) {
318245 const node = current();319246 if (!node) return { ok: false, reason: 'empty' };320247 if (node.locked) return { ok: false, reason: 'locked', node };321- return { ok: true, packId: node.packId, levelId: node.levelId, level: clone(node.level), node };248+ return { ok: true, packId: node.packId, levelId: node.levelId, level: cloneData(node.level), node };322249 }323250 return {324251 get current() { return state(); },
A src/scenes/progress.js +79 -0
@@ -0,0 +1,79 @@
1+/** Progress persistence for the INDEX scene. */2+3+export const PROGRESS_PREFIX = 'miau.index.progress/';4+5+function nonEmpty(value, label) {6+ if (typeof value !== 'string' || value.length === 0) {7+ throw new TypeError(`${label} must be a non-empty string`);8+ }9+ return value;10+}11+12+function progressKey(packId, levelId) {13+ return `${nonEmpty(packId, 'packId')}/${nonEmpty(levelId, 'levelId')}`;14+}15+16+/** The storage key used for one proof. It is intentionally human-readable. */17+export function getProgressKey(packId, levelId) {18+ return `${PROGRESS_PREFIX}${progressKey(packId, levelId)}`;19+}20+21+function storageWorks(storage) {22+ return storage && typeof storage.getItem === 'function'23+ && typeof storage.setItem === 'function' && typeof storage.removeItem === 'function';24+}25+26+/**27+ * A localStorage backed solved set. A memory fallback keeps private browsing28+ * and server side rendering usable without changing the scene API.29+ */30+export function createProgressStore(storage) {31+ let candidate = storage;32+ if (candidate === undefined) {33+ try { candidate = globalThis.localStorage; } catch { candidate = null; }34+ }35+ const backing = storageWorks(candidate) ? candidate : null;36+ const memory = new Set();37+ const read = key => {38+ if (memory.has(key)) return true;39+ if (!backing) return false;40+ try {41+ const solved = backing.getItem(key) === '1';42+ if (solved) memory.add(key);43+ return solved;44+ } catch { return false; }45+ };46+ const write = (key, solved) => {47+ if (solved) memory.add(key); else memory.delete(key);48+ if (!backing) return;49+ try {50+ if (solved) backing.setItem(key, '1');51+ else backing.removeItem(key);52+ } catch { /* A full or blocked store still has the memory fallback. */ }53+ };54+ return Object.freeze({55+ key: getProgressKey,56+ isSolved(packId, levelId) { return read(getProgressKey(packId, levelId)); },57+ get(packId, levelId) { return read(getProgressKey(packId, levelId)); },58+ setSolved(packId, levelId, solved = true) {59+ const key = getProgressKey(packId, levelId);60+ write(key, Boolean(solved));61+ return Boolean(solved);62+ },63+ markSolved(packId, levelId) { write(getProgressKey(packId, levelId), true); return true; },64+ clear(packId, levelId) { write(getProgressKey(packId, levelId), false); },65+ clearAll() {66+ for (const key of [...memory]) write(key, false);67+ if (!backing) return;68+ try {69+ const keys = [];70+ for (let index = 0; index < backing.length; index++) {71+ const key = backing.key(index);72+ if (key?.startsWith(PROGRESS_PREFIX)) keys.push(key);73+ }74+ keys.forEach(key => backing.removeItem(key));75+ } catch { /* Clearing memory remains safe when storage is unavailable. */ }76+ },77+ snapshot() { return [...memory].map(key => key.slice(PROGRESS_PREFIX.length)); },78+ });79+}