Files
waving-hands/src/lib/game/gestures.ts
T
Eric WagonerandClaude Fable 5.1 5de352db43 Credibility pass: one conflict check, one hostile list, one set of chrome
Duplicated logic and styling that had drifted apart is unified: the
gesture-sharing check, the hostile spell list, the enchanted test, the
plan limits, and the shared button, link, field and disabled styles.
Dead plumbing is gone: the always-true implemented flag, the unread
cast length, an unreachable guard, an impossible time-stop condition,
the unused sequence formatter and utility class, the template
placeholder. The counter-spell tests now put a counter-spell in front
of a spell, the test helpers live in one file, and the resolver's
sections are numbered in order. The deploy script's cache headers now
do what its comment says, and the README describes the screen rather
than listing features in the order they arrived.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-22 16:37:54 -04:00

124 lines
3.8 KiB
TypeScript

// Gesture sequence matching: which spells a hand has completed, which it is
// working toward, and whether two chosen spells would reuse the same gesture.
import { SPELLS, type Gesture, type Hand, type Spell, type Token } from './spells';
/** One turn of gestures as seen from one hand. */
export interface HandTurn {
own: Gesture;
other: Gesture;
}
export interface TurnGestures {
left: Gesture;
right: Gesture;
}
export interface Completion {
spell: Spell;
seqIndex: number;
/** Number of turns the sequence spans. */
length: number;
}
export function handSequence(history: TurnGestures[], hand: Hand): HandTurn[] {
return history.map((t) =>
hand === 'left' ? { own: t.left, other: t.right } : { own: t.right, other: t.left }
);
}
export function tokenMatches(token: Token, turn: HandTurn): boolean {
if (token.both) return turn.own === token.gesture && turn.other === token.gesture;
return turn.own === token.gesture;
}
/**
* True when the first `count` tokens of a spell sequence match the most
* recent gestures of the hand, and none of those gestures fall before
* `start` (the first usable turn after an anti-spell).
*/
export function prefixMatchesTail(
tokens: Token[],
count: number,
seq: HandTurn[],
start: number
): boolean {
if (count === 0) return true;
const from = seq.length - count;
if (from < start) return false;
for (let i = 0; i < count; i++) {
if (!tokenMatches(tokens[i], seq[from + i])) return false;
}
return true;
}
/** Every spell the hand has just finished, longest first. */
export function completedSpells(seq: HandTurn[], start: number): Completion[] {
const out: Completion[] = [];
for (const spell of SPELLS) {
spell.tokens.forEach((tokens, seqIndex) => {
if (prefixMatchesTail(tokens, tokens.length, seq, start)) {
out.push({ spell, seqIndex, length: tokens.length });
}
});
}
return out.sort((a, b) => b.length - a.length);
}
/**
* Spells the hand could finish `turnsAway` turns from now if it keeps to the
* sequence. With turnsAway = 1 these are the spells one more gesture completes.
*/
export function completableIn(
seq: HandTurn[],
start: number,
turnsAway: number
): { spell: Spell; seqIndex: number; remaining: Token[] }[] {
const out: { spell: Spell; seqIndex: number; remaining: Token[] }[] = [];
for (const spell of SPELLS) {
spell.tokens.forEach((tokens, seqIndex) => {
const done = tokens.length - turnsAway;
if (done < 1) return;
if (prefixMatchesTail(tokens, done, seq, start)) {
out.push({ spell, seqIndex, remaining: tokens.slice(done) });
}
});
}
return out;
}
/** Longest prefix of `tokens` the hand's latest gestures satisfy (strictly shorter than the spell). */
export function progressToward(tokens: Token[], seq: HandTurn[], start: number): number {
for (let k = tokens.length - 1; k >= 1; k--) {
if (prefixMatchesTail(tokens, k, seq, start)) return k;
}
return 0;
}
/** The (hand, turn) cells a completed spell consumes; two-handed tokens use both hands. */
export function usedCells(completion: Completion, hand: Hand, turnCount: number): Set<string> {
const tokens = completion.spell.tokens[completion.seqIndex];
const cells = new Set<string>();
const from = turnCount - tokens.length;
tokens.forEach((token, i) => {
const turn = from + i;
cells.add(`${hand}:${turn}`);
if (token.both) cells.add(`${hand === 'left' ? 'right' : 'left'}:${turn}`);
});
return cells;
}
export function cellsOverlap(a: Set<string>, b: Set<string>): boolean {
for (const cell of b) if (a.has(cell)) return true;
return false;
}
/** No gesture may complete more than one spell. */
export function castsConflict(
a: { completion: Completion; hand: Hand },
b: { completion: Completion; hand: Hand },
turnCount: number
): boolean {
return cellsOverlap(usedCells(a.completion, a.hand, turnCount), usedCells(b.completion, b.hand, turnCount));
}