Retain Richese one-counter pricing through native Guild rate response

This commit is contained in:
Quintine committed 2026-10-15 23:55:02 +11:00
1 parent 8f9f980075
commit 2b8eb77e4e
11 files changed
+421 -13

No files matched your search

@@ -0,0 +1,88 @@
import assert from 'node:assert/strict';
import { viewGame, type Action } from '../game/engine';
import { TERRITORIES } from '../game/board';
import { presenceAt } from '../game/force-presence';
import { nexusCardAction } from '../game/nexus-card-options';
import { nexusRicheseAction, nexusRicheseQuote } from '../game/nexus-richese-options';
import { createGuildAmbassadorIncomeArrival } from './fixture-guild-ambassador-income-arrival';
import {
advancePairedE3Nexus, pairedE3NexusClean, pairedE3NexusPlayer,
pairedE3NexusReload, quietPairedE3NexusChoice, stepPairedE3Nexus,
} from './fixture-paired-e3-nexus-modules';
import { cancelNativeGuildRateModules } from './fixture-native-guild-rate-modules';
/** Original setup, real paid Ecaz Karama, real Ecaz/Moritani alliance and
* Guild Nexus draw. Only conserved UNDEALT Spice/Nexus order is selected;
* no played hand, wallet, army, phase or response is assigned. */
export function createRicheseNativeGuildRate(options: {
poor?: boolean;
priorCancellation?: boolean;
purchased?: boolean;
homeworlds?: boolean;
destinationType?: 'sand' | 'stronghold';
} = {}) {
const original = createGuildAmbassadorIncomeArrival({ homeworlds: !!options.homeworlds });
const prefix = original.actions.findIndex(s => s.actor === original.entrant &&
s.action.type === 'alliance' && s.action.target === original.guild);
assert.ok(prefix >= 0);
const actions = original.actions.slice(0, prefix);
let game = pairedE3NexusReload(original.beforeAlliance);
const cards = game.nexusCards!.cards!;
assert.ok(Object.values(cards.hands).every(card => card === null));
const at = cards.deck.indexOf('richese'); assert.ok(at >= 0);
cards.deck.unshift(cards.deck.splice(at, 1)[0]);
for (const [actor, target] of [[original.ecaz, original.entrant], [original.entrant, original.ecaz]])
game = stepPairedE3Nexus(game, { actor, action: { type: 'alliance', target } }, actions);
game = advancePairedE3Nexus(game, state => state.nexusCards?.phase?.stage === 'drawing' &&
pairedE3NexusClean(state), actions, quietPairedE3NexusChoice);
assert.equal(pairedE3NexusPlayer(game, original.guild).ally, null);
const draw = nexusCardAction(viewGame(game, original.guild), 'draw'); assert.ok(draw);
game = stepPairedE3Nexus(game, { actor: original.guild, action: draw }, actions);
assert.equal(game.nexusCards!.cards!.hands[original.guild], 'richese');
game = advancePairedE3Nexus(game, state => state.phase === 5 && state.active === original.guild &&
pairedE3NexusClean(state), actions, quietPairedE3NexusChoice);
const source = pairedE3NexusReload(game);
const destination = TERRITORIES.find(t => t.type === (options.destinationType ?? 'sand') &&
t.sectors.some(sector => sector !== game.storm) &&
game.players.every(player => presenceAt(player, t.id) === 0));
assert.ok(destination);
const sector = destination.sectors.find(s => s !== game.storm)!;
if (options.poor || options.priorCancellation) {
const excess = pairedE3NexusPlayer(game, original.guild).spice - 1;
assert.ok(excess >= 0);
if (excess) game = stepPairedE3Nexus(game, { actor: original.guild,
action: { type: 'bribe', target: original.ecaz, amount: excess } }, actions);
}
if (options.priorCancellation) {
game = stepPairedE3Nexus(game, { actor: original.guild,
action: { type: 'ship', territory: destination.id, sector, amount: 1, elite: 0 } }, actions);
if (game.decision?.kind === 'guildShipment') game = stepPairedE3Nexus(game,
{ actor: original.guild, action: { type: 'decision', allow: true } }, actions);
const canceled = cancelNativeGuildRateModules(game, original.ecaz, original.karama);
game = canceled.game; actions.push(...canceled.actions);
assert.equal(pairedE3NexusPlayer(game, original.guild).shipped, false);
assert.equal(game.nexusCards!.cards!.hands[original.guild], 'richese');
}
if (options.purchased) game = stepPairedE3Nexus(game, { actor: original.ecaz,
action: { type: 'card', card: original.karama, mode: 'shipment', target: original.guild } }, actions);
const view = viewGame(game, original.guild);
const quote = nexusRicheseQuote(view, destination.id, 5); assert.ok(quote);
const shipment: Action = { type: 'ship', territory: destination.id, sector,
amount: 5, elite: 0, allyPayment: 0 };
if (options.homeworlds) shipment.homeworldSources = { 'homeworld:guild': { normal: 5, elite: 0 } };
const action = nexusRicheseAction(view, view.nexusRichese!.event, shipment); assert.ok(action);
return { original, source, actions, beforeAction: pairedE3NexusReload(game), action, quote,
guild: original.guild, canceler: original.ecaz, karama: original.karama,
destination: destination.id, sector, target: `${destination.id}:${sector}`,
provenance: [...original.provenance,
'Conserved original undealt Richese Nexus moved to the next draw; no card identity invented.',
'Actual Ecaz/Moritani alliance leaves Guild unallied; original Guild chooses its real Nexus draw.',
'Actual quiet progression to Guild turn; optional bribe, prior rate cancellation or purchased price are real actions.'] };
}
export function declareRicheseNativeGuildRate(fixture: ReturnType<typeof createRicheseNativeGuildRate>) {
let game = stepPairedE3Nexus(fixture.beforeAction, { actor: fixture.guild, action: fixture.action });
if (game.decision?.kind === 'guildShipment') game = stepPairedE3Nexus(game,
{ actor: fixture.guild, action: { type: 'decision', allow: true } });
return game;
}
+143
View File
@@ -0,0 +1,143 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { applyAction, viewGame, type Action, type Game } from '../game/engine';
import { TERRITORIES } from '../game/board';
import { presenceAt } from '../game/force-presence';
import { nexusRicheseAction } from '../game/nexus-richese-options';
import { createRicheseNativeGuildRate, declareRicheseNativeGuildRate } from './fixture-richese-native-guild-rate';
import {
allowNativeGuildRateModules, cancelNativeGuildRateModules,
settleNativeGuildRateModules,
} from './fixture-native-guild-rate-modules';
import { pairedE3NexusPlayer, pairedE3NexusReload, stepPairedE3Nexus } from './fixture-paired-e3-nexus-modules';
function reject(game: Game, actor: string, action: Action) {
const before = pairedE3NexusReload(game);
assert.throws(() => applyAction(game, actor, action));
assert.deepEqual(game, before);
}
function spentOnce(game: Game, guild: string) {
assert.equal(game.nexusCards!.cards!.hands[guild], null);
assert.equal(game.nexusCards!.cards!.discard.filter(card => card === 'richese').length, 1);
}
for (const canceled of [false, true]) void test(`native Guild ${canceled ? 'cancellation' : 'allowance'} retains five actual Richese forces and one charged counter`, () => {
const f = createRicheseNativeGuildRate();
let game = declareRicheseNativeGuildRate(f);
assert.equal(game.response?.kind, 'guildRate');
assert.equal(pairedE3NexusPlayer(game, f.guild).reserves, 15);
assert.equal(pairedE3NexusPlayer(game, f.guild).spice, 6);
assert.equal(pairedE3NexusPlayer(game, f.guild).shipped, false);
spentOnce(game, f.guild);
assert.ok(viewGame(game, f.canceler).responseControls!.cancelCards.includes(f.karama));
reject(game, f.guild, f.action);
game = canceled ? cancelNativeGuildRateModules(game, f.canceler, f.karama).game
: allowNativeGuildRateModules(game).game;
game = settleNativeGuildRateModules(game).game;
const owner = pairedE3NexusPlayer(game, f.guild);
assert.equal(owner.spice, canceled ? 4 : 5);
assert.equal(owner.reserves, 10);
assert.equal(owner.forces[f.target], 5);
assert.equal(owner.shipped, true);
assert.equal(owner.moved, 0);
assert.equal(game.discard.filter(card => card.id === f.karama).length, canceled ? 1 : 0);
assert.equal(pairedE3NexusPlayer(game, f.canceler).hand.some(card => card.id === f.karama), !canceled);
spentOnce(game, f.guild);
reject(game, f.guild, f.action);
game = stepPairedE3Nexus(game, { actor: f.guild, action: { type: 'endMovement' } });
assert.notEqual(game.active, f.guild);
assert.equal(pairedE3NexusPlayer(game, f.guild).forces[f.target], 5);
});
void test('an unaffordable full one-counter fee ends the spent Nexus declaration without delivery, debt or a second card use', () => {
const f = createRicheseNativeGuildRate({ poor: true });
let game = declareRicheseNativeGuildRate(f);
assert.equal(game.response?.kind, 'guildRate');
game = cancelNativeGuildRateModules(game, f.canceler, f.karama).game;
const owner = pairedE3NexusPlayer(game, f.guild);
assert.equal(owner.spice, 1);
assert.equal(owner.reserves, 15);
assert.equal(owner.forces[f.target] ?? 0, 0);
assert.equal(owner.shipped, false);
assert.equal(owner.moved, 0);
assert.equal(game.response, null);
assert.equal(game.pendingShipment, null);
spentOnce(game, f.guild);
reject(game, f.guild, f.action);
const destination = TERRITORIES.find(t => t.type === 'stronghold' &&
t.sectors.some(s => s !== game.storm) && game.players.every(p => presenceAt(p, t.id) === 0));
assert.ok(destination);
const sector = destination.sectors.find(s => s !== game.storm)!;
game = stepPairedE3Nexus(game, { actor: f.guild,
action: { type: 'ship', territory: destination.id, sector, amount: 1, elite: 0 } });
if (game.decision?.kind === 'guildShipment') game = stepPairedE3Nexus(game,
{ actor: f.guild, action: { type: 'decision', allow: true } });
game = settleNativeGuildRateModules(game).game;
assert.equal(pairedE3NexusPlayer(game, f.guild).spice, 0);
assert.equal(pairedE3NexusPlayer(game, f.guild).reserves, 14);
assert.equal(pairedE3NexusPlayer(game, f.guild).forces[`${destination.id}:${sector}`], 1);
spentOnce(game, f.guild);
});
void test('earlier native cancellation quotes the held source at full price and keeps a legal one-spice alternative', () => {
const f = createRicheseNativeGuildRate({ priorCancellation: true });
assert.equal(f.quote.cost, 2);
assert.equal(viewGame(f.beforeAction, f.guild).guildRateCanceled, true);
assert.equal(pairedE3NexusPlayer(f.beforeAction, f.guild).spice, 1);
reject(f.beforeAction, f.guild, f.action);
const destination = TERRITORIES.find(t => t.type === 'stronghold' &&
t.sectors.some(s => s !== f.beforeAction.storm) &&
f.beforeAction.players.every(p => presenceAt(p, t.id) === 0));
assert.ok(destination);
const sector = destination.sectors.find(s => s !== f.beforeAction.storm)!;
const view = viewGame(f.beforeAction, f.guild);
const action = nexusRicheseAction(view, view.nexusRichese!.event,
{ type: 'ship', territory: destination.id, sector, amount: 5, elite: 0 });
assert.ok(action);
let game = declareRicheseNativeGuildRate({ ...f, action });
game = settleNativeGuildRateModules(game).game;
assert.equal(pairedE3NexusPlayer(game, f.guild).spice, 0);
assert.equal(pairedE3NexusPlayer(game, f.guild).reserves, 10);
assert.equal(pairedE3NexusPlayer(game, f.guild).forces[`${destination.id}:${sector}`], 5);
spentOnce(game, f.guild);
});
void test('the independently purchased price remains one bank spice for the original five-force source', () => {
const f = createRicheseNativeGuildRate({ purchased: true });
let game = declareRicheseNativeGuildRate(f);
assert.notEqual(game.response?.kind, 'guildRate');
game = settleNativeGuildRateModules(game).game;
assert.equal(pairedE3NexusPlayer(game, f.guild).spice, 5);
assert.equal(pairedE3NexusPlayer(game, f.guild).reserves, 10);
assert.equal(pairedE3NexusPlayer(game, f.guild).forces[f.target], 5);
assert.equal(game.karamaShipping, null);
assert.equal(game.discard.filter(card => card.id === f.karama).length, 1);
spentOnce(game, f.guild);
});
void test('a rounded stronghold price with no actual discount has no native rate counter', () => {
const f = createRicheseNativeGuildRate({ destinationType: 'stronghold' });
assert.equal(f.quote.cost, 1);
let game = declareRicheseNativeGuildRate(f);
assert.notEqual(game.response?.kind, 'guildRate');
game = settleNativeGuildRateModules(game).game;
assert.equal(pairedE3NexusPlayer(game, f.guild).spice, 5);
assert.equal(pairedE3NexusPlayer(game, f.guild).forces[f.target], 5);
assert.ok(pairedE3NexusPlayer(game, f.canceler).hand.some(card => card.id === f.karama));
spentOnce(game, f.guild);
});
void test('the native Junction withdrawal stays five physical counters while cancellation charges only one full counter', () => {
const f = createRicheseNativeGuildRate({ homeworlds: true });
reject(f.beforeAction, f.guild, { ...f.action,
homeworldSources: { 'homeworld:ecaz': { normal: 5, elite: 0 } } });
let game = declareRicheseNativeGuildRate(f);
assert.equal(game.response?.kind, 'guildRate');
game = cancelNativeGuildRateModules(game, f.canceler, f.karama).game;
game = settleNativeGuildRateModules(game).game;
assert.equal(pairedE3NexusPlayer(game, f.guild).spice, 4);
assert.equal(pairedE3NexusPlayer(game, f.guild).reserves, 10);
assert.equal(pairedE3NexusPlayer(game, f.guild).forces[f.target], 5);
assert.equal(pairedE3NexusPlayer(game, f.guild).moved, 0);
spentOnce(game, f.guild);
});