157 lines
8.1 KiB
TypeScript
157 lines
8.1 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
import {applyAction, viewGame, type Action, type Game} from '../game/engine';
|
|
import {finishHarassPreparation, harassCustody, harassWithdrawGame, takeHarassCard} from './fixture-harass-withdraw';
|
|
import {chooseEcazOccupyLead, ecazOccupyFixture, openEcazOccupyPlans} from './fixture-ecaz-occupy';
|
|
import {finishOccupyCardsAftermath, holdOccupyCardsTraitor} from './fixture-ecaz-occupy-cards';
|
|
|
|
const harass = 'ecaz-harass-withdraw';
|
|
const reinforcements = 'ecaz-reinforcements';
|
|
function fixture(advanced = false) {
|
|
// Rock has no collection income to obscure original battle support payments.
|
|
const game = harassWithdrawGame({advanced, factions: ['emperor', 'atreides', 'guild'],
|
|
territory: 'shield_wall', sector: 8});
|
|
takeHarassCard(game, 'a', reinforcements);
|
|
return game;
|
|
}
|
|
function ownPlan(game: Game, slot: 'weapon' | 'defense' = 'weapon'): Action {
|
|
return {type: 'battlePlan', dial: 1, support: game.advanced ? 1 : 0,
|
|
leader: 'emperor-4', weapon: slot === 'weapon' ? reinforcements : harass,
|
|
defense: slot === 'weapon' ? harass : reinforcements};
|
|
}
|
|
function seal(game: Game, slot: 'weapon' | 'defense' = 'weapon', other: Partial<Action> = {}) {
|
|
game = applyAction(game, 'a', ownPlan(game, slot));
|
|
return applyAction(game, 'd', {type: 'battlePlan', dial: 0, support: 0,
|
|
leader: 'atreides-2', ...other});
|
|
}
|
|
function finish(game: Game, calls: readonly string[] = []) {
|
|
for (const player of ['a', 'd']) game = applyAction(game, player,
|
|
{type: 'traitorCall', call: calls.includes(player)});
|
|
game = finishOccupyCardsAftermath(game);
|
|
harassCustody(game);
|
|
return game;
|
|
}
|
|
function assertDiscarded(game: Game) {
|
|
for (const id of [harass, reinforcements]) {
|
|
assert.equal(game.discard.filter(card => card.id === id).length, 1);
|
|
assert.ok(game.players.every(player => !player.hand.some(card => card.id === id)));
|
|
}
|
|
}
|
|
|
|
void test('both same-plan slot orders add the decisive bonus without changing Harass commitment, support or disposal', () => {
|
|
for (const advanced of [false, true]) for (const slot of ['weapon', 'defense'] as const) {
|
|
const game = fixture(advanced);
|
|
const done = finish(seal(game, slot));
|
|
assert.equal(done.lastBattleContext?.winner, 'a', 'dial1 + leader2 + bonus2 beats leader4');
|
|
assert.equal(done.players[0].reserves, 16, 'fifteen original reserves minus cost3 plus four returned');
|
|
assert.equal(done.players[0].tanks, 4, 'three reserve counters and only the one original dialed counter');
|
|
assert.deepEqual(done.players[0].forces, {});
|
|
assert.equal(done.players[0].spice, advanced ? 29 : 30);
|
|
assert.equal(done.players[0].leaders.find(leader => leader.id === 'emperor-4')!.dead, false);
|
|
assertDiscarded(done);
|
|
assert.equal(done.battle, null);
|
|
assert.equal(done.decision, null);
|
|
}
|
|
});
|
|
|
|
void test('returned ordinary counters cannot replace the original elite reserve cost', () => {
|
|
const game = fixture(true), owner = game.players[0];
|
|
owner.reserves = 3;
|
|
owner.tanks = 12;
|
|
owner.elites!.reserves = 1;
|
|
owner.elites!.tanks = 4;
|
|
harassCustody(game);
|
|
const preview = viewGame(game, 'a').battle!.reinforcements!;
|
|
assert.equal(preview.normal, 2);
|
|
assert.equal(preview.elite, 1);
|
|
const done = finish(seal(game));
|
|
assert.equal(done.players[0].reserves, 4);
|
|
assert.equal(done.players[0].elites!.reserves, 0);
|
|
assert.equal(done.players[0].elites!.tanks, 5, 'the original elite reserve was spent before ordinary returns');
|
|
assert.equal(done.players[0].tanks, 16);
|
|
assert.equal(done.players[0].spice, 29);
|
|
assertDiscarded(done);
|
|
});
|
|
|
|
void test('a future Harass return cannot fund an underfunded same-plan Reinforcements commitment', () => {
|
|
const game = fixture(true), owner = game.players[0];
|
|
owner.reserves = 2;
|
|
owner.tanks = 13;
|
|
owner.elites!.reserves = 1;
|
|
owner.elites!.tanks = 4;
|
|
harassCustody(game);
|
|
const before = structuredClone(game);
|
|
assert.throws(() => applyAction(game, 'a', ownPlan(game)), /three own forces in reserves/);
|
|
assert.deepEqual(game, before);
|
|
});
|
|
|
|
void test('own, opposing and mutual Traitors preserve the original Reinforcements cost and cancel only opposing Harass', () => {
|
|
for (const advanced of [false, true]) for (const calls of [['a'], ['d'], ['a', 'd']]) {
|
|
const game = fixture(advanced);
|
|
if (calls.includes('a')) holdOccupyCardsTraitor(game, 'a', 'atreides-2');
|
|
if (calls.includes('d')) holdOccupyCardsTraitor(game, 'd', 'emperor-4');
|
|
const done = finish(seal(game), calls);
|
|
const canceled = calls.includes('d');
|
|
assert.equal(done.lastBattleContext?.result, calls.length === 2 ? 'mutualTraitors' : 'traitor');
|
|
assert.equal(done.lastBattleContext?.winner, calls.length === 2 ? null : calls[0]);
|
|
assert.equal(done.players[0].reserves, canceled ? 12 : 16);
|
|
assert.equal(done.players[0].tanks, canceled ? 8 : 3);
|
|
assert.deepEqual(done.players[0].forces, canceled ? {} : {'shield_wall:8': 1});
|
|
const bounty = !canceled ? game.players[1].leaders.find(leader => leader.id === 'atreides-2')!.strength : 0;
|
|
assert.equal(done.players[0].spice, game.players[0].spice - (advanced && canceled ? 1 : 0) + bounty);
|
|
assertDiscarded(done);
|
|
}
|
|
});
|
|
|
|
void test('same-plan utility cards both pay and discard while Harass returns precede a Lasgun Shield explosion', () => {
|
|
for (const advanced of [false, true]) {
|
|
const game = fixture(advanced);
|
|
const laser = game.deck.find(card => card.kind === 'lasgun')!;
|
|
const shield = game.deck.find(card => card.kind === 'shield')!;
|
|
takeHarassCard(game, 'd', laser.id);
|
|
takeHarassCard(game, 'd', shield.id);
|
|
const done = finish(seal(game, 'weapon', {weapon: laser.id, defense: shield.id}));
|
|
assert.equal(done.lastBattleContext?.result, 'explosion');
|
|
assert.equal(done.lastBattleContext?.winner, null);
|
|
assert.equal(done.players[0].reserves, 16);
|
|
assert.equal(done.players[0].tanks, 4);
|
|
assert.deepEqual(done.players[0].forces, {});
|
|
assert.equal(done.players[1].tanks, 5);
|
|
assertDiscarded(done);
|
|
}
|
|
});
|
|
|
|
void test('active Occupy same-plan utilities pay only the actual card user and retain original fixed versus variable losses', () => {
|
|
for (const lead of ['ecaz', 'ally'] as const) {
|
|
const source = ecazOccupyFixture({ecazTreachery: true, allyFaction: 'guild', opponentFaction: 'emperor',
|
|
ecazForces: 5, allyForces: 4});
|
|
const owner = lead === 'ecaz' ? source.ecaz : source.ally;
|
|
takeHarassCard(source.game, owner, harass);
|
|
takeHarassCard(source.game, owner, reinforcements);
|
|
let game = finishHarassPreparation(openEcazOccupyPlans(chooseEcazOccupyLead(source, lead)));
|
|
const before = structuredClone(game);
|
|
const profile = viewGame(game, owner).battle!.ecazOccupy!.profile!;
|
|
const player = game.players.find(player => player.id === owner)!;
|
|
const other = game.players.find(player => player.id === source.opponent)!;
|
|
game = applyAction(game, owner, {type: 'battlePlan', dial: profile.fixedEcazDial + 1, support: 1,
|
|
leader: player.leaders.find(leader => !leader.dead && !leader.usedAt)!.id,
|
|
weapon: reinforcements, defense: harass});
|
|
game = applyAction(game, source.opponent, {type: 'battlePlan', dial: 0, support: 0,
|
|
leader: [...other.leaders].sort((a,b) => a.strength - b.strength)[0].id});
|
|
for (const voter of viewGame(game, owner).battle!.traitorVoters)
|
|
game = applyAction(game, voter, {type: 'traitorCall', call: false});
|
|
game = finishOccupyCardsAftermath(game);
|
|
assert.equal(game.lastBattleContext?.winner, owner);
|
|
const delta = (id: string, field: 'reserves' | 'tanks') =>
|
|
game.players.find(player => player.id === id)![field] - before.players.find(player => player.id === id)![field];
|
|
assert.equal(delta(owner, 'reserves'), lead === 'ecaz' ? -1 : 0);
|
|
assert.equal(delta(source.ecaz, 'tanks'), lead === 'ecaz' ? 6 : 3);
|
|
assert.equal(delta(source.ally, 'tanks'), lead === 'ally' ? 4 : 1);
|
|
assert.equal(game.players.find(player => player.id === source.ecaz)!.forces[source.location] ?? 0,
|
|
lead === 'ecaz' ? 0 : 2);
|
|
assert.equal(game.players.find(player => player.id === source.ally)!.forces[source.location] ?? 0,
|
|
lead === 'ally' ? 0 : 3);
|
|
assertDiscarded(game);
|
|
}
|
|
});
|