Files
Dune/tests/fixture-native-guild-homeworld-rate.ts

198 lines
11 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 { junctionTransportChoice } from '../game/junction-transport-options';
import { createGuildAmbassadorIncomeArrival, passNativeGuildTransportPermission } from './fixture-guild-ambassador-income-arrival';
import {
advancePairedE3Nexus, nextPairedE3NexusStep, pairedE3NexusClean,
pairedE3NexusPlayer, pairedE3NexusReload, quietPairedE3NexusChoice,
stepPairedE3Nexus, type PairedE3NexusStep,
} from './fixture-paired-e3-nexus-modules';
import {
nexusGuildCunningFixture, originalGuildCunningTurn, nexusGuildCunningRequest,
nexusGuildCunningAllow, holdGuildCunningCard,
} from './fixture-nexus-guild-cunning';
export type NativeGuildHomeworldRoute = 'world' | 'arrakisForeign' | 'return';
export interface NativeGuildHomeworldRateFixture {
route: NativeGuildHomeworldRoute;
source: string;
destination: string;
sources: HomeworldShipmentSources;
guild: string;
entrant: string;
ecaz: string;
karama: string;
beforeAction: Game;
action: Action;
}
/** Original printed setup, original purchased Karama, and actual progression
* from Moritani's real turn to Guild's real turn. The shared fixture discloses
* its conserved undealt entropy ordering. No played custody, funding, control,
* phase or decision is assigned here. A return is Arrakis to native Junction. */
export function createNativeGuildHomeworldRate(route: NativeGuildHomeworldRoute = 'world',
options: { unaffordable?: boolean } = {}) {
const base = createGuildAmbassadorIncomeArrival({ homeworlds: true });
const actions = [...base.actions];
let game = advancePairedE3Nexus(base.beforeAction,
g => g.phase === 5 && g.active === base.guild && pairedE3NexusClean(g),
actions, quietPairedE3NexusChoice);
const beforeFunding = pairedE3NexusReload(game);
if (options.unaffordable) {
const excess = pairedE3NexusPlayer(game, base.guild).spice - 1;
assert.ok(excess >= 0);
if (excess) game = stepPairedE3Nexus(game, { actor: base.guild,
action: { type: 'bribe', target: base.ecaz, amount: excess } }, actions);
}
game = stepPairedE3Nexus(game, { actor: base.entrant,
action: { type: 'pledgeAid', amount: 1 } }, actions);
const p = pairedE3NexusPlayer(game, base.guild);
const source = route === 'world' ? 'homeworld:guild' : Object.keys(p.forces)
.find(key => p.forces[key] >= 3 && Number(key.split(':')[1]) !== game.storm);
assert.ok(source, 'The original Guild retains three physical counters outside storm.');
const destination = route === 'return' ? 'homeworld:guild' : 'homeworld:ecaz';
const sources: HomeworldShipmentSources = { [source]: { normal: 3, elite: 0 } };
const quote = route === 'world'
? homeworldShipmentChoice(viewGame(game, base.guild), destination, sources, 1)
: guildHomeworldShipmentChoice(viewGame(game, base.guild), destination, sources, 1);
assert.equal(quote.blocked, null);
assert.equal(quote.cost, 2);
assert.equal(quote.ownPayment, 1);
assert.ok(quote.action);
return { ...base, actions, route, source, destination, sources,
beforeFunding, beforeAction: pairedE3NexusReload(game), action: quote.action,
contributions: [{ payer: base.guild, amount: 1 }, { payer: base.entrant, amount: 1 }],
provenance: [...base.provenance,
'Actual quiet phase progression to the native Guild turn; original on-board and Junction pools retained.',
'Moritani pledges one actual spice; optional Guild bribe reduces only its real available wallet.',
'The owner-view canonical quote declares three normal counters and an explicit one-spice allied share.'] };
}
export function nativeGuildHomeworldAction(f: NativeGuildHomeworldRateFixture,
game: Game, amount = 3, allyPayment = 1): Action {
const sources = { [f.source]: { normal: amount, elite: 0 } };
const view = viewGame(game, f.guild);
const quote = f.route === 'world'
? homeworldShipmentChoice(view, f.destination, sources, allyPayment)
: guildHomeworldShipmentChoice(view, f.destination, sources, allyPayment);
assert.equal(quote.blocked, null);
assert.ok(quote.action);
return quote.action;
}
/** Retain the original Guild special-interception decision if physically
* offered. It is distinct from the native rate's subsequent prepayment queue. */
export function declareNativeGuildHomeworldRate(f: NativeGuildHomeworldRateFixture) {
const actions: PairedE3NexusStep[] = [];
let game = stepPairedE3Nexus(f.beforeAction, { actor: f.guild, action: f.action }, actions);
if (game.decision?.kind === 'homeworldShipmentGuild') {
game = stepPairedE3Nexus(game, { actor: f.guild,
action: { type: 'decision', event: game.decision.event, allow: true } }, actions);
}
const permission = passNativeGuildTransportPermission(game);
game = permission.game; actions.push(...permission.actions);
assert.equal(game.response?.kind, 'guildRate');
assert.ok(viewGame(game, f.ecaz).responseControls?.cancelCards.includes(f.karama));
return { game, actions };
}
/** Only pass the actual requested kind; never clear a queue or fabricate a seat. */
export function passNativeGuildHomeworldResponses(state: Game, kind: 'guildRate' | 'guildIncome') {
const actions: PairedE3NexusStep[] = [];
let game = pairedE3NexusReload(state);
for (let n = 0; game.response?.kind === kind && n < 20; n++) {
const step = nextPairedE3NexusStep(game);
assert.ok(step);
assert.equal(step.action.type, 'passResponse');
game = stepPairedE3Nexus(game, step, actions);
}
assert.notEqual(game.response?.kind, kind);
return { game, actions };
}
/** Export a real pending prepayment scene suitable for API/UI loading. */
export function createNativeGuildHomeworldRateScene(route: NativeGuildHomeworldRoute = 'world',
options: { unaffordable?: boolean } = {}) {
const fixture = createNativeGuildHomeworldRate(route, options);
const offered = declareNativeGuildHomeworldRate(fixture);
return { ...fixture, game: offered.game, declarationActions: offered.actions,
cancel: { actor: fixture.ecaz, action: { type: 'card', mode: 'cancel', card: fixture.karama } as Action } };
}
/** Original immune Junction offer, not a staged immunity flag: Moritani first
* loses its ordinary native alliance rate on a genuinely unaffordable six-force
* cross-transport. Its untouched original army then accepts Junction's own
* Homeworld return tariff while that cancellation marker remains active. */
export function createNativeImmuneJunctionRate(rate: 'half' | 'full' = 'half') {
const base = createGuildAmbassadorIncomeArrival({ homeworlds: true });
const actions = [...base.actions];
const excess = pairedE3NexusPlayer(base.beforeAction, base.entrant).spice - 3;
assert.ok(excess >= 0);
let game = pairedE3NexusReload(base.beforeAction);
if (excess) game = stepPairedE3Nexus(game, { actor: base.entrant,
action: { type: 'bribe', target: base.ecaz, amount: excess } }, actions);
game = stepPairedE3Nexus(game, { actor: base.entrant,
action: { ...base.action, amount: 6 } }, actions);
const permission = passNativeGuildTransportPermission(game);
game = permission.game; actions.push(...permission.actions);
assert.equal(game.response?.kind, 'guildRate');
game = stepPairedE3Nexus(game, { actor: base.ecaz,
action: { type: 'card', mode: 'cancel', card: base.karama } }, actions);
assert.equal(viewGame(game, base.entrant).guildRateCanceled, true);
assert.equal(pairedE3NexusPlayer(game, base.entrant).shipped, false);
const released = pairedE3NexusReload(game);
const sponsor = viewGame(game, base.guild).junctionTransport;
assert.ok(sponsor?.canOffer);
game = stepPairedE3Nexus(game, { actor: base.guild,
action: { type: 'offerJunctionTransport', event: sponsor.offerEvent, rate } }, actions);
const sources = { [base.source]: { normal: 3, elite: 0 } };
const destination = 'homeworld:moritani';
const quote = junctionTransportChoice(viewGame(game, base.entrant), destination, sources, 0);
assert.equal(quote.blocked, null);
assert.equal(quote.cost, rate === 'half' ? 2 : 3);
assert.ok(quote.action);
return { ...base, actions, rate, sources, destination, released,
beforeAction: pairedE3NexusReload(game), action: quote.action,
provenance: [...base.provenance,
'Actual unaffordable six-force alliance-rate cancellation leaves the original Moritani pool untouched.',
'Original high-population Guild offers an independent printed Junction half/full tariff.'] };
}
/** Explicitly controlled older fixture: its wallet/phase/army and conserved
* Nexus identity swap are not original action-only provenance. First combined
* turn, Cunning grant, optional bribe, second declaration and all rate choices
* are actual actions. The observer's conserved Karama is dealt by the existing
* controlled helper, not claimed as an original auction purchase. */
export function createControlledGuildCunningHomeworldRateScene(unaffordable = false) {
const base = nexusGuildCunningFixture({ homeworlds: true });
let game = originalGuildCunningTurn(base);
const karama = holdGuildCunningCard(game, base.observer, 'karama').id;
game = nexusGuildCunningAllow(stepPairedE3Nexus(game,
{ actor: base.owner, action: nexusGuildCunningRequest(base, game) }));
const actions: PairedE3NexusStep[] = [];
if (unaffordable) {
const amount = pairedE3NexusPlayer(game, base.owner).spice - 2;
assert.ok(amount >= 0);
if (amount) game = stepPairedE3Nexus(game, { actor: base.owner,
action: { type: 'bribe', target: base.observer, amount } }, actions);
}
const beforeAction = pairedE3NexusReload(game);
const sources = { 'homeworld:guild': { normal: 3, elite: 0 } };
const destination = 'homeworld:harkonnen';
const choice = homeworldShipmentChoice(viewGame(game, base.owner), destination, sources, 0);
assert.equal(choice.blocked, null);
assert.ok(choice.action);
game = stepPairedE3Nexus(game, { actor: base.owner, action: choice.action }, actions);
if (game.decision?.kind === 'homeworldShipmentGuild')
game = stepPairedE3Nexus(game, { actor: base.owner,
action: { type: 'decision', event: game.decision.event, allow: true } }, actions);
assert.equal(game.response?.kind, 'guildRate');
return { ...base, game, beforeAction, sources, destination, actions, karama,
action: choice.action,
cancel: { actor: base.observer, action: { type: 'card', mode: 'cancel', card: karama } as Action },
provenance: ['Controlled prior Nexus Guild fixture; physical hand acquisition is controlled, not auctioned.',
'Actual completed first combined turn, actual Cunning, and actual second typed Homeworld declaration.'] };
}