Files
Dune/tests/nexus-emperor-purchase-engine.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

141 lines
7.9 KiB
TypeScript

import assert from 'node:assert/strict';
import test from 'node:test';
import { applyAction, normalizeAutomaticGame, viewGame, type Action, type Game } from '../game/engine';
import { nexusInventory, nexusReload } from './fixture-nexus-cards';
import { wonEmperorNexusBankAuction } from './fixture-nexus-emperor-purchase';
const owner = 'q';
function reject(g: Game, player: string, action: Action, reason: RegExp) {
const before = JSON.stringify(g);
assert.throws(() => applyAction(g, player, action), reason);
assert.equal(JSON.stringify(g), before);
}
void test('real Basic/Advanced normal auction purchase keeps exactly the personally affordable bid and spends the physical Nexus card', () => {
for (const advanced of [false, true]) {
const g = wonEmperorNexusBankAuction(advanced);
const price = g.auction!.bid;
const lot = g.auction!.cards[g.auction!.index];
const before = JSON.stringify(g);
const ownerView = viewGame(g, owner);
assert.deepEqual(ownerView.nexusEmperorSecretAlly!.purchase, { blocked: null, price });
for (const id of ['p', 'r']) {
assert.equal(viewGame(g, id).nexusEmperorSecretAlly, null);
assert.equal(JSON.stringify(viewGame(g, id)).includes('nexusEmperorPurchaseHistory'), false);
}
const action = { type: 'nexusEmperorPurchase', event: ownerView.nexusEmperorSecretAlly!.event };
const done = applyAction(g, owner, action);
assert.equal(JSON.stringify(g), before);
nexusInventory(done);
assert.equal(done.players.find(p => p.id === owner)!.spice, g.players.find(p => p.id === owner)!.spice);
assert.equal(done.players.find(p => p.id === owner)!.hand.filter(card => card.id === lot.id).length, 1);
assert.equal(done.nexusCards!.cards!.hands[owner], null);
assert.equal(done.nexusCards!.cards!.discard.filter(card => card === 'emperor').length, 1);
assert.deepEqual(done.nexusEmperorPurchaseEvents, [action.event]);
assert.equal(done.nexusEmperorPurchaseHistory?.[0].card, lot.id);
assert.equal(done.nexusEmperorPurchaseHistory?.[0].price, price);
assert.equal(done.nexusEmperorPurchaseHistory?.[0].afterSpice, done.nexusEmperorPurchaseHistory?.[0].beforeSpice);
assert.equal(done.log.filter(entry => entry.text.includes('spent Emperor Nexus Secret Ally after proving')).length, 1);
for (const p of done.players)
assert.deepEqual(viewGame(nexusReload(done), p.id), viewGame(done, p.id));
reject(done, owner, action, /Emperor Nexus|current|payment|card/i);
}
});
void test('every eligible winner confirms payment regardless of concealed Emperor card ownership', () => {
const withCard = wonEmperorNexusBankAuction(false);
const without = nexusReload(withCard);
without.nexusCards!.cards!.hands[owner] = null;
without.nexusCards!.cards!.deck.push('emperor');
const waitingWith = normalizeAutomaticGame(withCard);
const waitingWithout = normalizeAutomaticGame(without);
assert.deepEqual(waitingWith.decision, { kind: 'auctionPayment', player: owner });
assert.deepEqual(waitingWithout.decision, waitingWith.decision);
assert.equal(viewGame(waitingWithout, owner).nexusEmperorSecretAlly, null);
const paid = applyAction(waitingWithout, owner, { type: 'decision', karama: false });
assert.equal(paid.players.find(p => p.id === owner)!.spice,
without.players.find(p => p.id === owner)!.spice - without.auction!.bid);
assert.equal(paid.players.find(p => p.id === owner)!.hand.filter(
card => card.id === without.auction!.cards[without.auction!.index].id).length, 1);
nexusInventory(paid);
});
void test('a prior normal purchase cannot authorize spending Emperor Nexus on a later lot or revised bid', () => {
let g = wonEmperorNexusBankAuction(false);
const first = { type: 'nexusEmperorPurchase', event: viewGame(g, owner).nexusEmperorSecretAlly!.event };
const revised = nexusReload(g);
revised.auction!.bid++;
assert.notEqual(viewGame(revised, owner).nexusEmperorSecretAlly!.event, first.event);
reject(revised, owner, first, /current Emperor Nexus/i);
g = applyAction(g, owner, { type: 'decision', karama: false });
for (let i = 0; g.decision?.kind !== 'auctionPayment' && i < 20; i++) {
// Native purchased-card choices now initialize at fresh setup for classic
// core games; the paid lot opens its native replacement window first.
if (g.decision?.kind === 'nexusIxianReplacement')
g = applyAction(g, g.decision.player, { type: 'nexusIxianReplacementPass', event: g.decision.event });
else if (g.auction?.active === owner && !g.auction.bid)
g = applyAction(g, owner, { type: 'bid', amount: 2 });
else if (g.auction)
g = applyAction(g, g.auction.active, { type: 'passBid' });
}
assert.equal(g.auction?.index, 1);
assert.equal(g.decision?.player, owner);
assert.notEqual(viewGame(g, owner).nexusEmperorSecretAlly!.event, first.event);
reject(g, owner, first, /current Emperor Nexus/i);
const second = { type: 'nexusEmperorPurchase', event: viewGame(g, owner).nexusEmperorSecretAlly!.event };
const done = applyAction(g, owner, second);
const receipt = done.nexusEmperorPurchaseHistory?.[0];
assert.ok(receipt && 'auctionIndex' in receipt);
assert.equal(receipt.auctionIndex, 1);
});
void test('invalid normal-lot custody and full winning hand cannot spend the physical Nexus card', () => {
const g = wonEmperorNexusBankAuction(false);
const action = { type: 'nexusEmperorPurchase', event: viewGame(g, owner).nexusEmperorSecretAlly!.event };
const duplicate = nexusReload(g);
duplicate.deck.push(structuredClone(g.auction!.cards[g.auction!.index]));
// Duplicate card custody is now rejected by the native purchased-card
// integrity check before the Emperor purchase gate is even consulted.
assert.throws(() => viewGame(duplicate, owner), /single physical custody/);
reject(duplicate, owner, action, /single physical custody/);
const full = nexusReload(g);
full.players.find(p => p.id === owner)!.hand.push(...full.deck.splice(0, 4));
assert.match(viewGame(full, owner).nexusEmperorSecretAlly!.purchase!.blocked!, /room for the committed/);
reject(full, owner, action, /room for the committed/);
});
void test('stale, foreign, malformed, underfunded and ally-funded purchase attempts reject before card or auction mutation', () => {
const g = wonEmperorNexusBankAuction(false);
const action = { type: 'nexusEmperorPurchase', event: viewGame(g, owner).nexusEmperorSecretAlly!.event };
reject(g, 'p', action, /Emperor Nexus|current|payment/i);
reject(g, owner, { ...action, event: 'stale' }, /current|payment/i);
reject(g, owner, { ...action, price: 0 }, /current|payment/i);
const underfunded = nexusReload(g);
underfunded.players.find(p => p.id === owner)!.spice = g.auction!.bid - 1;
assert.match(viewGame(underfunded, owner).nexusEmperorSecretAlly!.purchase!.blocked!, /own spice/);
reject(underfunded, owner, action, /own spice/);
const allyFunded = nexusReload(g);
allyFunded.auction!.allyPayment = 1;
assert.match(viewGame(allyFunded, owner).nexusEmperorSecretAlly!.purchase!.blocked!, /own spice/);
reject(allyFunded, owner, action, /own spice/);
});
void test('signed historical purchase rejects a changed price, lot, proof or missing independent-use marker after JSON restore', () => {
const g = wonEmperorNexusBankAuction(true);
const action = { type: 'nexusEmperorPurchase', event: viewGame(g, owner).nexusEmperorSecretAlly!.event };
const done = applyAction(g, owner, action);
for (const tamper of [
(copy: Game) => { copy.nexusEmperorPurchaseHistory![0].price++; },
(copy: Game) => { copy.nexusEmperorPurchaseHistory![0].card = 'forged-lot'; },
(copy: Game) => { copy.nexusEmperorPurchaseHistory![0].afterSpice--; },
(copy: Game) => { delete copy.nexusEmperorPurchaseEvents; },
(copy: Game) => { copy.nexusEmperorPurchaseEvents!.push(action.event); },
]) {
const copy = nexusReload(done);
tamper(copy);
const before = JSON.stringify(copy);
assert.throws(() => viewGame(copy, owner), /Emperor Nexus purchase/);
assert.equal(JSON.stringify(copy), before);
}
});