111 lines
5.2 KiB
TypeScript
111 lines
5.2 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 {
|
||
actFutureBattleLeader as act, advanceFutureBattleLeader as advance,
|
||
answerFutureBattleLeader as answer, createFutureBattleLeaderFixture as fixture,
|
||
declareFutureBattleLeader as declare, futureBattleLeaderClean as clean,
|
||
futureBattleLeaderInventory as inventory, futureBattleLeaderRecords as records,
|
||
reachFutureBattle as reachBattle,
|
||
} from './fixture-truth-future-battle-leader';
|
||
|
||
function rejected(game: Game, actor: string, action: Action) {
|
||
const before = structuredClone(game);
|
||
assert.throws(() => act(game, actor, action));
|
||
assert.deepEqual(game, before);
|
||
}
|
||
|
||
for (const expected of ['yes', 'no'] as const) {
|
||
void test(`Basic next battle leader ${expected} binds the first real battle and constrains its native plan`, () => {
|
||
const f = fixture();
|
||
const asked = declare(f);
|
||
assert.equal(asked.truthtrance?.stage, 'answer');
|
||
assert.deepEqual(asked.truthtrance?.question, f.question);
|
||
let game = answer(asked, f, expected);
|
||
assert.equal(game.truthtrance, null);
|
||
assert.equal(game.discard.filter(card => card.id === f.truth).length, 1);
|
||
assert.equal(records(game).length, 1);
|
||
assert.equal(records(game)[0].status, 'waiting');
|
||
assert.equal(records(game)[0].player, f.target);
|
||
assert.equal(records(game)[0].leader, f.leader);
|
||
assert.equal(records(game)[0].answer, expected === 'yes');
|
||
rejected(game, f.target, { type: 'truthAnswer', answer: expected });
|
||
assert.equal(records(game).length, 1);
|
||
|
||
game = reachBattle(game, f);
|
||
assert.equal(records(game)[0].status, 'bound');
|
||
assert.equal(records(game)[0].battle, game.battle!.event);
|
||
const yesPlan = { ...f.targetPlan, leader: f.leader } as Action;
|
||
const noPlan = { ...f.targetPlan, leader: f.otherLeader } as Action;
|
||
rejected(game, f.target, expected === 'yes' ? noPlan : yesPlan);
|
||
game = act(game, f.target, expected === 'yes' ? yesPlan : noPlan);
|
||
assert.equal(records(game)[0].status, 'bound');
|
||
game = act(game, f.asker, f.ownPlan);
|
||
assert.ok(game.battle?.revealed);
|
||
assert.equal(records(game)[0].status, 'settled');
|
||
assert.equal(records(game).length, 1);
|
||
assert.equal(game.discard.filter(card => card.id === f.truth).length, 1);
|
||
assert.deepEqual(inventory(game), f.originalCards);
|
||
});
|
||
}
|
||
|
||
void test('Basic next battle leader question expires without creating a battle or forcing a shipment', () => {
|
||
const f = fixture();
|
||
for (const expected of ['yes', 'no'] as const) {
|
||
let game = answer(declare(f), f, expected);
|
||
const before = structuredClone(game.players.find(player => player.id === f.target));
|
||
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).length, 1);
|
||
assert.equal(records(game)[0].status, 'expired');
|
||
const after = game.players.find(player => player.id === f.target); assert.ok(before && after);
|
||
assert.equal(after.reserves, before.reserves);
|
||
assert.deepEqual(after.forces, before.forces);
|
||
assert.equal(game.discard.filter(card => card.id === f.truth).length, 1);
|
||
assert.deepEqual(inventory(game), f.originalCards);
|
||
assert.ok(clean(game));
|
||
}
|
||
});
|
||
|
||
void test('future battle questions use a current turn, another seat and that faction’s native public leader', () => {
|
||
const f = fixture();
|
||
let game = act(f.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');
|
||
const invalid = [
|
||
{ ...f.question, turn: f.game.turn + 1 },
|
||
{ ...f.question, target: f.asker },
|
||
{ ...f.question, leader: f.base.leaders.atreides },
|
||
{ ...f.question, leader: 'unknown-leader' },
|
||
];
|
||
for (const question of invalid)
|
||
rejected(game, f.asker, { type: 'truthAsk', question } as unknown as Action);
|
||
const asked = act(game, f.asker, f.ask);
|
||
rejected(asked, f.asker, { type: 'truthAnswer', answer: 'yes' });
|
||
});
|
||
|
||
void test('each AI profile chooses a legal conditional answer and honors it in the real battle', () => {
|
||
for (const difficulty of DIFFICULTIES) {
|
||
const f = fixture();
|
||
const asked = declare(f);
|
||
const respondent = asked.players.find(player => player.id === f.target)!;
|
||
respondent.bot = difficulty;
|
||
const reply = botActions(viewGame(asked, f.target)).find(action => action.type === 'truthAnswer');
|
||
assert.ok(reply, difficulty);
|
||
let game = act(asked, f.target, reply);
|
||
assert.equal(records(game)[0].status, 'waiting');
|
||
game = reachBattle(game, f);
|
||
const planned = botActions(viewGame(game, f.target)).find(action => action.type === 'battlePlan');
|
||
assert.ok(planned, difficulty);
|
||
game = act(game, f.target, planned);
|
||
game = act(game, f.asker, f.ownPlan);
|
||
assert.equal(records(game)[0].status, 'settled');
|
||
}
|
||
});
|