Files
Dune/tests/classic-nexus-core.test.ts
Quintine 178251ad35 Restore native Ecaz simulation continuation and current rule fixtures
Match original Ecaz sample snapshots to their native Occupy and Advanced assassination state. Preserve older absent-power samples and resume the original scenario instead of rejecting fresh saved output. Update stale fixture reactions and module expectations while retaining meaningful Banker guidance and positive legal commitments. Correct the unsupported rules-completion claim.

Validation: typecheck/lint, 173 affected tests across 16 files, bounded independent source review, and one direct Ecaz continuation completed through 178 accepted actions. Runtime fingerprint 95d009d3957650b08bb24826b70aa5f00a74cdd9992b6a6c222f0665d3e3759c; documentation-only proof update a91ab248109bee74a74373fb544027a2e7cc8f178e26d2c9dad475a9618eb64d. Full rules, comprehensive assurance and deployed acceptance remain open. No saved-room or provider changes.
2026-10-18 10:08:55 +11:00

202 lines
14 KiB
TypeScript

import assert from 'node:assert/strict';
import { test } from 'node:test';
import { viewGame } from '../game/engine';
import { createIndependentHarkonnenCall, createIndependentIxianPurchase,
drawClassicNexusReaction, 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';
import { TERRITORIES } from '../game/board';
import { nativeShipmentSources } from '../game/homeworld-options';
function smugglerFeeGame(homeworlds: boolean) {
const fixture = drawClassicNexusReaction('harkonnen', 'atreides', {
advanced: true, skills: true, independent: false, homeworlds,
roster: ['atreides', 'guild', 'harkonnen', 'emperor'],
training: { atreides: 'smuggler', guild: 'mentat', harkonnen: 'bureaucrat', emperor: 'sandmaster' },
});
let game = fixture.game;
const cards = game.nexusCards!.cards!, index = cards.deck.indexOf('guild');
assert.ok(index >= 0);
cards.deck.unshift(cards.deck.splice(index, 1)[0]);
game = reactionAction(game, 'emperor', { type: 'nexusCardChoice', turn: game.turn,
card: cards.hands.emperor, choice: 'draw', ownRedraws: 0 });
game = advanceClassicIndependentNexus(game, state => state.phase === 5 &&
state.active === 'atreides' && classicDiscoveryNexusClean(state));
const destination = TERRITORIES.find(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(destination);
const before = structuredClone(game), key = `${destination.id}:${destination.sectors[0]}`;
const sources = homeworlds ? nativeShipmentSources(viewGame(game, 'atreides'), 2, 0) : undefined;
if (homeworlds) assert.ok(sources);
game = reactionAction(game, 'atreides', { type: 'ship', territory: destination.id,
sector: destination.sectors[0], amount: 2, elite: 0, allyPayment: 0, smuggler: true,
...(sources ? { homeworldSources: sources } : {}) });
game = advanceClassicIndependentNexus(game, state => !!state.pendingGuildBetrayal &&
classicDiscoveryNexusClean(state));
return { game, before, key };
}
void test('ordinary Smuggler ships two physical counters for one spice through the distinct Guild holder claim', () => {
const f = smugglerFeeGame(false), before = reactionPlayer(f.before, 'atreides');
assert.equal(f.game.pendingGuildBetrayal!.invoice.price, 1);
assert.equal(reactionPlayer(f.game, 'atreides').reserves, before.reserves);
assert.equal(reactionPlayer(f.game, 'atreides').forces[f.key] ?? 0, 0);
const game = reactionAction(f.game, 'emperor', { type: 'guildBetrayalUse',
event: f.game.pendingGuildBetrayal!.invoice.event });
assert.equal(reactionPlayer(game, 'atreides').spice, before.spice - 1);
assert.equal(reactionPlayer(game, 'atreides').reserves, before.reserves - 2);
assert.equal(reactionPlayer(game, 'atreides').forces[f.key], 2);
assert.equal(reactionPlayer(game, 'emperor').spice, reactionPlayer(f.before, 'emperor').spice + 1);
assert.equal(reactionPlayer(game, 'guild').spice, reactionPlayer(f.before, 'guild').spice);
assert.deepEqual(game.nexusCards!.cards!.discard, ['guild']);
});
void test('Homeworld Smuggler withdraws both Caladan counters only after Pass and pays the original discounted Guild invoice', () => {
const f = smugglerFeeGame(true), before = reactionPlayer(f.before, 'atreides');
const caladan = (game: typeof f.game) => viewGame(game, 'atreides').homeworlds!.worlds!
.find(world => world.id === 'homeworld:atreides')!.forces.atreides;
assert.deepEqual(caladan(f.game), { normal: 10, elite: 0 });
assert.equal(f.game.pendingGuildBetrayal!.invoice.price, 1);
let game = reactionAction(f.game, 'emperor', { type: 'guildBetrayalPass',
event: f.game.pendingGuildBetrayal!.invoice.event });
assert.deepEqual(caladan(game), { normal: 10, elite: 0 });
assert.equal(reactionPlayer(game, 'atreides').forces[f.key] ?? 0, 0);
game = reactionAction(game, 'atreides', { type: 'guildBetrayalPass',
event: game.pendingGuildBetrayal!.invoice.event });
assert.deepEqual(caladan(game), { normal: 8, elite: 0 });
assert.equal(reactionPlayer(game, 'atreides').reserves, before.reserves - 2);
assert.equal(reactionPlayer(game, 'atreides').forces[f.key], 2);
assert.equal(reactionPlayer(game, 'atreides').spice, before.spice - 1);
assert.equal(reactionPlayer(game, 'guild').spice, reactionPlayer(f.before, 'guild').spice + 1);
assert.equal(reactionPlayer(game, 'emperor').spice, reactionPlayer(f.before, 'emperor').spice);
assert.equal(game.nexusCards!.cards!.hands.emperor, 'guild');
assert.deepEqual(game.nexusCards!.cards!.discard, []);
});
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');
});