Compose native Guild rate with module and Homeworld price sources
This commit is contained in:
1 parent
0db84c81b7
commit
a12250320c
24 files changed
+1176
-50
No files matched your search
@@ -0,0 +1,193 @@
|
||||
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 } 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);
|
||||
}
|
||||
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);
|
||||
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.'] };
|
||||
}
|
||||
Reference in new issue
Block a user