Credibility pass: one teleport step, one bell, one step type, and the comments that argued are gone

Since 6e49fc9. Engine: the teleport BFS in the view and the engine's
wallIgnoringDistance share teleportStep; the counter trail is weighed
between steps instead of on six arms; the bush attack takes its
adjacency from SIDES like the wall does; a dead redirected clause, a
one-member set, an identity ternary, and a stray try/catch are gone;
two tests now assert what their names promise. Server: KEEPER and
MAX_SEATS live in rooms.ts, the three Sentry bells ring through one
helper, the unread reply field is dropped, actingSeat comes from the
engine, and the protocol header names every message. Web: ReplayStep
is one type, cardName one helper, the say box and the reference section
one snippet each, the keeper's name arrives from the server rather than
the source, and the lifesaver ruling that ruled nothing is out.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jm2auWk6RP71CjaAb4FMoG
This commit is contained in:
Eric WagonerandClaude Fable 5.1 committed 2026-09-22 09:56:49 -04:00
1 parent c4a555f8df
commit 66b5c06f17
21 files changed
+245 -239

No files matched your search

+12 -7
View File
@@ -8,6 +8,15 @@ import { RULES_SECTIONS } from "./rules";
import { RULEBOOK_BASE, RULEBOOK_EXPANSION } from "./rulebook";
import { FAQ_GENERAL } from "./faq-general";
/** A card's printed name, or the id itself for one the deck does not know. */
export function cardName(id: string): string {
try { return cardDef(id).name; } catch { return id; }
}
function escapeRe(text: string): string {
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
/** A stable anchor from a title: "Line of Sight (L.O.S.)" → "line-of-sight-l-o-s". */
export function slug(title: string): string {
return title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
@@ -132,13 +141,13 @@ export function mentionedCards(cardId: string): CardDef[] {
const def = cardDef(cardId);
let text = def.text ?? "";
if (!text) return [];
text = text.replace(new RegExp(`\\b${def.name.toUpperCase().replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`, "g"), " ");
text = text.replace(new RegExp(`\\b${escapeRe(def.name.toUpperCase())}\\b`, "g"), " ");
const found: CardDef[] = [];
const candidates = CARD_POOL
.filter((d) => d.id !== cardId && d.name.length >= 4 && d.cardType !== "number")
.sort((a, b) => b.name.length - a.name.length);
for (const d of candidates) {
const re = new RegExp(`\\b${d.name.toUpperCase().replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`);
const re = new RegExp(`\\b${escapeRe(d.name.toUpperCase())}\\b`);
if (re.test(text)) {
found.push(d);
text = text.replace(new RegExp(re.source, "g"), " ");
@@ -282,10 +291,6 @@ export const HOUSE_RULINGS: HouseRuling[] = [
id: "waterwall", title: "The waterwall's wave", cards: ["waterwall"],
body: ["A waterwall's wave names its victims before it pushes any of them, so no wizard is caught twice by the same wave. One square into a wall costs one point."],
},
{
id: "lifesaver", title: "Lifesaver", cards: ["lifesaver"],
body: ["A wizard holding LIFESAVER is not eliminated for losing both treasures, exactly as the card promises."],
},
{
id: "force-field", title: "Force Field", cards: ["force-field"],
body: ["FORCE FIELD, after stopping the spell, stands until the end of the opponent's turn: they may not enter its caster's square, nor cast on or past them, on every side — where the card says one side, this table gives all four."],
@@ -302,7 +307,7 @@ export function searchRulings(query: string): HouseRuling[] {
const words = query.toLowerCase().split(/\s+/).filter(Boolean);
if (words.length === 0) return HOUSE_RULINGS;
return HOUSE_RULINGS.filter((r) => {
const names = r.cards.map((id) => { try { return cardDef(id).name; } catch { return id; } });
const names = r.cards.map(cardName);
const hay = `${r.title}\n${r.body.join("\n")}\n${names.join("\n")}`.toLowerCase();
return words.every((w) => hay.includes(w));
});