105 lines
5.6 KiB
TypeScript
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);
|
|
}
|
|
});
|