124 lines
7.2 KiB
TypeScript
124 lines
7.2 KiB
TypeScript
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);
|
||
});
|