import assert from 'node:assert/strict'; import test from 'node:test'; import { viewGame, type Action, type Game } from '../game/engine'; import { actTruthBid as act, advanceTruthBid as advance, createTruthBidFixture, truthBidClean as clean, truthBidInventory as inventory, truthBidPlayer as player } from './fixture-truth-bid'; import { advanceTruthAlliance, closeTruthAllianceNexus, truthAllianceNexus } from './fixture-truth-alliance'; import { beforeIxSpiceDraw, orderIxSpiceCards } from './fixture-ix-spice-cards'; function reject(game: Game, actor: string, action: Action) { const before = structuredClone(game); assert.throws(() => act(game, actor, action)); assert.deepEqual(game, before, 'A rejected bribe preserves the original auction.'); } function pending(game: Game, winner: string, amount: number, allyPayment = 0) { assert.equal(game.auction?.active, winner); game = act(game, winner, {type:'bid', amount, allyPayment}); game = advance(game, state => state.decision?.kind === 'auctionPayment' && state.decision.player === winner); assert.equal(game.phase, 3); assert.equal(game.auction?.bidder, winner); assert.equal(game.auction?.bid, amount); assert.equal(game.auction?.allyPayment ?? 0, allyPayment); assert.equal(game.currentAuctionSale ?? null, null); return game; } function committed(game: Game) { return {decision:structuredClone(game.decision), auction:structuredClone(game.auction), aid:structuredClone(game.aid), response:structuredClone(game.response ?? null)}; } /** Original Basic Harkonnen/Atreides/Emperor game, with an original Karama * dealt to the Emperor. The shared source selects only unopened physical cards * and scalar lotteries, then uses native bid/pass actions to create the live * payment choice. No wallet, ally, commitment or decision is installed. */ function noAlliance() { const f = createTruthBidFixture({targetKarama:true, window:'active'}); const game = pending(f.game, f.target, 4); return {f, game, lot:game.auction!.cards[game.auction!.index].id}; } void test('a winning bidder may bribe from uncommitted spice while the original purchase stays pending', () => { const {f, game, lot} = noAlliance(); const saved = committed(game), payer = player(game, f.target), recipient = player(game, f.asker); const originalCash = payer.spice, recipientCash = recipient.spice, escrow = recipient.bribes; const options = viewGame(game, f.target).bribeOptions; assert.equal(options.blocked, null); assert.equal(options.available, originalCash - 4); assert.equal(options.targets.find(row => row.id === f.asker)?.maximum, originalCash - 4); reject(game, f.target, {type:'bribe', target:f.asker, amount:originalCash - 3}); reject(game, f.target, {type:'bribe', target:f.target, amount:1}); reject(game, f.target, {type:'bribe', target:f.asker, amount:0}); let after = act(game, f.target, {type:'bribe', target:f.asker, amount:originalCash - 4}); assert.deepEqual(committed(after), saved); assert.equal(player(after, f.target).spice, 4); assert.equal(player(after, f.asker).spice, recipientCash); assert.equal(player(after, f.asker).bribes, escrow + originalCash - 4); assert.deepEqual(inventory(after), f.originalCards); after = act(after, f.target, {type:'decision', karama:false}); after = advance(after, state => player(state, f.target).hand.some(card => card.id === lot)); assert.equal(player(after, f.target).spice, 0); assert.equal(player(after, f.asker).bribes, escrow + originalCash - 4); assert.equal(player(after, f.target).hand.filter(card => card.id === lot).length, 1); assert.deepEqual(inventory(after), f.originalCards); }); void test('another seat may pay a bribe without changing the winner, bid or purchase choice', () => { const {f, game, lot} = noAlliance(), saved = committed(game); const donor = player(game, f.opponent), recipient = player(game, f.asker); const donorCash = donor.spice, winnerCash = player(game, f.target).spice, recipientCash = recipient.spice, escrow = recipient.bribes; assert.ok(donorCash >= 2); assert.equal(viewGame(game, f.opponent).bribeOptions.blocked, null); let after = act(game, f.opponent, {type:'bribe', target:f.asker, amount:2}); assert.deepEqual(committed(after), saved); assert.equal(player(after, f.opponent).spice, donorCash - 2); assert.equal(player(after, f.target).spice, winnerCash); assert.equal(player(after, f.asker).spice, recipientCash); assert.equal(player(after, f.asker).bribes, escrow + 2); after = act(after, f.target, {type:'decision', karama:false}); after = advance(after, state => player(state, f.target).hand.some(card => card.id === lot)); assert.equal(player(after, f.target).hand.filter(card => card.id === lot).length, 1); assert.equal(player(after, f.asker).bribes, escrow + 2); assert.deepEqual(inventory(after), f.originalCards); }); /** The second turn uses a real worm and reciprocal Nexus offers; an actual * Atreides pledge is committed into the Emperor's original bid. */ function alliedAuction() { const f = createTruthBidFixture({targetKarama:true, window:'waiting'}); let game = advanceTruthAlliance(f.game, state => state.turn === 2 && beforeIxSpiceDraw(state)); orderIxSpiceCards(game, ['worm','land']); game = advanceTruthAlliance(game, truthAllianceNexus); game = act(game, f.opponent, {type:'alliance', target:f.target}); game = act(game, f.target, {type:'alliance', target:f.opponent}); assert.equal(player(game, f.opponent).ally, f.target); assert.equal(player(game, f.target).ally, f.opponent); game = closeTruthAllianceNexus(game); game = advanceTruthAlliance(game, state => state.turn === 2 && state.phase === 3 && !!state.auction && clean(state)); game = act(game, f.opponent, {type:'pledgeAid', amount:2}); assert.equal(game.aid[f.opponent]?.amount, 2); game = advanceTruthAlliance(game, state => state.phase === 3 && state.auction?.active === f.target && clean(state)); game = pending(game, f.target, 4, 2); return {f, game, lot:game.auction!.cards[game.auction!.index].id}; } void test('an ally-funded winning bid keeps both the pledged spice and the winner’s own share reserved', () => { const {f, game, lot} = alliedAuction(), saved = committed(game); const payer = player(game, f.target), donor = player(game, f.opponent), recipient = player(game, f.asker); const payerCash = payer.spice, donorCash = donor.spice, escrow = recipient.bribes; assert.equal(viewGame(game, f.target).bribeOptions.available, payerCash - 2); reject(game, f.target, {type:'bribe', target:f.opponent, amount:1}); reject(game, f.target, {type:'bribe', target:f.asker, amount:payerCash - 1}); let after = act(game, f.opponent, {type:'bribe', target:f.asker, amount:1}); assert.deepEqual(committed(after), saved); assert.equal(player(after, f.opponent).spice, donorCash - 1); assert.equal(after.aid[f.opponent]?.amount, 2); assert.equal(player(after, f.asker).bribes, escrow + 1); after = act(after, f.target, {type:'decision', karama:false}); after = advance(after, state => player(state, f.target).hand.some(card => card.id === lot)); assert.equal(player(after, f.target).spice, payerCash - 2); assert.equal(after.aid[f.opponent]?.amount ?? 0, 0); assert.equal(player(after, f.target).hand.filter(card => card.id === lot).length, 1); assert.deepEqual(inventory(after), f.originalCards); });