Easier to make a move, and plainer words around the game

From feedback on the BoardGameGeek thread. The hands are framed and
labelled as yours, so they no longer read as sitting under your
opponent's columns. A move can be typed: the first letter goes to the
left hand, the second to the right, K stabs, a dash rests, Backspace
takes one back and Enter seals. The chosen pair, what it will do, any
warning and the button now sit in one row that stays in view. A clap
with one hand is warned of before the turn is sealed. The button says
what the player does: Seal the turn. The chronicle reads oldest first
and scrolls to the newest line. Headings, tabs and notes lose the
label-and-colon construction and the abstract headings; the chronicle's
imagery stays.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141G6xqLeNRYEtviLWSB5Up
This commit is contained in:
Eric WagonerandClaude Opus 5.5 committed 2026-10-10 18:29:49 -04:00
1 parent db682814ea
commit ee630276b3
9 files changed
+213 -42

No files matched your search

+29
View File
@@ -696,6 +696,35 @@ export class Duel {
return list;
}
/** The hands a typed gesture fills, in the order they are filled: left then right, and the second pair after the first when hastened. */
private typingSlots(): { set: SetName; hand: Hand }[] {
return this.activeSets.flatMap((set) =>
HANDS.filter((h) => !this.forcedFor(set, h) && this.charmedHand !== h).map((hand) => ({ set, hand }))
);
}
/** A gesture typed on the keyboard goes to the first open hand. Returns false when no hand can take it. */
typeGesture(gesture: Gesture): boolean {
const slot = this.typingSlots().find((s) => this.sets[s.set][s.hand].gesture === null);
if (!slot || !this.allowed[slot.hand].includes(gesture)) return false;
this.sets[slot.set][slot.hand].gesture = gesture;
this.syncAll();
return true;
}
/** Backspace clears the last hand filled. */
untype(): void {
const slot = [...this.typingSlots()].reverse().find((s) => this.sets[s.set][s.hand].gesture !== null);
if (!slot) return;
this.sets[slot.set][slot.hand].gesture = null;
this.syncAll();
}
/** A clap with only one hand does nothing; said before the turn is sealed, not after. */
loneClap = $derived(
this.activeSets.some((set) => (this.gestureFor(set, 'left') === 'C') !== (this.gestureFor(set, 'right') === 'C'))
);
choose(set: SetName, hand: Hand, gesture: Gesture): void {
const draft = this.sets[set][hand];
draft.gesture = draft.gesture === gesture ? null : gesture;
+25
View File
@@ -49,3 +49,28 @@ describe('threats at a crowded table', () => {
expect(duel.targetLabel('C')).toBe('Cass · 8 HP · invisible');
});
});
describe('typing a move', () => {
it('fills the left hand, then the right, and backspace takes the last one back', () => {
const duel = new Duel(createGame({ A: 'You', B: 'Foe' }, 1));
expect(duel.typeGesture('S')).toBe(true);
expect(duel.typeGesture('P')).toBe(true);
expect(duel.sets.main.left.gesture).toBe('S');
expect(duel.sets.main.right.gesture).toBe('P');
expect(duel.typeGesture('W')).toBe(false);
duel.untype();
expect(duel.sets.main.right.gesture).toBeNull();
expect(duel.sets.main.left.gesture).toBe('S');
});
it('warns of a clap made with one hand before the turn is sealed', () => {
const duel = new Duel(createGame({ A: 'You', B: 'Foe' }, 1));
duel.typeGesture('C');
duel.typeGesture('W');
expect(duel.loneClap).toBe(true);
expect(duel.ready).toBe(true);
duel.untype();
duel.typeGesture('C');
expect(duel.loneClap).toBe(false);
});
});