Allow bribes during classic pending decisions
Verify and publish checkpoint container / container (push) Successful in 10m47s
Verify and publish checkpoint container / container (push) Successful in 10m47s
This commit is contained in:
1 parent
1e81ce8ad8
commit
6ecd26c8d1
8 files changed
+444
-5
No files matched your search
@@ -0,0 +1,133 @@
|
||||
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);
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user