import assert from 'node:assert/strict'; import test from 'node:test'; import { handLimit, viewGame, type Action } from '../game/engine'; import { SEMUTA_DRUG_ID } from '../game/semuta-drug'; import { actDistransPrebid, advanceDistransPrebid, distransPrebidClean, distransPrebidInventory, distransPrebidPlayer as player } from './fixture-distrans-prebid'; import { createDistransRichesePrebidFixture } from './fixture-distrans-richese-prebid'; import { moveSemutaEmperorCard } from './fixture-semuta-emperor-share'; for (const method of ['onceAround', 'silent'] as const) { for (const claim of [false, true]) { void test(`Basic ${method} started auction resumes its original lot after Semuta ${claim ? 'claims' : 'passes'} Distrans`, () => { const f = createDistransRichesePrebidFixture({ method }); // The game and bidding phase are reached through native actions. Stage // only the conserved physical Semuta card into another player's hand // before the real cache declaration; this is not an earned purchase. let game = structuredClone(f.beforeDeclaration); const semutaHolder = 'e'; moveSemutaEmperorCard(game, SEMUTA_DRUG_ID, semutaHolder); assert.ok(player(game, semutaHolder).hand.length <= handLimit(player(game, semutaHolder))); const offer = game.richeseCache![0]; assert.ok(offer); const declaration = { ...f.declaration, card: offer.id } as Action; game = actDistransPrebid(game, game.decision!.player, declaration); const lot = game.richeseAuction!; const bidder = method === 'silent' ? lot.order.find(id => id !== f.holder && player(game, id).spice >= 1)! : lot.active!; assert.ok(bidder && player(game, bidder).spice >= 1); game = actDistransPrebid(game, bidder, { type: 'richeseBid', event: lot.event, amount: 1, allyPayment: 0, }); assert.ok(game.richeseAuction && !game.richeseAuction.outcome); assert.ok(game.richeseAuction.acted.includes(bidder)); const recipient = game.players.find(p => p.id !== f.holder && p.id !== bidder && p.hand.length < handLimit(p)); assert.ok(recipient); const transfer = { ...f.action, target: recipient.id } as Action; const quote = viewGame(game, f.holder).distrans!; assert.equal(quote.blocked, null); assert.ok(quote.choices.find(c => c.recipient === recipient.id)!.cards.some(c => c.id === f.give)); const originalLot = structuredClone(game.richeseAuction); const originalFunding = structuredClone(game.richeseFunding); const originalInventory = distransPrebidInventory(game); const originalHand = structuredClone(player(game, recipient.id).hand); const transferred = player(game, f.holder).hand.find(c => c.id === f.give)!; game = actDistransPrebid(game, f.holder, transfer); const pending = game.pendingTreacheryDiscard; assert.ok(pending?.reaction, 'The public Distrans discard must offer a Semuta reaction'); assert.equal(pending.batch.entries[0].card.id, f.card); const event = pending.batch.event; if (method === 'silent' && claim) { const altered = structuredClone(game); altered.richeseAuction!.sealed[bidder] = 2; assert.throws(() => actDistransPrebid(altered, semutaHolder, { type: 'semutaCommit', event }), /completed private Distrans transfer|Semuta/); } assert.equal(viewGame(game, semutaHolder).semutaReaction!.canCommit, true); if (claim) { game = actDistransPrebid(game, semutaHolder, { type: 'semutaCommit', event }); } else { for (const seat of game.players) game = actDistransPrebid(game, seat.id, { type: 'semutaPass', event }); } assert.equal(game.pendingTreacheryDiscard, null); assert.deepEqual(game.richeseAuction, originalLot); assert.deepEqual(game.richeseFunding, originalFunding); assert.deepEqual(player(game, recipient.id).hand, recipient.id === semutaHolder && claim ? [...originalHand.filter(c => c.id !== SEMUTA_DRUG_ID), transferred, pending.batch.entries[0].card] : [...originalHand, transferred]); assert.equal(player(game, semutaHolder).hand.some(c => c.id === f.card), claim); assert.equal(game.discard.some(c => c.id === SEMUTA_DRUG_ID), claim); assert.equal(game.discard.some(c => c.id === f.card), !claim); assert.deepEqual(distransPrebidInventory(game), originalInventory); if (method === 'silent') { const observer = game.players.find(p => p.id !== bidder)!; const hidden = viewGame(game, observer.id).richeseAuction!; assert.equal(hidden.ownBid, null); assert.equal(hidden.revealedBids, null); assert.equal('sealed' in hidden, false); assert.equal(viewGame(game, bidder).richeseAuction!.ownBid, 1); } game = advanceDistransPrebid(game, state => distransPrebidClean(state) && state.richeseAuction?.event !== lot.event); assert.deepEqual(distransPrebidInventory(game), originalInventory); }); } } void test('a recipient can use the received Semuta to claim Distrans during a started Silent lot', () => { const f = createDistransRichesePrebidFixture({ method: 'silent' }); let game = structuredClone(f.beforeDeclaration); // Stage the one physical Semuta card in the donor's hand before the real // lot declaration; the recipient acquires it only through native Distrans. moveSemutaEmperorCard(game, SEMUTA_DRUG_ID, f.holder); assert.ok(player(game, f.holder).hand.length <= handLimit(player(game, f.holder))); game = actDistransPrebid(game, game.decision!.player, { ...f.declaration, card: game.richeseCache![0].id } as Action); const lot = game.richeseAuction!; const bidder = lot.order.find(id => id !== f.holder && id !== f.recipient && player(game, id).spice >= 1)!; assert.ok(bidder); game = actDistransPrebid(game, bidder, { type: 'richeseBid', event: lot.event, amount: 1, allyPayment: 0 }); const originalLot = structuredClone(game.richeseAuction), funding = structuredClone(game.richeseFunding); const inventory = distransPrebidInventory(game); game = actDistransPrebid(game, f.holder, { ...f.action, give: SEMUTA_DRUG_ID } as Action); const pending = game.pendingTreacheryDiscard!; assert.ok(pending?.reaction); assert.equal(viewGame(game, f.recipient).semutaReaction!.canCommit, true); game = actDistransPrebid(game, f.recipient, { type: 'semutaCommit', event: pending.batch.event }); assert.equal(game.pendingTreacheryDiscard, null); assert.ok(player(game, f.recipient).hand.some(c => c.id === f.card)); assert.ok(game.discard.some(c => c.id === SEMUTA_DRUG_ID)); assert.deepEqual(game.richeseAuction, originalLot); assert.deepEqual(game.richeseFunding, funding); assert.deepEqual(distransPrebidInventory(game), inventory); });