Files
Dune/tests/nexus-emperor-ambassador-runtime.test.ts

186 lines
12 KiB
TypeScript

import assert from 'node:assert/strict';
import test from 'node:test';
import { applyAction, viewGame, type Action, type Game } from '../game/engine';
import { botActions } from '../game/bots';
import {
createNexusEmperorAmbassadorFixture, finishNexusEmperorAmbassador,
nexusEmperorAmbassadorCards, nexusEmperorAmbassadorInventory, nexusEmperorAmbassadorPlayer,
nextNexusEmperorAmbassadorStep, stepNexusEmperorAmbassador,
} from './fixture-nexus-emperor-ambassador';
function rejectsUnchanged(game: Game, actor: string, action: Action): void {
const before = structuredClone(game);
assert.throws(() => applyAction(game, actor, action));
assert.deepEqual(game, before, 'Rejection must precede spending, drawing and Ambassador continuation');
}
for (const advanced of [false, true]) for (const arrival of ['shipment', 'worm'] as const) void test(
`${advanced ? 'Advanced' : 'Basic'} original ${arrival}: Pay 3 versus Use 0 purchases the same physical card exactly once`, () => {
const f = createNexusEmperorAmbassadorFixture({ advanced, arrival });
const before = structuredClone(f.game), own = nexusEmperorAmbassadorPlayer(before, f.buyer);
assert.deepEqual(f.game.deck, f.chronology.entry.deck, 'Offering confirmation cannot reserve or draw a card');
assert.deepEqual(f.game.discard, f.chronology.entry.discard);
assert.equal(own.hand.length, nexusEmperorAmbassadorPlayer(f.chronology.entry, f.buyer).hand.length);
if (arrival === 'shipment') assert.equal(f.game.active, f.entrant, 'The buyer does not replace the original movement actor');
else assert.equal(f.game.pendingAmbassador!.resume, 'wormRide');
const offer = viewGame(f.game, f.buyer).nexusEmperorAmbassadorPurchase!;
assert.equal(offer.canUse, true); assert.equal(offer.blocked, null);
for (const rival of f.game.players.filter(p => p.id !== f.buyer)) {
const view = viewGame(f.game, rival.id);
assert.equal(view.nexusEmperorAmbassadorPurchase, null);
assert.equal(view.players.find(p => p.id === f.buyer)!.hand, undefined);
assert.equal(JSON.stringify(view.ambassadorEntry).includes(f.card.id), false);
}
const outcomes = [false, true].map(use => {
const action = { type: 'nexusEmperorAmbassadorPurchase', event: f.event, use };
let game = stepNexusEmperorAmbassador(f.game, { actor: f.buyer, action });
game = finishNexusEmperorAmbassador(game);
assert.equal(nexusEmperorAmbassadorPlayer(game, f.buyer).spice, own.spice - (use ? 0 : 3));
assert.equal(nexusEmperorAmbassadorPlayer(game, f.buyer).hand.length, own.hand.length + 1,
'Ecaz must not receive a fabricated native Harkonnen bonus');
assert.equal(nexusEmperorAmbassadorPlayer(game, f.buyer).hand.filter(c => c.id === f.card.id).length, 1);
assert.deepEqual(game.deck, before.deck.slice(1));
assert.deepEqual(game.discard, before.discard);
assert.equal(game.nexusCards!.cards!.hands[f.buyer], use ? null : 'emperor');
assert.equal(game.nexusCards!.cards!.discard.filter(c => c === 'emperor').length, use ? 1 : 0);
assert.equal(game.response?.kind === 'emperorIncome', false);
assert.equal(game.auction ?? null, null); assert.equal(game.currentAuctionSale ?? null, null);
for (const rival of game.players.filter(p => p.id !== f.buyer))
assert.equal(rival.spice, f.wallets[rival.id], 'No seller, subsidy, reimbursement or income is invented');
assert.deepEqual(game.players.map(p => [p.id, p.forces, p.reserves, p.shipped, p.moved]),
before.players.map(p => [p.id, p.forces, p.reserves, p.shipped, p.moved]));
assert.deepEqual(game.movementRemaining, before.movementRemaining);
assert.deepEqual(nexusEmperorAmbassadorCards(game), nexusEmperorAmbassadorCards(before));
nexusEmperorAmbassadorInventory(game);
if (use) {
assert.equal(game.nexusEmperorPurchaseEvents!.filter(event => event === f.event).length, 1);
} else assert.deepEqual(game.nexusEmperorPurchaseHistory ?? [], before.nexusEmperorPurchaseHistory ?? []);
rejectsUnchanged(game, f.buyer, action);
return game;
});
assert.deepEqual(f.game, before);
assert.deepEqual(outcomes[0].decision, outcomes[1].decision);
assert.deepEqual(outcomes[0].wormRides, outcomes[1].wormRides);
if (arrival === 'shipment') {
for (const game of outcomes) {
assert.equal(game.active, f.entrant);
const next = stepNexusEmperorAmbassador(game, { actor: f.entrant, action: { type: 'endMovement' } });
assert.notEqual(next.active, f.entrant);
assert.deepEqual(next.deck, game.deck, 'The original movement successor cannot repeat the purchase');
}
} else {
const steps = outcomes.map(nextNexusEmperorAmbassadorStep);
for (const [i, game] of outcomes.entries()) {
const next = stepNexusEmperorAmbassador(game, steps[i]);
assert.deepEqual(next.deck, game.deck);
assert.equal(nexusEmperorAmbassadorPlayer(next, f.buyer).hand.length, own.hand.length + 1);
}
}
},
);
void test('wrong actor, stale event, extra cost fields, coerced input and original-trigger replay are atomic refusals', () => {
const f = createNexusEmperorAmbassadorFixture();
for (const rival of f.game.players.filter(p => p.id !== f.buyer))
rejectsUnchanged(f.game, rival.id, { type: 'nexusEmperorAmbassadorPurchase', event: f.event, use: true });
for (const action of [
{ type: 'nexusEmperorAmbassadorPurchase', event: 'stale', use: true },
{ type: 'nexusEmperorAmbassadorPurchase', event: f.event, use: true, price: 0 },
{ type: 'nexusEmperorAmbassadorPurchase', event: f.event, use: false, allyPayment: 3 },
{ type: 'nexusEmperorAmbassadorPurchase', event: f.event, use: true, card: f.card.id },
{ type: 'nexusEmperorAmbassadorPurchase', event: f.event, use: true, cost: f.card.id },
{ type: 'nexusEmperorAmbassadorPurchase', event: f.event, use: 'true' },
{ type: 'nexusEmperorAmbassadorPurchase', event: f.event, use: 1 },
{ type: 'nexusEmperorAmbassadorPurchase', event: f.event },
{ type: 'decision', event: f.event, use: true },
f.chronology.trigger.step.action,
]) rejectsUnchanged(f.game, f.buyer, action);
nexusEmperorAmbassadorInventory(f.game);
});
for (const advanced of [false, true]) void test(
`${advanced ? 'Advanced' : 'Basic'} irrelevant original Nexus draw still opens uniform buyer Pay, never Use`, () => {
const f = createNexusEmperorAmbassadorFixture({ advanced, nexusCard: 'guild' });
const offer = viewGame(f.game, f.buyer).nexusEmperorAmbassadorPurchase!;
assert.equal(offer.canUse, false);
rejectsUnchanged(f.game, f.buyer, { type: 'nexusEmperorAmbassadorPurchase', event: f.event, use: true });
const game = finishNexusEmperorAmbassador(stepNexusEmperorAmbassador(f.game, { actor: f.buyer,
action: { type: 'nexusEmperorAmbassadorPurchase', event: f.event, use: false } }));
assert.equal(nexusEmperorAmbassadorPlayer(game, f.buyer).spice, f.wallets[f.buyer] - 3);
assert.equal(game.nexusCards!.cards!.hands[f.buyer], 'guild');
assert.deepEqual(game.deck, f.game.deck.slice(1));
assert.equal(nexusEmperorAmbassadorPlayer(game, f.buyer).hand.filter(c => c.id === f.card.id).length, 1);
nexusEmperorAmbassadorInventory(game);
},
);
void test('actual setup-dealt Truthtrance suspends the original Ambassador confirmation and restores the same purchase', () => {
const f = createNexusEmperorAmbassadorFixture({ truthtrance: true });
const card = nexusEmperorAmbassadorPlayer(f.game, 'guild').hand.find(c => c.effect === 'truthtrance');
assert.ok(card);
assert.ok(nexusEmperorAmbassadorPlayer(f.chronology.afterSetup, 'guild').hand.some(c => c.id === card.id));
let game = stepNexusEmperorAmbassador(f.game, { actor: 'guild', action: { type: 'card', card: card.id } });
assert.equal(game.truthtrance!.stage, 'priority');
rejectsUnchanged(game, f.buyer, { type: 'nexusEmperorAmbassadorPurchase', event: f.event, use: true });
while (game.truthtrance?.stage === 'priority') {
const actor = game.players.find(p => !game.truthtrance!.passed.includes(p.id))!.id;
game = stepNexusEmperorAmbassador(game, { actor, action: { type: 'truthPass' } });
}
const name = nexusEmperorAmbassadorPlayer(game, f.buyer).hand[0].name;
game = stepNexusEmperorAmbassador(game, { actor: 'guild', action: { type: 'truthAsk',
question: { kind: 'fact', target: f.buyer, fact: { kind: 'hand', name } } } });
assert.equal(viewGame(game, f.buyer).truthAnswer, 'yes');
game = stepNexusEmperorAmbassador(game, { actor: f.buyer, action: { type: 'truthAnswer', answer: 'yes' } });
assert.equal(game.truthtrance, null);
assert.ok(game.decision?.kind === 'nexusEmperorAmbassadorPurchase');
assert.deepEqual(game.deck, f.game.deck);
assert.equal(nexusEmperorAmbassadorPlayer(game, f.buyer).spice, f.wallets[f.buyer]);
game = finishNexusEmperorAmbassador(stepNexusEmperorAmbassador(game, { actor: f.buyer,
action: { type: 'nexusEmperorAmbassadorPurchase', event: f.event, use: true } }));
assert.equal(nexusEmperorAmbassadorPlayer(game, f.buyer).hand.filter(c => c.id === f.card.id).length, 1);
assert.equal(game.discard.filter(c => c.id === card.id).length, 1);
nexusEmperorAmbassadorInventory(game);
});
for (const advanced of [false, true]) void test(
`${advanced ? 'Advanced' : 'Basic'} real later alliance gives native Harkonnen buyer Pay-only and one original bonus`, () => {
const f = createNexusEmperorAmbassadorFixture({ advanced, buyer: 'harkonnen' });
assert.equal(nexusEmperorAmbassadorPlayer(f.game, f.buyer).ally, f.owner);
assert.equal(nexusEmperorAmbassadorPlayer(f.game, f.owner).ally, f.buyer);
assert.equal(viewGame(f.game, f.buyer).nexusEmperorAmbassadorPurchase!.canUse, false);
assert.equal(viewGame(f.game, f.owner).nexusEmperorAmbassadorPurchase, null);
for (const actor of [f.owner, f.buyer])
rejectsUnchanged(f.game, actor, { type: 'nexusEmperorAmbassadorPurchase', event: f.event, use: true });
const hand = nexusEmperorAmbassadorPlayer(f.game, f.buyer).hand.length;
let game = stepNexusEmperorAmbassador(f.game, { actor: f.buyer,
action: { type: 'nexusEmperorAmbassadorPurchase', event: f.event, use: false } });
const bonus = f.game.deck[1]; assert.ok(bonus);
game = finishNexusEmperorAmbassador(game);
assert.equal(nexusEmperorAmbassadorPlayer(game, f.buyer).hand.length, hand + 2);
assert.equal(nexusEmperorAmbassadorPlayer(game, f.buyer).spice, f.wallets[f.buyer] - 3);
assert.equal(nexusEmperorAmbassadorPlayer(game, f.buyer).hand.filter(c => c.id === bonus.id).length, 1);
assert.deepEqual(game.deck, f.game.deck.slice(2));
assert.deepEqual(game.nexusCards!.cards, f.game.nexusCards!.cards,
'The original paid purchase must not consume another card after native alliance disposal.');
for (const rival of game.players.filter(p => p.id !== f.buyer)) assert.equal(rival.spice, f.wallets[rival.id]);
assert.equal(game.auction ?? null, null); assert.equal(game.currentAuctionSale ?? null, null);
nexusEmperorAmbassadorInventory(game);
},
);
void test('original Easy policy resolves both actual native buyer entitlements without borrowing movement control', () => {
for (const nexusCard of ['emperor', 'guild'] as const) {
const f = createNexusEmperorAmbassadorFixture({ nexusCard });
const view = viewGame(f.game, f.buyer);
view.players.find(p => p.id === f.buyer)!.bot = 'Easy';
const action = botActions(view)[0]; assert.ok(action);
const game = finishNexusEmperorAmbassador(stepNexusEmperorAmbassador(f.game, { actor: f.buyer, action }));
assert.equal(game.active, f.entrant);
assert.equal(nexusEmperorAmbassadorPlayer(game, f.buyer).spice,
f.wallets[f.buyer] - (nexusCard === 'emperor' ? 0 : 3));
assert.equal(game.nexusCards!.cards!.hands[f.buyer], nexusCard === 'emperor' ? null : 'guild');
assert.equal(nexusEmperorAmbassadorPlayer(game, f.buyer).hand.filter(c => c.id === f.card.id).length, 1);
nexusEmperorAmbassadorInventory(game);
}
});