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:
co-authored by
Claude Fable 5
parent
e83d70116f
commit
a1fdb37c54
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user