Connect canonical Moritani winning discs and queued Guild arrivals
This commit is contained in:
1 parent
a5a6db7868
commit
1cd7db7d0b
26 files changed
+1318
-148
No files matched your search
@@ -0,0 +1,149 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { viewGame, type Game } from '../game/engine';
|
||||
import { leaders } from '../game/cards';
|
||||
import { territory } from '../game/board';
|
||||
import { DUKE_VIDAL_ID } from '../game/duke-vidal';
|
||||
import {
|
||||
advancePairedE3Nexus, createPairedE3NexusSetup, nextPairedE3NexusStep,
|
||||
pairedE3NexusClean, pairedE3NexusInventory, pairedE3NexusPlayer,
|
||||
pairedE3NexusReload, quietPairedE3NexusChoice, revealPairedE3Nexus,
|
||||
stepPairedE3Nexus,
|
||||
type PairedE3NexusStep, type PairedE3NexusTransition,
|
||||
} from './fixture-paired-e3-nexus-modules';
|
||||
import { quietPlan } from './fixture-nexus-duke-capture';
|
||||
|
||||
/** Original Advanced paired Ecaz/Moritani/Guild Nexus, without Skills or optional
|
||||
* modules. The paired fixture selects only original undealt setup entropy,
|
||||
* including the Ecaz Traitor Card Moritani actually keeps. No played hand,
|
||||
* wallet, force, leader, battle, phase or custody field is assigned here.
|
||||
*
|
||||
* Ecaz pays for its original reusable Ambassador; Moritani's actual paid
|
||||
* shipment triggers its acquisition. Ecaz's own paid shipment makes the real
|
||||
* battle in that same stronghold. This is not a synthetic Duke controller. */
|
||||
export interface MoritaniEcazDukeWinner {
|
||||
initial: Game;
|
||||
offered: Game;
|
||||
afterSetup: Game;
|
||||
ecaz: string;
|
||||
moritani: string;
|
||||
token: string;
|
||||
location: string;
|
||||
sector: number;
|
||||
target: string;
|
||||
placement: PairedE3NexusTransition;
|
||||
acquisition: PairedE3NexusTransition;
|
||||
shipments: PairedE3NexusTransition[];
|
||||
beforeBattle: Game;
|
||||
choice: PairedE3NexusStep;
|
||||
plans: PairedE3NexusStep[];
|
||||
/** Both real sealed plans revealed; original traitor declarations still due. */
|
||||
ready: Game;
|
||||
/** The real missing-before-fix private Assassinate Leaders opportunity. */
|
||||
pending: Game;
|
||||
actions: PairedE3NexusStep[];
|
||||
}
|
||||
|
||||
function settlementChoice(g: Game): PairedE3NexusStep | null {
|
||||
const d = g.decision;
|
||||
if (!d || g.response || g.phaseOpening) return null;
|
||||
if (d.kind === 'battleLosses') return { actor: d.player, action: { type: 'decision', choice: 0 } };
|
||||
if (d.kind === 'battleCards') return { actor: d.player, action: { type: 'decision', discard: [] } };
|
||||
if (d.kind === 'moritaniRetention') return { actor: d.player, action: { type: 'decision', keep: null,
|
||||
...(d.source === 'nexus' ? { event: d.event } : {}) } };
|
||||
return quietPairedE3NexusChoice(g);
|
||||
}
|
||||
|
||||
/** Original cleanup only; no forged resolution or erased saved continuation. */
|
||||
export function settleMoritaniEcazDukeWinner(g: Game, stopAtChoice = false,
|
||||
actions?: PairedE3NexusStep[]): Game {
|
||||
for (let n = 0; n < 400; n++) {
|
||||
if (!g.response && !g.phaseOpening &&
|
||||
(stopAtChoice && g.decision?.kind === 'moritaniAssassinate' ||
|
||||
!g.battle && pairedE3NexusClean(g))) return g;
|
||||
const next = nextPairedE3NexusStep(g) ?? settlementChoice(g);
|
||||
assert.ok(next, `Original Ecaz/Duke cleanup needs its owner at ${g.decision?.kind ?? g.phase}`);
|
||||
g = stepPairedE3Nexus(g, next, actions);
|
||||
}
|
||||
throw Error('Original Ecaz/Duke battle cleanup did not finish');
|
||||
}
|
||||
|
||||
export function createMoritaniEcazDukeWinner(): MoritaniEcazDukeWinner {
|
||||
const f = createPairedE3NexusSetup({ roster: ['ecaz', 'moritani', 'guild'],
|
||||
advanced: true, skills: false, homeworlds: false, discovery: false,
|
||||
tech: false, strongholds: false, assassinationFaction: 'ecaz' });
|
||||
const actions = f.actions;
|
||||
let g = pairedE3NexusReload(f.afterSetup);
|
||||
// Reuse the native Duke fixture's original supply/empty-stronghold selection.
|
||||
const native = quietPlan(g), token = native.token, location = native.destination;
|
||||
const sector = territory(location).sectors.find(s => s !== g.storm)!;
|
||||
assert.ok(Number.isInteger(sector));
|
||||
const target = leaders('ecaz').find(l => pairedE3NexusPlayer(g, f.moritani).traitors.includes(l.id))!.id;
|
||||
assert.ok(target);
|
||||
|
||||
g = advancePairedE3Nexus(g, s => !s.response && !s.phaseOpening &&
|
||||
s.decision?.kind === 'ecazPlacement', actions, quietPairedE3NexusChoice);
|
||||
const placementBefore = pairedE3NexusReload(g);
|
||||
const placementStep: PairedE3NexusStep = { actor: f.ecaz,
|
||||
action: { type: 'decision', token, territory: location } };
|
||||
g = stepPairedE3Nexus(g, placementStep, actions);
|
||||
g = advancePairedE3Nexus(g, s => !s.response &&
|
||||
s.ecazAmbassadors!.tokens.find(t => t.id === token)!.zone === 'placed', actions, quietPairedE3NexusChoice);
|
||||
const placement = { before: placementBefore, step: placementStep, after: pairedE3NexusReload(g) };
|
||||
|
||||
const remaining = new Set([f.ecaz, f.moritani]);
|
||||
const shipments: PairedE3NexusTransition[] = [];
|
||||
let acquisition: PairedE3NexusTransition | null = null;
|
||||
while (remaining.size) {
|
||||
g = advancePairedE3Nexus(g, s => s.phase === 5 && pairedE3NexusClean(s) &&
|
||||
remaining.has(s.active!), actions, quietPairedE3NexusChoice);
|
||||
const actor = g.active!, before = pairedE3NexusReload(g);
|
||||
const step: PairedE3NexusStep = { actor, action: { type: 'ship', territory: location,
|
||||
sector, amount: 1, elite: 0, allyPayment: 0 } };
|
||||
g = stepPairedE3Nexus(g, step, actions);
|
||||
g = advancePairedE3Nexus(g, s => !s.response && !s.phaseOpening &&
|
||||
(s.decision?.kind === 'ecazAmbassador' || pairedE3NexusClean(s) && !s.pendingShipment),
|
||||
actions, quietPairedE3NexusChoice);
|
||||
if (actor === f.moritani) {
|
||||
assert.equal(g.decision?.kind, 'ecazAmbassador');
|
||||
const entry = g.pendingAmbassador!;
|
||||
assert.equal(entry.token, token);
|
||||
assert.equal(entry.stage, 'offer');
|
||||
assert.equal(entry.entrant, f.moritani);
|
||||
const acquire: PairedE3NexusStep = { actor: f.ecaz, action: { type: 'decision',
|
||||
event: entry.event, trigger: true, beneficiary: f.ecaz, choice: 'duke' } };
|
||||
const acquisitionBefore = pairedE3NexusReload(g);
|
||||
g = stepPairedE3Nexus(g, acquire, actions);
|
||||
acquisition = { before: acquisitionBefore, step: acquire, after: pairedE3NexusReload(g) };
|
||||
}
|
||||
g = advancePairedE3Nexus(g, s => pairedE3NexusClean(s) && !s.pendingShipment,
|
||||
actions, quietPairedE3NexusChoice);
|
||||
shipments.push({ before, step, after: pairedE3NexusReload(g) });
|
||||
remaining.delete(actor);
|
||||
if (remaining.size) g = stepPairedE3Nexus(g, { actor, action: { type: 'endMovement' } }, actions);
|
||||
}
|
||||
assert.ok(acquisition);
|
||||
assert.equal(g.dukeVidal!.controller, f.ecaz);
|
||||
assert.equal(g.dukeVidal!.source, 'ecaz');
|
||||
g = advancePairedE3Nexus(g, s => s.phase === 6 && pairedE3NexusClean(s), actions, quietPairedE3NexusChoice);
|
||||
const beforeBattle = pairedE3NexusReload(g);
|
||||
const physical = viewGame(g, g.active!).battleChoices.find(c => c.territory === location);
|
||||
assert.ok(physical);
|
||||
const choice: PairedE3NexusStep = { actor: physical.chooser, action: { type: 'chooseBattle',
|
||||
territory: location, target: physical.attacker === physical.chooser ? physical.defender : physical.attacker } };
|
||||
g = stepPairedE3Nexus(g, choice, actions);
|
||||
const losing = pairedE3NexusPlayer(g, f.moritani).leaders.filter(l => !l.dead && !l.usedAt)
|
||||
.sort((a, b) => a.strength - b.strength)[0];
|
||||
assert.ok(losing && losing.strength < 6);
|
||||
const plans: PairedE3NexusStep[] = [
|
||||
{ actor: f.ecaz, action: { type: 'battlePlan', leader: DUKE_VIDAL_ID, dial: 0, support: 0 } },
|
||||
{ actor: f.moritani, action: { type: 'battlePlan', leader: losing.id, dial: 0, support: 0 } },
|
||||
];
|
||||
const ready = revealPairedE3Nexus(g, plans, actions);
|
||||
const pending = settleMoritaniEcazDukeWinner(ready, true, actions);
|
||||
assert.equal(pending.decision?.kind, 'moritaniAssassinate',
|
||||
'A real normal Moritani loss to Ecaz using surviving Duke6 must open the private opportunity');
|
||||
pairedE3NexusInventory(pending);
|
||||
return { initial: f.initial, offered: f.offered, afterSetup: f.afterSetup,
|
||||
ecaz: f.ecaz, moritani: f.moritani, token, location, sector, target,
|
||||
placement, acquisition, shipments, beforeBattle, choice, plans, ready, pending, actions };
|
||||
}
|
||||
Reference in new issue
Block a user