Files
Dune/tests/fixture-purchased-homeworld-rate.ts

122 lines
6.4 KiB
TypeScript

import assert from 'node:assert/strict';
import { viewGame, type Action, type Game } from '../game/engine';
import { homeworldShipmentChoice, type HomeworldShipmentSources } from '../game/homeworld-shipment-options';
import { guildHomeworldShipmentChoice } from '../game/guild-homeworld-shipment-options';
import { createGuildAmbassadorIncomeArrival, passNativeGuildTransportPermission } from './fixture-guild-ambassador-income-arrival';
import {
createNativeGuildHomeworldRate, createNativeGuildHomeworldRateScene,
type NativeGuildHomeworldRoute,
} from './fixture-native-guild-homeworld-rate';
import { holdGuildCunningCard } from './fixture-nexus-guild-cunning';
import {
pairedE3NexusReload, stepPairedE3Nexus, type PairedE3NexusStep,
} from './fixture-paired-e3-nexus-modules';
export type PurchasedHomeworldRoute = 'world' | 'guildWorld' | 'arrakisForeign' | 'return';
export interface PurchasedHomeworldRateFixture {
route: PurchasedHomeworldRoute;
shipper: string;
ecaz: string;
guild: string;
entrant: string;
karama: string;
source: string;
destination: string;
sources: HomeworldShipmentSources;
allyPayment: number;
beforePurchase: Game;
beforeAction: Game;
action: Action;
actions: PairedE3NexusStep[];
provenance: string[];
}
function purchase(base: {
beforeAction: Game; ecaz: string; guild: string; entrant: string; karama: string;
actions: PairedE3NexusStep[]; provenance: string[];
}, route: PurchasedHomeworldRoute, source: string, destination: string, allyPayment: number) {
const actions = [...base.actions];
const shipper = route === 'world' ? base.entrant : base.guild;
const beforePurchase = pairedE3NexusReload(base.beforeAction);
assert.equal(beforePurchase.active, shipper);
assert.ok(beforePurchase.players.find(p => p.id === base.ecaz)!.hand.some(c => c.id === base.karama));
const game = stepPairedE3Nexus(beforePurchase, { actor: base.ecaz,
action: { type: 'card', mode: 'shipment', card: base.karama, target: shipper } }, actions);
assert.deepEqual(game.karamaShipping, { player: shipper, owner: base.ecaz, card: base.karama });
const sources = { [source]: { normal: 3, elite: 0 } };
const view = viewGame(game, shipper);
const quote = route === 'world' || route === 'guildWorld'
? homeworldShipmentChoice(view, destination, sources, allyPayment)
: guildHomeworldShipmentChoice(view, destination, sources, allyPayment);
assert.equal(quote.blocked, null);
assert.equal(quote.cost, 2);
assert.ok(quote.action);
return { ...base, route, shipper, source, destination, sources, allyPayment,
actions, beforePurchase, beforeAction: pairedE3NexusReload(game), action: quote.action,
provenance: [...base.provenance,
'Ecaz plays its actual held Karama in shipment mode for the active beneficiary; no card identity is transferred.',
'The original typed three-normal-counter route and explicit approved share are quoted after the independent purchase.'],
} satisfies PurchasedHomeworldRateFixture;
}
/** Original printed setup and auctioned Ecaz card from the native Homeworld
* factory. No played hand, wallet, army, phase, grant or decision is assigned.
* The shared factories disclose conserved undealt Treachery/Spice ordering. */
export function createPurchasedHomeworldRate(route: PurchasedHomeworldRoute = 'world') {
if (route === 'world') {
const base = createGuildAmbassadorIncomeArrival({ homeworlds: true });
return purchase(base, route, 'homeworld:moritani', 'homeworld:ecaz', 0);
}
const nativeRoute: NativeGuildHomeworldRoute = route === 'guildWorld' ? 'world' : route;
const base = createNativeGuildHomeworldRate(nativeRoute);
return purchase(base, route, base.source, base.destination, 1);
}
/** Actual declaration; the original automatic stop policy may complete it immediately. */
export function declarePurchasedHomeworldRate(f: PurchasedHomeworldRateFixture) {
const actions: PairedE3NexusStep[] = [];
const game = stepPairedE3Nexus(f.beforeAction, { actor: f.shipper, action: f.action }, actions);
assert.notEqual(game.response?.kind, 'guildRate');
return { game, actions };
}
export function createPurchasedHomeworldRateScene(route: PurchasedHomeworldRoute = 'world') {
const fixture = createPurchasedHomeworldRate(route);
const declared = declarePurchasedHomeworldRate(fixture);
return { ...fixture, game: declared.game, declarationActions: declared.actions,
allow: declared.game.decision?.kind === 'homeworldShipmentGuild'
? { actor: fixture.guild, action: { type: 'decision',
event: declared.game.decision.event, allow: true } as Action } : null };
}
/** Continue only actual Guild interception/permission controls, never a native rate or income. */
export function completePurchasedHomeworldRate(state: Game) {
const actions: PairedE3NexusStep[] = [];
let game = pairedE3NexusReload(state);
if (game.decision?.kind === 'homeworldShipmentGuild')
game = stepPairedE3Nexus(game, { actor: game.decision.player,
action: { type: 'decision', event: game.decision.event, allow: true } }, actions);
const permission = passNativeGuildTransportPermission(game);
return { game: permission.game, actions: [...actions, ...permission.actions] };
}
/** Explicitly controlled SECOND card acquisition only: the first auctioned
* Ecaz Karama really cancels an unaffordable native Guild return. The existing
* controlled physical-card helper supplies a second Karama to Ecaz; its later
* purchase and typed return are real actions. Not original action-only custody. */
export function createControlledPurchasedHomeworldCanceledRate() {
const native = createNativeGuildHomeworldRateScene('return', { unaffordable: true });
const actions = [...native.actions, ...native.declarationActions];
let game = stepPairedE3Nexus(native.game, native.cancel, actions);
assert.equal(viewGame(game, native.guild).guildRateCanceled, true);
assert.ok(!game.pendingHomeworldShipment);
assert.equal(game.players.find(p => p.id === native.guild)!.shipped, false);
const karama = holdGuildCunningCard(game, native.ecaz, 'karama').id;
game = pairedE3NexusReload(game);
const fixture = purchase({ ...native, beforeAction: game, karama, actions,
provenance: [...native.provenance,
'CONTROLLED second physical Karama acquisition via the existing held-card helper; no original-auction claim for this second card.'],
}, 'return', native.source, native.destination, 1);
return { ...fixture, canceledKarama: native.karama };
}