161 lines
11 KiB
TypeScript
161 lines
11 KiB
TypeScript
import assert from 'node:assert/strict';
|
||
import test from 'node:test';
|
||
import { applyAction, RuleError, viewGame } from '../game/engine';
|
||
import { combatArmy } from '../game/combat-location';
|
||
import {
|
||
advanceNativeWorldBattle, assertNativeWorldBattleInventory, createNativeWorldBattleFixture,
|
||
HARASS_WITHDRAW_CARD, nativeWorldBattleFinished, nativeWorldBattlePlayer, nativeWorldBattlePool,
|
||
REINFORCEMENTS_CARD, revealNativeWorldBattle, type NativeWorldBattleStep,
|
||
} from './fixture-native-world-battle';
|
||
|
||
for (const advanced of [false, true]) {
|
||
const rules = advanced ? 'Advanced' : 'Basic';
|
||
|
||
void test(`${rules}: nine paid Suboids retain original 1N4E reserves; Reinforcements pays 1N2E before Harass returns`, () => {
|
||
const f = createNativeWorldBattleFixture({ advanced });
|
||
const shipment = f.shipments[0], original = nativeWorldBattlePlayer(shipment.before, f.owner);
|
||
const shipped = nativeWorldBattlePlayer(shipment.after, f.owner);
|
||
assert.equal(shipment.cost, 9);
|
||
assert.equal(shipped.spice, original.spice - 9);
|
||
assert.deepEqual(combatArmy(shipment.after, f.owner, f.location), { normal: 9, elite: 0 });
|
||
assert.deepEqual(nativeWorldBattlePool(f.game, f.owner, 'homeworld:ixians'), { normal: 1, elite: 4 });
|
||
assert.deepEqual(f.sources, { 'homeworld:ixians': { normal: 1, elite: 2 } });
|
||
const before = structuredClone(f.game);
|
||
assert.throws(() => applyAction(f.game, f.owner, { ...f.planActions[0].action,
|
||
reinforcementsSources: { 'homeworld:ixians': { normal: 3, elite: 0 } } }), RuleError);
|
||
assert.deepEqual(f.game, before, 'Seven future returned Suboids cannot replace the two original Cyborg fees');
|
||
assert.throws(() => applyAction(f.game, f.owner, { ...f.planActions[0].action,
|
||
reinforcementsSources: { [f.location]: { normal: 1, elite: 2 } } }));
|
||
assert.deepEqual(f.game, before, 'Foreign visitor stock is not a native fee source');
|
||
|
||
const revealed = revealNativeWorldBattle(f);
|
||
assert.deepEqual(viewGame(revealed, f.owner).battle!.traitorVoters, ['tleilaxu']);
|
||
assert.throws(() => applyAction(revealed, f.owner, { type: 'traitorCall', call: false }));
|
||
const steps: NativeWorldBattleStep[] = [];
|
||
const done = advanceNativeWorldBattle(revealed, nativeWorldBattleFinished, steps, { returns: f.returns });
|
||
const p = nativeWorldBattlePlayer(done, f.owner);
|
||
assert.equal(done.lastBattleContext?.winner, 'tleilaxu');
|
||
assert.equal(p.reserves, 9, 'Original five minus fee three plus seven returned normals');
|
||
assert.equal(p.elites!.reserves, 2, 'Returned normals never undo the original two-Cyborg payment');
|
||
assert.equal(p.tanks, 5, 'Fee three plus only the two committed Suboids');
|
||
assert.equal(p.elites!.tanks, 2);
|
||
assert.deepEqual(nativeWorldBattlePool(done, f.owner, 'homeworld:ixians'), { normal: 7, elite: 2 });
|
||
assert.deepEqual(combatArmy(done, f.owner, f.location), { normal: 0, elite: 0 });
|
||
assert.ok(!done.pendingIxSubstitution);
|
||
for (const id of [REINFORCEMENTS_CARD, HARASS_WITHDRAW_CARD])
|
||
assert.equal(done.discard.filter(c => c.id === id).length, 1);
|
||
assert.deepEqual(p.forces, nativeWorldBattlePlayer(f.afterSetup, f.owner).forces, 'Original HMS garrison stays physical');
|
||
assertNativeWorldBattleInventory(done, f.afterSetup);
|
||
});
|
||
|
||
void test(`${rules}: own Ix removes local fees from the physical dial, retains Cyborg/Suboid traits and forbids Harass`, () => {
|
||
const f = createNativeWorldBattleFixture({ advanced, route: 'ix-native', faceDancer: true });
|
||
assert.deepEqual(nativeWorldBattlePool(f.game, f.owner, f.location), { normal: 1, elite: 4 });
|
||
const before = structuredClone(f.game);
|
||
assert.throws(() => applyAction(f.game, f.owner, { ...f.planActions[0].action, defense: HARASS_WITHDRAW_CARD }),
|
||
RuleError);
|
||
assert.throws(() => applyAction(f.game, f.owner, { ...f.planActions[0].action, dial: 4.5 }),
|
||
RuleError);
|
||
assert.deepEqual(f.game, before);
|
||
const revealed = revealNativeWorldBattle(f), view = viewGame(revealed, f.owner).battle!;
|
||
assert.ok(view.ownForces);
|
||
assert.equal(view.ownForces.normal, 0);
|
||
assert.equal(view.ownForces.elite, 2, 'Two local Cyborg fee counters cannot also be dialed');
|
||
assert.equal(view.ownForces.normalFixedHalf, true);
|
||
assert.equal(view.ownForces.eliteStrength, 2);
|
||
assert.equal(view.ownForces.freeSupport, !advanced);
|
||
assert.deepEqual(view.traitorVoters, ['ixians']);
|
||
assert.throws(() => applyAction(revealed, 'guild', { type: 'traitorCall', call: false }));
|
||
const done = advanceNativeWorldBattle(revealed, nativeWorldBattleFinished);
|
||
const p = nativeWorldBattlePlayer(done, f.owner);
|
||
assert.equal(done.lastBattleContext?.winner, f.owner);
|
||
assert.equal(p.tanks, advanced ? 5 : 4);
|
||
assert.equal(p.elites!.tanks, advanced ? 4 : 3, 'Fee two plus only this dial’s actual Cyborg casualties');
|
||
assert.deepEqual(combatArmy(done, f.owner, f.location), { normal: 0, elite: advanced ? 0 : 1 });
|
||
assert.deepEqual(combatArmy(done, f.owner, 'homeworld:tleilaxu'), { normal: 9, elite: 0 },
|
||
'The separate real exported garrison is not a casualty or fee origin for this battle');
|
||
assert.ok(nativeWorldBattlePlayer(done, 'tleilaxu').faceDancers!.some(d =>
|
||
d.leader === f.planActions[0].action.leader && !d.revealed),
|
||
'A matching spectator Face Dancer cannot act on another faction’s World');
|
||
assertNativeWorldBattleInventory(done, f.afterSetup);
|
||
});
|
||
|
||
void test(`${rules}: paid typed invasion separates two fee Cyborgs from one battle Cyborg and resumes native Face Dance`, () => {
|
||
const f = createNativeWorldBattleFixture({ advanced, exportNormal: 9, exportElite: 1,
|
||
ownerDial: advanced ? 1 : 2, harass: false, outcome: 'win', faceDancer: true });
|
||
assert.equal(f.shipments[0].cost, 10);
|
||
assert.equal(nativeWorldBattlePlayer(f.shipments[0].after, f.owner).spice, 0);
|
||
assert.deepEqual(combatArmy(f.game, f.owner, f.location), { normal: 9, elite: 1 });
|
||
const steps: NativeWorldBattleStep[] = [];
|
||
let game = advanceNativeWorldBattle(revealNativeWorldBattle(f), g => g.decision?.kind === 'ixSubstitution', steps,
|
||
{ casualties: { normal: 0, elite: 1 } });
|
||
const ix = nativeWorldBattlePlayer(game, f.owner);
|
||
assert.equal(ix.tanks, 4);
|
||
assert.equal(ix.elites!.tanks, 3, 'Two reserve-fee Cyborgs and exactly one actual board casualty');
|
||
assert.deepEqual(combatArmy(game, f.owner, f.location), { normal: 9, elite: 0 });
|
||
const before = structuredClone(game);
|
||
assert.throws(() => applyAction(game, f.owner, { type: 'decision', sources: { [f.location]: 3 },
|
||
recover: { [f.location]: 3 } }), RuleError);
|
||
assert.deepEqual(game, before, 'The fee cannot become two extra board-loss origins');
|
||
game = applyAction(game, f.owner, { type: 'decision', sources: { [f.location]: 1 }, recover: { [f.location]: 1 } });
|
||
game = advanceNativeWorldBattle(game, g => g.decision?.kind === 'faceDance', steps);
|
||
assert.deepEqual(combatArmy(game, f.owner, f.location), { normal: 8, elite: 1 });
|
||
assert.equal(nativeWorldBattlePlayer(game, f.owner).elites!.tanks, 2, 'Only the battle Cyborg was recovered');
|
||
assert.equal(nativeWorldBattlePlayer(game, f.owner).tanks, 4, 'One surviving Suboid replaces that Cyborg in Tanks');
|
||
assert.equal(game.decision?.player, 'tleilaxu');
|
||
assert.equal(game.decision?.kind === 'faceDance' && game.decision.territory, 'homeworld:tleilaxu');
|
||
const offer = viewGame(game, 'tleilaxu').decision;
|
||
assert.ok(offer?.kind === 'faceDance');
|
||
assert.equal(offer.blocked, undefined);
|
||
const dancer = nativeWorldBattlePlayer(game, 'tleilaxu').faceDancers!.find(d => d.leader === f.planActions[0].action.leader)!;
|
||
assert.ok(dancer && !dancer.revealed);
|
||
assertNativeWorldBattleInventory(game, f.afterSetup);
|
||
game = applyAction(game, 'tleilaxu', { type: 'decision', reveal: true, sources: {} });
|
||
game = advanceNativeWorldBattle(game, nativeWorldBattleFinished, steps);
|
||
assert.deepEqual(combatArmy(game, f.owner, f.location), { normal: 0, elite: 0 });
|
||
assert.deepEqual(nativeWorldBattlePool(game, f.owner, 'homeworld:ixians'), { normal: 8, elite: 2 },
|
||
'Eight Suboids and the one recovered Cyborg return to native Ix, joining its one remaining native Cyborg');
|
||
const returned = nativeWorldBattlePlayer(game, f.owner);
|
||
assert.equal(returned.reserves, 10);
|
||
assert.equal(returned.elites!.reserves, 2);
|
||
assert.equal(returned.tanks, 4);
|
||
assert.equal(returned.elites!.tanks, 2);
|
||
assert.equal(returned.leaders.find(l => l.id === f.planActions[0].action.leader)!.dead, true);
|
||
assert.ok(nativeWorldBattlePlayer(game, 'tleilaxu').faceDancers!.find(d => d.leader === dancer.leader)!.revealed);
|
||
assertNativeWorldBattleInventory(game, f.afterSetup);
|
||
});
|
||
}
|
||
|
||
void test('Advanced: native CHOAM Reinforcements preserves the original Auditor response, payment and real inspection', () => {
|
||
const f = createNativeWorldBattleFixture({ advanced: true, route: 'choam-native' });
|
||
let game = advanceNativeWorldBattle(revealNativeWorldBattle(f), g => g.decision?.kind === 'choamAudit');
|
||
assert.equal(game.decision?.player, 'choam');
|
||
assert.equal(game.lastBattleContext?.winner, 'choam');
|
||
assert.deepEqual(combatArmy(game, 'choam', f.location), { normal: 17, elite: 0 });
|
||
assert.equal(nativeWorldBattlePlayer(game, 'choam').tanks, 3);
|
||
const event = game.pendingAuditor!.event;
|
||
const held = structuredClone(nativeWorldBattlePlayer(game, 'guild').hand);
|
||
assert.equal(held.length, 1, 'One actual unused original card makes inspection deterministic without a replacement RNG');
|
||
game = applyAction(game, 'choam', { type: 'decision', event, audit: true });
|
||
game = advanceNativeWorldBattle(game, g => g.decision?.kind === 'choamAuditPayment');
|
||
assert.equal(game.decision?.player, 'guild');
|
||
const before = structuredClone(game);
|
||
const count = viewGame(game, 'choam').auditor!.count;
|
||
assert.equal(count, 1);
|
||
const paid = applyAction(game, 'guild', { type: 'decision', event, pay: true, count });
|
||
assert.equal(nativeWorldBattlePlayer(paid, 'guild').spice, nativeWorldBattlePlayer(before, 'guild').spice - 1);
|
||
assert.equal(nativeWorldBattlePlayer(paid, 'choam').spice, nativeWorldBattlePlayer(before, 'choam').spice + 1);
|
||
assert.ok(!paid.auditorInsight, 'Payment cancels the inspection');
|
||
assertNativeWorldBattleInventory(paid, f.afterSetup);
|
||
|
||
game = applyAction(game, 'guild', { type: 'decision', event, pay: false });
|
||
assert.deepEqual(game.auditorInsight?.cards.map(c => c.id), held.map(c => c.id));
|
||
assert.equal(game.auditorInsight?.target, 'guild');
|
||
assert.deepEqual(nativeWorldBattlePlayer(game, 'guild').hand.map(c => c.id), held.map(c => c.id),
|
||
'Inspection transfers no physical card');
|
||
assert.equal(nativeWorldBattlePlayer(game, 'guild').spice, nativeWorldBattlePlayer(before, 'guild').spice);
|
||
assert.equal(nativeWorldBattlePlayer(game, 'choam').spice, nativeWorldBattlePlayer(before, 'choam').spice);
|
||
game = advanceNativeWorldBattle(game, nativeWorldBattleFinished);
|
||
assertNativeWorldBattleInventory(game, f.afterSetup);
|
||
});
|