Files
Dune/tests/bribe-classic-response-runtime.test.ts
T
Quintine 6ecd26c8d1
Verify and publish checkpoint container / container (push) Successful in 10m47s
Allow bribes during classic pending decisions
2026-10-29 08:26:34 +11:00

134 lines
7.3 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 { actFremenRevivalGrant as act, advanceFremenRevivalGrant as advance,
createFremenRevivalGrantFixture as fixture, fremenRevivalGrantInventory as inventory,
fremenRevivalGrantPlayer as player } from './fixture-fremen-revival-grant';
const pay = (target: string, amount: number): Action => ({type:'bribe', target, amount});
function reject(game: Game, actor: string, action: Action) {
const before = structuredClone(game);
assert.throws(() => act(game, actor, action));
assert.deepEqual(game, before, 'A rejected bribe leaves its pending parent unchanged.');
}
/** Real classic Basic setup, native first auction and an originally dealt
* Emperor Karama. The shared fixture selects unopened starting positions and
* uses actual setup actions; no response, wallet or auction is fabricated. */
function auctionSource() {
const f = fixture();
const game = advance(structuredClone(f.afterSetup), state => state.turn === 1 && state.phase === 3 &&
state.response?.kind === 'atreidesAuction' && !!state.auction && !state.richeseAuction);
assert.equal(game.response?.owner, f.ally);
assert.ok(player(game, f.observer).hand.some(card => card.id === f.karama));
return {f, game};
}
let auctionCache: ReturnType<typeof auctionSource> | undefined;
const auction = () => auctionCache ??= auctionSource();
/** Real original paid battle, casualties, later worm/Nexus and alliance lead
* to Fremen's actual revival grant, held open by Emperor's physical Karama. */
function grantSource(advanced = false) {
const f = fixture({advanced});
const game = act(f.game, f.fremen, f.grant);
assert.equal(game.response?.kind, 'fremenRevivalGrant');
assert.ok(player(game, f.observer).hand.some(card => card.id === f.karama));
return {f, game};
}
let grantCache: ReturnType<typeof grantSource> | undefined;
const grant = () => grantCache ??= grantSource();
void test('a Basic auction inspection response retains its original lot and passes during an out-of-turn bribe', () => {
const {f, game: source} = auction();
const before = structuredClone(source), response = structuredClone(before.response), lot = structuredClone(before.auction);
const oldPurse = player(before, f.fremen).spice, oldCash = player(before, f.observer).spice;
const oldBribes = player(before, f.observer).bribes, cards = inventory(before);
assert.ok(oldPurse >= 1);
assert.equal(viewGame(before, f.fremen).bribeOptions.blocked, null);
const paid = act(before, f.fremen, pay(f.observer, 1));
assert.deepEqual(paid.response, response); assert.deepEqual(paid.auction, lot);
assert.equal(player(paid, f.fremen).spice, oldPurse - 1);
assert.equal(player(paid, f.observer).spice, oldCash);
assert.equal(player(paid, f.observer).bribes, oldBribes + 1);
assert.deepEqual(inventory(paid), cards);
const control = act(before, f.observer, {type:'passResponse'});
const continued = act(paid, f.observer, {type:'passResponse'});
assert.notEqual(continued.response?.kind, 'atreidesAuction');
assert.deepEqual(continued.auction, control.auction);
assert.equal(player(continued, f.observer).bribes, oldBribes + 1);
assert.deepEqual(inventory(continued), cards);
});
void test('a Basic auction inspection can still be canceled by the original Karama after a bribe', () => {
const {f, game: source} = auction(), before = structuredClone(source);
const paid = act(before, f.fremen, pay(f.observer, 1));
const control = act(before, f.observer, {type:'card', card:f.karama, mode:'cancel'});
const canceled = act(paid, f.observer, {type:'card', card:f.karama, mode:'cancel'});
assert.deepEqual(canceled.auction, control.auction);
assert.equal(canceled.discard.filter(card => card.id === f.karama).length, 1);
assert.equal(player(canceled, f.observer).bribes, player(before, f.observer).bribes + 1);
});
void test('a Basic Fremen grant response keeps its exact source and grants the ally after the original pass', () => {
const {f, game: source} = grant(), before = structuredClone(source);
const response = structuredClone(before.response), oldPurse = player(before, f.observer).spice;
const oldCash = player(before, f.ally).spice, oldBribes = player(before, f.ally).bribes, cards = inventory(before);
assert.ok(oldPurse >= 1);
assert.equal(viewGame(before, f.observer).bribeOptions.blocked, null);
const paid = act(before, f.observer, pay(f.ally, 1));
assert.deepEqual(paid.response, response);
assert.equal(player(paid, f.observer).spice, oldPurse - 1);
assert.equal(player(paid, f.ally).spice, oldCash);
assert.equal(player(paid, f.ally).bribes, oldBribes + 1);
assert.ok(!paid.freeRevival.includes(f.ally));
const control = act(before, f.observer, {type:'passResponse'});
const continued = act(paid, f.observer, {type:'passResponse'});
assert.notEqual(continued.response?.kind, 'fremenRevivalGrant');
assert.deepEqual(continued.freeRevival, control.freeRevival);
assert.ok(continued.freeRevival.includes(f.ally));
assert.deepEqual(inventory(continued), cards);
});
void test('a Basic Fremen grant can still be canceled after the payment', () => {
const {f, game: source} = grant(), before = structuredClone(source);
const paid = act(before, f.observer, pay(f.ally, 1));
const control = act(before, f.observer, {type:'card', card:f.karama, mode:'cancel'});
const canceled = act(paid, f.observer, {type:'card', card:f.karama, mode:'cancel'});
assert.deepEqual(canceled.freeRevival, control.freeRevival);
assert.ok(!canceled.freeRevival.includes(f.ally));
assert.equal(canceled.discard.filter(card => card.id === f.karama).length, 1);
assert.equal(player(canceled, f.ally).bribes, player(before, f.ally).bribes + 1);
});
void test('invalid or allied bribes leave a pending Basic grant untouched', () => {
const {f, game: source} = grant(), game = structuredClone(source);
reject(game, f.fremen, pay(f.ally, 1));
reject(game, f.observer, pay(f.observer, 1));
reject(game, f.observer, pay(f.ally, 0));
reject(game, f.observer, pay(f.ally, player(game, f.observer).spice + 1));
});
void test('an Advanced grant remains outside the Basic interruption profile', () => {
const {f, game} = grantSource(true);
assert.ok(viewGame(game, f.observer).bribeOptions.blocked);
reject(game, f.observer, pay(f.ally, 1));
});
void test('all four minimal bot profiles can continue the preserved Basic grant response', () => {
const {f, game: source} = grant(), paid = act(source, f.observer, pay(f.ally, 1));
for (const difficulty of DIFFICULTIES) {
const game = structuredClone(paid), view = viewGame(game, f.observer);
view.players.find(seat => seat.id === f.observer)!.bot = difficulty;
const action = botActions(view).find(choice => choice.type === 'passResponse' ||
choice.type === 'card' && choice.mode === 'cancel');
assert.ok(action, `${difficulty} needs an original legal response`);
const next = act(game, f.observer, action);
assert.ok(next.response?.kind !== 'fremenRevivalGrant' || next.response.passed.includes(f.observer) ||
next.pendingKull || next.pendingKarama || next.pendingTreacheryDiscard,
`${difficulty} must advance the original response`);
assert.equal(player(next, f.ally).bribes, player(source, f.ally).bribes + 1);
}
});