Files
Dune/tests/truth-future-battle-leader-advanced.test.ts
T
Quintine 50703664cf
Verify and publish checkpoint container / container (push) Canceled after 0s
Extend conditional battle questions across previews
2026-10-29 02:21:12 +11:00

141 lines
7.8 KiB
TypeScript

import assert from 'node:assert/strict';
import test from 'node:test';
import { viewGame, type Action, type Game } from '../game/engine';
import { botActions } from '../game/bots';
import { DIFFICULTIES } from '../game/bot-profiles';
import { createTruthMovementAdvancedFixture, actTruthMovementAdvanced as act,
truthMovementAdvancedClean as clean } from './fixture-truth-movement-advanced';
import { createTruthMovementIxianFixture } from './fixture-truth-movement-ixian';
import { advanceFutureShipment as advance } from './fixture-truthtrance-future-shipment';
import { futureBattleLeaderRecords as records } from './fixture-truth-future-battle-leader';
import { futureBattleLeaderWindow } from '../game/future-battle-leader';
type Fixture = ReturnType<typeof createTruthMovementAdvancedFixture>;
/** The fixture earns the physical Truthtrance and Atreides' Arrakeen army via
* native Advanced setup and phase actions. Nothing installs a battle or answer. */
function ask(f: {game: Game; target: string; asker: string; truth: string}): { game: Game; leader: string; other: string } {
let game = f.game;
const target = game.players.find(player => player.id === f.target)!;
const leader = target.leaders[0].id, other = target.leaders[1].id;
assert.ok(viewGame(game, f.asker).truthFutureBattleLeaderTargets.includes(f.target));
game = act(game, f.asker, { type: 'card', card: f.truth });
for (let count = 0; game.truthtrance?.stage === 'priority' && count < game.players.length; count++) {
const player = game.players.find(seat => !game.truthtrance!.passed.includes(seat.id));
assert.ok(player);
game = act(game, player.id, { type: 'truthPass' });
}
assert.equal(game.truthtrance?.stage, 'ask');
game = act(game, f.asker, { type: 'truthAsk', question: {
kind: 'futureBattleLeader', target: f.target, turn: game.turn, leader,
} } as unknown as Action);
assert.equal(game.truthtrance?.stage, 'answer');
return { game, leader, other };
}
function battle(game: Game, f: Fixture): Game {
game = act(game, f.target, { type: 'endMovement' });
game = advance(game, state => state.phase === 5 && state.active === f.asker && clean(state));
game = act(game, f.asker, { type: 'ship', amount: 1, elite: 0,
territory: 'arrakeen', sector: 10, allyPayment: 0 });
game = advance(game, clean);
game = act(game, f.asker, { type: 'endMovement' });
game = advance(game, state => state.phase === 6 && clean(state) && !!state.active && !state.battle);
const choice = viewGame(game, game.active!).battleChoices.find(row => row.territory === 'arrakeen');
assert.ok(choice);
game = act(game, choice.chooser, { type: 'chooseBattle', territory: choice.territory,
target: choice.attacker === choice.chooser ? choice.defender : choice.attacker });
return advance(game, state => !!state.battle && clean(state) && !state.battle.preparation &&
state.battle.preLeader?.closed !== false);
}
for (const answer of ['yes', 'no'] as const) {
void test(`Advanced classic ${answer} binds and settles in a real paid-support battle`, () => {
const f = createTruthMovementAdvancedFixture({ hajr: false });
const asked = ask(f);
assert.ok(viewGame(asked.game, f.target).truthFutureBattleLeaderAnswers?.includes(answer));
let game = act(asked.game, f.target, { type: 'truthAnswer', answer });
assert.equal(records(game)[0].status, 'waiting');
game = battle(game, f);
assert.equal(records(game)[0].status, 'bound');
assert.equal(records(game)[0].battle, game.battle!.event);
const plan = (leader: string): Action => ({ type: 'battlePlan', leader, dial: 1,
support: 1, allyPayment: 0, weapon: null, defense: null });
const rejected = plan(answer === 'yes' ? asked.other : asked.leader);
const before = structuredClone(game);
assert.throws(() => act(game, f.target, rejected));
assert.deepEqual(game, before);
game = act(game, f.target, plan(answer === 'yes' ? asked.leader : asked.other));
const emperor = game.players.find(player => player.id === f.asker)!;
game = act(game, f.asker, { type: 'battlePlan', leader: emperor.leaders[0].id,
dial: 0, support: 0, allyPayment: 0, weapon: null, defense: null });
assert.ok(game.battle?.revealed);
assert.equal(records(game)[0].status, 'settled');
assert.equal(game.discard.filter(card => card.id === f.truth).length, 1);
});
}
void test('Advanced conditional question expires without requiring a battle', () => {
const f = createTruthMovementAdvancedFixture({ hajr: false });
const asked = ask(f);
let game = act(asked.game, f.target, { type: 'truthAnswer', answer: 'yes' });
assert.equal(records(game)[0].status, 'waiting');
game = act(game, f.target, { type: 'endMovement' });
game = advance(game, state => state.turn > f.game.turn || state.phase > 6 || state.status === 'finished');
assert.equal(game.battle, null);
assert.equal(records(game)[0].status, 'expired');
});
void test('all bot profiles provide an available Advanced conditional answer', () => {
const f = createTruthMovementAdvancedFixture({ hajr: false });
const asked = ask(f);
for (const difficulty of DIFFICULTIES) {
const game = structuredClone(asked.game);
game.players.find(player => player.id === f.target)!.bot = difficulty;
const reply = botActions(viewGame(game, f.target)).find(action => action.type === 'truthAnswer');
assert.ok(reply, difficulty);
const answered = act(game, f.target, reply);
assert.equal(records(answered)[0].status, 'waiting');
}
});
void test('Advanced Ixians bind and settle their own leader in a real Cyborg battle', () => {
const f = createTruthMovementIxianFixture();
const asked = ask(f);
let game = act(asked.game, f.target, { type: 'truthAnswer', answer: 'yes' });
assert.equal(records(game)[0].status, 'waiting');
game = act(game, f.target, f.matchingMove);
game = advance(game, clean);
game = act(game, f.target, { type: 'endMovement' });
game = advance(game, state => state.phase === 5 && state.active === f.asker && clean(state));
game = act(game, f.asker, { type: 'ship', amount: 1, elite: 0,
territory: f.destination.territory, sector: f.destination.sector, allyPayment: 0 });
game = advance(game, clean);
game = act(game, f.asker, { type: 'endMovement' });
game = advance(game, state => state.phase === 6 && clean(state) && !!state.active && !state.battle);
const choice = viewGame(game, game.active!).battleChoices.find(row => row.territory === f.destination.territory);
assert.ok(choice);
game = act(game, choice.chooser, { type: 'chooseBattle', territory: choice.territory,
target: choice.attacker === choice.chooser ? choice.defender : choice.attacker });
game = advance(game, state => !!state.battle && clean(state) && !state.battle.preparation &&
state.battle.preLeader?.closed !== false);
assert.equal(records(game)[0].status, 'bound');
assert.equal(records(game)[0].battle, game.battle!.event);
const common = { dial: 0, support: 0, allyPayment: 0, weapon: null, defense: null };
const before = structuredClone(game);
assert.throws(() => act(game, f.target, { type: 'battlePlan', leader: asked.other, ...common }));
assert.deepEqual(game, before);
game = act(game, f.target, { type: 'battlePlan', leader: asked.leader, ...common });
game = act(game, f.asker, { type: 'battlePlan',
leader: game.players.find(player => player.id === f.asker)!.leaders[0].id, ...common });
assert.ok(game.battle?.revealed);
assert.equal(records(game)[0].status, 'settled');
});
void test('Advanced optional-module and Ecaz previews remain outside the shared question window', () => {
const f = createTruthMovementAdvancedFixture({ hajr: false });
assert.ok(futureBattleLeaderWindow(f.game));
assert.equal(futureBattleLeaderWindow({ ...f.game, homeworlds: { custody: null } }), false);
assert.equal(futureBattleLeaderWindow({ ...f.game, ecazOccupyPreview: true }), false);
});