Waving Hands: a browser duel after Bartle's 1977 game
SvelteKit 5 single-player version against a heuristic bot. The engine resolves all forty spells simultaneously as the rules describe, the ledger of gestures is the interface, and the duel is saved in the browser. Tester feedback rounds added per-hand planning, threat evidence, a result strip with one-time rule explanations, a phone layout and a structured rules page. Deploys as a static site behind Caddy. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
// 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;
|
||||
}
|
||||
|
||||
/** 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];
|
||||
}
|
||||
Reference in New Issue
Block a user