Restore fresh Ecaz coalition and Advanced Moritani initialization; retain printed native continuations and selected-family card eligibility. Validate Mentat fallback hands against explicitly selected independent cards. Frozen25-file274/274, types/lint/build and genuine runtime/mobile entry proofs pass. No save retrofit or deployment.
146 lines
8.7 KiB
TypeScript
146 lines
8.7 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
import { applyAction, viewGame, type Game } from '../game/engine';
|
|
import { MOBILE_LOCATION } from '../game/board';
|
|
import { REINFORCEMENTS_CARD } from '../game/reinforcements';
|
|
import {
|
|
assertNativeFamilyReinforcementsInventory,
|
|
createNativeFamilyReinforcementsFixture,
|
|
finishNativeFamilyReinforcementsBattle,
|
|
nativeFamilyReinforcementsClean,
|
|
nativeFamilyReinforcementsPlayer as player,
|
|
revealNativeFamilyReinforcementsBattle,
|
|
sealNativeFamilyReinforcementsPlan,
|
|
type NativeFamilyReinforcementsFixture,
|
|
type NativeFamilyReinforcementsStep,
|
|
} from './fixture-native-family-reinforcements';
|
|
|
|
function assertDisposed(game: Game,
|
|
fixture: NativeFamilyReinforcementsFixture): void {
|
|
assert.equal(game.battle, null);
|
|
assert.ok(nativeFamilyReinforcementsClean(game));
|
|
assert.equal(game.discard.filter(c => c.id === REINFORCEMENTS_CARD).length, 1);
|
|
assert.ok(!player(game, fixture.owner).hand.some(c => c.id === REINFORCEMENTS_CARD));
|
|
assertNativeFamilyReinforcementsInventory(game);
|
|
}
|
|
|
|
for (const advanced of [false, true]) {
|
|
for (const faction of ['ixians', 'tleilaxu', 'choam'] as const) {
|
|
for (const slot of ['weapon', 'defense'] as const) {
|
|
void test(`${advanced ? 'Advanced' : 'Basic'} native ${faction} ${slot}: fee3/+2 wins without more physical dial or support`, () => {
|
|
const f = createNativeFamilyReinforcementsFixture({ faction, advanced, slot });
|
|
const own = player(f.game, f.owner), opponent = player(f.game, f.opponent);
|
|
const preview = viewGame(f.game, f.owner).battle!.reinforcements;
|
|
assert.equal(preview?.blocked, null);
|
|
|
|
const first = sealNativeFamilyReinforcementsPlan(structuredClone(f.game), f.planActions[0]);
|
|
assert.equal(player(first, f.owner).reserves, own.reserves, 'A private plan does not spend the eventual reveal fee');
|
|
assert.equal(player(first, f.owner).tanks, own.tanks);
|
|
const revealed = sealNativeFamilyReinforcementsPlan(first, f.planActions[1]);
|
|
assert.deepEqual(player(revealed, f.owner).forces, own.forces, 'Reinforcements has not shipped an army into the battle');
|
|
assert.deepEqual(player(revealed, f.owner).elites?.forces, own.elites?.forces);
|
|
|
|
const continuation: NativeFamilyReinforcementsStep[] = [];
|
|
const done = finishNativeFamilyReinforcementsBattle(revealed, { actions: continuation });
|
|
const after = player(done, f.owner);
|
|
assert.equal(done.lastBattleContext!.result, 'normal');
|
|
assert.equal(done.lastBattleContext!.winner, f.owner);
|
|
assert.equal(after.reserves, own.reserves - 3);
|
|
assert.equal(after.elites?.reserves, own.elites?.reserves, 'Normal reserves pay before untouched cyborg reserves');
|
|
assert.equal(after.tanks, 5, 'Fee3 plus exactly two original physical committed tokens');
|
|
assert.equal(after.battleLosses, 5);
|
|
assert.equal(after.elites?.tanks ?? 0, faction === 'ixians' ? 1 : 0);
|
|
assert.equal(after.forces[f.location], 1);
|
|
assert.equal(after.elites?.forces[f.location] ?? 0, faction === 'ixians' ? 1 : 0);
|
|
assert.equal(player(done, f.opponent).reserves, opponent.reserves);
|
|
assert.equal(player(done, f.opponent).tanks, 3);
|
|
if (faction === 'choam' && advanced) assert.ok(continuation.some(s =>
|
|
s.action.type === 'decision' && s.action.audit === false), 'The surviving original Auditor still offers its real inspection');
|
|
assertDisposed(done, f);
|
|
|
|
// Holding all original armies, leaders, dial and support fixed, remove
|
|
// only R: total 3.5 (Ix) or 4 (others) loses to the same printed five.
|
|
// R adds precisely two strength, not extra supported physical tokens.
|
|
const plain = structuredClone(f.planActions[0]);
|
|
plain.action.weapon = null; plain.action.defense = null;
|
|
let control = sealNativeFamilyReinforcementsPlan(structuredClone(f.game), plain);
|
|
control = sealNativeFamilyReinforcementsPlan(control, f.planActions[1]);
|
|
control = finishNativeFamilyReinforcementsBattle(control);
|
|
assert.equal(control.lastBattleContext!.winner, f.opponent);
|
|
assert.equal(player(control, f.owner).reserves, own.reserves);
|
|
assert.equal(player(control, f.owner).tanks, 3);
|
|
assertNativeFamilyReinforcementsInventory(control);
|
|
|
|
const impossible = structuredClone(f.planActions[0].action);
|
|
impossible.dial = faction === 'ixians' ? 6.5 : 5;
|
|
impossible.support = advanced ? faction === 'ixians' ? 2 : 3 : 0;
|
|
const unchanged = structuredClone(f.game);
|
|
assert.throws(() => applyAction(f.game, f.owner, impossible), 'The +2 card does not allow dialing two additional tokens');
|
|
assert.deepEqual(f.game, unchanged);
|
|
if (faction === 'ixians' && advanced) {
|
|
const overSupported = { ...f.planActions[0].action, support: 3 };
|
|
assert.throws(() => applyAction(f.game, f.owner, overSupported), 'The original suboid cannot receive spice support');
|
|
assert.deepEqual(f.game, unchanged);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// Distinct native consequences, rather than repeated roster/default rows:
|
|
// cyborg losses/explosion, Tleilaxu's genuine Face Dancer window, and the
|
|
// Auditor's exceptional-outcome continuation all retain the revealed fee.
|
|
for (const { faction, outcome } of [
|
|
{ faction: 'ixians', outcome: 'loser' },
|
|
{ faction: 'ixians', outcome: 'explosion' },
|
|
{ faction: 'tleilaxu', outcome: 'loser' },
|
|
{ faction: 'tleilaxu', outcome: 'traitor' },
|
|
{ faction: 'choam', outcome: 'traitor' },
|
|
{ faction: 'choam', outcome: 'explosion' },
|
|
] as const) {
|
|
void test(`${advanced ? 'Advanced' : 'Basic'} native ${faction} ${outcome}: revealed fee and mandatory discard survive native aftermath`, () => {
|
|
const f = createNativeFamilyReinforcementsFixture({ faction, advanced, outcome,
|
|
slot: advanced ? 'defense' : 'weapon' });
|
|
const before = player(f.game, f.owner), continuation: NativeFamilyReinforcementsStep[] = [];
|
|
const done = finishNativeFamilyReinforcementsBattle(revealNativeFamilyReinforcementsBattle(f), {
|
|
callers: outcome === 'traitor' ? [f.opponent] : [], actions: continuation,
|
|
});
|
|
const own = player(done, f.owner);
|
|
assert.equal(done.lastBattleContext!.result, outcome === 'loser' ? 'normal' : outcome);
|
|
assert.equal(done.lastBattleContext!.winner, outcome === 'explosion' ? null : f.opponent);
|
|
assert.equal(own.reserves, before.reserves - 3);
|
|
assert.equal(own.elites?.reserves, before.elites?.reserves);
|
|
assert.equal(own.tanks, 6, 'Fee3 and only the three original losing/exploding counters');
|
|
assert.equal(own.battleLosses, 6);
|
|
assert.equal(own.elites?.tanks ?? 0, faction === 'ixians' ? 2 : 0);
|
|
assert.equal(own.forces[f.location] ?? 0, 0);
|
|
assert.equal(own.spice, before.spice - Number(f.planActions[0].action.support), 'Only the printed physical support is paid, with no support for fee counters or +2');
|
|
if (faction === 'choam' && advanced) assert.ok(continuation.some(s =>
|
|
s.action.type === 'decision' && s.action.audit === false), 'The dead or surviving Auditor is handled by its original control');
|
|
if (faction === 'tleilaxu' && outcome === 'loser') assert.ok(continuation.some(s =>
|
|
s.action.type === 'decision' && s.action.reveal === false), 'The actual matching native Face Dancer is offered after the Guild victory');
|
|
assertDisposed(done, f);
|
|
});
|
|
}
|
|
|
|
void test(`${advanced ? 'Advanced' : 'Basic'} selected Ix+CHOAM+Ecaz cards: nine paid suboids leave a real one-suboid/two-cyborg fee`, () => {
|
|
const f = createNativeFamilyReinforcementsFixture({ advanced, faction: 'ixians', mixedFee: true,
|
|
expansions: ['ix', 'choam', 'ecaz'], slot: advanced ? 'weapon' : 'defense' });
|
|
const own = player(f.game, f.owner);
|
|
assert.equal(own.reserves, 5);
|
|
assert.equal(own.elites!.reserves, 4);
|
|
const done = finishNativeFamilyReinforcementsBattle(revealNativeFamilyReinforcementsBattle(f));
|
|
const after = player(done, f.owner);
|
|
assert.equal(done.lastBattleContext!.winner, f.owner);
|
|
assert.equal(after.reserves, 2);
|
|
assert.equal(after.elites!.reserves, 2, 'Exactly two original cyborgs supplement the one remaining suboid');
|
|
assert.equal(after.tanks, 4, 'Three fee tokens plus one half-strength dialed suboid');
|
|
assert.equal(after.elites!.tanks, 2, 'Reserve cyborg fee enters matching elite Tanks, not normal Tanks');
|
|
assert.equal(after.battleLosses, 4);
|
|
assert.equal(after.forces[f.location], 8);
|
|
assert.equal(after.elites!.forces[f.location] ?? 0, 0);
|
|
assert.equal(after.forces[MOBILE_LOCATION], 6);
|
|
assert.equal(after.elites!.forces[MOBILE_LOCATION], 3);
|
|
assertDisposed(done, f);
|
|
});
|
|
}
|