Retain selected decks and original optional modules through normal setup, distinguish native Ixian Betrayal from absent-faction replacement, route explicit CHOAM fuel before printed effects, and preserve native No-Field invoices and pair continuation. Connect canonical native cancellation stock and existing human/minimal AI consumers. Validation: 194 affected tests across 19 files, types/lint/build, bounded native invoice review and seven finite original-action/rendered-control programmes. Runtime ec5ad6c18996d8d958ec0d01132ef3e3ea6d23d36188902eca9a63d4db2efbf8; documentation-only proof overlay a5892004937434429916496497c07a3838a324b4a5d64d4a0eb804ab5212d5bd. Other envelopes, full combinations, comprehensive assurance and deployed acceptance remain open. No saved-room/provider changes.
161 lines
11 KiB
TypeScript
161 lines
11 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import { test } from 'node:test';
|
|
import { applyAction, viewGame } from '../game/engine';
|
|
import { botActions } from '../game/bots';
|
|
import { MOBILE_LOCATION } from '../game/board';
|
|
import { REINFORCEMENTS_CARD } from '../game/reinforcements';
|
|
import { HARASS_WITHDRAW_CARD } from '../game/harass-withdraw';
|
|
import {
|
|
activateSelectedIndependentSuboids, actSelectedIndependentNexusBattle,
|
|
advanceSelectedIndependentNexusBattle, createSelectedIndependentNexusBattleFixture,
|
|
revealSelectedIndependentNexusPlans, selectedIndependentNexusClean,
|
|
selectedIndependentNexusPlayer as player,
|
|
} from './fixture-selected-independent-nexus-battles';
|
|
|
|
for (const advanced of [false, true]) {
|
|
void test(`${advanced ? 'Advanced' : 'Basic'} original selected Ix Cunning pays its mixed physical fee before Harass returns eight Suboids`, () => {
|
|
const f = createSelectedIndependentNexusBattleFixture({ advanced, program: 'mixed-return', expansions: ['ix'] });
|
|
assert.deepEqual(f.fee, { normal: 1, elite: 2 });
|
|
assert.equal(f.ownShipment.action.amount, 9);
|
|
assert.equal(f.ownShipment.action.elite, 0);
|
|
assert.equal(player(f.beforeShipment, 'ixians').spice - player(f.afterShipment, 'ixians').spice, 9);
|
|
assert.equal(f.purchases.length, 2);
|
|
assert.ok(f.purchases.every(purchase => purchase.step.action.type === 'bid' && purchase.step.action.amount === 1));
|
|
if (!advanced) assert.ok(f.actions.some(step => step.actor === 'ixians' && step.action.type === 'charity'),
|
|
'After spending the original ten spice, Basic funds the second bid through its actual eligible charity claim');
|
|
assert.ok(player(f.purchases[1].before, 'ixians').spice >= 1);
|
|
assert.equal(f.afterNexusDraw.nexusCards!.cards!.hands.ixians, 'ixians');
|
|
assert.equal(player(f.afterNexusDraw, 'guild').ally, 'emperor');
|
|
assert.equal(player(f.afterNexusDraw, 'ixians').ally, null);
|
|
assert.ok(player(f.game, 'ixians').hand.some(card => card.id === REINFORCEMENTS_CARD));
|
|
assert.ok(player(f.game, 'ixians').hand.some(card => card.id === HARASS_WITHDRAW_CARD));
|
|
const before = player(f.game, 'ixians');
|
|
let game = activateSelectedIndependentSuboids(f.game, f.actions);
|
|
const own = viewGame(game, 'ixians').battle!.ownForces!;
|
|
assert.deepEqual([own.normal, own.elite, own.normalFixedHalf, own.normalFreeSupport], [9, 0, false, true]);
|
|
assert.deepEqual(viewGame(game, 'ixians').battle!.reinforcements!.normal, 1);
|
|
assert.deepEqual(viewGame(game, 'ixians').battle!.reinforcements!.elite, 2);
|
|
game = revealSelectedIndependentNexusPlans(game, f.plans, f.actions);
|
|
assert.equal(game.decision, null,
|
|
'One ordinary pool has a unique undialed return and needs no invented allocation prompt');
|
|
game = advanceSelectedIndependentNexusBattle(game, state => !state.battle && selectedIndependentNexusClean(state), f.actions);
|
|
const after = player(game, 'ixians');
|
|
assert.equal(game.lastBattleContext!.winner, 'ixians');
|
|
assert.equal(game.lastBattleContext!.result, 'normal');
|
|
assert.equal(after.forces[f.location] ?? 0, 0);
|
|
assert.equal(after.reserves, before.reserves - 3 + 8);
|
|
assert.equal(after.elites!.reserves, before.elites!.reserves - 2,
|
|
'Returned ordinary Suboids cannot replace the originally quoted two Cyborg fee counters');
|
|
assert.equal(after.tanks, before.tanks + 4, 'The original fee3 and one committed physical Suboid pay once');
|
|
assert.equal(after.elites!.tanks, before.elites!.tanks + 2);
|
|
assert.equal(after.forces[MOBILE_LOCATION], before.forces[MOBILE_LOCATION]);
|
|
assert.equal(after.elites!.forces[MOBILE_LOCATION], before.elites!.forces[MOBILE_LOCATION]);
|
|
assert.ok(!game.pendingIxSubstitution,
|
|
'Reserve-fee Cyborgs are not actual battlefield Cyborg casualties available for substitution');
|
|
assert.equal(player(game, 'guild').tanks, player(f.game, 'guild').tanks + 3);
|
|
for (const card of [REINFORCEMENTS_CARD, HARASS_WITHDRAW_CARD])
|
|
assert.equal(game.discard.filter(physical => physical.id === card).length, 1);
|
|
assert.equal(game.nexusCards!.cards!.discard.filter(card => card === 'ixians').length, 1);
|
|
assert.equal(viewGame(game, 'ixians').nexusSuboids!.active, true);
|
|
});
|
|
}
|
|
|
|
for (const modules of [false, true]) {
|
|
void test(`original selected ${modules ? 'union E1/E2 Worlds/all14/phase' : 'E1'} Cunning preserves actual Cyborg losses and equal residual Suboid substitution`, () => {
|
|
const f = createSelectedIndependentNexusBattleFixture({ advanced: true, program: 'substitution',
|
|
expansions: modules ? ['ix', 'choam'] : ['ix'], homeworlds: modules, skills: modules,
|
|
tech: modules, strongholds: modules });
|
|
assert.deepEqual(f.fee, { normal: 3, elite: 0 });
|
|
assert.equal(f.ownShipment.action.amount, 6);
|
|
assert.equal(f.ownShipment.action.elite, 3);
|
|
if (modules) {
|
|
assert.ok(f.plans[0].action.reinforcementsSources);
|
|
assert.equal(f.afterFirstMentat.strongholdCards!.owners.arrakeen, 'ixians');
|
|
}
|
|
const original = player(f.game, 'ixians');
|
|
let game = activateSelectedIndependentSuboids(f.game, f.actions);
|
|
game = revealSelectedIndependentNexusPlans(game, f.plans, f.actions);
|
|
game = advanceSelectedIndependentNexusBattle(game, state => !state.response && !state.phaseOpening &&
|
|
(state.decision?.kind === 'battleLosses' || state.decision?.kind === 'ixSubstitution'), f.actions);
|
|
if (game.decision?.kind === 'battleLosses') {
|
|
const choice = game.decision.options.findIndex(loss => loss.normal === 1 && loss.elite === 1); assert.ok(choice >= 0);
|
|
game = actSelectedIndependentNexusBattle(game, { actor: 'ixians', action: { type: 'decision', choice } }, f.actions);
|
|
}
|
|
game = advanceSelectedIndependentNexusBattle(game, state => !state.response && !state.phaseOpening &&
|
|
state.decision?.kind === 'ixSubstitution', f.actions);
|
|
assert.equal(player(game, 'ixians').forces[f.location], 4);
|
|
assert.equal(player(game, 'ixians').elites!.forces[f.location], 2);
|
|
assert.equal(player(game, 'ixians').tanks, original.tanks + 5);
|
|
assert.equal(player(game, 'ixians').elites!.tanks, original.elites!.tanks + 1);
|
|
assert.deepEqual(game.pendingIxSubstitution!.losses, { [f.location]: 1 });
|
|
assert.throws(() => applyAction(game, 'ixians', { type: 'decision',
|
|
sources: { [f.location]: 2 }, recover: { [f.location]: 2 } }));
|
|
game = actSelectedIndependentNexusBattle(game, { actor: 'ixians', action: { type: 'decision',
|
|
sources: { [f.location]: 1 }, recover: { [f.location]: 1 } } }, f.actions);
|
|
game = advanceSelectedIndependentNexusBattle(game, state => !state.battle && selectedIndependentNexusClean(state), f.actions);
|
|
const after = player(game, 'ixians');
|
|
assert.equal(game.lastBattleContext!.winner, 'ixians');
|
|
assert.equal(after.forces[f.location], 4);
|
|
assert.equal(after.elites!.forces[f.location], 3);
|
|
assert.equal(after.tanks, original.tanks + 5);
|
|
assert.equal(after.elites!.tanks, original.elites!.tanks);
|
|
assert.equal(after.reserves, original.reserves - 3);
|
|
assert.equal(after.elites!.reserves, original.elites!.reserves);
|
|
assert.equal(game.discard.filter(card => card.id === REINFORCEMENTS_CARD).length, 1);
|
|
assert.equal(after.forces[MOBILE_LOCATION], original.forces[MOBILE_LOCATION]);
|
|
assert.equal(after.elites!.forces[MOBILE_LOCATION], original.elites!.forces[MOBILE_LOCATION]);
|
|
});
|
|
}
|
|
|
|
void test('original selected independent Cunning completes actual reserve fees and residual substitution before a genuinely dealt Face Dance', () => {
|
|
const f = createSelectedIndependentNexusBattleFixture({ advanced: true, program: 'faceDance', expansions: ['ix', 'choam'] });
|
|
assert.ok(player(f.afterSetup, 'tleilaxu').faceDancers!.some(dancer => dancer.leader === f.leader && !dancer.revealed));
|
|
let game = revealSelectedIndependentNexusPlans(activateSelectedIndependentSuboids(f.game, f.actions), f.plans, f.actions);
|
|
game = advanceSelectedIndependentNexusBattle(game, state => !state.response && !state.phaseOpening &&
|
|
(state.decision?.kind === 'battleLosses' || state.decision?.kind === 'ixSubstitution'), f.actions);
|
|
if (game.decision?.kind === 'battleLosses') {
|
|
const choice = game.decision.options.findIndex(loss => loss.normal === 1 && loss.elite === 1); assert.ok(choice >= 0);
|
|
game = actSelectedIndependentNexusBattle(game, { actor: 'ixians', action: { type: 'decision', choice } }, f.actions);
|
|
}
|
|
game = advanceSelectedIndependentNexusBattle(game, state => !state.response && !state.phaseOpening &&
|
|
state.decision?.kind === 'ixSubstitution', f.actions);
|
|
game = actSelectedIndependentNexusBattle(game, { actor: 'ixians', action: { type: 'decision',
|
|
sources: { [f.location]: 1 }, recover: { [f.location]: 1 } } }, f.actions);
|
|
game = advanceSelectedIndependentNexusBattle(game, state => !state.response && !state.phaseOpening &&
|
|
state.decision?.kind === 'faceDance', f.actions);
|
|
assert.equal(game.decision!.kind, 'faceDance');
|
|
const before = player(game, 'ixians'), native = player(game, 'tleilaxu');
|
|
assert.equal(before.forces[f.location], 4);
|
|
assert.equal(before.elites!.forces[f.location], 3);
|
|
assert.equal(game.discard.filter(card => card.id === REINFORCEMENTS_CARD).length, 1);
|
|
game = actSelectedIndependentNexusBattle(game, { actor: 'tleilaxu', action: {
|
|
type: 'decision', reveal: true, sources: { reserves: 4 }, sector: f.sector,
|
|
} }, f.actions);
|
|
game = advanceSelectedIndependentNexusBattle(game, state => !state.battle && selectedIndependentNexusClean(state), f.actions);
|
|
const after = player(game, 'ixians');
|
|
assert.equal(after.forces[f.location] ?? 0, 0);
|
|
assert.equal(after.elites!.forces[f.location] ?? 0, 0);
|
|
assert.equal(after.reserves, before.reserves + 4);
|
|
assert.equal(after.elites!.reserves, before.elites!.reserves + 3);
|
|
assert.equal(after.tanks, before.tanks);
|
|
assert.equal(after.elites!.tanks, before.elites!.tanks);
|
|
assert.equal(after.leaders.find(leader => leader.id === f.leader)!.dead, true);
|
|
assert.equal(player(game, 'tleilaxu').reserves, native.reserves - 4);
|
|
assert.equal(player(game, 'tleilaxu').forces[f.location], 4);
|
|
assert.equal(player(game, 'tleilaxu').faceDancers!.find(dancer => dancer.leader === f.leader)!.revealed, true);
|
|
});
|
|
|
|
void test('all four minimal policies retain a positive native Reinforcements path after genuine selected-family Suboid Cunning', () => {
|
|
const f = createSelectedIndependentNexusBattleFixture({ advanced: true, program: 'substitution', expansions: ['ix', 'choam'], homeworlds: true });
|
|
const game = activateSelectedIndependentSuboids(f.game, f.actions);
|
|
for (const difficulty of ['Easy', 'Medium', 'Hard', 'Brutal'] as const) {
|
|
const view = viewGame(game, 'ixians');
|
|
view.players.find(player => player.id === 'ixians')!.bot = difficulty;
|
|
const action = botActions(view).find(action => action.type === 'battlePlan' && Number(action.dial) > 0 &&
|
|
(action.weapon === REINFORCEMENTS_CARD || action.defense === REINFORCEMENTS_CARD));
|
|
assert.ok(action, `${difficulty} must expose a positive original card plan`);
|
|
assert.ok(action.reinforcementsSources, `${difficulty} must retain the original native source quote`);
|
|
assert.ok(applyAction(game, 'ixians', action).battle!.plans.ixians);
|
|
}
|
|
});
|