Files
wizwar6e/packages/server/src/shares.ts
T
Eric WagonerandClaude Fable 5.1 6e49fc9020 Credibility pass: comments say what the code can't, and the seams are gone
Engine: pit-rim exits live in one helper (pitRimExits) shared by the
resolver and the automaton; chargeReach names the six-stride cap; dust
sight is inDust + dustAtEnd; PushPending is exported once; the force-field
counter builds its events in one list; drawUnderSlowDeath says what it
draws under. Test groups are named for the rule they pin, not the
revision that introduced it.

Client: one smoothstep; one edgeScar per battle-scarred edge; one
board-zone rule (which also seats the caption strip on phones); the
compass names live in SIDE_NAMES; net.flash() is the one toast; MediaQuery
replaces two hand-rolled matchMedia listeners; sprites carry their sizes
as plain numbers and their CSS in one rule each; canvas helpers are
formatted like the rest of the tree. Comments that told how a cel or a
fallback used to look now say what it draws. The actions-over notice no
longer blames a pickup when slime ended the turn.

Server and ops: dataRoot() is the one data directory; headlineOf builds
its deeds without a cast; the rollup and skills read the same way the
code behaves.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jm2auWk6RP71CjaAb4FMoG
2026-09-16 00:21:34 -04:00

59 lines
1.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Share links: a minted slug names one turn of one game, and anyone
// holding it may watch that turn — spectator-redacted, nothing else of
// the game visible. Shares persist beside the rooms and survive restarts.
import { appendFileSync, existsSync, readFileSync } from "node:fs";
import { join } from "node:path";
import { randomInt } from "node:crypto";
import { dataRoot } from "./store";
export interface Share {
id: string;
roomId: string;
turn: number;
createdAt: string;
}
const SHARE_ALPHABET = "abcdefghjkmnpqrstuvwxyz23456789";
const SHARE_ID_LENGTH = 10; // 31^10 ≈ 8×10^14 — unguessable, typeable
const shares = new Map<string, Share>();
function sharesFile(): string {
return join(dataRoot(), "shares.jsonl");
}
export function loadShares(): void {
const file = sharesFile();
if (!existsSync(file)) return;
for (const line of readFileSync(file, "utf8").trim().split("\n")) {
if (!line) continue;
try {
const s = JSON.parse(line) as Share;
if (s.id) shares.set(s.id, s);
} catch {
// A torn tail line loses one share, never the server.
}
}
}
export function mintShare(roomId: string, turn: number): Share {
// One link per (room, turn): sharing the same turn twice hands back
// the same slug, so a re-share never splits an audience.
for (const s of shares.values()) {
if (s.roomId === roomId && s.turn === turn) return s;
}
let id = "";
for (let i = 0; i < SHARE_ID_LENGTH; i++) {
id += SHARE_ALPHABET[randomInt(SHARE_ALPHABET.length)];
}
const share: Share = { id, roomId, turn, createdAt: new Date().toISOString() };
shares.set(id, share);
appendFileSync(sharesFile(), JSON.stringify(share) + "\n", "utf8");
return share;
}
export function getShare(id: string): Share | undefined {
return shares.get(id);
}