Unify classic Nexus core and funded native shipments

This commit is contained in:
Quintine committed 2026-10-18 05:34:36 +11:00
1 parent 366c967f8a
commit a7cc25ae29
24 files changed
+575 -150

No files matched your search

+133
View File
@@ -0,0 +1,133 @@
import assert from 'node:assert/strict';
import { test } from 'node:test';
import { viewGame } from '../game/engine';
import { createIndependentHarkonnenCall, createIndependentIxianPurchase,
reactionAction, reactionPlayer } from './fixture-classic-independent-nexus-reactions';
import { advanceClassicIndependentNexus } from './fixture-classic-independent-nexus';
import { classicDiscoveryNexusClean } from './fixture-discovery-classic-nexus';
import { createClassicNexusGuildPayment, createClassicNexusCunning,
submitClassicNexusSecondShipment } from './fixture-classic-nexus-core';
void test('ordinary base33 all14 supports the native declared-Traitor cancellation and real Mentat draw', () => {
const f = createIndependentHarkonnenCall({ independent: false, advanced: true, skills: true });
assert.equal(f.game.deck.length + f.game.discard.length + f.game.players.reduce((sum, player) => sum + player.hand.length, 0), 33);
let game = reactionAction(f.game, 'atreides', { type: 'nexusHarkonnenBetrayalUse',
event: f.game.pendingNexusHarkonnenBetrayal!.source.event });
assert.equal(reactionPlayer(game, 'atreides').leaders.find(leader => leader.id === f.identity)?.dead, false);
assert.equal(reactionPlayer(game, 'atreides').tanks, 1, 'Without Harass, the actual losing board counter is lost normally.');
assert.equal(reactionPlayer(game, 'harkonnen').traitors.includes(f.identity), false);
const top = game.traitorReserve![0];
game = advanceClassicIndependentNexus(game, state => state.phase === 8 && classicDiscoveryNexusClean(state));
assert.equal(game.nexusHarkonnenReplacementHistory!.length, 1);
assert.equal(game.nexusHarkonnenReplacementHistory![0].drawn, top);
assert.equal(reactionPlayer(game, 'harkonnen').traitors.length, 4);
assert.deepEqual(game.nexusCards!.cards!.discard, ['harkonnen']);
});
void test('ordinary base33 all14 replaces the actual paid lot without introducing independent cards', () => {
const f = createIndependentIxianPurchase({ independent: false, advanced: true, skills: true });
const purchased = f.game.pendingNexusIxianReplacement!.card, top = f.game.deck[0];
const paid = reactionPlayer(f.game, 'atreides').spice;
const game = reactionAction(f.game, 'atreides', { type: 'nexusIxianReplacementUse',
event: f.game.pendingNexusIxianReplacement!.event });
assert.equal(reactionPlayer(game, 'atreides').spice, paid);
assert.equal(paid, reactionPlayer(f.beforePayment, 'atreides').spice - 2);
assert.equal(reactionPlayer(game, 'atreides').hand.some(card => card.id === purchased.id), false);
assert.deepEqual(reactionPlayer(game, 'atreides').hand.find(card => card.id === top.id), top);
assert.equal(game.discard.filter(card => card.id === purchased.id).length, 1);
assert.equal(game.auction?.index, f.game.auction!.index + 1);
assert.deepEqual(game.nexusCards!.cards!.discard, ['ixians']);
});
void test('Richese Secret Ally five-force price and a distinct Guild fee claim retain both original singleton costs', () => {
const f = createClassicNexusGuildPayment({ advanced: true, skills: true });
assert.equal(f.game.pendingGuildBetrayal!.invoice.price, 1);
assert.equal(reactionPlayer(f.game, 'atreides').reserves, reactionPlayer(f.beforeShipment, 'atreides').reserves);
assert.equal(f.game.nexusRicheseHistory!.at(-1)!.stage, 'pending');
assert.deepEqual(f.game.nexusCards!.cards!.discard, ['richese']);
const game = reactionAction(f.game, 'emperor', { type: 'guildBetrayalUse',
event: f.game.pendingGuildBetrayal!.invoice.event });
assert.equal(reactionPlayer(game, 'atreides').spice, reactionPlayer(f.beforeShipment, 'atreides').spice - 1);
assert.equal(reactionPlayer(game, 'emperor').spice, reactionPlayer(f.beforeShipment, 'emperor').spice + 1);
assert.equal(reactionPlayer(game, 'guild').spice, reactionPlayer(f.beforeShipment, 'guild').spice);
assert.equal(reactionPlayer(game, 'atreides').reserves, reactionPlayer(f.beforeShipment, 'atreides').reserves - 5);
assert.equal(reactionPlayer(game, 'atreides').forces[f.key], 5);
assert.equal(game.nexusRicheseHistory!.at(-1)!.stage, 'shipped');
assert.deepEqual(game.nexusCards!.cards!.discard, ['richese', 'guild']);
});
void test('passing the discounted shipment keeps Guild Nexus and pays only the original one-spice Guild receipt', () => {
const f = createClassicNexusGuildPayment({ independent: true });
const game = reactionAction(f.game, 'emperor', { type: 'guildBetrayalPass',
event: f.game.pendingGuildBetrayal!.invoice.event });
assert.equal(reactionPlayer(game, 'atreides').spice, reactionPlayer(f.beforeShipment, 'atreides').spice - 1);
assert.equal(reactionPlayer(game, 'guild').spice, reactionPlayer(f.beforeShipment, 'guild').spice + 1);
assert.equal(reactionPlayer(game, 'emperor').spice, reactionPlayer(f.beforeShipment, 'emperor').spice);
assert.equal(reactionPlayer(game, 'atreides').forces[f.key], 5);
assert.equal(game.nexusCards!.cards!.hands.emperor, 'guild');
assert.deepEqual(game.nexusCards!.cards!.discard, ['richese']);
});
void test('the paying Bureaucrat cannot divert its own five-spice Guild Nexus payment', () => {
const f = createClassicNexusGuildPayment({ advanced: true, skills: true, discounted: false });
const game = reactionAction(f.game, 'emperor', { type: 'guildBetrayalUse',
event: f.game.pendingGuildBetrayal!.invoice.event });
assert.notEqual(game.decision?.kind, 'bureaucratPayment');
assert.equal(reactionPlayer(game, 'atreides').spice, reactionPlayer(f.beforeShipment, 'atreides').spice - 5);
assert.equal(reactionPlayer(game, 'emperor').spice, reactionPlayer(f.beforeShipment, 'emperor').spice + 5);
assert.equal(reactionPlayer(game, 'atreides').forces[f.key], 5);
});
void test('a genuine third-party Bureaucrat redirects two from the actual Guild Nexus recipient without repeating delivery', () => {
const f = createClassicNexusGuildPayment({ advanced: true, skills: true, discounted: false,
training: { atreides: 'diplomat', guild: 'mentat', harkonnen: 'bureaucrat', emperor: 'sandmaster' } });
let game = reactionAction(f.game, 'emperor', { type: 'guildBetrayalUse',
event: f.game.pendingGuildBetrayal!.invoice.event });
const offer = viewGame(game, 'harkonnen').bureaucrat!.pending!;
assert.equal(offer.payee, 'emperor');
assert.equal(offer.amount, 5);
game = reactionAction(game, 'harkonnen', { type: 'decision', event: offer.event, redirect: true });
assert.equal(reactionPlayer(game, 'atreides').spice, reactionPlayer(f.beforeShipment, 'atreides').spice - 5);
assert.equal(reactionPlayer(game, 'emperor').spice, reactionPlayer(f.beforeShipment, 'emperor').spice + 3);
assert.equal(reactionPlayer(game, 'harkonnen').spice, reactionPlayer(f.beforeShipment, 'harkonnen').spice);
assert.equal(reactionPlayer(game, 'guild').spice, reactionPlayer(f.beforeShipment, 'guild').spice);
assert.equal(reactionPlayer(game, 'atreides').forces[f.key], 5);
assert.equal(reactionPlayer(game, 'atreides').reserves, reactionPlayer(f.beforeShipment, 'atreides').reserves - 5);
assert.deepEqual(game.nexusCards!.cards!.discard, ['guild']);
});
for (const kind of ['reserve', 'cross', 'return'] as const) {
void test(`native Guild Cunning ${kind} shipment survives the neutral fee gate with one real paid group`, () => {
const f = submitClassicNexusSecondShipment(createClassicNexusCunning({ advanced: true, skills: true }), kind);
const offer = viewGame(f.game, 'emperor').guildBetrayalReaction!;
assert.equal(offer.canUse, false, 'The actual Guild singleton was already spent for Cunning.');
assert.equal(f.game.pendingGuildBetrayal!.invoice.price, 1);
const before = reactionPlayer(f.beforeSecond, 'guild');
let game = reactionAction(f.game, 'emperor', { type: 'guildBetrayalPass', event: offer.event });
const guild = reactionPlayer(game, 'guild'), destination = `${f.second}:${f.secondSector}`;
assert.equal(guild.spice, before.spice - 1);
assert.equal(reactionPlayer(game, 'emperor').spice, reactionPlayer(f.beforeSecond, 'emperor').spice);
assert.equal(guild.reserves, before.reserves + (kind === 'reserve' ? -1 : kind === 'return' ? 1 : 0));
assert.equal(guild.forces[f.origin] ?? 0, kind === 'reserve' ? 1 : 0);
assert.equal(guild.forces[destination] ?? 0, kind === 'return' ? 0 : 1);
assert.deepEqual(game.nexusCards!.cards!.discard, ['guild']);
assert.equal(game.nexusCards!.cards!.hands.emperor, 'harkonnen');
assert.equal(viewGame(game, 'guild').nexusGuildCunning!.active!.movesLeft, 0);
const settledForces = structuredClone(guild.forces);
game = reactionAction(game, 'guild', { type: 'endMovement' });
assert.deepEqual(reactionPlayer(game, 'guild').forces, settledForces);
assert.equal(game.nexusGuildCunningHistory!.at(-1)!.stage, 'completed');
});
}
void test('native Karama cancels only Cunning and retains the completed first shipment and its price', () => {
const f = createClassicNexusCunning({ advanced: true, skills: true });
const game = reactionAction(f.game, 'atreides', { type: 'card', card: f.counter, mode: 'cancel' });
assert.deepEqual(reactionPlayer(game, 'guild').forces, reactionPlayer(f.beforeCunning, 'guild').forces);
assert.equal(reactionPlayer(game, 'guild').reserves, reactionPlayer(f.beforeCunning, 'guild').reserves);
assert.equal(reactionPlayer(game, 'guild').spice, reactionPlayer(f.beforeCunning, 'guild').spice);
assert.equal(game.discard.filter(card => card.id === f.counter).length, 1);
assert.deepEqual(game.nexusCards!.cards!.discard, ['guild']);
assert.equal(game.nexusGuildCunningHistory!.at(-1)!.stage, 'canceled');
assert.notEqual(game.active, 'guild');
});
@@ -1,6 +1,7 @@
import assert from 'node:assert/strict';
import { viewGame, type Action, type Game } from '../game/engine';
import { baseDeck } from '../game/cards';
import type { FactionId } from '../game/catalog';
import { territory } from '../game/board';
import { HARASS_WITHDRAW_CARD } from '../game/harass-withdraw';
import { REINFORCEMENTS_CARD } from '../game/reinforcements';
@@ -20,10 +21,10 @@ export function reactionAction(game: Game, actor: string, action: Action, action
/** Original setup and turn-one play, followed by a real worm, reciprocal
* alliance and closing draw. Only unused original deck singletons move. */
function drawIndependentReaction(face: 'harkonnen' | 'ixians', owner: string,
export function drawClassicNexusReaction(face: FactionId, owner: string,
options: ClassicIndependentNexusOptions) {
const fixture = createClassicIndependentNexusFixture({ ...options,
roster: ['atreides', 'harkonnen', 'guild'] });
roster: options.roster ?? ['atreides', 'harkonnen', 'guild'] });
const actions = fixture.actions;
let game = fixture.game;
const take = (worm: boolean) => {
@@ -48,10 +49,11 @@ function drawIndependentReaction(face: 'harkonnen' | 'ixians', owner: string,
return { ...fixture, game, owner, actions, beforeDraw, afterDraw: structuredClone(game) };
}
export function createIndependentHarkonnenCall(options: { advanced?: boolean; skills?: boolean; remote?: boolean } = {}) {
export function createIndependentHarkonnenCall(options: { advanced?: boolean; skills?: boolean; remote?: boolean; independent?: boolean } = {}) {
const counter = baseDeck().find(card => card.effect === 'karama')!.id;
const fixture = drawIndependentReaction('harkonnen', 'atreides', { ...options,
startingCards: { atreides: [HARASS_WITHDRAW_CARD], guild: [counter] } });
const starting = options.independent === false ? baseDeck().find(card => card.kind === 'worthless')!.id : HARASS_WITHDRAW_CARD;
const fixture = drawClassicNexusReaction('harkonnen', 'atreides', { ...options,
startingCards: { atreides: [starting], guild: [counter] } });
const actions = fixture.actions;
let game = advanceClassicIndependentNexus(fixture.game, state => state.phase === 5 &&
state.active === 'atreides' && classicDiscoveryNexusClean(state), actions);
@@ -81,7 +83,7 @@ export function createIndependentHarkonnenCall(options: { advanced?: boolean; sk
for (const actor of [game.battle!.attacker, game.battle!.defender]) {
game = reactionAction(game, actor, { type: 'battlePlan', dial: 0, support: 0,
leader: actor === 'atreides' ? identity : opponentLeader,
weapon: actor === 'atreides' ? HARASS_WITHDRAW_CARD : null, defense: null }, actions);
weapon: actor === 'atreides' && options.independent !== false ? HARASS_WITHDRAW_CARD : null, defense: null }, actions);
while (game.response || game.phaseOpening || game.decision?.kind === 'fullPlanOffer') {
const step = nextClassicDiscoveryNexusStep(game); assert.ok(step);
game = applyClassicDiscoveryNexusStep(game, step, actions);
@@ -96,13 +98,17 @@ export function createIndependentHarkonnenCall(options: { advanced?: boolean; sk
return { ...fixture, game, beforeCall, beforeShipment, afterShipment, identity, opponent, key, counter };
}
export function createIndependentIxianPurchase(options: { advanced?: boolean; skills?: boolean; owner?: 'atreides' | 'harkonnen' } = {}) {
export function createIndependentIxianPurchase(options: { advanced?: boolean; skills?: boolean; owner?: 'atreides' | 'harkonnen'; independent?: boolean } = {}) {
const owner = options.owner ?? 'atreides';
const fixture = drawIndependentReaction('ixians', owner, { ...options,
startingCards: { [owner]: ['ecaz-recruits'] } });
const starting = options.independent === false ? baseDeck().find(card => card.kind === 'worthless')!.id : 'ecaz-recruits';
const fixture = drawClassicNexusReaction('ixians', owner, { ...options,
startingCards: { [owner]: [starting] } });
const actions = fixture.actions;
let game = advanceClassicIndependentNexus(fixture.game, state => state.phase === 2 && classicDiscoveryNexusClean(state), actions);
const ordered = [REINFORCEMENTS_CARD, HARASS_WITHDRAW_CARD].map(id => {
const lots = options.independent === false
? [baseDeck().find(card => card.kind === 'poison')!.id, baseDeck().find(card => card.kind === 'shield')!.id]
: [REINFORCEMENTS_CARD, HARASS_WITHDRAW_CARD];
const ordered = lots.map(id => {
const index = game.deck.findIndex(card => card.id === id); assert.ok(index >= 0);
return game.deck.splice(index, 1)[0];
});
+15 -3
View File
@@ -12,6 +12,8 @@ import { nextClassicDiscoveryNexusStep, applyClassicDiscoveryNexusStep } from '.
export type ClassicIndependentNexusOptions = {
advanced?: boolean;
skills?: boolean;
/** False exercises the original base33 without selecting the independent variant. */
independent?: boolean;
roster?: readonly FactionId[];
training?: Partial<Record<FactionId, LeaderSkillId>>;
/** Only original, still-undealt starting Treachery slots are ordered. */
@@ -22,6 +24,7 @@ export type ClassicIndependentNexusStep = { actor: string; action: Action };
export function createClassicIndependentNexusFixture(options: ClassicIndependentNexusOptions = {}) {
const roster = options.roster ?? ['atreides', 'guild', 'emperor'];
const advanced = options.advanced ?? false;
const independent = options.independent ?? true;
let game = createGame('CLASSICINDEPENDENTNEXUS', newPlayer(roster[0], roster[0], roster[0]), advanced, []);
for (const faction of roster.slice(1)) joinGame(game, newPlayer(faction, faction, faction));
const actions: ClassicIndependentNexusStep[] = [], staging: string[] = [];
@@ -31,14 +34,14 @@ export function createClassicIndependentNexusFixture(options: ClassicIndependent
? applyClassicDiscoveryNexusStep(game, { actor, action })
: applyAction(game, actor, action);
};
for (const option of ['ecazTreachery', 'nexusCards', ...(options.skills ? ['leaderSkills'] : [])])
for (const option of [...(independent ? ['ecazTreachery'] : []), 'nexusCards', ...(options.skills ? ['leaderSkills'] : [])])
act(game.host, { type: 'moduleOption', module: option, enabled: true });
for (const player of game.players) act(player.id, { type: 'ready' });
const lobby = structuredClone(game);
const rolls = options.skills ? skillOfferRolls(game, options.training) : [];
rolls.push(...Array<number>(NEXUS_FACTIONS.length - 1).fill(0xffffffff));
if (options.startingCards) {
const source = [...treacheryDeck([]), ...ecazTreacheryCards()].map(card => card.id);
const source = [...treacheryDeck([]), ...(independent ? ecazTreacheryCards() : [])].map(card => card.id);
const used = new Set<string>(), prefix: string[] = [];
const reserved = new Set(Object.values(options.startingCards).flatMap(cards => cards ?? []));
for (const player of game.players) {
@@ -109,7 +112,16 @@ export function advanceClassicIndependentNexus(game: Game, until: (state: Game)
actions?: ClassicIndependentNexusStep[], max = 1200): Game {
for (let count = 0; !until(game) && count < max; count++) {
assert.notEqual(game.status, 'finished', 'Original lifecycle ended before requested continuation');
const step = nextClassicDiscoveryNexusStep(game);
let step: ClassicIndependentNexusStep | null;
if (game.pendingGuildBetrayal) {
const actor = game.pendingGuildBetrayal.required.find(id => !game.pendingGuildBetrayal!.passed.includes(id));
assert.ok(actor);
const view = viewGame(game, actor);
view.players.find(player => player.id === actor)!.bot = 'Easy';
const action = botActions(view)[0];
assert.ok(action && ['guildBetrayalUse', 'guildBetrayalPass'].includes(action.type));
step = { actor, action };
} else step = nextClassicDiscoveryNexusStep(game);
assert.ok(step, `Use the real sealed control at ${game.turn}/${game.phase}/${game.decision?.kind}`);
game = applyClassicDiscoveryNexusStep(game, step, actions);
}
+94
View File
@@ -0,0 +1,94 @@
import assert from 'node:assert/strict';
import { viewGame, type Action, type Game } from '../game/engine';
import { TERRITORIES, territory } from '../game/board';
import { baseDeck } from '../game/cards';
import type { FactionId } from '../game/catalog';
import { drawClassicNexusReaction, reactionAction, reactionPlayer } from './fixture-classic-independent-nexus-reactions';
import { advanceClassicIndependentNexus, type ClassicIndependentNexusOptions, type ClassicIndependentNexusStep } from './fixture-classic-independent-nexus';
import { classicDiscoveryNexusClean } from './fixture-discovery-classic-nexus';
export type ClassicNexusCoreOptions = Pick<ClassicIndependentNexusOptions, 'advanced' | 'skills' | 'independent' | 'training'>;
function drawSecondFace(game: Game, owner: string, face: FactionId, actions: ClassicIndependentNexusStep[]) {
assert.equal(game.nexusCards!.phase!.stage, 'drawing');
const cards = game.nexusCards!.cards!, index = cards.deck.indexOf(face);
assert.ok(index >= 0);
cards.deck.unshift(cards.deck.splice(index, 1)[0]);
return reactionAction(game, owner, { type: 'nexusCardChoice', turn: game.turn,
card: cards.hands[owner], choice: 'draw', ownRedraws: 0 }, actions);
}
/** Two distinct original cards: A holds Richese (or an irrelevant Harkonnen),
* Emperor holds Guild, and actual Guild/Harkonnen are allied. */
export function createClassicNexusGuildPayment(options: ClassicNexusCoreOptions & { discounted?: boolean } = {}) {
const discounted = options.discounted ?? true;
const fixture = drawClassicNexusReaction(discounted ? 'richese' : 'harkonnen', 'atreides', {
...options, independent: options.independent ?? false,
roster: ['atreides', 'guild', 'harkonnen', 'emperor'],
training: options.training ?? (options.skills ? { atreides: 'bureaucrat' } : undefined),
});
const actions = fixture.actions;
let game = drawSecondFace(fixture.game, 'emperor', 'guild', actions);
const afterDraws = structuredClone(game);
game = advanceClassicIndependentNexus(game, state => state.phase === 5 && state.active === 'atreides' &&
classicDiscoveryNexusClean(state), actions);
const beforeShipment = structuredClone(game), place = territory('carthag');
assert.ok(!place.sectors.includes(game.storm));
const amount = 5;
const action: Action = { type: 'ship', territory: place.id, sector: place.sectors[0],
amount, elite: 0, allyPayment: 0 };
if (discounted) {
const offer = viewGame(game, 'atreides').nexusRichese!;
assert.equal(offer.blocked, null);
action.nexus = offer.event;
}
game = reactionAction(game, 'atreides', action, actions);
game = advanceClassicIndependentNexus(game, state => !!state.pendingGuildBetrayal &&
classicDiscoveryNexusClean(state), actions);
return { ...fixture, game, afterDraws, beforeShipment, actions, action, amount,
key: `${place.id}:${place.sectors[0]}`, holder: 'emperor' };
}
/** A/H form the real alliance; native Guild and Emperor draw distinct faces.
* The latter holds Harkonnen, never a second copy of Guild's singleton. */
export function createClassicNexusCunning(options: ClassicNexusCoreOptions = {}) {
const counter = baseDeck().find(card => card.effect === 'karama')!.id;
const fixture = drawClassicNexusReaction('guild', 'guild', { ...options,
independent: options.independent ?? false, roster: ['atreides', 'harkonnen', 'guild', 'emperor'],
startingCards: { atreides: [counter] },
});
const actions = fixture.actions;
let game = drawSecondFace(fixture.game, 'emperor', 'harkonnen', actions);
game = advanceClassicIndependentNexus(game, state => state.phase === 5 && state.active === 'guild' &&
classicDiscoveryNexusClean(state), actions);
const empty = TERRITORIES.filter(place => place.type === 'stronghold' &&
!place.sectors.includes(game.storm) && game.players.every(player =>
Object.keys(player.forces).every(key => !key.startsWith(`${place.id}:`))));
assert.ok(empty.length >= 2);
const first = empty[0], second = empty[1], origin = `${first.id}:${first.sectors[0]}`;
const beforeFirst = structuredClone(game);
game = reactionAction(game, 'guild', { type: 'ship', territory: first.id, sector: first.sectors[0],
amount: 1, elite: 0, allyPayment: 0 }, actions);
game = advanceClassicIndependentNexus(game, state => reactionPlayer(state, 'guild').shipped &&
!state.pendingGuildBetrayal && classicDiscoveryNexusClean(state), actions);
const beforeCunning = structuredClone(game), offer = viewGame(game, 'guild').nexusGuildCunning!.offer!;
assert.equal(offer.blocked, null);
game = reactionAction(game, 'guild', { type: 'endMovement', nexus: offer.event }, actions);
return { ...fixture, game, beforeFirst, beforeCunning, actions, counter, origin,
second: second.id, secondSector: second.sectors[0] };
}
export function submitClassicNexusSecondShipment(fixture: ReturnType<typeof createClassicNexusCunning>,
kind: 'reserve' | 'cross' | 'return') {
let game = advanceClassicIndependentNexus(fixture.game, state =>
state.nexusGuildCunningHistory?.at(-1)?.stage === 'secondShipment' && classicDiscoveryNexusClean(state), fixture.actions);
const beforeSecond = structuredClone(game);
const action: Action = kind === 'reserve'
? { type: 'ship', territory: fixture.second, sector: fixture.secondSector, amount: 1, elite: 0, allyPayment: 0 }
: { type: 'guildShip', from: fixture.origin,
territory: kind === 'return' ? 'reserves' : fixture.second,
...(kind === 'return' ? {} : { sector: fixture.secondSector }), amount: 1, elite: 0, allyPayment: 0 };
game = reactionAction(game, 'guild', action, fixture.actions);
game = advanceClassicIndependentNexus(game, state => !!state.pendingGuildBetrayal &&
classicDiscoveryNexusClean(state), fixture.actions);
return { ...fixture, game, beforeSecond, action, kind };
}
-53
View File
@@ -249,18 +249,6 @@ void test('No publicly possible holder auto-settles the native fee without a fab
validateNexusCards(done.nexusCards!.cards!, done.players);
});
void test('Unsupported Cunning producer is visibly blocked before its singleton is spent in the bounded payment profile', () => {
const fixture = createGuildBetrayalFixture({payer: 'guild'});
const before = structuredClone(fixture.before);
const cards = before.nexusCards!.cards!;
cards.hands[fixture.holder] = cards.hands[fixture.guild];
cards.hands[fixture.guild] = 'guild';
const offer = viewGame(before, fixture.guild).nexusGuildCunning!.offer!;
assert.match(offer.blocked!, /second-shipment overlay/);
rejects(before, fixture.guild, {type: 'endMovement', nexus: offer.event});
assert.equal(before.nexusCards!.cards!.hands[fixture.guild], 'guild');
assert.equal(before.nexusGuildCunningHistory, undefined);
});
function outsidePaymentProfile(game: Game): Game {
const native = structuredClone(game);
@@ -272,47 +260,6 @@ function outsidePaymentProfile(game: Game): Game {
return native;
}
for (const advanced of [false, true]) {
void test(`${advanced ? 'Advanced' : 'Basic'} Richese overlay rejects before spend while ordinary and outside-profile shipments finish`, () => {
const fixture = createGuildBetrayalFixture({advanced, payer: 'holder'});
const before = structuredClone(fixture.before);
const cards = before.nexusCards!.cards!;
const index = cards.deck.indexOf('richese');
assert.ok(index >= 0);
cards.deck[index] = cards.hands[fixture.holder]!;
cards.hands[fixture.holder] = 'richese';
validateNexusCards(cards, before.players);
const offer = viewGame(before, fixture.holder).nexusRichese!;
assert.match(offer.blocked!, /Richese Secret Ally shipment overlay/);
const action: Action = {...fixture.declaration, amount: 3, nexus: offer.event};
const saved = structuredClone(before);
assert.throws(() => applyAction(before, fixture.holder, action), /Richese Secret Ally shipment overlay/);
assert.deepEqual(before, saved, 'No singleton spend, parent, history, fee or forces change on rejection.');
assert.equal(before.nexusRicheseHistory, undefined);
const ordinary = settleGuildBetrayalFixture(applyAction(before, fixture.holder,
{...fixture.declaration, amount: 3}));
const prior = before.players.find(p => p.id === fixture.holder)!;
const shipped = ordinary.players.find(p => p.id === fixture.holder)!;
assert.equal(shipped.reserves, prior.reserves - 3);
assert.equal(shipped.spice, prior.spice - 3);
assert.equal(shipped.shipped, true);
assert.equal(ordinary.nexusCards!.cards!.hands[fixture.holder], 'richese');
const native = outsidePaymentProfile(before);
const nativeOffer = viewGame(native, fixture.holder).nexusRichese!;
assert.equal(nativeOffer.blocked, null);
const done = settleGuildBetrayalFixture(applyAction(native, fixture.holder,
{...action, nexus: nativeOffer.event}));
const recipient = done.players.find(p => p.id === fixture.holder)!;
assert.equal(recipient.reserves, prior.reserves - 3);
assert.equal(recipient.spice, prior.spice - 1);
assert.equal(recipient.shipped, true);
assert.equal(done.nexusCards!.cards!.hands[fixture.holder], null);
assert.equal(done.nexusRicheseHistory!.at(-1)!.stage, 'shipped');
assert.equal(done.nexusCards!.cards!.discard.filter(face => face === 'richese').length, 1);
});
}
void test('Saved native cross permission is independently required even for a self-consistent paid frame or purchased Karama rate', () => {
for (const purchasedRate of [false, true]) {