Files
Dune/tests/bribe-classic-auction-runtime.test.ts
T
Quintine 6ecd26c8d1
Verify and publish checkpoint container / container (push) Successful in 10m47s
Allow bribes during classic pending decisions
2026-10-29 08:26:34 +11:00

124 lines
7.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);
});