Selected Ecaz/Moritani decks with independent cards and Nexus now retain native Occupy, Advanced assassination, arrival ordering and shared Duke consumers, including original optional modules. Keep the trained E3/Harkonnen capture boundary and remaining individual Nexus effect limits explicit. Validation: frozen 17-file 168/168, types/lint/build, seven finite original-action and table-SSR programmes, plus four minimal legal Cunning/expiry policies. Fixture corrections preserve the printed alliance draw gate and real paid placement affordability. No stored-game or production change; comprehensive and deployed acceptance remain open.
110 lines
7.4 KiB
TypeScript
110 lines
7.4 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 { REINFORCEMENTS_CARD } from '../game/reinforcements';
|
|
import { HARASS_WITHDRAW_CARD } from '../game/harass-withdraw';
|
|
import {
|
|
createE3IndependentNexusOccupyFixture, e3IndependentNexusOccupyClean,
|
|
e3IndependentNexusOccupyPlayer as player, quoteE3IndependentNexusOccupy,
|
|
revealE3IndependentNexusOccupy, settleE3IndependentNexusOccupy,
|
|
} from './fixture-e3-independent-nexus-occupy';
|
|
|
|
for (const modules of [false, true]) for (const lead of ['ecaz', 'ally'] as const) {
|
|
void test(`original ${modules ? 'all-family World/all14/phase' : 'Ecaz'} independent Nexus ${lead} Occupy lead pays only its own fee and typed undialed return`, () => {
|
|
const f = createE3IndependentNexusOccupyFixture({ lead,
|
|
expansions: modules ? ['ix', 'choam', 'ecaz'] : ['ecaz'],
|
|
homeworlds: modules, skills: modules, tech: modules, strongholds: modules });
|
|
const beforeEcaz = player(f.game, 'ecaz'), beforeAlly = player(f.game, 'emperor'), beforeEnemy = player(f.game, 'guild');
|
|
const originalEcaz = Object.fromEntries(Object.entries(player(f.afterSetup, 'ecaz').forces)
|
|
.filter(([key]) => key.startsWith('imperial_basin:')));
|
|
assert.equal(Object.values(originalEcaz).reduce((sum, count) => sum + count, 0), 6);
|
|
assert.equal(f.shipments.length, 3);
|
|
assert.deepEqual(f.shipments.map(shipment => [shipment.step.actor, shipment.step.action.amount, shipment.step.action.elite])
|
|
.sort(([one], [two]) => String(one).localeCompare(String(two))),
|
|
[['ecaz', 5, 0], ['emperor', 5, 2], ['guild', 3, 0]]);
|
|
assert.equal(f.purchases.length, 2);
|
|
assert.ok(f.purchases.every(purchase => purchase.step.actor === f.cardUser && purchase.step.action.amount === 1));
|
|
assert.equal(f.beforeAlliance.turn, 2);
|
|
assert.equal(f.afterNexusDraw.nexusCards!.cards!.hands.guild, 'guild');
|
|
assert.equal(f.drawStep.action.type, 'nexusCardChoice');
|
|
assert.equal(player(f.afterNexusDraw, 'ecaz').ally, 'emperor');
|
|
assert.equal(player(f.afterNexusDraw, 'emperor').ally, 'ecaz');
|
|
assert.equal(player(f.afterNexusDraw, 'guild').ally, null);
|
|
assert.equal(f.beforeLead.decision?.kind, 'ecazBattleLead');
|
|
assert.equal(f.leadStep.action.lead, f.cardUser);
|
|
assert.deepEqual([f.profile.planOwner, f.profile.payer, f.profile.forceOwner], [f.cardUser, f.cardUser, 'emperor']);
|
|
assert.deepEqual([f.profile.ecazForces.normal, f.profile.ecazForces.elite], [5, 0]);
|
|
assert.deepEqual([f.profile.forces.normal, f.profile.forces.elite], [3, 2]);
|
|
assert.equal(f.profile.fixedEcazDial, 3);
|
|
assert.equal(f.plans[0].action.dial, 5);
|
|
assert.equal(f.support, modules ? 0 : 1, 'Actual high Salusa supports the real stars; ordinary Advanced uses one paid star');
|
|
assert.ok(player(f.game, f.cardUser).hand.some(card => card.id === REINFORCEMENTS_CARD));
|
|
assert.ok(player(f.game, f.cardUser).hand.some(card => card.id === HARASS_WITHDRAW_CARD));
|
|
if (modules) {
|
|
assert.ok(f.plans[0].action.reinforcementsSources);
|
|
assert.equal(f.afterFirstMentat.strongholdCards!.owners.arrakeen, 'ecaz');
|
|
}
|
|
|
|
const revealed = revealE3IndependentNexusOccupy(f.game, f.plans, f.actions);
|
|
const quote = quoteE3IndependentNexusOccupy(revealed, f.cardUser, f.returns);
|
|
assert.equal(quote.result, 'normal');
|
|
assert.equal(quote.winner, f.cardUser);
|
|
assert.deepEqual(quote.reinforcements, [{ player: f.cardUser, card: REINFORCEMENTS_CARD, normal: 3, elite: 0 }]);
|
|
assert.deepEqual(quote.fixedLosses, [{ owner: 'ecaz', normal: 3, elite: 0 }]);
|
|
assert.equal(quote.casualties!.owner, 'emperor');
|
|
assert.equal(quote.casualties!.dial, 2, 'The original fixed contribution and score bonus do not enter the variable physical dial');
|
|
assert.ok(quote.casualties!.options.some(loss => loss.normal === 0 && loss.elite === 1));
|
|
const withdrawal = quote.harassWithdraw!.find(entry => entry.player === f.cardUser)!;
|
|
assert.deepEqual(withdrawal.returned, lead === 'ecaz' ? { normal: 2, elite: 0 } : { normal: 3, elite: 1 });
|
|
assert.deepEqual(withdrawal.locations, f.returns);
|
|
assert.deepEqual([withdrawal.remaining.normal, withdrawal.remaining.elite], lead === 'ecaz' ? [3, 0] : [0, 1]);
|
|
const plan = revealed.battle!.plans[f.cardUser];
|
|
const leader = player(revealed, f.cardUser).leaders.find(leader => leader.id === plan.leader)!;
|
|
const side = revealed.battle!.attacker === f.cardUser ? 'attacker' : 'defender';
|
|
assert.equal(quote.scores![side], leader.strength + 5 + 2, 'Reinforcements contributes exactly two normal-score points');
|
|
|
|
const game = settleE3IndependentNexusOccupy(revealed, f.returns, f.actions);
|
|
assert.equal(game.phase, 7);
|
|
assert.equal(game.battle, null);
|
|
assert.ok(e3IndependentNexusOccupyClean(game));
|
|
assert.equal(game.lastBattleContext!.winner, f.cardUser);
|
|
assert.equal(game.lastBattleContext!.result, 'normal');
|
|
const ecaz = player(game, 'ecaz'), ally = player(game, 'emperor'), enemy = player(game, 'guild');
|
|
assert.deepEqual(Object.fromEntries(Object.entries(ecaz.forces).filter(([key]) => key.startsWith('imperial_basin:'))), originalEcaz);
|
|
assert.equal(ecaz.forces[f.location] ?? 0, lead === 'ecaz' ? 0 : 2);
|
|
assert.equal(ecaz.reserves, beforeEcaz.reserves + (lead === 'ecaz' ? -1 : 0));
|
|
assert.equal(ecaz.tanks, beforeEcaz.tanks + (lead === 'ecaz' ? 6 : 3));
|
|
assert.equal(ally.forces[f.location] ?? 0, lead === 'ecaz' ? 4 : 0);
|
|
assert.equal(ally.elites!.forces[f.location] ?? 0, lead === 'ecaz' ? 1 : 0);
|
|
assert.equal(ally.reserves, beforeAlly.reserves + (lead === 'ecaz' ? 0 : 1));
|
|
assert.equal(ally.elites!.reserves, beforeAlly.elites!.reserves + (lead === 'ecaz' ? 0 : 1));
|
|
assert.equal(ally.tanks, beforeAlly.tanks + (lead === 'ecaz' ? 1 : 4));
|
|
assert.equal(ally.elites!.tanks, beforeAlly.elites!.tanks + 1);
|
|
assert.equal(enemy.forces[f.location] ?? 0, 0);
|
|
assert.equal(enemy.tanks, beforeEnemy.tanks + 3);
|
|
assert.equal(enemy.reserves, beforeEnemy.reserves);
|
|
assert.equal(game.nexusCards!.cards!.hands.guild, 'guild', 'The original unspent closing-draw card remains held');
|
|
for (const card of [REINFORCEMENTS_CARD, HARASS_WITHDRAW_CARD]) {
|
|
assert.equal(game.discard.filter(physical => physical.id === card).length, 1);
|
|
assert.ok(!player(game, f.cardUser).hand.some(physical => physical.id === card));
|
|
}
|
|
});
|
|
}
|
|
|
|
void test('four minimal legal policies use original independent cards through the selected Occupy owner with real World sources', () => {
|
|
for (const lead of ['ecaz', 'ally'] as const) {
|
|
const f = createE3IndependentNexusOccupyFixture({ lead, homeworlds: true, expansions: ['ix', 'choam', 'ecaz'] });
|
|
for (const difficulty of ['Easy', 'Medium', 'Hard', 'Brutal'] as const) {
|
|
const view = viewGame(f.game, f.cardUser);
|
|
view.players.find(player => player.id === f.cardUser)!.bot = difficulty;
|
|
const action = botActions(view).find(action => action.type === 'battlePlan' &&
|
|
Number(action.dial) > f.profile.fixedEcazDial &&
|
|
(action.weapon === REINFORCEMENTS_CARD || action.defense === REINFORCEMENTS_CARD));
|
|
assert.ok(action, `${lead} ${difficulty} must expose its own card with a positive variable commitment`);
|
|
assert.ok(action.reinforcementsSources, 'The selected payer declares an actual own native reserve source');
|
|
assert.ok(applyAction(f.game, f.cardUser, action).battle!.plans[f.cardUser]);
|
|
}
|
|
}
|
|
});
|