115 lines
6.1 KiB
TypeScript
115 lines
6.1 KiB
TypeScript
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);
|
|
});
|