clone

an endless marble-race generator in the browser; the url is the seed, so every track can be shared

main
marbles / src / client / seed.ts
seed.ts 1.8K
1/**2 * Seed ↔ URL hash binding.3 *4 * The hash is the single source of truth: everything that changes the seed goes5 * through `location.hash`, and `hashchange` is the ONE regeneration entry point.6 * That makes back/forward navigation work for free.7 */89const SEED_LEN = 10;1011export function mintSeed(): string {12 const bytes = new Uint32Array(SEED_LEN);13 crypto.getRandomValues(bytes);14 let out = "";15 for (let i = 0; i < SEED_LEN; i++) {16 out += (bytes[i] % 36).toString(36);17 }18 return out;19}2021function readHash(): string {22 try {23 return decodeURIComponent(location.hash.slice(1));24 } catch {25 return location.hash.slice(1);26 }27}2829/**30 * Wire the hash to `onSeed`. Fires once immediately (minting + `replaceState`31 * if the hash is empty, so the first load doesn't leave a blank history entry),32 * then once per genuine hash change.33 */34export function initSeed(onSeed: (seed: string) => void): void {35 let current: string | null = null;3637 const emit = (seed: string) => {38 if (seed === current) return; // echo guard39 current = seed;40 // The title carries the seed so history entries read as tracks.41 document.title = `marbles - #${seed}`;42 onSeed(seed);43 };4445 addEventListener("hashchange", () => {46 const seed = readHash();47 if (seed === "") {48 // Someone cleared the hash — mint rather than regenerate nothing.49 newSeed();50 return;51 }52 emit(seed);53 });5455 const initial = readHash();56 if (initial === "") {57 const seed = mintSeed();58 history.replaceState(null, "", "#" + seed);59 emit(seed);60 } else {61 emit(initial);62 }63}6465/** Mint a fresh seed and push it — the hashchange does the regeneration. */66export function newSeed(): string {67 const seed = mintSeed();68 location.hash = seed;69 return seed;70}