// 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 { const tokens = completion.spell.tokens[completion.seqIndex]; const cells = new Set(); 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; } /** No gesture may complete more than one spell. */ export function castsConflict( a: { completion: Completion; hand: Hand }, b: { completion: Completion; hand: Hand }, turnCount: number ): boolean { const cellsA = usedCells(a.completion, a.hand, turnCount); for (const cell of usedCells(b.completion, b.hand, turnCount)) { if (cellsA.has(cell)) return true; } return false; } export function formatSequence(spell: Spell, seqIndex = 0): string { return spell.sequences[seqIndex]; }