99 lines
4.4 KiB
TypeScript
99 lines
4.4 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
import { applyAction, viewGame, type Action, type Game } from '../game/engine';
|
|
import { botActions } from '../game/bots';
|
|
import { DIFFICULTIES } from '../game/bot-profiles';
|
|
import { harassCustody, harassWithdrawGame, takeHarassCard } from './fixture-harass-withdraw';
|
|
|
|
const card = 'ecaz-reinforcements';
|
|
|
|
/** Genuine Advanced setup and native battle admission; the shared fixture
|
|
* stages physical board forces and card custody, preserving every counter. */
|
|
function fixture(): Game {
|
|
const game = harassWithdrawGame({ advanced: true, factions: ['emperor','atreides','harkonnen'] });
|
|
takeHarassCard(game, 'a', card);
|
|
harassCustody(game);
|
|
return game;
|
|
}
|
|
function plan(elite?: number, useCard = true): Action {
|
|
return { type: 'battlePlan', dial: 1, support: 1, leader: 'emperor-0',
|
|
weapon: useCard ? card : null, defense: null,
|
|
...(elite === undefined ? {} : { reinforcementsElite: elite }),
|
|
} as Action;
|
|
}
|
|
function second(game: Game): Game {
|
|
return applyAction(game, 'd', { type:'battlePlan', dial:1, support:1,
|
|
leader:'atreides-0', weapon:null, defense:null });
|
|
}
|
|
function finish(game: Game): Game {
|
|
let next = applyAction(game, 'a', {type:'traitorCall',call:false});
|
|
next = applyAction(next, 'd', {type:'traitorCall',call:false});
|
|
if (next.decision?.kind === 'moritaniAssassinate')
|
|
next = applyAction(next, next.decision.player,
|
|
{type:'decision', event:next.decision.event, decline:true});
|
|
harassCustody(next);
|
|
return next;
|
|
}
|
|
|
|
for (const selected of [0,1,2,3] as const) {
|
|
void test(`Reinforcements commits the selected ${selected} elite reserve cost`, () => {
|
|
const start = fixture(), initial = structuredClone(start);
|
|
const owner = start.players.find(row => row.id === 'a')!;
|
|
assert.ok(owner.reserves - owner.elites!.reserves >= 3);
|
|
assert.ok(owner.elites!.reserves >= 3);
|
|
const one = applyAction(start, 'a', plan(selected));
|
|
assert.deepEqual(start, initial);
|
|
assert.equal(one.players[0].reserves, owner.reserves, 'Sealing does not spend the cost');
|
|
assert.equal(one.players[0].elites!.reserves, owner.elites!.reserves);
|
|
assert.deepEqual(viewGame(one, 'd').battle?.plans, {}, 'A sealed plan remains private');
|
|
const resolved = finish(second(one));
|
|
const after = resolved.players.find(row => row.id === 'a')!;
|
|
assert.equal(after.reserves, owner.reserves - 3);
|
|
assert.equal(after.elites!.reserves, owner.elites!.reserves - selected);
|
|
assert.equal(after.elites!.tanks, owner.elites!.tanks + selected);
|
|
assert.equal(resolved.discard.filter(row => row.id === card).length, 1);
|
|
assert.equal(resolved.log.filter(row => row.text.includes('used Reinforcements: three reserve forces')).length, 1);
|
|
});
|
|
}
|
|
|
|
void test('legacy omitted selection preserves normal-first reserve costs', () => {
|
|
const initial = fixture();
|
|
const owner = initial.players[0];
|
|
const done = finish(second(applyAction(initial, 'a', plan())));
|
|
assert.equal(done.players[0].reserves, owner.reserves - 3);
|
|
assert.equal(done.players[0].elites!.reserves, owner.elites!.reserves);
|
|
});
|
|
|
|
void test('invalid or irrelevant elite-cost selections reject without mutating the game', () => {
|
|
const game = fixture();
|
|
for (const action of [plan(-1), plan(4), plan(1.5), plan(Number.NaN), plan(1, false)]) {
|
|
const before = structuredClone(game);
|
|
assert.throws(() => applyAction(game, 'a', action));
|
|
assert.deepEqual(game, before);
|
|
}
|
|
const restricted = fixture();
|
|
restricted.players[0].elites!.reserves = 2;
|
|
restricted.players[0].elites!.tanks = 3;
|
|
restricted.players[0].reserves = 3;
|
|
restricted.players[0].tanks = 12;
|
|
harassCustody(restricted);
|
|
for (const invalid of [0,3]) {
|
|
const before = structuredClone(restricted);
|
|
assert.throws(() => applyAction(restricted, 'a', plan(invalid)));
|
|
assert.deepEqual(restricted, before);
|
|
}
|
|
assert.doesNotThrow(() => applyAction(restricted, 'a', plan(2)));
|
|
});
|
|
|
|
void test('all bot profiles retain a legal battle plan at an eligible typed-cost opportunity', () => {
|
|
const game = fixture();
|
|
for (const difficulty of DIFFICULTIES) {
|
|
const view = viewGame(game, 'a');
|
|
view.players.find(row => row.id === 'a')!.bot = difficulty;
|
|
const actions = botActions(view);
|
|
const battle = actions.find(action => action.type === 'battlePlan');
|
|
assert.ok(battle, `${difficulty} must have a legal battle plan`);
|
|
assert.doesNotThrow(() => applyAction(game, 'a', battle));
|
|
}
|
|
});
|