220 lines
12 KiB
TypeScript
220 lines
12 KiB
TypeScript
import { applyAction, battles, createGame, initializeNexusKullGameForAudit, joinGame, newPlayer, viewGame, type Game } from '../game/engine';
|
|
import { botActions } from '../game/bots';
|
|
import { takeKullCard } from './fixture-choam-kull';
|
|
import { orderNexusSpice } from './fixture-nexus-cards';
|
|
import type { FactionId } from '../game/catalog';
|
|
import { nextStrongholdFactionsNativeStep } from './fixture-stronghold-factions';
|
|
|
|
export function createNexusChoamKullFixture(options: {
|
|
advanced?: boolean; bg?: boolean; seatIds?: string[]; nexusFace?: 'choam' | 'richese';
|
|
actorFaction?: Extract<FactionId, 'atreides' | 'emperor' | 'beneGesserit' | 'guild' | 'fremen'>;
|
|
fuel?: 'karama' | 'worthless' | 'weapon'; counter?: boolean;
|
|
} = {}): { game: Game; choam: string; actor: string; other: string | null; event: string;
|
|
original: string; fuel: string; counter: string | null } {
|
|
const ids = options.seatIds ?? ['c', 'e', 'h'];
|
|
if (ids.length !== 3 || new Set(ids).size !== 3) throw new Error('Nexus Kull fixture requires three distinct seats.');
|
|
const [choam, actor, other] = ids;
|
|
const needsSubstitute = options.fuel === 'karama' && !options.bg;
|
|
const substitute = needsSubstitute && (options.advanced ?? true);
|
|
if (needsSubstitute && options.advanced === false && options.counter === true)
|
|
throw new Error('Basic has only two physical Karama cards: original and fuel leave no distinct counter.');
|
|
let game = createGame('NXKULL01', newPlayer(choam, 'CHOAM', 'choam'), options.advanced ?? true, ['choam', 'ix']);
|
|
joinGame(game, newPlayer(actor, 'Actor', options.actorFaction ?? (options.bg ? 'beneGesserit' : 'emperor')));
|
|
joinGame(game, newPlayer(other, 'Other', substitute ? 'beneGesserit' : 'harkonnen'));
|
|
for (const p of game.players) { p.bot = 'Medium'; p.ready = true; }
|
|
game.nexusCards = { cards: null, phase: null };
|
|
game = initializeNexusKullGameForAudit(game);
|
|
for (let step = 0; game.status === 'setup' && step < 100; step++) {
|
|
let next: Game | undefined;
|
|
for (const p of game.players) {
|
|
const action = botActions(viewGame(game, p.id))[0];
|
|
if (action) { next = applyAction(game, p.id, action); break; }
|
|
}
|
|
if (!next) throw new Error('Genuine Nexus Kull setup has no legal action.');
|
|
game = next;
|
|
}
|
|
if (game.status !== 'playing') throw new Error('Genuine Nexus Kull setup did not complete.');
|
|
for (const phase of [0, 1]) {
|
|
if (phase === 1) orderNexusSpice(game, ['land', 'land']);
|
|
for (let step = 0; game.phase === phase && step < 80; step++) {
|
|
let next: Game | undefined;
|
|
for (const p of game.players) {
|
|
const view = viewGame(game, p.id);
|
|
view.players.find(seat => seat.id === p.id)!.bot = 'Easy';
|
|
const action = botActions(view)[0];
|
|
if (action) { next = applyAction(game, p.id, action); break; }
|
|
}
|
|
if (!next) throw new Error('Genuine CHOAM/Ix Nexus phase has no legal continuation.');
|
|
game = next;
|
|
}
|
|
if (game.phase !== phase + 1) throw new Error('Genuine CHOAM/Ix Nexus phase did not finish.');
|
|
}
|
|
game.deck.push(...game.players.flatMap(p => p.hand));
|
|
for (const p of game.players) { p.hand = []; delete p.bot; }
|
|
const original = takeKullCard(game, options.bg ? 'Baliset' : 'karama');
|
|
game.players[1].hand.push(original);
|
|
const fuel = takeKullCard(game, options.fuel === 'karama' ? 'karama' : options.fuel === 'weapon' ? 'ix-hunter-seeker' : 'La La La');
|
|
game.players[0].hand.push(fuel);
|
|
const counter = options.counter === false || (needsSubstitute && !substitute)
|
|
? null : takeKullCard(game, substitute ? 'Jubba Cloak' : 'karama');
|
|
if (counter) game.players[2].hand.push(counter);
|
|
const cards = game.nexusCards!.cards!;
|
|
const face = options.nexusFace ?? 'choam';
|
|
const old = cards.hands[choam];
|
|
const holder = game.players.find(p => cards.hands[p.id] === face);
|
|
if (holder) cards.hands[holder.id] = old;
|
|
else {
|
|
const zone = cards.deck.includes(face) ? cards.deck : cards.discard;
|
|
const index = zone.indexOf(face);
|
|
if (index < 0) throw new Error('Missing physical Nexus fixture face.');
|
|
zone.splice(index, 1);
|
|
if (old) cards.deck.push(old);
|
|
}
|
|
cards.hands[choam] = face;
|
|
Object.assign(game, { turn: 2, phase: 5, active: actor, storm: 18, ready: [], decision: null,
|
|
response: null, pendingKarama: null, phaseOpening: null, stormPending: null,
|
|
stormResolution: null, movementRemaining: [...ids] });
|
|
game = applyAction(game, actor, { type: 'card', mode: 'shipment', card: original.id, target: actor });
|
|
if (!game.pendingKull) throw new Error('Genuine Karama attempt did not create the Kull opportunity.');
|
|
return { game, choam, actor, other, event: game.pendingKull.event,
|
|
original: original.id, fuel: fuel.id, counter: counter?.id ?? null };
|
|
}
|
|
|
|
/** Follow the real pre-inspection queue; never clear native preparation by hand. */
|
|
export function passNexusKullBorrowedVoice(state: Game): Game {
|
|
let game = state;
|
|
for (let n = 0; game.battle && n < 30 &&
|
|
((game.battle.preLeader && !game.battle.preLeader.closed) ||
|
|
game.battle.preparation?.kind === 'nexusVoice'); n++) {
|
|
const next = nextStrongholdFactionsNativeStep(game);
|
|
if (!next) throw new Error('The original pre-Voice preparation needs a legal native action.');
|
|
game = applyAction(game, next.actor, next.action);
|
|
}
|
|
if ((game.battle?.preLeader && !game.battle.preLeader.closed) ||
|
|
game.battle?.preparation?.kind === 'nexusVoice')
|
|
throw new Error('The original pre-leader and borrowed Voice queues must actually finish.');
|
|
return game;
|
|
}
|
|
|
|
/** A native declaration is produced by actions before its Karama/Kull parent
|
|
* is suspended. The final faction roster is fixed before genuine setup. */
|
|
export function createNexusKullNativeParent(kind: 'inspection' | 'sardaukar' | 'advisors' | 'guild',
|
|
seatIds?: string[]) {
|
|
const actorFaction = { inspection: 'atreides', sardaukar: 'emperor',
|
|
advisors: 'beneGesserit', guild: 'guild' } as const;
|
|
const f = createNexusChoamKullFixture({ actorFaction: actorFaction[kind], fuel: 'weapon', counter: false, seatIds });
|
|
let game = applyAction(f.game, f.choam, { type: 'kullDecision', event: f.event, decline: true });
|
|
game.players[1].hand.push(takeKullCard(game, f.original));
|
|
const counter = takeKullCard(game, 'karama');
|
|
game.players[2].hand.push(counter);
|
|
const face = actorFaction[kind], cards = game.nexusCards!.cards!;
|
|
const old = cards.hands[f.actor];
|
|
const holder = game.players.find(p => cards.hands[p.id] === face);
|
|
if (holder) cards.hands[holder.id] = old;
|
|
else {
|
|
const zone = cards.deck.includes(face) ? cards.deck : cards.discard;
|
|
const index = zone.indexOf(face);
|
|
if (index < 0) throw new Error(`Missing native ${face} Nexus.`);
|
|
zone.splice(index, 1);
|
|
if (old) cards.deck.push(old);
|
|
}
|
|
cards.hands[f.actor] = face;
|
|
Object.assign(game, { phase: kind === 'inspection' || kind === 'sardaukar' ? 6 : 5,
|
|
active: f.actor, karamaShipping: null, movementRemaining: null, ready: [] });
|
|
for (const p of game.players) {
|
|
p.forces = {}; p.reserves = 20; p.tanks = 0; p.spice = 20; p.shipped = false; p.moved = 0;
|
|
if (p.elites) { p.elites.forces = {}; p.elites.tanks = 0; p.elites.reserves = p.faction === 'emperor' ? 5 : 3; }
|
|
if (p.advisors) p.advisors = {};
|
|
}
|
|
if (kind === 'inspection' || kind === 'sardaukar') {
|
|
for (const p of game.players.slice(1)) { p.forces = { 'pasty_mesa:5': 7 }; p.reserves = 13; }
|
|
const choice = battles(game).find(battle => battle.territory === 'pasty_mesa');
|
|
if (!choice) throw new Error('Native parent setup has no contested battle.');
|
|
game.active = choice.chooser;
|
|
game = applyAction(game, choice.chooser, { type: 'chooseBattle', territory: choice.territory,
|
|
target: choice.attacker === choice.chooser ? choice.defender : choice.attacker });
|
|
game = passNexusKullBorrowedVoice(game);
|
|
if (kind === 'inspection') {
|
|
if (game.response) game = allowNativeParent(game);
|
|
game = applyAction(game, f.actor, { type: 'prescience', field: 'dial' });
|
|
game = allowNativeParent(game);
|
|
game = applyAction(game, f.other!, { type: 'prescienceAnswer', value: 0 });
|
|
if (game.decision?.kind === 'fullPlanOffer')
|
|
game = applyAction(game, game.decision.player, { type: 'decision', decline: true });
|
|
game = applyAction(game, f.actor, { type: 'nexusAtreides', event: game.battle!.event, mode: 'cunning', field: 'weapon' });
|
|
} else {
|
|
while (game.battle!.preparation)
|
|
game = applyAction(game, game.battle!.preparation.owner, { type: 'declineBattlePower' });
|
|
if (game.decision?.kind === 'fullPlanOffer')
|
|
game = applyAction(game, game.decision.player, { type: 'decision', decline: true });
|
|
game = applyAction(game, f.actor, { type: 'nexusSardaukar', event: viewGame(game, f.actor).nexusSardaukar!.offer!.event });
|
|
}
|
|
} else if (kind === 'advisors') {
|
|
game.players[1].forces = { 'pasty_mesa:5': 2 }; game.players[1].reserves = 18;
|
|
game.players[1].advisors = { pasty_mesa: {} };
|
|
game.players[2].forces = { 'pasty_mesa:5': 1 }; game.players[2].reserves = 19;
|
|
game = applyAction(game, f.actor, { type: 'nexusAdvisors',
|
|
event: viewGame(game, f.actor).nexusAdvisors!.offer!.event, territories: ['pasty_mesa'] });
|
|
} else {
|
|
game = applyAction(game, f.actor, { type: 'ship', territory: 'arrakeen', sector: 10, amount: 1 });
|
|
if (game.decision?.kind === 'guildShipment')
|
|
game = applyAction(game, game.decision.player, { type: 'decision', allow: true });
|
|
game = allowNativeParent(game);
|
|
game = applyAction(game, f.actor, { type: 'move', from: 'arrakeen:10', territory: 'hagga_basin', sector: 12, amount: 1 });
|
|
game = applyAction(game, f.actor, { type: 'endMovement', nexus: viewGame(game, f.actor).nexusGuildCunning!.offer!.event });
|
|
}
|
|
return { ...f, game, reactor: f.other!, reaction: counter.id };
|
|
}
|
|
|
|
function allowNativeParent(game: Game): Game {
|
|
for (let step = 0; game.response && step < 20; step++) {
|
|
const responder = game.players.find(p => {
|
|
const controls = viewGame(game, p.id).responseControls;
|
|
return controls && !controls.hasPassed && controls.cancelCards.length > 0;
|
|
});
|
|
if (!responder) throw new Error('Native response has no explicit responder.');
|
|
game = applyAction(game, responder.id, { type: 'passResponse' });
|
|
}
|
|
return game;
|
|
}
|
|
|
|
export function createNexusKullNestedParent(seatIds?: string[]) {
|
|
const f = createNexusChoamKullFixture({ bg: true, fuel: 'karama', seatIds });
|
|
let game = applyAction(f.game, f.choam, { type: 'kullDecision', event: f.event, decline: true });
|
|
game = allowNativeParent(game);
|
|
game.players[1].hand.push(takeKullCard(game, f.original));
|
|
const printed = takeKullCard(game, 'ix-kull-wahad');
|
|
game.players[0].hand.push(printed);
|
|
const bgCounter = takeKullCard(game, 'Jubba Cloak');
|
|
game.players[1].hand.push(bgCounter);
|
|
Object.assign(game, { phase: 4, karamaShipping: null });
|
|
const play = viewGame(game, f.choam).choamWorthless!.plays.find(play =>
|
|
play.source === 'nexus' && play.effect === 'laLaLa' && play.card.id === f.fuel);
|
|
if (!play || !('event' in play)) throw new Error('Missing genuine Nexus parent quote.');
|
|
game = applyAction(game, f.choam, { type: 'card', mode: 'choam', card: f.fuel,
|
|
nexus: play.event, effect: 'laLaLa', target: f.actor });
|
|
return { ...f, game, printed: printed.id, bgCounter: bgCounter.id, nexusEvent: play.event };
|
|
}
|
|
|
|
export function createNexusKullAuctionParent(seatIds?: string[]) {
|
|
const f = createNexusChoamKullFixture({ fuel: 'weapon', counter: false, seatIds });
|
|
let game = applyAction(f.game, f.choam, { type: 'kullDecision', event: f.event, decline: true });
|
|
const payment = takeKullCard(game, f.original);
|
|
game.players[0].hand.push(payment);
|
|
const printed = takeKullCard(game, 'ix-kull-wahad');
|
|
game.players[0].hand.push(printed);
|
|
const reaction = takeKullCard(game, 'karama');
|
|
game.players[2].hand.push(reaction);
|
|
game.players[1].hand.push(takeKullCard(game, 'Shield'));
|
|
const lot = takeKullCard(game, 'ix-snooper');
|
|
Object.assign(game, { phase: 3, active: f.choam, karamaShipping: null, movementRemaining: [],
|
|
auction: { cards: [lot], index: 0, bid: 0, bidder: null, active: f.choam, passed: [], opener: 0 } });
|
|
game.players[0].spice = 1;
|
|
game = applyAction(game, f.choam, { type: 'bid', amount: 2 });
|
|
for (let step = 0; game.auction && !game.decision && step < 12; step++)
|
|
game = applyAction(game, game.auction.active!, { type: 'passBid' });
|
|
if (game.decision?.kind !== 'auctionPayment') throw new Error('CHOAM did not reach genuine winning payment.');
|
|
return { ...f, game, payment: payment.id, printed: printed.id, reactor: f.other!,
|
|
reaction: reaction.id, lot: lot.id };
|
|
}
|