import assert from 'node:assert/strict'; import test from 'node:test'; import { FutureBattleLeaderError, bindFutureBattleLeader, createFutureBattleLeader, expireFutureBattleLeader, releaseFutureBattleLeader, settleFutureBattleLeader, type FutureBattleLeaderReceipt, type FutureBattleLeaderSource, } from '../game/future-battle-leader'; const question = { turn: 2, asker: 'atreides', player: 'emperor', leader: 'fenring', answer: true }; const names = ['fenring', 'aramsham']; const battle: FutureBattleLeaderSource = { turn: 2, event: 'battle-1', attacker: 'emperor', defender: 'guild', native: true, plansOpen: true, }; const revealed = { turn: 2, battle: 'battle-1', player: 'emperor', revealed: true, leader: 'fenring' }; void test('conditional question waits for the first actual qualifying native battle', () => { const waiting = createFutureBattleLeader(question, names); const original = structuredClone(waiting); assert.equal(waiting.status, 'waiting'); for (const unrelated of [ { ...battle, turn: 3 }, { ...battle, attacker: 'fremen' }, { ...battle, native: false }, ]) assert.deepEqual(bindFutureBattleLeader(waiting, unrelated), waiting); assert.throws(() => bindFutureBattleLeader(waiting, { ...battle, plansOpen: false }), FutureBattleLeaderError, 'Missing an already-opened qualifying battle must not silently bind a later one.'); const bound = bindFutureBattleLeader(waiting, battle); assert.deepEqual(bound, { ...original, battle: 'battle-1', status: 'bound' }); assert.deepEqual(waiting, original, 'Binding cannot mutate the original receipt.'); assert.deepEqual(bindFutureBattleLeader(bound, { ...battle, event: 'battle-2' }), bound, 'A later battle must not replace the original qualifying battle.'); assert.deepEqual(JSON.parse(JSON.stringify(bound)), bound); }); void test('yes settles only on an actual compliant revealed plan and the original battle', () => { const bound = bindFutureBattleLeader(createFutureBattleLeader(question, names), battle); assert.throws(() => settleFutureBattleLeader(bound, { ...revealed, revealed: false }), FutureBattleLeaderError); assert.throws(() => settleFutureBattleLeader(bound, { ...revealed, battle: 'battle-2' }), FutureBattleLeaderError); assert.throws(() => settleFutureBattleLeader(bound, { ...revealed, player: 'guild' }), FutureBattleLeaderError); assert.throws(() => settleFutureBattleLeader(bound, { ...revealed, leader: 'aramsham' }), FutureBattleLeaderError); const settled = settleFutureBattleLeader(bound, revealed); assert.equal(settled.status, 'settled'); assert.equal(bound.status, 'bound'); assert.deepEqual(settleFutureBattleLeader(settled, revealed), settled); assert.throws(() => settleFutureBattleLeader(settled, { ...revealed, leader: null }), FutureBattleLeaderError); }); void test('no forbids only the named leader at that first battle', () => { const waiting = createFutureBattleLeader({ ...question, answer: false }, names); const bound = bindFutureBattleLeader(waiting, battle); assert.throws(() => settleFutureBattleLeader(bound, revealed), FutureBattleLeaderError); assert.equal(settleFutureBattleLeader(bound, { ...revealed, leader: 'aramsham' }).status, 'settled'); assert.equal(settleFutureBattleLeader(bound, { ...revealed, leader: null }).status, 'settled'); }); void test('a turn with no eligible battle expires without inventing a battle', () => { const waiting = createFutureBattleLeader(question, names); assert.deepEqual(expireFutureBattleLeader(waiting, { turn: 2, battlePhaseEnded: false }), waiting); const expired = expireFutureBattleLeader(waiting, { turn: 2, battlePhaseEnded: true }); assert.equal(expired.status, 'expired'); assert.equal(expired.battle, undefined); assert.deepEqual(bindFutureBattleLeader(expired, battle), expired); assert.throws(() => expireFutureBattleLeader(waiting, { turn: 3, battlePhaseEnded: true }), FutureBattleLeaderError); const bound = bindFutureBattleLeader(waiting, battle); assert.deepEqual(expireFutureBattleLeader(bound, { turn: 2, battlePhaseEnded: true }), bound, 'An opened battle must be resolved or released, not treated as no opportunity.'); }); void test('release requires the correct turn and original bound battle', () => { const waiting = createFutureBattleLeader(question, names); const bound = bindFutureBattleLeader(waiting, battle); assert.throws(() => releaseFutureBattleLeader(bound, { turn: 2, battle: 'battle-2' }), FutureBattleLeaderError); assert.throws(() => releaseFutureBattleLeader(bound, { turn: 3, battle: 'battle-1' }), FutureBattleLeaderError); assert.throws(() => releaseFutureBattleLeader(bound, { turn: 2 }), FutureBattleLeaderError); const released = releaseFutureBattleLeader(bound, { turn: 2, battle: 'battle-1' }); assert.equal(released.status, 'released'); assert.deepEqual(releaseFutureBattleLeader(released, { turn: 2, battle: 'battle-1' }), released); assert.throws(() => settleFutureBattleLeader(released, revealed), FutureBattleLeaderError); assert.equal(releaseFutureBattleLeader(waiting, { turn: 2 }).status, 'released'); }); void test('malformed questions, saved receipts and battle identities are rejected', () => { assert.throws(() => createFutureBattleLeader({ ...question, leader: 'unknown' }, names), FutureBattleLeaderError); assert.throws(() => createFutureBattleLeader({ ...question, asker: question.player }, names), FutureBattleLeaderError); assert.throws(() => createFutureBattleLeader({ ...question, turn: 0 }, names), FutureBattleLeaderError); assert.throws(() => createFutureBattleLeader(question, ['fenring', 'fenring']), FutureBattleLeaderError); const waiting = createFutureBattleLeader(question, names); assert.throws(() => bindFutureBattleLeader(waiting, { ...battle, attacker: battle.defender }), FutureBattleLeaderError); assert.throws(() => bindFutureBattleLeader(waiting, { ...battle, event: '' }), FutureBattleLeaderError); assert.throws(() => bindFutureBattleLeader({ ...waiting, status: 'bound' } as FutureBattleLeaderReceipt, battle), FutureBattleLeaderError); assert.throws(() => bindFutureBattleLeader({ ...waiting, status: 'waiting', battle: 'old' }, battle), FutureBattleLeaderError); });