87 lines
4.5 KiB
TypeScript
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);
|
|
});
|