Files
Dune/tests/basic-early-leaderless-voice.test.ts
Quintine 685bd8bdfc
Verify and publish checkpoint container / container (push) Successful in 10m34s
Announce eligible Basic leaderless battles before plans
2026-10-29 06:59:11 +11:00

87 lines
4.5 KiB
TypeScript

import assert from 'node:assert/strict';
import test from 'node:test';
import { applyAction, viewGame, type Action, type Game } from '../game/engine';
import { nextStrongholdFactionsNativeStep } from './fixture-stronghold-factions';
import { advanceTruthTurnPower, createTruthTurnPowerFixture, truthTurnPowerClean } from './fixture-truth-turn-power';
const act = (game: Game, player: string, action: Action) => applyAction(game, player, action);
const seat = (game: Game, id: string) => {
const player = game.players.find(row => row.id === id); assert.ok(player); return player;
};
const notices = (game: Game) => game.log.filter(entry =>
entry.text === 'Atreides has no available leader or Cheap Hero for this battle.');
const nullPlan: Action = {type:'battlePlan', leader:null, dial:0, support:0, weapon:null, defense:null};
let fixture: ReturnType<typeof createTruthTurnPowerFixture> | undefined;
const source = () => fixture ??= createTruthTurnPowerFixture({power:'voice', remote:false});
/** A real public Basic game: BG has shipped into Atreides' Arrakeen. Stage
* only Atreides' alive leader-use history and custody of one physical Cheap
* Hero before making the real battle choice and native Voice decision. */
function beforeVoice() {
const f = source();
let game = advanceTruthTurnPower(structuredClone(f.shipment.after), state =>
state.phase === 6 && !!state.active && !state.battle && truthTurnPowerClean(state), [], true);
const target = seat(game, 'atreides');
assert.ok(target.leaders.every(leader => !leader.dead && !leader.capturedBy));
for (const leader of target.leaders) leader.usedAt = 'carthag';
const heroIndex = game.deck.findIndex(card => card.kind === 'hero'); assert.ok(heroIndex >= 0);
const hero = game.deck.splice(heroIndex, 1)[0]; target.hand.push(hero);
const chooser = game.active!;
const choice = viewGame(game, chooser).battleChoices.find(row => row.territory === 'arrakeen' &&
[row.attacker,row.defender].includes('atreides') && [row.attacker,row.defender].includes('beneGesserit'));
assert.ok(choice);
game = act(game, chooser, {type:'chooseBattle', territory:choice.territory,
target:choice.attacker === chooser ? choice.defender : choice.attacker});
assert.equal(game.battle?.basicEarlyLeaderlessVersion, 1);
assert.equal(game.battle?.preparation?.kind, 'voice');
assert.equal(game.battle.preparation.owner, 'beneGesserit');
assert.equal(viewGame(game, 'emperor').battle!.earlyLeaderless, undefined);
assert.equal(notices(game).length, 0);
return {game, hero:hero.id, karama:f.karama};
}
function finishPreparation(game: Game) {
for (let count = 0; count < 20 && (game.response || game.battle?.preparation); count++) {
const step = nextStrongholdFactionsNativeStep(game); assert.ok(step);
game = act(game, step.actor, step.action);
}
assert.equal(game.response, null);
assert.equal(game.battle?.preparation ?? null, null);
return game;
}
void test('Basic Voice forbidding a held Cheap Hero announces leaderlessness only after preparation', () => {
let {game} = beforeVoice();
game = act(game, 'beneGesserit', {type:'voice', kind:'hero', must:false});
if (game.response || game.battle?.preparation) {
assert.equal(viewGame(game, 'emperor').battle!.earlyLeaderless, undefined);
assert.equal(notices(game).length, 0);
}
game = finishPreparation(game);
assert.equal(game.battle!.voice?.target, 'atreides');
assert.deepEqual(game.battle!.plans, {});
for (const viewer of ['atreides','beneGesserit','emperor'])
assert.deepEqual(viewGame(game, viewer).battle!.earlyLeaderless, ['atreides']);
assert.equal(notices(game).length, 1);
game = act(game, 'atreides', nullPlan);
assert.equal(game.battle!.plans.atreides.leader, null);
assert.equal(notices(game).length, 1);
});
void test('Basic Karama cancellation of Voice leaves the Cheap Hero legal and no early notice', () => {
const {hero, karama, game: initial} = beforeVoice();
let game = initial;
assert.ok(seat(game, 'emperor').hand.some(card => card.id === karama));
game = act(game, 'beneGesserit', {type:'voice', kind:'hero', must:false});
assert.equal(game.response?.kind, 'voice');
game = act(game, 'emperor', {type:'card', card:karama, mode:'cancel'});
game = finishPreparation(game);
assert.equal(game.battle!.voice, undefined);
assert.equal(viewGame(game, 'emperor').battle!.earlyLeaderless, undefined);
assert.equal(notices(game).length, 0);
game = act(game, 'atreides', {...nullPlan, leader:hero});
assert.equal(game.battle!.plans.atreides.leader, hero);
assert.equal(notices(game).length, 0);
});