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

105 lines
5.6 KiB
TypeScript

import assert from 'node:assert/strict';
import test from 'node:test';
import { viewGame, type Action, type Game } from '../game/engine';
import { botActions } from '../game/bots';
import { DIFFICULTIES } from '../game/bot-profiles';
import {
actTruthtranceRichesePromises as act,
advanceTruthtranceRichesePromises as advance,
askTruthtranceRichesePromises as ask,
createTruthtranceRichesePromisesFixture as fixture,
passTruthRicheseFreshDiscard,
truthtranceRichesePromisesClean as clean,
truthtranceRichesePromisesInventory as inventory,
truthtranceRichesePromisesPlayer as player,
} from './fixture-truthtrance-richese-promises';
/** Stop at actual Charity: the existing fixture only orders original unopened
* cards and Spice Cards; setup, starting hands, funding and phases are real. */
function begin(answer: 'yes' | 'no') {
const f = fixture({ advanced: false });
const source = structuredClone(f.earlySource);
const question = { kind: 'earlyRicheseBid' as const, target: f.emperor,
turn: source.turn, minimum: 4 };
const asked = ask(source, f, question);
assert.ok(viewGame(asked, f.emperor).truthEarlyRicheseBidAnswers?.includes(answer));
const bound = passTruthRicheseFreshDiscard(act(asked, f.emperor, { type: 'truthAnswer', answer }));
assert.equal(bound.discard.filter(card => card.id === f.truth).length, 1);
assert.deepEqual(inventory(bound), f.originalCards);
return { f, bound, asked };
}
function reachLot(source: Game, method: 'onceAround' | 'silent', position: 'first' | 'last') {
return advance(source, game => clean(game) && !!game.richeseAuction &&
game.richeseAuction.source === 'cache' && game.richeseAuction.method === method &&
!game.richeseAuction.outcome, [], { position, method });
}
function reachTarget(source: Game, target: string, method: 'onceAround' | 'silent') {
if (method === 'silent') return source;
return advance(source, game => clean(game) && game.richeseAuction?.active === target);
}
function reject(source: Game, actor: string, action: Action) {
const before = structuredClone(source);
assert.throws(() => act(source, actor, action));
assert.deepEqual(source, before, 'A rejected action leaves the original state intact');
}
for (const method of ['onceAround', 'silent'] as const) {
for (const position of ['first', 'last'] as const) {
void test(`Basic early Richese Yes reaches a real ${position} ${method} lot and requires its qualifying bid`, () => {
const { f, bound } = begin('yes');
const lot = reachLot(bound, method, position);
assert.equal(lot.richeseAuction?.source, 'cache');
const event = lot.richeseAuction!.event, card = lot.richeseAuction!.cardId;
assert.ok(event);
let current = reachTarget(lot, f.emperor, method);
assert.ok(current.richeseBidPromises?.some(row => row.event === event &&
row.player === f.emperor && row.answer && !row.fulfilled && !row.released));
reject(current, f.emperor, { type: 'richeseBid', event, amount: method === 'silent' ? 0 : null });
reject(current, f.emperor, { type: 'richeseBid', event, amount: 3, allyPayment: 0 });
const guide = viewGame(current, f.emperor).richeseBidPromiseGuide;
assert.ok(guide); assert.equal(guide.action.type, 'richeseBid');
assert.equal(guide.action.event, event); assert.equal(guide.action.amount, 4);
current = act(current, f.emperor, guide.action);
assert.ok(current.richeseBidPromises?.some(row => row.event === event && row.player === f.emperor && row.fulfilled));
const won = advance(current, game => player(game, f.emperor).hand.some(item => item.id === card));
assert.ok(!won.richeseCache?.some(item => item.id === card));
assert.deepEqual(inventory(won), f.originalCards);
});
void test(`Basic early Richese No limits a real ${position} ${method} cache bid`, () => {
const { f, bound } = begin('no');
const opened = reachLot(bound, method, position);
const event = opened.richeseAuction!.event;
const current = reachTarget(opened, f.emperor, method);
assert.ok(current.richeseBidPromises?.some(row => row.event === event && row.player === f.emperor && !row.answer));
reject(current, f.emperor, { type: 'richeseBid', event, amount: 4, allyPayment: 0 });
const bid = act(current, f.emperor, { type: 'richeseBid', event, amount: 2, allyPayment: 0 });
assert.ok(bid.richeseBidPromises?.some(row => row.event === event && row.player === f.emperor && row.fulfilled));
assert.deepEqual(inventory(bid), f.originalCards);
});
}
}
void test('all four legal bot profiles answer an early Richese question and follow the actual lot guide', () => {
const { f, asked } = begin('yes');
for (const difficulty of DIFFICULTIES) {
const answerView = viewGame(asked, f.emperor);
answerView.players.find(row => row.id === f.emperor)!.bot = difficulty;
const answer = botActions(answerView)[0];
assert.equal(answer?.type, 'truthAnswer');
assert.ok(answer.answer === 'yes' || answer.answer === 'no');
const bound = passTruthRicheseFreshDiscard(act(structuredClone(asked), f.emperor, answer));
const opened = reachLot(bound, 'silent', 'first');
const bidView = viewGame(opened, f.emperor);
bidView.players.find(row => row.id === f.emperor)!.bot = difficulty;
const guide = bidView.richeseBidPromiseGuide; assert.ok(guide);
const bid = botActions(bidView)[0]; assert.deepEqual(bid, guide.action);
const complete = act(opened, f.emperor, bid);
assert.ok(complete.richeseBidPromises?.some(row => row.player === f.emperor && row.fulfilled));
assert.deepEqual(inventory(complete), f.originalCards);
}
});