141 lines
7.8 KiB
TypeScript
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);
|
|
});
|