import assert from 'node:assert/strict'; import test from 'node:test'; import { applyAction, viewGame, type Action, type Game } from '../game/engine'; import { richeseCards } from '../game/richese-cards'; import { settleIxianReplacementFixture } from './fixture-nexus-ixian-replacement'; import { createRicheseIxianReplacementFixture, type RicheseIxianReplacementFixture, } from './fixture-richese-ixian-replacement'; const owner = (game: Game, id: string) => game.players.find(player => player.id === id)!; const reload = (game: Game): Game => JSON.parse(JSON.stringify(game)); function reject(game: Game, id: string, action: Action): void { const before = structuredClone(game); assert.throws(() => applyAction(game, id, action)); assert.deepEqual(game, before); } const BUILT: Record = {}; /** One genuine native Richese run per roster/mode; each test reloads its own copy. */ function fixtureFor( advanced: boolean, buyerFaction: 'atreides' | 'richese', endingAuction = false, ): RicheseIxianReplacementFixture { const key = `${advanced}:${buyerFaction}:${endingAuction}`; const held = BUILT[key]; if (held) return held; const created = createRicheseIxianReplacementFixture({ advanced, buyerFaction, endingAuction }); BUILT[key] = created; return created; } for (const advanced of [false, true]) for (const buyerFaction of ['atreides', 'richese'] as const) void test(`${advanced ? 'Advanced' : 'Basic'} native ${buyerFaction} normal CHOAM purchase spends the Nexus for the real top replacement`, () => { const f = fixtureFor(advanced, buyerFaction); assert.equal(f.purchased.id, 'ix-poison-tooth'); assert.equal(f.nextCard.id, 'ix-artillery'); assert.equal(f.rowSize, 2); // The cache stayed its own original collection: only the one genuine Once // Around keep left it, and the normal purchase itself never touched it. assert.equal(f.keptCacheCards.length, 1); assert.ok(f.setup.richeseCache!.some(card => card.id === f.keptCacheCards[0])); assert.equal(f.cacheAtPurchase.length, richeseCards().length - 1); assert.equal(f.beforePayment.currentAuctionSale, null); assert.equal(f.beforePayment.auction!.cards[f.auctionIndex].id, f.purchased.id); const offer = viewGame(f.game, f.buyer).nexusIxianReplacement!; assert.equal(offer.canUse, true); assert.equal(offer.canPass, true); assert.deepEqual(offer.purchased, f.purchased); assert.equal(owner(f.game, f.buyer).spice, f.beforeSpice - 2); assert.equal(owner(f.game, f.emperor!).spice, f.beforeEmperorSpice); const partners = f.beforeNexusDraw.players.filter(player => player.id !== f.buyer); for (const partner of partners) for (const type of ['nexusIxianReplacementUse', 'nexusIxianReplacementPass'] as const) reject(f.game, partner.id, { type, event: offer.event }); const deckBefore = structuredClone(f.game.deck); const unsold = structuredClone(f.game.auction!.cards.slice(f.auctionIndex + 1)); const use = settleIxianReplacementFixture(applyAction(reload(f.game), f.buyer, { type: 'nexusIxianReplacementUse', event: offer.event })); assert.deepEqual(owner(use, f.buyer).hand, [...owner(f.beforePayment, f.buyer).hand, f.nextCard]); assert.deepEqual(use.deck, deckBefore.slice(1)); assert.deepEqual(use.discard, [...f.game.discard, f.purchased]); assert.equal(use.nexusCards!.cards!.hands[f.buyer], null); assert.deepEqual(use.nexusCards!.cards!.discard, [...f.game.nexusCards!.cards!.discard, 'ixians']); assert.equal(owner(use, f.buyer).spice, f.paidSpice); assert.equal(owner(use, f.emperor!).spice, f.beforeEmperorSpice! + 2); assert.equal(use.currentAuctionSale, null); assert.equal(use.pendingNexusIxianReplacement, null); assert.deepEqual(use.richeseCache, f.cacheAtPurchase); assert.equal(use.richeseBidding!.stage, 'normal'); assert.equal(use.auction!.index, f.auctionIndex + 1); assert.deepEqual(use.auction!.cards.slice(use.auction!.index), unsold); reject(use, f.buyer, { type: 'nexusIxianReplacementUse', event: f.event }); }); for (const advanced of [false, true]) for (const buyerFaction of ['atreides', 'richese'] as const) void test(`${advanced ? 'Advanced' : 'Basic'} native ${buyerFaction} normal CHOAM purchase retains its card and Nexus card`, () => { const f = fixtureFor(advanced, buyerFaction); const offer = viewGame(f.game, f.buyer).nexusIxianReplacement!; const pass = settleIxianReplacementFixture(applyAction(reload(f.game), f.buyer, { type: 'nexusIxianReplacementPass', event: offer.event })); assert.deepEqual(owner(pass, f.buyer).hand, owner(f.game, f.buyer).hand); assert.ok(owner(pass, f.buyer).hand.some(card => card.id === f.purchased.id)); assert.deepEqual(pass.deck, f.game.deck); assert.deepEqual(pass.discard, f.game.discard); assert.deepEqual(pass.nexusCards!.cards, f.game.nexusCards!.cards); assert.equal(pass.nexusCards!.cards!.hands[f.buyer], 'ixians'); assert.equal(owner(pass, f.buyer).spice, f.paidSpice); assert.equal(owner(pass, f.emperor!).spice, f.beforeEmperorSpice! + 2); assert.equal(pass.currentAuctionSale, null); assert.equal(pass.pendingNexusIxianReplacement, null); assert.deepEqual(pass.richeseCache, f.cacheAtPurchase); assert.equal(pass.auction!.index, f.auctionIndex + 1); }); for (const advanced of [false, true]) void test(`${advanced ? 'Advanced' : 'Basic'} native final-lot CHOAM purchase closes the pool into the untouched Richese cache offer`, () => { const f = fixtureFor(advanced, 'atreides', true); assert.equal(f.auctionIndex, f.rowSize - 1); assert.equal(f.purchased.id, 'ix-poison-tooth'); assert.equal(f.nextCard.id, 'ix-artillery'); const offer = viewGame(f.game, f.buyer).nexusIxianReplacement!; assert.deepEqual(offer.purchased, f.purchased); const richese = f.game.players.find(player => player.faction === 'richese')!.id; const use = settleIxianReplacementFixture(applyAction(reload(f.game), f.buyer, { type: 'nexusIxianReplacementUse', event: offer.event })); assert.deepEqual(owner(use, f.buyer).hand, [...owner(f.beforePayment, f.buyer).hand, f.nextCard]); assert.deepEqual(use.discard, [...f.game.discard, f.purchased]); assert.equal(owner(use, f.buyer).spice, f.paidSpice); assert.equal(owner(use, f.emperor!).spice, f.beforeEmperorSpice! + 2); assert.equal(use.auction, null); assert.deepEqual(use.decision, { kind: 'richeseCache', player: richese }); assert.equal(use.richeseBidding!.stage, 'cacheOffer'); assert.equal(use.richeseBidding!.cacheCanceled, false); assert.deepEqual(use.richeseCache, f.cacheAtPurchase); assert.equal(use.pendingNexusIxianReplacement, null); assert.equal(use.nexusCards!.cards!.hands[f.buyer], null); });