Files
Dune/tests/kull-phase-bid-runtime.test.ts
T
Quintine bf73c41ae0
Verify and publish checkpoint container / container (push) Canceled after 0s
Support Kull phase bids with Richese
2026-10-29 00:22:11 +11:00

136 lines
7.1 KiB
TypeScript

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<typeof createKullBribeFixture>) {
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), false);
});
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);
});