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>
124 lines
3.8 KiB
TypeScript
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));
|
|
}
|