211 lines
13 KiB
TypeScript
211 lines
13 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
import { applyAction, handLimit, viewGame, type Action, type Game } from '../game/engine';
|
|
import { botActions } from '../game/bots';
|
|
import { quoteEmperorNexusSpecialPurchase } from '../game/nexus-emperor-purchase';
|
|
import { createNexusEmperorSellerFixture } from './fixture-nexus-emperor-seller';
|
|
import {
|
|
advanceNexusEmperorSpecial, createNexusEmperorSpecialFixture, nexusEmperorSpecialCards,
|
|
nexusEmperorSpecialClean, nexusEmperorSpecialInventory, nexusEmperorSpecialPlayer,
|
|
nextNexusEmperorSpecialStep, stepNexusEmperorSpecial, type NexusEmperorSpecialFixture,
|
|
} from './fixture-nexus-emperor-special';
|
|
|
|
function purchase(f: NexusEmperorSpecialFixture, joint: boolean): Action {
|
|
return { type: 'card', mode: 'special', card: f.activation, acquire: f.acquire,
|
|
...(joint ? { emperorNexus: true, nexusEvent: f.event } : {}) };
|
|
}
|
|
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 either card disposal, payment or physical acquisition');
|
|
}
|
|
function assertPurchase(f: NexusEmperorSpecialFixture, game: Game, joint: boolean): void {
|
|
const own = nexusEmperorSpecialPlayer(game, f.owner), before = nexusEmperorSpecialPlayer(f.game, f.owner);
|
|
assert.equal(own.spice, f.wallets[f.owner] - (joint ? 0 : 3));
|
|
for (const rival of game.players.filter(p => p.id !== f.owner)) assert.equal(rival.spice, f.wallets[rival.id]);
|
|
assert.equal(own.hand.length, before.hand.length, 'One spent physical Karama opens exactly one native hand slot');
|
|
assert.equal(own.hand.filter(c => c.id === f.acquire).length, 1);
|
|
assert.equal(own.hand.some(c => c.id === f.activation), false);
|
|
assert.equal(game.discard.filter(c => c.id === f.activation).length, 1);
|
|
assert.ok(before.hand.filter(c => c.id !== f.activation).every(c => own.hand.some(held => held.id === c.id)));
|
|
assert.equal(game.richeseCache!.length, f.game.richeseCache!.length - 1);
|
|
assert.equal(game.richeseCache!.some(c => c.id === f.acquire), false);
|
|
assert.equal(own.specialKaramaUsed, true);
|
|
assert.equal(game.nexusCards!.cards!.hands[f.owner], joint ? null : 'emperor');
|
|
assert.equal(game.nexusCards!.cards!.discard.filter(c => c === 'emperor').length, joint ? 1 : 0);
|
|
assert.equal(game.pendingRichesePurchaseIncome ?? null, null);
|
|
assert.equal(game.response?.kind === 'richesePurchaseIncome', false);
|
|
assert.equal(game.response?.kind === 'harkonnenBonus', false);
|
|
assert.deepEqual(game.auction, f.game.auction);
|
|
assert.deepEqual(game.richeseAuction, f.game.richeseAuction);
|
|
assert.deepEqual(game.currentAuctionSale, f.game.currentAuctionSale);
|
|
assert.deepEqual(game.decision, f.game.decision);
|
|
assert.deepEqual(game.response, f.game.response);
|
|
assert.equal(game.active, f.game.active);
|
|
assert.deepEqual(game.movementRemaining, f.game.movementRemaining);
|
|
assert.deepEqual(game.players.map(p => [p.id, p.forces, p.reserves, p.shipped, p.moved]),
|
|
f.game.players.map(p => [p.id, p.forces, p.reserves, p.shipped, p.moved]));
|
|
assert.deepEqual(nexusEmperorSpecialCards(game), nexusEmperorSpecialCards(f.game));
|
|
nexusEmperorSpecialInventory(game);
|
|
if (joint) {
|
|
assert.equal(game.nexusEmperorPurchaseEvents!.filter(event => event === f.event).length, 1);
|
|
} else {
|
|
assert.deepEqual(game.nexusEmperorPurchaseHistory ?? [], f.game.nexusEmperorPurchaseHistory ?? []);
|
|
assert.deepEqual(game.nexusEmperorPurchaseEvents ?? [], f.game.nexusEmperorPurchaseEvents ?? []);
|
|
}
|
|
}
|
|
|
|
|
|
for (const phase of [2, 4, 5] as const) void test(
|
|
`original phase ${phase} owner action: Pay 3 versus Karama + Emperor Nexus keep 3, with the same native successor`, () => {
|
|
const f = createNexusEmperorSpecialFixture({ phase }), original = structuredClone(f.game);
|
|
const own = viewGame(f.game, f.owner).richeseSpecialKarama!;
|
|
assert.equal(own.blocked, null); assert.equal(own.emperorNexus.canUse, true);
|
|
assert.equal(own.emperorNexus.blocked, null);
|
|
for (const rival of f.game.players.filter(p => p.id !== f.owner)) {
|
|
const view = viewGame(f.game, rival.id);
|
|
assert.equal(view.richeseSpecialKarama, null);
|
|
assert.equal(view.players.find(p => p.id === f.owner)!.hand, undefined);
|
|
}
|
|
const outcomes = [false, true].map(joint => {
|
|
const game = stepNexusEmperorSpecial(f.game, { actor: f.owner, action: purchase(f, joint) });
|
|
assertPurchase(f, game, joint);
|
|
rejectsUnchanged(game, f.owner, purchase(f, joint));
|
|
const next = stepNexusEmperorSpecial(game, nextNexusEmperorSpecialStep(game));
|
|
assert.equal(nexusEmperorSpecialPlayer(next, f.owner).hand.filter(c => c.id === f.acquire).length, 1);
|
|
assert.equal(next.discard.filter(c => c.id === f.activation).length, 1);
|
|
nexusEmperorSpecialInventory(next);
|
|
return next;
|
|
});
|
|
assert.deepEqual(f.game, original);
|
|
assert.equal(outcomes[0].active, outcomes[1].active);
|
|
assert.deepEqual(outcomes[0].decision, outcomes[1].decision);
|
|
assert.deepEqual(outcomes[0].movementRemaining, outcomes[1].movementRemaining);
|
|
},
|
|
);
|
|
|
|
void test('wrong owner, stale or coerced fields and unpaired Nexus fields fail before either original cost', () => {
|
|
const f = createNexusEmperorSpecialFixture(), valid = purchase(f, true);
|
|
for (const rival of f.game.players.filter(p => p.id !== f.owner)) rejectsUnchanged(f.game, rival.id, valid);
|
|
for (const action of [
|
|
{ ...valid, nexusEvent: 'stale' }, { ...valid, nexusEvent: 1 },
|
|
{ ...valid, nexusEvent: null }, { ...valid, nexusEvent: '' },
|
|
{ ...valid, emperorNexus: 'true' }, { ...valid, emperorNexus: 1 },
|
|
{ ...valid, emperorNexus: false }, { ...valid, emperorNexus: null },
|
|
{ type: 'card', mode: 'special', card: f.activation, acquire: f.acquire, emperorNexus: true },
|
|
{ type: 'card', mode: 'special', card: f.activation, acquire: f.acquire, nexusEvent: f.event },
|
|
{ ...valid, card: f.acquire }, { ...valid, acquire: f.activation },
|
|
{ type: 'nexusEmperorPurchase', event: f.event },
|
|
{ type: 'nexusEmperorSellerPurchase', event: f.event, use: true },
|
|
]) rejectsUnchanged(f.game, f.owner, action);
|
|
const seller = createNexusEmperorSellerFixture({ advanced: true, winnerFaction: 'richese' });
|
|
rejectsUnchanged(f.game, f.owner, { ...valid, nexusEvent: seller.event });
|
|
nexusEmperorSpecialInventory(f.game);
|
|
});
|
|
|
|
void test('a real later phase invalidates the earlier joint quote without consuming the owner entitlement', () => {
|
|
const f = createNexusEmperorSpecialFixture();
|
|
const game = advanceNexusEmperorSpecial(f.game, g => g.turn === 2 && g.phase === 4 && nexusEmperorSpecialClean(g));
|
|
assert.equal(game.nexusCards!.cards!.hands[f.owner], 'emperor');
|
|
assert.ok(nexusEmperorSpecialPlayer(game, f.owner).hand.some(c => c.id === f.activation));
|
|
rejectsUnchanged(game, f.owner, purchase(f, true));
|
|
const event = viewGame(game, f.owner).richeseSpecialKarama!.emperorNexus.event;
|
|
assert.notEqual(event, f.event);
|
|
const next = stepNexusEmperorSpecial(game, { actor: f.owner, action: { ...purchase(f, true), nexusEvent: event } });
|
|
assert.equal(nexusEmperorSpecialPlayer(next, f.owner).spice, nexusEmperorSpecialPlayer(game, f.owner).spice);
|
|
assert.equal(next.nexusCards!.cards!.hands[f.owner], null);
|
|
assert.equal(nexusEmperorSpecialPlayer(next, f.owner).hand.filter(c => c.id === f.acquire).length, 1);
|
|
nexusEmperorSpecialInventory(next);
|
|
});
|
|
|
|
void test('the physical cache card reserved in a real original lot cannot be acquired through the joint action', () => {
|
|
const f = createNexusEmperorSpecialFixture();
|
|
let game = advanceNexusEmperorSpecial(f.game, g => g.decision?.kind === 'richeseCache' && !g.phaseOpening && !g.response);
|
|
game = stepNexusEmperorSpecial(game, { actor: f.owner, action: { type: 'decision',
|
|
event: game.richeseBidding!.event, card: f.acquire, method: 'onceAround', direction: 'counterclockwise' } });
|
|
game = advanceNexusEmperorSpecial(game, nexusEmperorSpecialClean);
|
|
assert.equal(game.richeseAuction!.cardId, f.acquire);
|
|
const event = quoteEmperorNexusSpecialPurchase(game, f.owner).event;
|
|
rejectsUnchanged(game, f.owner, { ...purchase(f, true), nexusEvent: event });
|
|
assert.equal(game.nexusCards!.cards!.hands[f.owner], 'emperor');
|
|
assert.equal(game.discard.some(c => c.id === f.activation), false);
|
|
assert.ok(game.richeseCache!.some(c => c.id === f.acquire));
|
|
nexusEmperorSpecialInventory(game);
|
|
});
|
|
|
|
void test('the irrelevant real closing Guild draw preserves ordinary Pay 3 and never grants joint retention', () => {
|
|
const f = createNexusEmperorSpecialFixture({ nexusCard: 'guild' });
|
|
const own = viewGame(f.game, f.owner).richeseSpecialKarama!;
|
|
assert.equal(own.blocked, null);
|
|
assert.equal(own.emperorNexus.canUse, false);
|
|
rejectsUnchanged(f.game, f.owner, purchase(f, true));
|
|
const game = stepNexusEmperorSpecial(f.game, { actor: f.owner, action: purchase(f, false) });
|
|
assert.equal(nexusEmperorSpecialPlayer(game, f.owner).spice, f.wallets[f.owner] - 3);
|
|
assert.equal(game.nexusCards!.cards!.hands[f.owner], 'guild');
|
|
assert.equal(game.nexusCards!.cards!.discard.filter(c => c === 'guild').length, 0);
|
|
assert.equal(nexusEmperorSpecialPlayer(game, f.owner).hand.filter(c => c.id === f.acquire).length, 1);
|
|
assert.equal(game.discard.filter(c => c.id === f.activation).length, 1);
|
|
nexusEmperorSpecialInventory(game);
|
|
});
|
|
|
|
void test('genuine Basic setup and closing Emperor draw do not unlock the Advanced native special power', () => {
|
|
const f = createNexusEmperorSpecialFixture({ advanced: false });
|
|
assert.equal(f.game.nexusCards!.cards!.hands[f.owner], 'emperor');
|
|
assert.equal(viewGame(f.game, f.owner).richeseSpecialKarama, null);
|
|
for (const joint of [false, true]) rejectsUnchanged(f.game, f.owner, purchase(f, joint));
|
|
nexusEmperorSpecialInventory(f.game);
|
|
});
|
|
|
|
for (const lastCache of [false, true]) void test(
|
|
`native full-hand ${lastCache ? 'final-cache' : 'remaining-cache'} exchange uses genuine acquired cards, preserves original capacity and once-use`, () => {
|
|
const f = createNexusEmperorSpecialFixture({ fullHand: true, lastCache });
|
|
assert.equal(nexusEmperorSpecialPlayer(f.game, f.owner).hand.length, handLimit(nexusEmperorSpecialPlayer(f.game, f.owner)));
|
|
if (lastCache) {
|
|
assert.equal(f.game.richeseCache!.length, 1);
|
|
assert.equal(f.game.richeseRemoved!.length, 8);
|
|
for (const transition of f.chronology.cacheResolutions) {
|
|
assert.equal(transition.after.richeseCache!.length, transition.before.richeseCache!.length - 1);
|
|
assert.deepEqual(nexusEmperorSpecialCards(transition.after), nexusEmperorSpecialCards(transition.before));
|
|
}
|
|
}
|
|
for (const joint of [false, true]) {
|
|
const game = stepNexusEmperorSpecial(f.game, { actor: f.owner, action: purchase(f, joint) });
|
|
assertPurchase(f, game, joint);
|
|
assert.equal(nexusEmperorSpecialPlayer(game, f.owner).hand.length, handLimit(nexusEmperorSpecialPlayer(game, f.owner)));
|
|
if (lastCache) assert.equal(game.richeseCache!.length, 0);
|
|
rejectsUnchanged(game, f.owner, purchase(f, joint));
|
|
}
|
|
},
|
|
);
|
|
|
|
void test('projected Easy policy executes the actual native owner acquisition with only its own original entitlement', () => {
|
|
for (const nexusCard of ['emperor', 'guild'] as const) {
|
|
const f = createNexusEmperorSpecialFixture({ nexusCard, sellerIncome: true });
|
|
const view = viewGame(f.game, f.owner);
|
|
view.players.find(p => p.id === f.owner)!.bot = 'Easy';
|
|
const action = botActions(view)[0]; assert.ok(action);
|
|
const game = stepNexusEmperorSpecial(f.game, { actor: f.owner, action });
|
|
assert.equal(nexusEmperorSpecialPlayer(game, f.owner).hand.filter(c => c.id === f.acquire).length, 1);
|
|
assert.equal(game.discard.filter(c => c.id === f.activation).length, 1);
|
|
assert.equal(nexusEmperorSpecialPlayer(game, f.owner).specialKaramaUsed, true);
|
|
assert.equal(nexusEmperorSpecialPlayer(game, f.owner).spice, f.wallets[f.owner] - (nexusCard === 'emperor' ? 0 : 3));
|
|
assert.equal(game.nexusCards!.cards!.hands[f.owner], nexusCard === 'emperor' ? null : 'guild');
|
|
assert.equal(game.active, f.game.active);
|
|
for (const rival of game.players.filter(p => p.id !== f.owner)) assert.equal(rival.spice, f.wallets[rival.id]);
|
|
nexusEmperorSpecialInventory(game);
|
|
}
|
|
});
|
|
|
|
void test('joint acquisition continues through the next actual turn without repeating either spend or acquisition', () => {
|
|
const f = createNexusEmperorSpecialFixture();
|
|
let game = stepNexusEmperorSpecial(f.game, { actor: f.owner, action: purchase(f, true) });
|
|
game = advanceNexusEmperorSpecial(game, g => g.turn === 3 && g.phase === 2 && nexusEmperorSpecialClean(g));
|
|
assert.equal(game.discard.filter(c => c.id === f.activation).length, 1);
|
|
assert.equal(game.nexusCards!.cards!.discard.filter(c => c === 'emperor').length, 1);
|
|
assert.equal(game.nexusEmperorPurchaseEvents!.filter(event => event === f.event).length, 1);
|
|
assert.equal(nexusEmperorSpecialPlayer(game, f.owner).hand.filter(c => c.id === f.acquire).length, 1);
|
|
assert.equal(nexusEmperorSpecialPlayer(game, f.owner).specialKaramaUsed, true);
|
|
assert.equal(viewGame(game, f.owner).richeseSpecialKarama, null);
|
|
nexusEmperorSpecialInventory(game);
|
|
});
|