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
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
85690f0e23
commit
6e49fc9020
@@ -204,9 +204,10 @@ function safeBase(rawHost: string, rawProto: string): string {
|
||||
* carried home, a blow that landed, a spell cast — before the bare fact
|
||||
* of whose turn it was. */
|
||||
function headlineOf(data: ShareData): string | null {
|
||||
// A card retired since the share was written keeps its id as its name.
|
||||
const name = (id: string | null) => { if (!id) return "a spell"; try { return cardDef(id).name; } catch { return id; } };
|
||||
let best: { rank: number; text: string } | null = null;
|
||||
const offer = (rank: number, text: string) => { if (!best || rank > best.rank) best = { rank, text }; };
|
||||
const deeds: { rank: number; text: string }[] = [];
|
||||
const offer = (rank: number, text: string) => { deeds.push({ rank, text }); };
|
||||
for (const step of data.steps) {
|
||||
for (const e of step.events) {
|
||||
switch (e.type) {
|
||||
@@ -225,7 +226,9 @@ function headlineOf(data: ShareData): string | null {
|
||||
}
|
||||
}
|
||||
}
|
||||
return best ? (best as { text: string }).text : null;
|
||||
let best: { rank: number; text: string } | null = null;
|
||||
for (const d of deeds) if (!best || d.rank > best.rank) best = d;
|
||||
return best?.text ?? null;
|
||||
}
|
||||
|
||||
function shareHtml(id: string, data: ShareData, rawHost: string, rawProto: string): string {
|
||||
|
||||
@@ -165,7 +165,7 @@ export function evictIdleRooms(hasSockets: (roomId: string) => boolean): number
|
||||
evicted++;
|
||||
try {
|
||||
const stat = ledgerStat(id);
|
||||
if (stat) writeStubFile(id, stat.bytes, { ...stub, tokens: Object.fromEntries(stub.tokens) });
|
||||
if (stat) writeStubFile(id, stat.bytes, toStored(stub));
|
||||
} catch (e) {
|
||||
console.error(`could not write stub for ${id}:`, e);
|
||||
}
|
||||
@@ -185,6 +185,8 @@ interface RoomStub {
|
||||
const sleepingStubs = new Map<string, RoomStub>();
|
||||
/** A stub as it rests on disk — the token map spelled as an object. */
|
||||
type StoredStub = Omit<RoomStub, "tokens"> & { tokens: Record<PlayerId, string> };
|
||||
const toStored = (stub: RoomStub): StoredStub => ({ ...stub, tokens: Object.fromEntries(stub.tokens) });
|
||||
const fromStored = (stored: StoredStub): RoomStub => ({ ...stored, tokens: new Map(Object.entries(stored.tokens)) });
|
||||
|
||||
function stubOf(room: Room): RoomStub {
|
||||
const { active, turnHolder, waitKind } = turnFacts(room.state);
|
||||
@@ -797,7 +799,7 @@ export function loadPersistedRooms(): void {
|
||||
if (!stat) continue;
|
||||
const stored = readStubFile<StoredStub>(id);
|
||||
if (stored && stored.bytes === stat.bytes) {
|
||||
sleepingStubs.set(id, { ...stored.stub, tokens: new Map(Object.entries(stored.stub.tokens)) });
|
||||
sleepingStubs.set(id, fromStored(stored.stub));
|
||||
asleep++;
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import { appendFileSync, existsSync, readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { randomInt } from "node:crypto";
|
||||
import { statsDir } from "./store";
|
||||
import { dataRoot } from "./store";
|
||||
|
||||
export interface Share {
|
||||
id: string;
|
||||
@@ -20,7 +20,7 @@ const SHARE_ID_LENGTH = 10; // 31^10 ≈ 8×10^14 — unguessable, typeable
|
||||
const shares = new Map<string, Share>();
|
||||
|
||||
function sharesFile(): string {
|
||||
return join(statsDir(), "shares.jsonl");
|
||||
return join(dataRoot(), "shares.jsonl");
|
||||
}
|
||||
|
||||
export function loadShares(): void {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
import { readFileSync, renameSync, writeFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { statsDir } from "./store";
|
||||
import { dataRoot } from "./store";
|
||||
import type { Room } from "./rooms";
|
||||
|
||||
export interface EngagementStats {
|
||||
@@ -35,7 +35,7 @@ interface StatsFile extends Omit<EngagementStats, "wizardsSeated"> {
|
||||
roomStages: Record<string, "created" | "started" | "finished">;
|
||||
}
|
||||
|
||||
const FILE = () => join(statsDir(), "stats.json");
|
||||
const FILE = () => join(dataRoot(), "stats.json");
|
||||
|
||||
let data: StatsFile = {
|
||||
gamesCreated: 0, gamesStarted: 0, gamesFinished: 0,
|
||||
|
||||
@@ -86,8 +86,10 @@ function fileFor(roomId: string): string {
|
||||
return join(DATA_DIR, `${roomId}.jsonl`);
|
||||
}
|
||||
|
||||
/** Directory holding stats.json — the parent of the rooms dir. */
|
||||
export function statsDir(): string {
|
||||
/** The data root — the parent of the rooms dir — where everything that
|
||||
* is not a room ledger lives: stats, feedback, stubs, the graveyard, the
|
||||
* clip vault. */
|
||||
export function dataRoot(): string {
|
||||
return join(DATA_DIR, "..");
|
||||
}
|
||||
|
||||
@@ -149,7 +151,7 @@ export function ledgerStat(roomId: string): { bytes: number; mtimeMs: number } |
|
||||
// ledger only ever grows, so at boot a stub whose size still matches is
|
||||
// the room's whole truth and the room stays asleep, unreplayed.
|
||||
|
||||
const stubDir = () => join(DATA_DIR, "..", "stubs");
|
||||
const stubDir = () => join(dataRoot(), "stubs");
|
||||
function stubFor(roomId: string): string {
|
||||
if (!safeRoomId(roomId)) throw new Error(`unsafe room id: ${JSON.stringify(roomId)}`);
|
||||
return join(stubDir(), `${roomId}.json`);
|
||||
@@ -168,7 +170,7 @@ export function readStubFile<T>(roomId: string): { bytes: number; stub: T } | nu
|
||||
}
|
||||
}
|
||||
|
||||
export function writeStubFile(roomId: string, bytes: number, stub: unknown): void {
|
||||
export function writeStubFile<T>(roomId: string, bytes: number, stub: T): void {
|
||||
const file = stubFor(roomId);
|
||||
mkdirSync(stubDir(), { recursive: true });
|
||||
writeFileSync(file + ".tmp", JSON.stringify({ bytes, stub }), "utf8");
|
||||
@@ -182,7 +184,7 @@ export function writeStubFile(roomId: string, bytes: number, stub: unknown): voi
|
||||
* timestamp. A report line may carry fields (deckRev, player context)
|
||||
* that only the operator's raw read uses; readFeedback keeps the
|
||||
* player-facing subset. */
|
||||
const feedbackFile = () => join(DATA_DIR, "..", "feedback.jsonl");
|
||||
const feedbackFile = () => join(dataRoot(), "feedback.jsonl");
|
||||
|
||||
export function appendFeedback(entry: Record<string, unknown>): void {
|
||||
ensureDataDir();
|
||||
@@ -239,7 +241,7 @@ export function readFeedback(): FeedbackReport[] {
|
||||
export function archiveRoomFile(roomId: string): void {
|
||||
const src = fileFor(roomId);
|
||||
if (!existsSync(src)) return;
|
||||
const graveyard = join(DATA_DIR, "..", "rooms-abandoned");
|
||||
const graveyard = join(dataRoot(), "rooms-abandoned");
|
||||
mkdirSync(graveyard, { recursive: true });
|
||||
renameSync(src, join(graveyard, `${roomId}.${Date.now()}.jsonl`));
|
||||
}
|
||||
@@ -260,7 +262,7 @@ export interface ClipMeta {
|
||||
height?: number;
|
||||
}
|
||||
|
||||
const clipsDir = () => join(DATA_DIR, "..", "clips");
|
||||
const clipsDir = () => join(dataRoot(), "clips");
|
||||
|
||||
/** A clip's name, which is also its file stem: no separators, no dots,
|
||||
* so a name can never name a path. */
|
||||
|
||||
Reference in New Issue
Block a user