143 lines
8.3 KiB
TypeScript
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);
|
|
});
|
|
}
|