Allow late Mentat bribes and retain turn-end promises

This commit is contained in:
Quintine committed 2026-10-26 15:41:10 +11:00
1 parent f5ae61f382
commit c81e00e6dd
19 files changed
+590 -51

No files matched your search

+12 -5
View File
@@ -31,7 +31,7 @@ function reject(g: Game, actor: string, action: object) {
assert.equal(JSON.stringify(g), before);
}
void test('out-of-turn bribes debit once, stay private as escrow and collect once at Mentat after JSON restoration', () => {
void test('out-of-turn bribes collect at a controlled Mentat opening and late payments remain deferred after JSON restoration', () => {
let g = game();
g.active = 'e';
const before = JSON.stringify(g);
@@ -55,8 +55,14 @@ void test('out-of-turn bribes debit once, stay private as escrow and collect onc
assert.equal(g.phase, 8);
assert.equal(g.players[1].spice, 14);
assert.equal(options(g, 'h').incoming, 0);
assert.match(options(g).blocked!, /Mentat/);
assert.equal(options(g).blocked, null);
assert.deepEqual(options(JSON.parse(JSON.stringify(g)), 'h'), options(g, 'h'));
g = applyAction(g, 'a', bribeAction(options(g), 'h', 2)!);
assert.equal(g.phase, 8);
assert.equal(g.players[0].spice, 4);
assert.equal(g.players[1].spice, 14);
assert.equal(options(g, 'h').incoming, 2);
assert.equal(options(g, 'h').available, 14);
});
void test('invalid amounts, self/allied recipients and stale client options never authorize a payment', () => {
@@ -73,7 +79,6 @@ void test('invalid amounts, self/allied recipients and stale client options neve
reject(g, 'a', { type: 'bribe', target: 'e', amount });
}
for (const paused of [
{ phase: 8 },
{ inflation: { side: 'double', turn: 2 } },
{ phaseOpening: { passed: [], initialize: false } },
{ response: { kind: 'emperorGift', owner: 'e', recipient: 'h', amount: 1, passed: [] } },
@@ -126,12 +131,14 @@ void test('bribe controls render recipient choices, private escrow and disabled
assert.match(html, /4 spice awaiting Mentat/);
assert.match(html, /Bribe recipient/);
assert.match(html, /Harkonnen recipient/);
assert.match(html, /the next Mentat Pause/);
assert.match(html, /collects their share at the start of Mentat Pause/);
assert.match(html, /Payments made during Mentat Pause wait until the start of next turn's Mentat Pause/);
assert.match(html, /disabled=""[^>]*>Pay bribe/);
assert.match(html, /rules\?topic=bribes#bribes/);
assert.ok((render(g, true).match(/disabled=""/g) ?? []).length >= 3);
g.phase = 8;
assert.match(render(g), /Bribes cannot be made during Mentat Pause/);
assert.equal(options(g).blocked, null);
assert.match(render(g), /Payments made during Mentat Pause wait until the start of next turn's Mentat Pause/);
g.status = 'lobby';
assert.equal(render(g), '');
});
+96
View File
@@ -0,0 +1,96 @@
import assert from 'node:assert/strict';
import type { Game } from '../game/engine';
import {
actTruthBribe, advanceTruthBribe, answerTruthBribe, askTruthBribe, createTruthBribeFixture,
declareTruthBribe, finishTruthBribe, nextTruthBribe, settleTruthBribe, truthBribeBalances,
truthBribeClean, truthBribeDeferred, truthBribeInventory, truthBribePlayer,
type TruthBribeStep, type TruthBribeTransition,
} from './fixture-truth-bribe';
export type MentatBribeStep = TruthBribeStep;
export type MentatBribeTransition = TruthBribeTransition;
export const actMentatBribe = actTruthBribe;
export const nextMentatBribe = nextTruthBribe;
export const advanceMentatBribe = advanceTruthBribe;
export const settleMentatBribe = settleTruthBribe;
export const finishMentatBribe = finishTruthBribe;
export const mentatBribeClean = truthBribeClean;
export const mentatBribeInventory = truthBribeInventory;
export const mentatBribePlayer = truthBribePlayer;
export const mentatBribeBalances = truthBribeBalances;
export const mentatBribeDeferred = truthBribeDeferred;
export const declareMentatBribe = declareTruthBribe;
export const askMentatBribe = askTruthBribe;
export const answerMentatBribe = answerTruthBribe;
/** Reach the current turn's actual Mentat from earlier phases, or the next
* actual opening when this turn's collection has already happened. */
export function nextMentatBribeOpening(game: Game, actions: MentatBribeStep[] = []): Game {
const turn = game.turn + Number(game.phase === 8);
return advanceMentatBribe(game, state => state.turn === turn && state.phase === 8 && mentatBribeClean(state), actions);
}
/** The inherited original H/A/E base33 source earns Collection and Mentat by
* real native actions. H still owns both original, unplayed Truthtrances and E
* its original Shield and ten spendable spice. No played state is installed. */
export function createMentatBribeFixture(options: { advanced?: boolean } = {}) {
const base = createTruthBribeFixture({ advanced: options.advanced, phase: 7 });
const actions = structuredClone(base.sourceActions), before = structuredClone(base.game), from = actions.length;
const game = nextMentatBribeOpening(base.game, actions);
const phase7: MentatBribeTransition = {
before: structuredClone(base.base.game),
steps: structuredClone(base.sourceActions.slice(base.base.sourceActionCount)),
after: structuredClone(base.game),
};
const mentat: MentatBribeTransition = {
before, steps: structuredClone(actions.slice(from)), after: structuredClone(game),
};
assert.equal(game.phase, 8); assert.equal(game.turn, 1); assert.ok(mentatBribeClean(game));
assert.equal(mentatBribePlayer(game, base.payer).spice, 10);
assert.ok(game.players.every(player => player.bribes === 0 && player.ally === null));
assert.deepEqual(mentatBribePlayer(game, base.asker).hand.map(card => card.id), base.truths);
assert.deepEqual(mentatBribeInventory(game), base.originalCards);
return { ...base, game, sourceGame: structuredClone(game), base, phase: 8 as const, phase7, mentat,
originalBalances: mentatBribeBalances(game), originalDeferred: mentatBribeDeferred(game),
actions, sourceActions: structuredClone(actions), sourceActionCount: actions.length,
staging: [] as string[],
sourceDisclosure: 'Inherited genuine public original H/A/E base33 setup, ordinary auction and native Collection/Mentat actions. H retains both original unplayed Truthtrances; E retains its original Shield and10spice. Only inherited unopened deal/lot/Spice positions and scalar lotteries are selected. The current Mentat opening already collected its receipts. A new payment spends the payer now and waits in the exact recipient bribes until the next actual Mentat opening. No played clock, purse, hand, receipt or promise is installed.' };
}
export type MentatBribeFixture = ReturnType<typeof createMentatBribeFixture>;
export function bindMentatBribe(f: MentatBribeFixture, answer: 'yes' | 'no' = 'yes', minimum = f.minimum,
recipient = f.recipient, state = f.game) {
const declared = declareMentatBribe(f, state), actions = [...declared.actions];
const asked = askMentatBribe(declared.game, f.asker, f.target, recipient, minimum, actions);
const game = answerMentatBribe(asked, f.target, answer, actions);
return { game, declared, asked, actions, actionCount: actions.length };
}
/** Keep next Storm and ordinary Collection visible before the next real
* Mentat opening. Incoming escrow cannot masquerade as cash along this path. */
export function completeMentatBribe(f: MentatBribeFixture, state = f.game) {
assert.equal(state.phase, 8);
let game = state; const turn = game.turn, actions: MentatBribeStep[] = [];
const transition = (until: (state: Game) => boolean): MentatBribeTransition => {
const before = structuredClone(game), from = actions.length;
game = advanceMentatBribe(game, until, actions);
return { before, steps: structuredClone(actions.slice(from)), after: structuredClone(game) };
};
const nextTurn = transition(state => state.turn === turn + 1 && state.phase === 0 && mentatBribeClean(state));
const collection = transition(state => state.turn === turn + 1 && state.phase === 7 && mentatBribeClean(state));
const mentat = transition(state => state.turn === turn + 1 && state.phase === 8 && mentatBribeClean(state));
assert.deepEqual(mentatBribeInventory(game), f.originalCards);
return { game, nextTurn, collection, mentat, actions, actionCount: actions.length };
}
/** Move Collection readiness explicitly so an unpaid Yes is carried into
* Mentat instead of automatically consuming the adapter's suggested payment. */
export function enterMentatBribeWithUnpaidPromise(game: Game, actions: MentatBribeStep[] = []): Game {
assert.equal(game.phase, 7); assert.ok(mentatBribeClean(game));
const turn = game.turn;
for (const player of game.players) {
if (game.phase === 7 && !game.ready.includes(player.id))
game = actMentatBribe(game, player.id, { type: 'ready' }, actions);
}
return advanceMentatBribe(game, state => state.turn === turn && state.phase === 8 && mentatBribeClean(state), actions);
}
+175
View File
@@ -0,0 +1,175 @@
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 { bindTruthBribe } from './fixture-truth-bribe';
import {
actMentatBribe as act, advanceMentatBribe as advance, bindMentatBribe as bind,
completeMentatBribe as complete, createMentatBribeFixture as fixture,
enterMentatBribeWithUnpaidPromise as enterMentat, finishMentatBribe as finish,
mentatBribeBalances as balances, mentatBribeClean as clean,
mentatBribeDeferred as deferred, mentatBribeInventory as inventory, mentatBribePlayer as player,
nextMentatBribeOpening as opening, settleMentatBribe as settle,
} from './fixture-mentat-bribes';
function rejected(game: Game, actor: string, action: Action) {
const before = structuredClone(game); assert.throws(() => act(game, actor, action)); assert.deepEqual(game, before);
}
const promise = (game: Game, target: string, recipient: string) =>
game.bribePromises!.find(promise => promise.player === target && promise.recipient === recipient)!;
const pay = (game: Game, actor: string, target: string, amount: number) => act(game, actor, { type: 'bribe', target, amount });
for (const advanced of [false, true]) void test(`native ${advanced ? 'Advanced' : 'Basic'} late Mentat payment waits through real next Storm and Collection for one opening receipt`, () => {
const f = fixture({ advanced });
assert.equal(f.initial.status, 'lobby'); assert.equal(f.setup.status, 'setup');
assert.equal(f.originalCards.length, 33); assert.deepEqual(f.staging, []);
assert.equal(f.sourceActionCount, f.sourceActions.length); assert.ok(f.mentat.steps.length > 0);
assert.equal(f.phase7.before.phase, 3); assert.equal(f.phase7.after.phase, 7);
assert.equal(f.mentat.before.phase, 7); assert.equal(f.mentat.after.phase, 8);
assert.deepEqual(player(f.game, f.asker).hand.map(card => card.id), f.truths);
const source = structuredClone(f.game), before = balances(f.game);
let game = pay(f.game, f.payer, f.recipient, 3);
assert.deepEqual(f.game, source);
assert.equal(player(game, f.payer).spice, 7); assert.equal(player(game, f.recipient).spice, before[f.recipient]);
assert.equal(player(game, f.recipient).bribes, 3);
const result = complete(f, game);
assert.equal(result.nextTurn.after.turn, 2); assert.equal(result.nextTurn.after.phase, 0);
for (const state of [result.nextTurn.after, result.collection.after, result.mentat.before]) {
assert.equal(player(state, f.recipient).bribes, 3);
assert.equal(player(state, f.payer).spice, 7);
assert.deepEqual(player(state, f.asker).hand.map(card => card.id), f.truths);
assert.deepEqual(inventory(state), f.originalCards);
}
assert.equal(player(result.nextTurn.after, f.recipient).spice, before[f.recipient]);
assert.equal(player(result.collection.after, f.recipient).spice - before[f.recipient], advanced ? 2 : 0);
assert.equal(player(result.mentat.after, f.recipient).spice - player(result.mentat.before, f.recipient).spice, 3);
assert.equal(player(result.mentat.after, f.recipient).bribes, 0);
const received = balances(result.game);
game = settle(act(result.game, f.payer, { type: 'ready' }));
assert.equal(game.phase, 8); assert.deepEqual(balances(game), received);
assert.ok(game.players.every(seat => seat.bribes === 0)); assert.deepEqual(inventory(game), f.originalCards);
});
void test('native late incoming Mentat spice cannot fund another payment or the next actual auction bid before collection', () => {
const f = fixture(), game = pay(f.game, f.payer, f.recipient, 3), cash = player(game, f.recipient).spice;
rejected(game, f.recipient, { type: 'bribe', target: f.opponent, amount: cash + 1 });
const auction = advance(game, state => state.turn === 2 && state.phase === 3 && clean(state) && state.auction?.active === f.recipient);
assert.equal(player(auction, f.recipient).spice, cash); assert.equal(player(auction, f.recipient).bribes, 3);
rejected(auction, f.recipient, { type: 'bid', amount: cash + 1 });
rejected(auction, f.recipient, { type: 'bribe', target: f.opponent, amount: cash + 1 });
assert.deepEqual(player(auction, f.asker).hand.map(card => card.id), f.truths);
assert.deepEqual(inventory(auction), f.originalCards);
});
void test('native multiple late payer payments sum once in the exact next Mentat recipient receipt', () => {
const f = fixture(), before = balances(f.game);
let game = pay(f.game, f.payer, f.recipient, 3); game = pay(game, f.opponent, f.recipient, 2);
assert.equal(player(game, f.payer).spice, before[f.payer] - 3);
assert.equal(player(game, f.opponent).spice, before[f.opponent] - 2);
assert.equal(player(game, f.recipient).spice, before[f.recipient]); assert.equal(player(game, f.recipient).bribes, 5);
const result = complete(f, game);
assert.equal(player(result.mentat.before, f.recipient).bribes, 5);
assert.equal(player(result.game, f.recipient).spice - player(result.mentat.before, f.recipient).spice, 5);
assert.ok(result.game.players.every(seat => seat.bribes === 0)); assert.deepEqual(inventory(result.game), f.originalCards);
});
void test('native pre-Mentat and late-Mentat payments belong to their separate actual openings', () => {
const f = fixture(); let game = pay(f.base.game, f.payer, f.recipient, 2);
const firstCash = player(game, f.recipient).spice;
game = opening(game);
assert.equal(player(game, f.recipient).spice, firstCash + 2); assert.equal(player(game, f.recipient).bribes, 0);
game = pay(game, f.payer, f.recipient, 3);
assert.equal(player(game, f.payer).spice, 5); assert.equal(player(game, f.recipient).spice, firstCash + 2);
assert.equal(player(game, f.recipient).bribes, 3);
const result = complete(f, game);
assert.equal(player(result.nextTurn.after, f.recipient).spice, firstCash + 2);
assert.equal(player(result.game, f.recipient).spice - player(result.mentat.before, f.recipient).spice, 3);
assert.equal(player(result.game, f.recipient).bribes, 0); assert.deepEqual(inventory(result.game), f.originalCards);
});
void test('native new Mentat Truth Yes requires the real payment before payer Ready and leaves its receipt deferred', () => {
const f = fixture(), bound = bind(f); let game = bound.game;
assert.equal(game.phase, 8); assert.deepEqual(viewGame(bound.asked, f.payer).truthBribeAnswers, ['yes', 'no']);
rejected(game, f.payer, { type: 'ready' });
const guide = viewGame(game, f.payer).bribePromiseGuide; assert.ok(guide);
assert.equal(guide.action.target, f.recipient); assert.equal(guide.action.amount, 3);
const before = balances(game); game = act(game, f.payer, guide.action);
assert.equal(player(game, f.payer).spice, 7); assert.equal(player(game, f.recipient).spice, before[f.recipient]);
assert.equal(player(game, f.recipient).bribes, 3); assert.equal(promise(game, f.payer, f.recipient).fulfilled, true);
game = act(game, f.payer, { type: 'ready' }); assert.ok(game.ready.includes(f.payer)); assert.equal(game.phase, 8);
const result = complete(f, game);
assert.equal(player(result.game, f.recipient).spice - player(result.mentat.before, f.recipient).spice, 3);
assert.equal(game.discard.filter(card => card.id === f.truth).length, 1); assert.deepEqual(inventory(result.game), f.originalCards);
});
void test('native Collection Truth Yes survives Collection Ready and remains binding in actual Mentat', () => {
const f = fixture(); let game = bindTruthBribe(f.base).game;
const before = balances(game); game = enterMentat(game);
assert.equal(game.phase, 8); assert.deepEqual(balances(game), before); assert.equal(player(game, f.recipient).bribes, 0);
assert.equal(promise(game, f.payer, f.recipient).progress, 0); assert.ok(!promise(game, f.payer, f.recipient).released);
rejected(game, f.payer, { type: 'ready' });
const guide = viewGame(game, f.payer).bribePromiseGuide; assert.ok(guide); game = act(game, f.payer, guide.action);
assert.equal(player(game, f.payer).spice, 7); assert.equal(player(game, f.recipient).spice, before[f.recipient]);
assert.equal(player(game, f.recipient).bribes, 3); assert.equal(promise(game, f.payer, f.recipient).fulfilled, true);
assert.deepEqual(inventory(game), f.originalCards);
});
void test('native Collection No cap spans its old receipt and late Mentat payments until actual turn advance', () => {
const f = fixture(); let game = bindTruthBribe(f.base, 'no').game;
game = pay(game, f.payer, f.recipient, 1); const before = player(game, f.recipient).spice;
game = enterMentat(game);
assert.equal(player(game, f.recipient).spice, before + 1); assert.equal(player(game, f.recipient).bribes, 0);
assert.equal(promise(game, f.payer, f.recipient).progress, 1);
game = pay(game, f.payer, f.recipient, 1);
assert.equal(promise(game, f.payer, f.recipient).progress, 2); assert.equal(player(game, f.recipient).bribes, 1);
rejected(game, f.payer, { type: 'bribe', target: f.recipient, amount: 1 });
game = finish(game); assert.equal(game.turn, 2); assert.equal(game.phase, 0);
game = pay(game, f.payer, f.recipient, 3);
assert.equal(player(game, f.payer).spice, 5); assert.equal(player(game, f.recipient).bribes, 4);
assert.equal(promise(game, f.payer, f.recipient).progress, 2); assert.deepEqual(inventory(game), f.originalCards);
});
void test('native new Mentat Yes reopens only its already-ready payer while another seat is still unready', () => {
const f = fixture(); let game = act(f.game, f.payer, { type: 'ready' });
game = act(game, f.asker, { type: 'ready' }); assert.equal(game.phase, 8);
assert.ok(game.ready.includes(f.payer)); assert.ok(game.ready.includes(f.asker)); assert.ok(!game.ready.includes(f.opponent));
game = bind(f, 'yes', 3, f.recipient, game).game;
assert.ok(!game.ready.includes(f.payer)); assert.ok(game.ready.includes(f.asker)); assert.ok(!game.ready.includes(f.opponent));
rejected(game, f.payer, { type: 'ready' });
const guide = viewGame(game, f.payer).bribePromiseGuide; assert.ok(guide); game = act(game, f.payer, guide.action);
game = act(game, f.payer, { type: 'ready' });
assert.equal(game.phase, 8); assert.ok(game.ready.includes(f.payer)); assert.ok(game.ready.includes(f.asker));
assert.equal(player(game, f.recipient).bribes, 3); assert.deepEqual(inventory(game), f.originalCards);
});
void test('native an already-ready Mentat payer can make an ordinary late payment while other seats wait', () => {
const f = fixture(); let game = act(f.game, f.payer, { type: 'ready' });
const before = balances(game), ready = [...game.ready]; game = pay(game, f.payer, f.recipient, 3);
assert.equal(game.phase, 8); assert.deepEqual(game.ready, ready);
assert.equal(player(game, f.payer).spice, 7); assert.equal(player(game, f.recipient).spice, before[f.recipient]);
assert.equal(player(game, f.recipient).bribes, 3);
assert.deepEqual(player(game, f.asker).hand.map(card => card.id), f.truths); assert.deepEqual(inventory(game), f.originalCards);
});
void test('native a new Mentat No still caps cumulative payments until the real next turn', () => {
const f = fixture(); let game = bind(f, 'no').game;
game = pay(game, f.payer, f.recipient, 1); rejected(game, f.payer, { type: 'bribe', target: f.recipient, amount: 2 });
const cash = balances(game), receipts = deferred(game); game = finish(game);
assert.equal(game.turn, 2); assert.deepEqual(balances(game), cash); assert.deepEqual(deferred(game), receipts);
game = pay(game, f.payer, f.recipient, 3);
assert.equal(player(game, f.payer).spice, 6); assert.equal(player(game, f.recipient).bribes, 4);
assert.equal(promise(game, f.payer, f.recipient).progress, 1); assert.deepEqual(inventory(game), f.originalCards);
});
void test('all four minimal profiles execute the exact legal Mentat Truth Yes payment before Ready', () => {
const f = fixture(), game = bind(f).game;
for (const difficulty of DIFFICULTIES) {
const view = viewGame(game, f.payer); view.players.find(seat => seat.id === f.payer)!.bot = difficulty;
const action = botActions(view).find(action => action.type === 'bribe' && action.target === f.recipient && action.amount === 3);
assert.ok(action, difficulty); const paid = act(structuredClone(game), f.payer, action);
assert.equal(player(paid, f.payer).spice, 7); assert.equal(player(paid, f.recipient).spice, player(game, f.recipient).spice);
assert.equal(player(paid, f.recipient).bribes, 3); assert.equal(promise(paid, f.payer, f.recipient).fulfilled, true);
assert.deepEqual(inventory(paid), f.originalCards);
}
});
+34 -2
View File
@@ -75,6 +75,8 @@ void test('bribe claim fields keep the turn fixed and accept only current recipi
assert.match(html, /Turn 2/); assert.match(html, /Recipient/); assert.match(html, /min="1"/);
assert.match(html, /max="9007199254740991"/); assert.match(html, /smaller payments add together/);
assert.match(html, /Earlier payments, gifts and ally aid do not count/);
assert.match(html, /recipient collects the payment at the start of Mentat Pause/);
assert.match(html, /Payments made during Mentat Pause wait until the start of next turn&#x27;s Mentat Pause/);
minimum(props, 1); minimum(props, Number.MAX_SAFE_INTEGER); recipient(props, 'a');
assert.deepEqual(changes, [{ ...value, minimum: 1 }, { ...value, minimum: Number.MAX_SAFE_INTEGER }, { ...value, recipient: 'a' }]);
for (const next of [0, -1, 1.5, NaN, Infinity, Number.MAX_SAFE_INTEGER + 1]) {
@@ -102,7 +104,8 @@ void test('own bribe promises persist with named cumulative progress, delayed sp
assert.match(html, /No forbids reaching that minimum during the rest of this turn/);
assert.match(html, /Smaller payments to the named recipient add together/);
assert.match(html, /Prior payments, gifts and ally aid do not count/);
assert.match(html, /The sender pays immediately/); assert.match(html, /recipient can spend the payment only at Mentat Pause/);
assert.match(html, /The sender pays immediately/); assert.match(html, /recipient collects the payment at the start of Mentat Pause/);
assert.match(html, /Payments made during Mentat Pause wait until the start of next turn&#x27;s Mentat Pause/);
assert.match(html, /Fulfilled: this commitment is complete/); assert.match(html, /Released: this commitment is no longer possible/);
assert.match(html, /href="\/rules#truthtrance-bribe"/);
assert.doesNotMatch(html, /at least 99|foreign-event|controlled-bribe-event/);
@@ -127,10 +130,39 @@ void test('actual guide callbacks forward exact ordinary bribes out of turn thro
}
});
void test('controlled quiet Mentat guide forwards the exact payment for an already-ready payer and explains next-turn collection', () => {
const game = projected(8), sent: Action[] = [];
game.auction = null; game.active = null; game.ready = ['p'];
const before = structuredClone(game);
const button = guide({ game, busy: false, send(action) { sent.push(action); } });
assert.equal(button.props.disabled, false);
press(button);
assert.deepEqual(sent, [game.bribePromiseGuide!.action]);
assert.equal(sent[0], game.bribePromiseGuide!.action);
assert.deepEqual(game, before);
assert.match(markup(game), /Payments made during Mentat Pause wait until the start of next turn&#x27;s Mentat Pause/);
});
void test('controlled bribe callbacks expire when their original phase changes between otherwise supported windows', () => {
for (const [original, current] of [[3, 8], [8, 7], [7, 8]]) {
const game = projected(original), sent: Action[] = [];
const props = { game, busy: false, send(action: Action) { sent.push(action); } };
const button = guide(props);
assert.equal(button.props.disabled, false);
game.phase = current;
press(button);
assert.deepEqual(sent, []);
const currentButton = guide(props);
assert.equal(currentButton.props.disabled, false);
press(currentButton);
assert.deepEqual(sent, [game.bribePromiseGuide!.action]);
}
});
void test('stale bribe guides respect current scope, exact recipient, strictly clear timing and projected payment bounds', () => {
const changes: ((game: GameView) => void)[] = [
game => { game.me = 'a'; }, game => { game.turn = 3; }, game => { game.status = 'setup'; },
game => { game.phase = 8; }, game => { game.phase = -1; },
game => { game.phase = 9; }, game => { game.phase = -1; },
game => { game.bribePromiseGuide = { action: { ...game.bribePromiseGuide!.action } }; },
game => { game.bribePromiseGuide = null; }, game => { game.bribePromiseGuide!.action.amount = 3; },
game => { game.bribePromises[0].event = 'expired'; }, game => { game.bribePromises[0].player = 'a'; },
+38 -9
View File
@@ -6,7 +6,7 @@ import { DIFFICULTIES } from '../game/bot-profiles';
import {
actTruthBribe as act, advanceTruthBribe as advance, answerTruthBribe as answer,
askTruthBribe as ask, bindTruthBribe as bind, completeTruthBribe as complete,
createTruthBribeFixture as fixture, declareTruthBribe as declare, settleTruthBribe as settle,
createTruthBribeFixture as fixture, declareTruthBribe as declare, settleTruthBribe as settle, finishTruthBribe as finish,
truthBribeBalances as balances, truthBribeClean as clean, truthBribeInventory as inventory, truthBribePlayer as player,
} from './fixture-truth-bribe';
import { createTruthRevivalFixture } from './fixture-truth-revival';
@@ -135,32 +135,61 @@ void test('native already committed auction spice cannot supply an unfunded thre
assert.equal(player(game, f.target).spice, 10); assert.deepEqual(inventory(game), f.originalCards);
});
void test('native last Collection readiness cannot erase an unpaid Bribe Yes before actual Mentat', () => {
void test('native unpaid Bribe Yes passes Collection but cannot finish Mentat before its late payment', () => {
const f = fixture({ phase: 7 }); let game = act(f.game, f.asker, { type: 'ready' });
game = act(game, f.opponent, { type: 'ready' }); assert.equal(game.phase, 7);
game = bind(f, 'yes', 3, f.recipient, game).game;
const before = player(game, f.recipient).spice;
game = act(game, f.target, { type: 'ready' });
assert.equal(game.phase, 8); assert.equal(player(game, f.recipient).spice, before);
assert.ok(!promise(game, f.target, f.recipient).fulfilled);
assert.ok(!promise(game, f.target, f.recipient).released);
rejected(game, f.target, { type: 'ready' });
const guide = viewGame(game, f.target).bribePromiseGuide; assert.ok(guide); game = act(game, f.target, guide.action);
const before = player(game, f.recipient).spice; game = act(game, f.target, { type: 'ready' });
assert.equal(game.phase, 8); assert.equal(player(game, f.recipient).spice, before + 3);
assert.equal(player(game, f.recipient).bribes, 0); assert.deepEqual(inventory(game), f.originalCards);
const guide = viewGame(game, f.target).bribePromiseGuide; assert.ok(guide);
game = act(game, f.target, guide.action);
assert.equal(player(game, f.recipient).spice, before);
assert.equal(player(game, f.recipient).bribes, 3);
game = finish(game);
assert.equal(game.turn, 2); assert.equal(player(game, f.recipient).bribes, 3);
assert.deepEqual(inventory(game), f.originalCards);
});
void test('a real already-ready Collection payer reopens readiness when accepting a new Bribe Yes', () => {
void test('a new Bribe Yes retains already-ready Collection status and permits its original out-of-turn payment', () => {
const f = fixture({ phase: 7 });
let game = act(f.game, f.target, { type: 'ready' });
assert.equal(game.phase, 7); assert.ok(game.ready.includes(f.target));
game = bind(f, 'yes', 3, f.recipient, game).game;
assert.equal(game.phase, 7); assert.ok(!game.ready.includes(f.target));
rejected(game, f.target, { type: 'ready' });
assert.equal(game.phase, 7); assert.ok(game.ready.includes(f.target));
const guide = viewGame(game, f.target).bribePromiseGuide; assert.ok(guide);
game = act(game, f.target, guide.action);
assert.ok(game.ready.includes(f.target));
assert.equal(player(game, f.recipient).bribes, 3);
const result = complete(f, game);
assert.equal(player(result.mentat.after, f.recipient).spice - player(result.mentat.before, f.recipient).spice, 3);
assert.deepEqual(inventory(result.game), f.originalCards);
});
void test('controlled known Inflation Double closure reopens a ready payer and requires payment before Collection readiness', () => {
const f = fixture({ phase: 7 });
const controlled = structuredClone(f.game);
// Only the token frame is controlled; the real H/A/E source has no CHOAM
// player and does not claim a native token-placement history.
controlled.inflation = { side: 'cancel', placedTurn: 0, updatedTurn: 0, flipped: false };
let game = act(controlled, f.target, { type: 'ready' });
assert.ok(game.ready.includes(f.target));
game = bind(f, 'yes', 3, f.recipient, game).game;
assert.ok(!game.ready.includes(f.target));
const before = structuredClone(game);
assert.throws(() => act(game, f.target, { type: 'ready' }), /Inflation Double/);
assert.deepEqual(game, before);
const guide = viewGame(game, f.target).bribePromiseGuide; assert.ok(guide);
game = act(game, f.target, guide.action);
game = act(game, f.target, { type: 'ready' });
assert.ok(game.ready.includes(f.target));
assert.equal(promise(game, f.target, f.recipient).fulfilled, true);
assert.deepEqual(inventory(game), f.originalCards);
});
void test('native Truth bribe owner current-turn and guided event checks retain out-of-turn legal payment', () => {
const f = fixture(), declared = declare(f);
rejected(declared.game, f.opponent, f.questionAction);