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
77 lines
2.9 KiB
TypeScript
77 lines
2.9 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import { Duel } from './duel.svelte';
|
|
import { resolveTurn } from './resolve';
|
|
import { createGame, type GameState } from './state';
|
|
import type { Gesture } from './spells';
|
|
import { noop, play } from './test-helpers';
|
|
|
|
/** A is one gesture from a lightning bolt and has just been struck by amnesia. */
|
|
function amnesiaBeforeLightning(): GameState {
|
|
let s = createGame({ A: 'You', B: 'Foe' }, 1);
|
|
const left = 'DFFD';
|
|
const foe = '-DPP';
|
|
for (let i = 0; i < 4; i++) {
|
|
s = play(s, { left: left[i] as Gesture }, { left: foe[i] as Gesture, casts: i === 3 ? [{ hand: 'left', spellId: 'amnesia' }] : [] });
|
|
}
|
|
expect(s.wizards.A.constraints.amnesia).toBe(true);
|
|
return s;
|
|
}
|
|
|
|
describe('the duel store', () => {
|
|
it('casts the spell a forced repeat completes, without a click', () => {
|
|
const duel = new Duel(amnesiaBeforeLightning());
|
|
expect(duel.forced.left).toBe('D');
|
|
expect(duel.completions.main.left.map((c) => c.spell.id)).toContain('lightning_bolt');
|
|
expect(duel.sets.main.left.spellId).toBe('lightning_bolt');
|
|
expect(duel.ready).toBe(true);
|
|
duel.reveal();
|
|
expect(duel.foe.hp).toBe(10);
|
|
});
|
|
});
|
|
|
|
describe('threats at a crowded table', () => {
|
|
it('keys each opponent apart when two build the same spell with the same hand', () => {
|
|
let s = createGame({ A: 'You', B: 'Bryn', C: 'Cass' }, 1);
|
|
s = resolveTurn(s, { A: noop(), B: { ...noop(), left: 'S' }, C: { ...noop(), left: 'S' } });
|
|
const duel = new Duel(s);
|
|
const missiles = duel.threatsNow.filter((t) => t.spell.id === 'missile');
|
|
expect(missiles.map((t) => t.who).sort()).toEqual(['B', 'C']);
|
|
const keys = duel.threats.map((t) => duel.threatKey(t));
|
|
expect(new Set(keys).size).toBe(keys.length);
|
|
});
|
|
|
|
it('labels targets with health and condition', () => {
|
|
let s = createGame({ A: 'You', B: 'Bryn', C: 'Cass' }, 1);
|
|
s = resolveTurn(s, { A: noop(), B: noop(), C: noop() });
|
|
s.wizards.C.hp = 8;
|
|
s.wizards.C.invisibleTurns = 2;
|
|
const duel = new Duel(s);
|
|
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);
|
|
});
|
|
});
|