186 lines
12 KiB
TypeScript
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);
|
|
}
|
|
});
|