Support early Truthtrance cache bid commitments
This commit is contained in:
1 parent
ee3e1a0ab2
commit
b64b732e50
16 files changed
+795
-5
No files matched your search
@@ -0,0 +1,68 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { registerHooks } from 'node:module';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { createElement, isValidElement, type ReactElement, type ReactNode } from 'react';
|
||||
import { renderToStaticMarkup } from 'react-dom/server';
|
||||
import { viewGame } from '../game/engine';
|
||||
import { createTruthtranceRichesePromisesFixture, actTruthtranceRichesePromises as act,
|
||||
advanceTruthtranceRichesePromises as advance, askTruthtranceRichesePromises } from './fixture-truthtrance-richese-promises';
|
||||
|
||||
const aliases = registerHooks({
|
||||
resolve(specifier, context, next) {
|
||||
return next(specifier === 'next/image' ? 'vinext/shims/image' : specifier === 'next/link' ? 'vinext/shims/link' : specifier, context);
|
||||
},
|
||||
load(url, context, next) {
|
||||
if (!url.endsWith('.module.css')) return next(url, context);
|
||||
const css = readFileSync(new URL(url), 'utf8');
|
||||
const classes = Object.fromEntries([...css.matchAll(/\.([a-zA-Z_][\w-]*)/g)].map(match => [match[1], match[1]]));
|
||||
return { format: 'module', source: `export default ${JSON.stringify(classes)}`, shortCircuit: true };
|
||||
},
|
||||
});
|
||||
const { EarlyRicheseBidFields, Truthtrance } = await import('../components/truthtrance');
|
||||
aliases.deregister();
|
||||
|
||||
type ElementProps = { children?: ReactNode; [key: string]: unknown };
|
||||
function elements(node: ReactNode): ReactElement<ElementProps>[] {
|
||||
if (Array.isArray(node)) return node.flatMap(elements);
|
||||
if (!isValidElement<ElementProps>(node)) return [];
|
||||
return [node, ...elements(node.props.children)];
|
||||
}
|
||||
|
||||
void test('early Richese threshold controls describe the undecided first cache lot and reject invalid edits', () => {
|
||||
const changes: number[] = [];
|
||||
const props = { minimum: 3, disabled: false, onMinimum(value: number) { changes.push(value); } };
|
||||
const html = renderToStaticMarkup(createElement(EarlyRicheseBidFields, props));
|
||||
assert.match(html, /first Richese cache auction this turn/i);
|
||||
assert.match(html, /card and method have not yet been decided/);
|
||||
assert.match(html, /does not promise a win/);
|
||||
const edit = (value: number, disabled = false) => {
|
||||
const input = elements(EarlyRicheseBidFields({ ...props, disabled })).find(element =>
|
||||
element.props.id === 'truth-early-richese-minimum' && typeof element.props.onChange === 'function');
|
||||
assert.ok(input);
|
||||
(input.props.onChange as (event: { currentTarget: { valueAsNumber: number } }) => void)({ currentTarget: { valueAsNumber: value } });
|
||||
};
|
||||
edit(4); for (const value of [0, -1, 1.5, NaN, Infinity, Number.MAX_SAFE_INTEGER + 1]) edit(value);
|
||||
edit(7, true);
|
||||
assert.deepEqual(changes, [4]);
|
||||
});
|
||||
|
||||
void test('native Basic Truthtrance exposes the early selector and target-only answer controls', () => {
|
||||
const f = createTruthtranceRichesePromisesFixture({ advanced: false });
|
||||
let declared = act(f.earlySource, f.asker, f.declarationStep.action);
|
||||
declared = advance(declared, state => state.truthtrance?.stage === 'ask');
|
||||
const askerView = viewGame(declared, f.asker);
|
||||
assert.ok(askerView.truthEarlyRicheseBidTargets.includes(f.emperor));
|
||||
const asking = renderToStaticMarkup(createElement(Truthtrance, { game: askerView, busy: false, act() {} }));
|
||||
assert.match(asking, /<option value="earlyRicheseBid">Bind a bid in the first Richese cache auction<\/option>/);
|
||||
const asked = askTruthtranceRichesePromises(f.earlySource, f, {
|
||||
kind: 'earlyRicheseBid', target: f.emperor, turn: f.earlySource.turn, minimum: 1,
|
||||
});
|
||||
const respondent = viewGame(asked, f.emperor);
|
||||
assert.ok(respondent.truthEarlyRicheseBidAnswers?.length);
|
||||
assert.equal(viewGame(asked, f.asker).truthEarlyRicheseBidAnswers, null);
|
||||
const answer = renderToStaticMarkup(createElement(Truthtrance, { game: respondent, busy: false, act() {} }));
|
||||
assert.match(answer, /first Richese cache auction this turn/i);
|
||||
assert.match(answer, /Its card and method are not yet decided/);
|
||||
assert.match(answer, /Answer (Yes|No)|I don’t know/);
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { createElement } from 'react';
|
||||
import { renderToStaticMarkup } from 'react-dom/server';
|
||||
import { createGame, joinGame, newPlayer, viewGame } from '../game/engine';
|
||||
import { EarlyRicheseBidPromises } from '../components/early-richese-bid-promises';
|
||||
|
||||
void test('waiting receipts explain only the current player’s live unattached cache bid answers', () => {
|
||||
const game = createGame('EARLYWAIT', newPlayer('e', 'Emperor', 'emperor'), false, ['choam']);
|
||||
joinGame(game, newPlayer('r', 'Richese', 'richese'));
|
||||
const view = viewGame(game, 'e');
|
||||
view.ownEarlyRicheseBidPromises = [
|
||||
{ id: 'private-id', player: 'e', asker: 'r', turn: view.turn, minimum: 4, answer: true },
|
||||
{ id: 'negative-id', player: 'e', asker: 'r', turn: view.turn, minimum: 7, answer: false },
|
||||
{ id: 'other-id', player: 'r', asker: 'e', turn: view.turn, minimum: 9, answer: true },
|
||||
{ id: 'done-id', player: 'e', asker: 'r', turn: view.turn, minimum: 11, answer: true, fulfilled: true },
|
||||
{ id: 'release-id', player: 'e', asker: 'r', turn: view.turn, minimum: 12, answer: true, released: true },
|
||||
{ id: 'bound-id', player: 'e', asker: 'r', turn: view.turn, minimum: 13, answer: true, boundEvent: 'private-lot' },
|
||||
{ id: 'old-id', player: 'e', asker: 'r', turn: view.turn + 1, minimum: 14, answer: true },
|
||||
];
|
||||
const html = renderToStaticMarkup(createElement(EarlyRicheseBidPromises, { game: view }));
|
||||
assert.match(html, /You answered Yes to bidding at least 4 spice/);
|
||||
assert.match(html, /You answered No to bidding at least 7 spice/);
|
||||
assert.match(html, /Waiting for the first Richese cache auction/);
|
||||
assert.match(html, /Its card and method are not yet decided/);
|
||||
assert.doesNotMatch(html, /at least (9|11|12|13|14) spice/);
|
||||
assert.doesNotMatch(html, /private-id|negative-id|other-id|private-lot|<button/);
|
||||
view.ownEarlyRicheseBidPromises = view.ownEarlyRicheseBidPromises.filter(row => !!row.boundEvent || row.fulfilled || row.released || row.turn !== view.turn);
|
||||
assert.equal(renderToStaticMarkup(createElement(EarlyRicheseBidPromises, { game: view })), '');
|
||||
});
|
||||
@@ -0,0 +1,84 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import {
|
||||
EarlyRicheseBidError, attachEarlyRicheseBids, bindEarlyRicheseBid,
|
||||
earlyRicheseBidAnswerOptions, earlyRicheseBidClaimText,
|
||||
liveEarlyRicheseBidPromises, parseEarlyRicheseBidClaim,
|
||||
reconcileEarlyRicheseBids, validateEarlyRicheseBidPromises,
|
||||
type EarlyRicheseBidPromise,
|
||||
} from '../game/early-richese-bid';
|
||||
|
||||
const claim = { turn: 3, minimum: 4 };
|
||||
const bind = (rows: EarlyRicheseBidPromise[], minimum: number, answer: boolean, id: string, maximum = 8) =>
|
||||
bindEarlyRicheseBid(rows, { turn: 3, minimum }, 'target', 'asker', answer, id, maximum);
|
||||
|
||||
void test('the early claim binds a turn and positive bid without a speculative lot', () => {
|
||||
assert.deepEqual(parseEarlyRicheseBidClaim({ ...claim, event: 'discarded' }), claim);
|
||||
assert.match(earlyRicheseBidClaimText(claim), /first Richese cache auction/);
|
||||
for (const input of [null, {}, { turn: 0, minimum: 1 }, { turn: 2, minimum: 0 },
|
||||
{ turn: 2, minimum: 1.5 }, { turn: 2, minimum: Infinity }])
|
||||
assert.throws(() => parseEarlyRicheseBidClaim(input), EarlyRicheseBidError);
|
||||
});
|
||||
|
||||
void test('candidate answers preserve all jointly feasible waiting promises', () => {
|
||||
assert.deepEqual(earlyRicheseBidAnswerOptions([], claim, 'target', 'asker', 8), [true, false]);
|
||||
assert.deepEqual(earlyRicheseBidAnswerOptions([], claim, 'target', 'asker', 3), [false]);
|
||||
assert.deepEqual(earlyRicheseBidAnswerOptions([], claim, 'target', 'asker', 0), [false]);
|
||||
const yes = bind([], 4, true, 'yes');
|
||||
assert.deepEqual(earlyRicheseBidAnswerOptions(yes, { turn: 3, minimum: 4 }, 'target', 'asker', 8), [true]);
|
||||
const no = bind([], 5, false, 'no');
|
||||
assert.deepEqual(earlyRicheseBidAnswerOptions(no, { turn: 3, minimum: 4 }, 'target', 'asker', 8), [true, false]);
|
||||
assert.deepEqual(earlyRicheseBidAnswerOptions(no, { turn: 3, minimum: 5 }, 'target', 'asker', 8), [false]);
|
||||
assert.throws(() => bind(yes, 4, false, 'no'), /compatible/);
|
||||
assert.throws(() => bind(yes, 4, true, 'yes'), /distinct receipt/);
|
||||
assert.throws(() => earlyRicheseBidAnswerOptions([], claim, 'target', 'target', 8));
|
||||
assert.throws(() => earlyRicheseBidAnswerOptions([], claim, 'target', 'asker', -1));
|
||||
});
|
||||
|
||||
void test('first actual cache event attaches waiting receipts once without changing other turns', () => {
|
||||
const source = [...bind([], 4, true, 'yes'),
|
||||
{ ...claim, turn: 2, id: 'old', player: 'target', asker: 'asker', answer: false },
|
||||
{ ...claim, id: 'closed', player: 'target', asker: 'asker', answer: true, released: true }];
|
||||
const before = structuredClone(source);
|
||||
const attached = attachEarlyRicheseBids(source, 3, 'real-cache-event');
|
||||
assert.deepEqual(source, before);
|
||||
assert.deepEqual(attached.bound, [{ ...claim, id: 'yes', player: 'target', asker: 'asker', answer: true, event: 'real-cache-event' }]);
|
||||
assert.equal(attached.promises[0].boundEvent, 'real-cache-event');
|
||||
assert.deepEqual(liveEarlyRicheseBidPromises(attached.promises, 'target', 3), []);
|
||||
assert.deepEqual(attachEarlyRicheseBids(attached.promises, 3, 'another-event').bound, []);
|
||||
validateEarlyRicheseBidPromises(attached.promises, ['target', 'asker']);
|
||||
});
|
||||
|
||||
void test('voluntary losses cannot abandon a Yes, external losses release only impossible promises', () => {
|
||||
const rows = bind(bind([], 4, true, 'y4'), 7, true, 'y7');
|
||||
const witness = { cachePending: true, canBid: true, maximum: 5, change: 'voluntary' as const };
|
||||
assert.throws(() => reconcileEarlyRicheseBids(rows, 'target', 3, witness), /last funded/);
|
||||
assert.deepEqual(rows, bind(bind([], 4, true, 'y4'), 7, true, 'y7'));
|
||||
const external = reconcileEarlyRicheseBids(rows, 'target', 3, { ...witness, change: 'external' });
|
||||
assert.equal(external[0].released, undefined);
|
||||
assert.equal(external[1].released, true);
|
||||
const noCapacity = reconcileEarlyRicheseBids(external, 'target', 3,
|
||||
{ cachePending: true, canBid: false, maximum: 5, change: 'external' });
|
||||
assert.equal(noCapacity[0].released, true);
|
||||
});
|
||||
|
||||
void test('closing unavailable cache releases Yes and establishes No', () => {
|
||||
const rows = bind(bind([], 4, true, 'yes'), 7, false, 'no');
|
||||
assert.throws(() => reconcileEarlyRicheseBids(rows, 'target', 3,
|
||||
{ cachePending: false, canBid: false, maximum: 0, change: 'voluntary' }));
|
||||
const closed = reconcileEarlyRicheseBids(rows, 'target', 3,
|
||||
{ cachePending: false, canBid: false, maximum: 0, change: 'external' });
|
||||
assert.equal(closed[0].released, true);
|
||||
assert.equal(closed[1].fulfilled, true);
|
||||
assert.deepEqual(attachEarlyRicheseBids(closed, 3, 'later-cache').bound, []);
|
||||
validateEarlyRicheseBidPromises(closed, ['target', 'asker']);
|
||||
});
|
||||
|
||||
void test('validation rejects malformed or duplicated receipts and conflicting states', () => {
|
||||
const valid = bind([], 4, true, 'one');
|
||||
validateEarlyRicheseBidPromises(valid, ['target', 'asker']);
|
||||
assert.throws(() => validateEarlyRicheseBidPromises([...valid, valid[0]], ['target', 'asker']));
|
||||
assert.throws(() => validateEarlyRicheseBidPromises(valid, ['target']));
|
||||
assert.throws(() => validateEarlyRicheseBidPromises([{ ...valid[0], fulfilled: true, released: true }], ['target', 'asker']));
|
||||
assert.throws(() => validateEarlyRicheseBidPromises([{ ...valid[0], boundEvent: 'event', released: true }], ['target', 'asker']));
|
||||
});
|
||||
@@ -0,0 +1,114 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { viewGame, type Action, type Game } from '../game/engine';
|
||||
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';
|
||||
|
||||
function bind(f: ReturnType<typeof fixture>, source: Game, target: string, minimum = 4) {
|
||||
const asked = ask(source, f, { kind: 'earlyRicheseBid', target, turn: source.turn, minimum });
|
||||
assert.ok(viewGame(asked, target).truthEarlyRicheseBidAnswers?.includes('yes'));
|
||||
return passTruthRicheseFreshDiscard(act(asked, target, { type: 'truthAnswer', answer: 'yes' }));
|
||||
}
|
||||
|
||||
function rejects(source: Game, actor: string, action: Action) {
|
||||
const before = structuredClone(source);
|
||||
assert.throws(() => act(source, actor, action));
|
||||
assert.deepEqual(source, before, 'A rejected action must leave its input unchanged');
|
||||
}
|
||||
|
||||
function onceAround(source: Game) {
|
||||
return advance(source, game => clean(game) && game.richeseAuction?.source === 'cache' &&
|
||||
game.richeseAuction.method === 'onceAround' && !game.richeseAuction.outcome,
|
||||
[], { position: 'first', method: 'onceAround' });
|
||||
}
|
||||
|
||||
void test('an early funded Yes rejects an otherwise legal bribe that spends its last bid funds', () => {
|
||||
const f = fixture({ advanced: false });
|
||||
const bound = bind(f, structuredClone(f.earlySource), f.emperor);
|
||||
assert.equal(player(bound, f.emperor).spice, 10);
|
||||
rejects(bound, f.emperor, { type: 'bribe', target: f.asker, amount: 7 });
|
||||
const affordable = act(bound, f.emperor, { type: 'bribe', target: f.asker, amount: 6 });
|
||||
assert.equal(player(affordable, f.emperor).spice, 4);
|
||||
assert.ok(affordable.earlyRicheseBidPromises?.some(row => row.player === f.emperor &&
|
||||
row.answer && !row.boundEvent && !row.released));
|
||||
assert.deepEqual(inventory(affordable), f.originalCards);
|
||||
});
|
||||
|
||||
void test('a voluntary ordinary bid cannot eliminate the last cache hand slot for a waiting Yes', () => {
|
||||
const f = fixture({ advanced: false });
|
||||
const staged = structuredClone(f.earlySource);
|
||||
// Controlled physical supplement: transfer two original unopened ordinary
|
||||
// cards to Emperor before the question, preserving every card and position.
|
||||
for (let count = 0; count < 2; count++) {
|
||||
const index = staged.deck.findIndex(card => card.id !== f.ordinaryLot);
|
||||
assert.ok(index >= 0);
|
||||
player(staged, f.emperor).hand.push(staged.deck.splice(index, 1)[0]);
|
||||
}
|
||||
assert.equal(player(staged, f.emperor).hand.length, 3);
|
||||
assert.deepEqual(inventory(staged), f.originalCards);
|
||||
const bound = bind(f, staged, f.emperor);
|
||||
const current = advance(bound, game => clean(game) && game.phase === 3 &&
|
||||
game.auction?.active === f.emperor, [], { position: 'last' });
|
||||
rejects(current, f.emperor, { type: 'bid', amount: 1, allyPayment: 0 });
|
||||
assert.ok(current.earlyRicheseBidPromises?.some(row => row.player === f.emperor && !row.released));
|
||||
assert.deepEqual(inventory(current), f.originalCards);
|
||||
});
|
||||
|
||||
void test('an external printed Karama canceling the cache releases the waiting Yes', () => {
|
||||
const f = fixture({ advanced: false });
|
||||
const staged = structuredClone(f.earlySource);
|
||||
// Controlled physical supplement: the actual printed Karama moves from the
|
||||
// unopened main deck to Atreides before the original Truthtrance is played.
|
||||
const index = staged.deck.findIndex(card => card.effect === 'karama'); assert.ok(index >= 0);
|
||||
const card = staged.deck.splice(index, 1)[0];
|
||||
player(staged, f.asker).hand.push(card);
|
||||
assert.deepEqual(inventory(staged), f.originalCards);
|
||||
const bound = bind(f, staged, f.emperor);
|
||||
const declaration = advance(bound, game => game.decision?.kind === 'richeseDeclaration');
|
||||
const opened = act(declaration, f.richese, { type: 'decision', event: declaration.richeseBidding!.event, position: 'first' });
|
||||
assert.equal(opened.response?.kind, 'richeseAuction');
|
||||
let canceled = act(opened, f.asker, { type: 'card', card: card.id, mode: 'cancel' });
|
||||
canceled = passTruthRicheseFreshDiscard(canceled);
|
||||
assert.equal(canceled.richeseBidding?.cacheCanceled, true);
|
||||
assert.ok(canceled.earlyRicheseBidPromises?.some(row => row.player === f.emperor && row.released));
|
||||
assert.deepEqual(inventory(canceled), f.originalCards);
|
||||
});
|
||||
|
||||
void test('a waiting Richese owner may honor a Yes when a rival starts the Once Around lot', () => {
|
||||
const f = fixture({ advanced: false });
|
||||
const bound = bind(f, structuredClone(f.earlySource), f.richese);
|
||||
let game = onceAround(bound);
|
||||
const event = game.richeseAuction!.event;
|
||||
assert.ok(game.richeseBidPromises?.some(row => row.event === event && row.player === f.richese &&
|
||||
row.answer && !row.released));
|
||||
const first = game.richeseAuction!.active; assert.ok(first && first !== f.richese);
|
||||
game = act(game, first, { type: 'richeseBid', event, amount: 1, allyPayment: 0 });
|
||||
while (game.richeseAuction?.active && game.richeseAuction.active !== f.richese) {
|
||||
const actor = game.richeseAuction.active;
|
||||
game = act(game, actor, { type: 'richeseBid', event, amount: null });
|
||||
}
|
||||
assert.equal(game.richeseAuction?.active, f.richese);
|
||||
rejects(game, f.richese, { type: 'richeseBid', event, amount: null });
|
||||
const guide = viewGame(game, f.richese).richeseBidPromiseGuide; assert.ok(guide);
|
||||
assert.equal(guide.action.amount, 4);
|
||||
const bid = act(game, f.richese, guide.action);
|
||||
assert.ok(bid.richeseBidPromises?.some(row => row.event === event && row.player === f.richese && row.fulfilled));
|
||||
assert.deepEqual(inventory(bid), f.originalCards);
|
||||
});
|
||||
|
||||
void test('a waiting Richese owner is released when all rivals pass the Once Around lot', () => {
|
||||
const f = fixture({ advanced: false });
|
||||
const bound = bind(f, structuredClone(f.earlySource), f.richese);
|
||||
const opened = onceAround(bound), event = opened.richeseAuction!.event;
|
||||
const closed = advance(opened, game => !!game.richeseBidPromises?.some(row =>
|
||||
row.event === event && row.player === f.richese && row.released));
|
||||
assert.deepEqual(inventory(closed), f.originalCards);
|
||||
});
|
||||
@@ -0,0 +1,104 @@
|
||||
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);
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user