import assert from 'node:assert/strict'; import test from 'node:test'; import { viewGame, type Game } from '../game/engine'; import { DIFFICULTIES } from '../game/bot-profiles'; import { botActions } from '../game/bots'; import { phaseBidPromiseModeSupported } from '../game/turn-bid-promises'; import { bidPromiseModeSupported } from '../game/bid-promises'; import { bribePromiseModeSupported } from '../game/bribe-promises'; import { act, advance, clean, createKullBribeFixture, inventory, player } from './fixture-kull-bribe'; /** The fixture selects physical cards in the unopened deal; the Truthtrance, * Kull and auction actions below all use the genuine public Basic start. */ function bound(answer: 'yes' | 'no') { const f = createKullBribeFixture(); let game = advance(structuredClone(f.charity), state => state.phase === 3 && clean(state) && state.auction?.active === f.actor); game = act(game, f.asker, {type:'card', card:f.truth}); while (game.truthtrance?.stage === 'priority') { const actor = game.players.find(seat => !game.truthtrance!.passed.includes(seat.id)); assert.ok(actor); game = act(game, actor.id, {type:'truthPass'}); } game = act(game, f.asker, {type:'truthAsk', question:{ kind:'phaseBid', target:f.actor, turn:game.turn, minimum:4, }}); assert.ok(viewGame(game, f.actor).truthPhaseBidAnswers?.includes(answer)); for (const difficulty of DIFFICULTIES) { const view = viewGame(game, f.actor); view.players.find(seat => seat.id === f.actor)!.bot = difficulty; const choice = botActions(view)[0]; assert.equal(choice?.type, 'truthAnswer'); assert.ok(view.truthPhaseBidAnswers!.includes(choice.answer as 'yes' | 'no' | 'unknown')); } game = act(game, f.actor, {type:'truthAnswer', answer}); assert.deepEqual(inventory(game), f.originalCards); return {f, game}; } function offered(source: Game, f: ReturnType) { const before = structuredClone(source.phaseBidPromises); const lot = source.auction!.cards[source.auction!.index].id; const game = act(source, f.counterOwner, {type:'card', mode:'purchase', card:f.counter}); assert.equal(game.pendingKull?.stage, 'offer'); assert.deepEqual(game.phaseBidPromises, before); assert.equal(viewGame(game, f.actor).phaseBidPromiseGuide, null); const reaction = viewGame(game, f.reactor).kullReaction; assert.ok(reaction); return {game, reaction, before, lot}; } void test('pre-bidding phase Yes cannot rely on an unfunded Karama bid in Kull preview', () => { const f = createKullBribeFixture(); let game = structuredClone(f.charity); // Controlled wallet supplement: the original physical Karama remains in // Emperor's hand, but its only spice cannot fund the promised four bid. player(game, f.actor).spice = 1; assert.ok(player(game, f.actor).hand.some(card => card.id === f.karama)); game = act(game, f.asker, {type:'card', card:f.truth}); while (game.truthtrance?.stage === 'priority') { const actor = game.players.find(seat => !game.truthtrance!.passed.includes(seat.id)); assert.ok(actor); game = act(game, actor.id, {type:'truthPass'}); } game = act(game, f.asker, {type:'truthAsk', question:{ kind:'phaseBid', target:f.actor, turn:game.turn, minimum:4, }}); assert.ok(!viewGame(game, f.actor).truthPhaseBidAnswers?.includes('yes'), 'A non-CHOAM Karama cannot witness the Kull-fenced unfunded overbid.'); const before = structuredClone(game); assert.throws(() => act(game, f.actor, {type:'truthAnswer', answer:'yes'})); assert.deepEqual(game, before); assert.deepEqual(inventory(game), f.originalCards); }); void test('phase-wide Kull questions admit the supported native Richese and Semuta profile', () => { const scope = {advanced:false, expansions:['choam','ix'], kullPreview:true, semutaPreview:true, players:[{faction:'choam'}, {faction:'richese'}]}; assert.equal(phaseBidPromiseModeSupported(scope), true); assert.equal(bidPromiseModeSupported(scope), false); assert.equal(bribePromiseModeSupported(scope), true); }); void test('phase-wide Yes survives a real Kull interception and completes its original bid', () => { const {f, game: source} = bound('yes'); const pending = offered(source, f); let game = act(pending.game, f.reactor, {type:'kullDecision', event:pending.reaction.event, source:'printed', card:f.kull}); assert.equal(game.pendingKull?.stage, 'counter'); assert.deepEqual(game.phaseBidPromises, pending.before); game = advance(game, clean); assert.equal(game.auction!.cards[game.auction!.index].id, pending.lot); assert.deepEqual(game.phaseBidPromises, pending.before); const guide = viewGame(game, f.actor).phaseBidPromiseGuide; assert.ok(guide); assert.equal(guide.action.type, 'bid'); assert.equal(guide.action.amount, 4); game = act(game, f.actor, guide.action); assert.ok(game.phaseBidPromises?.some(row => row.player === f.actor && row.fulfilled)); assert.ok(player(game, f.counterOwner).hand.some(card => card.id === f.counter)); assert.deepEqual(inventory(game), f.originalCards); }); void test('phase-wide No still caps a bid after the Kull interception', () => { const {f, game: source} = bound('no'); const pending = offered(source, f); let game = act(pending.game, f.reactor, {type:'kullDecision', event:pending.reaction.event, source:'printed', card:f.kull}); game = advance(game, clean); assert.deepEqual(game.phaseBidPromises, pending.before); const before = structuredClone(game); assert.throws(() => act(game, f.actor, {type:'bid', amount:4, allyPayment:0})); assert.deepEqual(game, before); game = act(game, f.actor, {type:'bid', amount:3, allyPayment:0}); assert.equal(game.auction?.bid, 3); assert.deepEqual(inventory(game), f.originalCards); }); void test('countering Kull keeps a phase Yes for a real later lot', () => { const {f, game: source} = bound('yes'); const pending = offered(source, f); const originalIndex = source.auction!.index; assert.ok(source.auction!.cards[originalIndex + 1], 'The real auction has another physical lot.'); let game = act(pending.game, f.reactor, {type:'kullDecision', event:pending.reaction.event, source:'printed', card:f.kull}); assert.equal(game.pendingKull?.stage, 'counter'); assert.ok(viewGame(game, f.actor).responseControls?.cancelCards?.includes(f.karama)); game = act(game, f.actor, {type:'card', mode:'cancel', card:f.karama}); game = advance(game, state => clean(state) && !!state.auction && state.auction.index > originalIndex && state.auction.active === f.actor); const promise = game.phaseBidPromises?.find(row => row.player === f.actor); assert.ok(promise && !promise.fulfilled && !promise.released, 'A real future lot keeps the original phase-wide answer binding.'); assert.ok(player(game, f.counterOwner).hand.some(card => card.id === pending.lot)); const guide = viewGame(game, f.actor).phaseBidPromiseGuide; assert.ok(guide); assert.equal(guide.action.type, 'bid'); assert.equal(guide.action.amount, 4); game = act(game, f.actor, guide.action); assert.ok(game.phaseBidPromises?.some(row => row.player === f.actor && row.fulfilled)); assert.deepEqual(inventory(game), f.originalCards); });