Allow bribes during classic pending decisions
Verify and publish checkpoint container / container (push) Successful in 10m47s
Verify and publish checkpoint container / container (push) Successful in 10m47s
This commit is contained in:
1 parent
1e81ce8ad8
commit
6ecd26c8d1
8 files changed
+444
-5
No files matched your search
@@ -0,0 +1,123 @@
|
||||
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);
|
||||
});
|
||||
Reference in new issue
Block a user