SOURCE / PINNED RELEASE
Made of little things.
Conway's Game of Life
- Release
- 230955df1900…
- Author-recorded commit
- c08196c2b050…
- License
- LICENSE
- Author’s source reference
- nostr://npub16nzc4ql748r2kv9202rgzrt0fmtk4n3tzsjl86yf6ty9agqe9u0qjcc7hc/wss%3A%2F%2Fgit.napplet.soy%2F/n-644c45e22c7
Archive hash verified: 2627ce678935a213…. The source-to-build association is the author’s claim; it has not been independently rebuilt.
// Save wire (Phase 7, spec §7 / ratified default D6): the painted board,
// speed setting and generation counter persist through the host NAP-STORAGE
// capability. Browser storage APIs (the window-local kind) are banned for
// durable state by the platform contract — the host proxies storage over the
// napplet wire via `@napplet/sdk`.
//
// Degradation law (tested pair in tests/browser.test.mjs): every call is
// gated by runtimeHasDomain('storage') AND wrapped in try/catch, because a
// host can advertise the namespace without working methods (conformance
// harness behavior, docs/napplet-space.md). A missing or broken storage
// reads as "no save": the game keeps playing session-only and never throws
// at window level.
import { storage } from '@napplet/sdk';
import { decodeRle } from './engine.js';
import { runtimeHasDomain } from './domain-availability.js';
/** Save-key prefix per build plan Phase 7. */
export const SAVE_KEY_BOARD = 'game-of-life:board';
export const SAVE_KEY_SPEED = 'game-of-life:speed';
export const SAVE_KEY_GENERATION = 'game-of-life:generation';
/** Generation-rate range (D3) — clamps loaded speeds to the legal band. */
export const SPEED_MIN = 1;
export const SPEED_MAX = 30;
export interface SavedBoard {
/** Full-board RLE exactly as encodeRle emits it (spec §7 wire shape). */
boardRle: string;
speed: number;
generation: number;
}
/** Whether THIS load has a storage domain injected by the host. */
export function storageAvailable(): boolean {
return runtimeHasDomain('storage');
}
/**
* Read the save triple at boot. Returns null when there is nothing to
* restore: no storage domain, no prior save, a failing read, or a corrupt
* board encoding. Numeric fields clamp into their legal bands rather than
* inheriting junk.
*/
export async function loadSavedBoard(
w: number,
h: number,
defaultSpeed: number,
): Promise<SavedBoard | null> {
if (!storageAvailable()) return null;
try {
const [boardRle, speedRaw, genRaw] = await Promise.all([
storage.getItem(SAVE_KEY_BOARD),
storage.getItem(SAVE_KEY_SPEED),
storage.getItem(SAVE_KEY_GENERATION),
]);
if (boardRle === null && speedRaw === null && genRaw === null) return null;
if (typeof boardRle !== 'string') return null;
const d = decodeRle(boardRle);
// Wire-shape law: saved boards are full-board encodes. Anything that
// does not decode to exactly the board dimensions is junk — defaults win.
if (d.w !== w || d.h !== h) return null;
const parsed = Math.round(Number(speedRaw));
const speed = speedRaw === null || !Number.isFinite(parsed)
? defaultSpeed
: Math.min(SPEED_MAX, Math.max(SPEED_MIN, parsed));
const generation = Math.max(0, Math.trunc(Number(genRaw)) || 0);
return { boardRle, speed, generation };
} catch {
return null; // a storage namespace that exists but cannot serve reads
}
}
/** Write the save triple. Returns false when storage is absent or failed. */
export async function saveBoard(
boardRle: string,
speed: number,
generation: number,
): Promise<boolean> {
if (!storageAvailable()) return false;
try {
await storage.setItem(SAVE_KEY_BOARD, boardRle);
await storage.setItem(SAVE_KEY_SPEED, String(speed));
await storage.setItem(SAVE_KEY_GENERATION, String(generation));
return true;
} catch {
return false; // host storage broke mid-play; session-only continues
}
}
