Files
Dune/tests/truth-richese-bid-runtime.test.ts
T

143 lines
8.3 KiB
TypeScript

import assert from 'node:assert/strict';
import test from 'node:test';
import { viewGame } from '../game/engine';
import { botActions } from '../game/bots';
import { DIFFICULTIES } from '../game/bot-profiles';
import {
act, inventory, player, createTruthRicheseBidFixture, askTruthRicheseBid,
bindTruthRicheseBid, rejectTruthRicheseBid as reject,
} from './fixture-truth-richese-bid';
for (const advanced of [false, true]) {
const mode = advanced ? 'Advanced' : 'Basic';
void test(`${mode} Once Around Yes requires a real qualifying funded bid`, () => {
const f = createTruthRicheseBidFixture(advanced);
const source = act(f.game, f.asker, { type: 'richeseBid', event: f.lot, amount: null });
assert.equal(source.richeseAuction?.active, f.target);
const { game: bound } = bindTruthRicheseBid(f, 'yes', 4, source);
assert.equal(bound.discard.filter(card => card.id === f.truth).length, 1);
assert.equal(bound.richeseAuction?.event, f.lot);
assert.equal(bound.richeseAuction?.cardId, f.card);
reject(bound, f.target, { type: 'richeseBid', event: f.lot, amount: null });
reject(bound, f.target, { type: 'richeseBid', event: f.lot, amount: 3, allyPayment: 0 });
const guide = viewGame(bound, f.target).richeseBidPromiseGuide; assert.ok(guide);
assert.equal(guide.action.type, 'richeseBid'); assert.equal(guide.action.amount, 4);
const bid = act(bound, f.target, guide.action);
assert.ok(bid.richeseBidPromises?.some(row => row.player === f.target && row.fulfilled));
assert.equal(player(bid, f.target).spice, 10, 'Declaring a bid does not pay the eventual invoice');
assert.deepEqual(inventory(bid), f.originalCards);
});
void test(`${mode} Once Around No caps a real bid and preserves the lot`, () => {
const f = createTruthRicheseBidFixture(advanced);
const source = act(f.game, f.asker, { type: 'richeseBid', event: f.lot, amount: null });
assert.equal(source.richeseAuction?.active, f.target);
const { game: bound } = bindTruthRicheseBid(f, 'no', 4, source);
reject(bound, f.target, { type: 'richeseBid', event: f.lot, amount: 4, allyPayment: 0 });
const bid = act(bound, f.target, { type: 'richeseBid', event: f.lot, amount: 2, allyPayment: 0 });
assert.equal(bid.richeseAuction?.event, f.lot); assert.equal(bid.richeseAuction?.cardId, f.card);
assert.equal(bid.richeseAuction?.bid, 2); assert.equal(bid.richeseAuction?.bidder, f.target);
assert.deepEqual(inventory(bid), f.originalCards);
});
void test(`${mode} Richese cannot promise to bid when its final owner turn has no earlier sale`, () => {
const f = createTruthRicheseBidFixture(advanced);
const asked = askTruthRicheseBid(f, f.owner, 1);
assert.ok(!viewGame(asked, f.owner).truthRicheseBidAnswers?.includes('yes'));
reject(asked, f.owner, { type: 'truthAnswer', answer: 'yes' });
assert.deepEqual(inventory(asked), f.originalCards);
});
void test(`${mode} an actual opposing high bid releases an impossible waiting Yes`, () => {
const f = createTruthRicheseBidFixture(advanced);
const { game: bound } = bindTruthRicheseBid(f, 'yes', 4);
assert.equal(bound.richeseAuction?.active, f.asker);
assert.equal(viewGame(bound, f.target).richeseBidPromiseGuide, null);
const raised = act(bound, f.asker, { type: 'richeseBid', event: f.lot, amount: 10, allyPayment: 0 });
assert.equal(raised.richeseAuction?.active, f.target);
assert.ok(raised.richeseBidPromises?.some(row => row.player === f.target && row.released));
const pass = act(raised, f.target, { type: 'richeseBid', event: f.lot, amount: null });
assert.deepEqual(inventory(pass), f.originalCards);
});
}
for (const advanced of [false, true]) {
const mode = advanced ? 'Advanced' : 'Basic';
void test(`${mode} stale Once Around guided callbacks are rejected without changing the lot`, () => {
const f = createTruthRicheseBidFixture(advanced);
const source = act(f.game, f.asker, { type: 'richeseBid', event: f.lot, amount: null });
const { game: bound } = bindTruthRicheseBid(f, 'yes', 4, source);
const guide = viewGame(bound, f.target).richeseBidPromiseGuide; assert.ok(guide);
reject(bound, f.target, { ...guide.action, richeseBidPromiseId: 'stale-promise' });
reject(bound, f.target, { ...guide.action, event: 'stale-lot' });
const accepted = act(bound, f.target, guide.action);
assert.ok(accepted.richeseBidPromises?.some(row => row.player === f.target && row.fulfilled));
assert.deepEqual(inventory(accepted), f.originalCards);
});
void test(`${mode} four legal bot profiles answer and follow the actual Once Around guide`, () => {
const f = createTruthRicheseBidFixture(advanced);
const source = act(f.game, f.asker, { type: 'richeseBid', event: f.lot, amount: null });
const asked = askTruthRicheseBid(f, f.target, 4, source);
for (const difficulty of DIFFICULTIES) {
const answerView = viewGame(asked, f.target);
answerView.players.find(row => row.id === f.target)!.bot = difficulty;
const answer = botActions(answerView)[0]; assert.ok(answer, difficulty);
assert.equal(answer.type, 'truthAnswer');
assert.ok(['yes', 'no'].includes(String(answer.answer)));
const bound = act(structuredClone(asked), f.target, answer);
const bidView = viewGame(bound, f.target);
bidView.players.find(row => row.id === f.target)!.bot = difficulty;
const guide = bidView.richeseBidPromiseGuide; assert.ok(guide, difficulty);
const chosen = botActions(bidView)[0]; assert.deepEqual(chosen, guide.action);
const completed = act(bound, f.target, chosen);
assert.ok(completed.richeseBidPromises?.some(row => row.player === f.target && row.fulfilled));
assert.deepEqual(inventory(completed), f.originalCards);
}
});
}
// This Basic Richese fixture enables the Semuta preview, whose existing
// bribe promise profile does not support a Basic bribe question. The Advanced
// native fixture supports this independent-bribe admission path.
for (const advanced of [true]) {
const mode = advanced ? 'Advanced' : 'Basic';
void test(`${mode} a Once Around Yes excludes an independently unfunded later bribe Yes`, () => {
const f = createTruthRicheseBidFixture(advanced);
let source = act(f.game, f.asker, { type: 'richeseBid', event: f.lot, amount: null });
assert.equal(source.richeseAuction?.active, f.target);
assert.equal(player(source, f.target).spice, 10);
// Controlled physical supplement: move the second original Truthtrance
// from the unopened native main deck to the questioner's hand. No funds,
// promise, lot, phase or other card is installed; card custody is conserved.
source = structuredClone(source);
const at = source.deck.findIndex(card => card.effect === 'truthtrance' && card.id !== f.truth);
assert.ok(at >= 0);
const [second] = source.deck.splice(at, 1);
player(source, f.asker).hand.push(second);
assert.deepEqual(inventory(source), f.originalCards);
let game = bindTruthRicheseBid(f, 'yes', 8, source).game;
const prior = structuredClone(game.richeseBidPromises![0]);
game = act(game, f.asker, { type: 'card', card: second.id });
for (let count = 0; game.truthtrance?.stage === 'priority' && count < game.players.length; count++) {
const actor = game.players.find(row => !game.truthtrance!.passed.includes(row.id)); assert.ok(actor);
game = act(game, actor.id, { type: 'truthPass' });
}
assert.equal(game.truthtrance?.stage, 'ask');
game = act(game, f.asker, { type: 'truthAsk', question: {
kind: 'bribe', target: f.target, turn: game.turn, recipient: f.asker, minimum: 5,
} });
const answers = viewGame(game, f.target).truthBribeAnswers;
assert.ok(!answers?.includes('yes')); assert.ok(answers?.includes('no'));
reject(game, f.target, { type: 'truthAnswer', answer: 'yes' });
assert.deepEqual(game.richeseBidPromises![0], prior);
game = act(game, f.target, { type: 'truthAnswer', answer: 'no' });
assert.deepEqual(game.richeseBidPromises![0], prior);
const guide = viewGame(game, f.target).richeseBidPromiseGuide; assert.ok(guide);
assert.equal(guide.action.amount, 8);
game = act(game, f.target, guide.action);
assert.ok(game.richeseBidPromises?.some(row => row.player === f.target && row.fulfilled));
assert.deepEqual(inventory(game), f.originalCards);
});
}