Credibility pass: the splice marks sanded from the rev-11 batch

The extracted rebuildRoom loop reflowed to the file's indent and its
doc comments unstacked; liftIdiotIfSatisfied moves below idiotBlocked
so each keeps its own comment; the pickup's duplicated owner lookup
folds to one; the cloned grab-refusal chain hoists to grabRefused; a
one-off #8a7a5c rejoins the #8a7a5e palette; an inert overflow-x line
and a duplicate import go; span 6 and the two cache strategies say why;
the reversed walking-dead test moves beside its biting sibling. Also:
the replay harness now honors lobby kicks, which VVZU (five bots
seated, two kicked) was the first production ledger to exercise.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015RCWSTnb1KYTPyL4GmhGnF
This commit is contained in:
Eric Wagoner
2026-08-30 13:58:02 -04:00
co-authored by Claude Fable 5
parent e83d70116f
commit a1fdb37c54
8 changed files with 96 additions and 87 deletions
+4 -1
View File
@@ -225,7 +225,10 @@ function inviteHtml(room: Room, rawHost: string, rawProto: string): string {
];
return ogPage(metas);
}
/** Rendered share cards, by share id (immutable once minted). */
/** Rendered share cards, by share id. Shares are immutable, so unlike the
* TTL'd shareCache above this never invalidates — it only rotates out the
* oldest entry when full. */
const ogPngCache = new Map<string, Buffer>();
const OG_PNG_CACHE_MAX = 200;
+37 -37
View File
@@ -651,7 +651,6 @@ export function claimTransferCode(code: string): { roomId: string; name: PlayerI
return { roomId: t.roomId, name: t.name, token: t.token };
}
/** Rebuild every persisted room by replaying its file. */
/** Rebuild one room from its ledger lines — the seed plus the log IS the
* game. Returns null for an abandoned room; throws on a corrupt ledger. */
function rebuildRoom(id: string, lines: RoomLine[]): Room | null {
@@ -676,46 +675,47 @@ function rebuildRoom(id: string, lines: RoomLine[]): Room | null {
};
if (lines.some((l) => l.kind === "abandon")) return null;
for (const line of lines.slice(1)) {
if (line.kind === "kick") {
room.players = room.players.filter((p) => p !== line.name);
room.tokens.delete(line.name);
room.bots.delete(line.name);
room.colorChoices.delete(line.name);
} else if (line.kind === "join") {
if (line.bot) {
room.players.push(line.name);
room.bots.set(line.name, {
style: (line.style as AutomatonStyle) ?? "hunter",
secret: line.secret === true,
// Tier-less join lines predate tiers, when every automaton
// played the full repertoire — not the "adept" lobby default.
tier: (line.tier as AutomatonTier) ?? "archmage",
});
} else {
const joinHash = line.tokenHash ?? (line.token ? hashToken(line.token) : null);
if (!joinHash) throw new Error("join line has no token");
room.players.push(line.name);
room.tokens.set(line.name, joinHash);
}
} else if (line.kind === "start") {
const r = startInMemory(room, line.expansion, line.colors, line.deckRev);
if ("error" in r) throw new Error(`replay start failed: ${r.error}`);
} else if (line.kind === "chat") {
// File order preserves the interleaving with commands.
room.chat.push({ player: line.player, text: line.text, at: line.at });
room.events.push({ type: "tableTalk", player: line.player, text: line.text });
} else if (line.kind === "command") {
if (!room.state) throw new Error("command before start in log");
const result = applyCommand(room.state, line.playerId, line.command as Command);
if (!result.ok) throw new Error(`replay failed at seq ${line.seq}: ${result.error}`);
room.state = result.state;
room.log.push({ seq: line.seq, playerId: line.playerId, command: line.command as Command, at: line.at });
room.events.push(...result.events);
}
if (line.kind === "kick") {
room.players = room.players.filter((p) => p !== line.name);
room.tokens.delete(line.name);
room.bots.delete(line.name);
room.colorChoices.delete(line.name);
} else if (line.kind === "join") {
if (line.bot) {
room.players.push(line.name);
room.bots.set(line.name, {
style: (line.style as AutomatonStyle) ?? "hunter",
secret: line.secret === true,
// Tier-less join lines predate tiers, when every automaton
// played the full repertoire — not the "adept" lobby default.
tier: (line.tier as AutomatonTier) ?? "archmage",
});
} else {
const joinHash = line.tokenHash ?? (line.token ? hashToken(line.token) : null);
if (!joinHash) throw new Error("join line has no token");
room.players.push(line.name);
room.tokens.set(line.name, joinHash);
}
} else if (line.kind === "start") {
const r = startInMemory(room, line.expansion, line.colors, line.deckRev);
if ("error" in r) throw new Error(`replay start failed: ${r.error}`);
} else if (line.kind === "chat") {
// File order preserves the interleaving with commands.
room.chat.push({ player: line.player, text: line.text, at: line.at });
room.events.push({ type: "tableTalk", player: line.player, text: line.text });
} else if (line.kind === "command") {
if (!room.state) throw new Error("command before start in log");
const result = applyCommand(room.state, line.playerId, line.command as Command);
if (!result.ok) throw new Error(`replay failed at seq ${line.seq}: ${result.error}`);
room.state = result.state;
room.log.push({ seq: line.seq, playerId: line.playerId, command: line.command as Command, at: line.at });
room.events.push(...result.events);
}
}
return room;
}
/** Rebuild every persisted room by replaying its file. */
export function loadPersistedRooms(): void {
ensureDataDir();
let restored = 0;