136 lines
7.1 KiB
TypeScript
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), true);
|
|
});
|
|
|
|
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);
|
|
});
|