import assert from 'node:assert/strict'; import test from 'node:test'; import { handLimit, viewGame, type Action, type Game } from '../game/engine'; import { botActions } from '../game/bots'; import { DIFFICULTIES } from '../game/bot-profiles'; import { distransAuctionBlock } from '../game/distrans-timing'; import { actDistransPrebid, advanceDistransPrebid, distransPrebidClean, distransPrebidInventory, distransPrebidPlayer as player, distransPrebidPosition, } from './fixture-distrans-prebid'; import { createDistransRichesePrebidFixture } from './fixture-distrans-richese-prebid'; const reload = (g: Game): Game => JSON.parse(JSON.stringify(g)); function rejected(game: Game, actor: string, action: Action) { const before = structuredClone(game); assert.throws(() => actDistransPrebid(game, actor, action)); assert.deepEqual(game, before); } function firstSubmission(game: Game, amount?: number | null) { const lot = game.richeseAuction!; const actor = lot.method === 'silent' ? lot.order.find(id => lot.eligible.includes(id) && !Object.hasOwn(lot.sealed, id))! : lot.active!; assert.ok(actor); return { actor, action: { type: 'richeseBid', event: lot.event, amount: amount === undefined ? (lot.method === 'silent' ? 0 : null) : amount, allyPayment: 0 } as Action }; } for (const advanced of [false, true]) for (const method of ['onceAround', 'silent'] as const) { const prefix = `${advanced ? 'Advanced' : 'Basic'} native ${method}`; void test(`${prefix} preserves the unbid original cache lot through a genuine Distrans transfer`, () => { const f = createDistransRichesePrebidFixture({ advanced, method }); assert.equal(f.initial.status, 'lobby'); assert.equal(f.acquisition.before.richeseCache!.some(card => card.id === f.card), true); assert.ok(f.acquisition.actions.some(step => step.actor === f.holder && step.action.type === 'richeseBid' && step.action.amount === 1)); assert.equal(f.beforeDeclaration.turn, 2); assert.equal(f.beforeDeclaration.decision?.kind, 'richeseCache'); const before = structuredClone(f.game); assert.equal(viewGame(before, f.holder).distrans!.blocked, null); assert.ok(viewGame(before, f.holder).distrans!.choices.find(c => c.recipient === f.recipient)!.cards.some(c => c.id === f.give)); let game = actDistransPrebid(f.game, f.holder, f.action); assert.deepEqual(f.game, before); game = advanceDistransPrebid(game, distransPrebidClean); assert.deepEqual(distransPrebidPosition(game), distransPrebidPosition(before)); assert.deepEqual(player(game, f.holder).hand, player(before, f.holder).hand.filter(c => c.id !== f.card && c.id !== f.give)); assert.deepEqual(player(game, f.recipient).hand, [...player(before, f.recipient).hand, player(before, f.holder).hand.find(c => c.id === f.give)!]); assert.equal(game.discard.filter(c => c.id === f.card).length, 1); assert.deepEqual(distransPrebidInventory(game), f.originalInventory); rejected(game, f.holder, f.action); const start = firstSubmission(game); for (const difficulty of DIFFICULTIES) { const view = viewGame(game, start.actor); view.players.find(p => p.id === start.actor)!.bot = difficulty; const action = botActions(view)[0]; assert.equal(action?.type, 'richeseBid', `${difficulty} can act on the original lot`); const next = actDistransPrebid(game, start.actor, action); assert.ok(next.richeseAuction?.acted.includes(start.actor)); } game = actDistransPrebid(game, start.actor, start.action); assert.ok(game.richeseAuction?.acted.includes(start.actor)); assert.deepEqual(distransPrebidInventory(game), f.originalInventory); }); for (const first of method === 'silent' ? [0, 1] : [null, 1]) { void test(`${prefix} first ${first === null ? 'pass' : first === 0 ? 'sealed zero' : 'bid'} allows Distrans without changing the original special lot`, () => { const f = createDistransRichesePrebidFixture({ advanced, method }); const start = firstSubmission(f.game, first); const game = actDistransPrebid(reload(f.game), start.actor, start.action); assert.ok(game.richeseAuction?.acted.includes(start.actor)); assert.equal(viewGame(game, f.holder).distrans!.blocked, null); const after = actDistransPrebid(game, f.holder, f.action); assert.deepEqual(after.richeseAuction, game.richeseAuction); assert.deepEqual(after.richeseFunding, game.richeseFunding); assert.ok(player(after, f.recipient).hand.some(c => c.id === f.give)); assert.deepEqual(distransPrebidInventory(after), f.originalInventory); }); } void test(`${prefix} rejects a full recipient before spending either physical card`, () => { const f = createDistransRichesePrebidFixture({ advanced, method }); // Controlled custody supplement: move real undealt deck cards into the // recipient hand. This is not claimed as an earned acquisition sequence. const game = structuredClone(f.game), recipient = player(game, f.recipient); while (recipient.hand.length < handLimit(recipient)) recipient.hand.push(game.deck.shift()!); assert.deepEqual(distransPrebidInventory(game), f.originalInventory); const choice = viewGame(game, f.holder).distrans!.choices.find(c => c.recipient === f.recipient)!; assert.ok(choice.blocked); rejected(game, f.holder, f.action); assert.ok(player(game, f.holder).hand.some(c => c.id === f.card)); assert.ok(player(game, f.holder).hand.some(c => c.id === f.give)); assert.deepEqual(distransPrebidInventory(game), f.originalInventory); }); void test(`${prefix} fills the active recipient, automatically closes that bid and keeps an originally full donor ineligible`, () => { const f = createDistransRichesePrebidFixture({ advanced, method }); // Controlled custody supplement: move only actual undealt deck cards to // hands. These are not claimed as earned purchases. The declaration and // transfer still use real actions and preserve the physical inventory. const staged = structuredClone(f.beforeDeclaration), donor = player(staged, f.holder); while (donor.hand.length < handLimit(donor)) { const card = staged.deck.shift(); assert.ok(card); donor.hand.push(card); } assert.deepEqual(distransPrebidInventory(staged), f.originalInventory); let game = actDistransPrebid(staged, staged.decision!.player, f.declaration); const lot = game.richeseAuction!; assert.ok(lot); assert.ok(!lot.eligible.includes(f.holder), 'The full donor was excluded at declaration'); const recipientId = method === 'silent' ? lot.order.find(id => lot.eligible.includes(id) && id !== f.holder)! : lot.active!; assert.ok(recipientId && recipientId !== f.holder); const recipient = player(game, recipientId); while (recipient.hand.length < handLimit(recipient) - 1) { const card = game.deck.shift(); assert.ok(card); recipient.hand.push(card); } assert.equal(recipient.hand.length, handLimit(recipient) - 1); assert.deepEqual(distransPrebidInventory(game), f.originalInventory); const originalLot = structuredClone(lot); const action: Action = { ...f.action, target: recipientId }; const choice = viewGame(game, f.holder).distrans!.choices.find(c => c.recipient === recipientId)!; assert.ok(choice.cards.some(c => c.id === f.give)); game = actDistransPrebid(game, f.holder, action); game = advanceDistransPrebid(game, distransPrebidClean); assert.equal(player(game, recipientId).hand.length, handLimit(recipient)); assert.equal(player(game, f.holder).hand.length, handLimit(donor) - 2); assert.deepEqual(distransPrebidInventory(game), f.originalInventory); const next = game.richeseAuction!; assert.ok(next); assert.equal(next.event, originalLot.event); assert.equal(next.cardId, originalLot.cardId); assert.equal(next.source, originalLot.source); assert.deepEqual(next.order, originalLot.order); assert.deepEqual(next.eligible, originalLot.eligible); assert.deepEqual(next.tieOrder, originalLot.tieOrder); assert.ok(!next.eligible.includes(f.holder), 'Freeing space does not add a bidder to this lot'); assert.ok(next.acted.includes(recipientId)); if (method === 'silent') assert.equal(next.sealed[recipientId], 0); else assert.ok(next.passed.includes(recipientId)); assert.equal(distransAuctionBlock(game), null, 'The automatic pass or zero is a completed action'); }); }