Files
Dune/tests/emperor-extortion-gift-mixed-basic.test.ts
T
Quintine 3bfcd10140
Verify and publish checkpoint container / container (push) Successful in 12m11s
Enable Basic mixed-deck Emperor Extortion gifts
2026-10-30 19:13:54 +11:00

159 lines
8.8 KiB
TypeScript

import assert from 'node:assert/strict';
import test from 'node:test';
import { applyAction, createGame, joinGame, newPlayer, viewGame, type Action, type Game } from '../game/engine';
import { territory } from '../game/board';
import { placeTerror } from '../game/moritani-terror';
import { botActions, runBots } from '../game/bots';
import { DIFFICULTIES } from '../game/bot-profiles';
import { nextStrongholdFactionsNativeStep } from './fixture-stronghold-factions';
const act = (game: Game, actor: string, action: Action) => applyAction(game, actor, action);
const seat = (game: Game, id: string) => {
const player = game.players.find(row => row.id === id); assert.ok(player); return player;
};
const copy = (game: Game): Game => structuredClone(game);
function passResponse(game: Game): Game {
for (let count = 0; game.response && count < 30; count++) {
const player = game.players.find(row => !game.response!.passed.includes(row.id)); assert.ok(player);
game = act(game, player.id, {type:'passResponse'});
}
assert.equal(game.response, null);
return game;
}
function payment(game: Game, pay: boolean): Action {
assert.ok(game.decision?.kind === 'moritaniExtortion');
return {type:'decision', event:game.decision.event, pay};
}
/** Actual mixed-deck public setup, physical Extortion placement, shipment,
* reveal and Mentat queue. Alliance, turn boundary, wallets and physical
* Karama custody are staged, as in the existing native Extortion fixture. */
function pending(native: 'ixians' | 'tleilaxu') {
let game = createGame('MIXEDEXTORTION', newPlayer('m','Moritani','moritani'), false, ['ecaz','ix']);
for (const [id, faction] of [['e','emperor'], ['a','atreides'], ['h','harkonnen'], ['i',native]] as const)
joinGame(game, newPlayer(id, faction, faction));
for (const player of game.players) game = act(game, player.id, {type:'ready'});
game = act(game, 'm', {type:'start'});
for (let count = 0; game.status === 'setup' && count < 100; count++) {
if (game.decision?.kind === 'moritaniSetup')
game = act(game, 'm', {type:'decision', territory:'sietch_tabr', sector:territory('sietch_tabr').sectors[0]});
else {
const step = nextStrongholdFactionsNativeStep(game); assert.ok(step);
game = act(game, step.actor, step.action);
}
}
assert.equal(game.status, 'playing');
seat(game,'e').ally = 'a'; seat(game,'a').ally = 'e';
const token = game.moritaniTerror!.tokens.find(row => row.kind === 'extortion'); assert.ok(token);
game.moritaniTerror = placeTerror(game.moritaniTerror!, token.id, 'habbanya_ridge_sietch', 1);
Object.assign(game, {phase:5, turn:2, storm:18, order:['e','a','h','i','m'], active:'e',
movementRemaining:['e','a','h','i','m'], response:null, decision:null, phaseOpening:null, ready:[]});
seat(game,'e').spice = 11; seat(game,'a').spice = 2;
const harkonnen = seat(game,'h');
let card = harkonnen.hand.find(row => row.effect === 'karama');
if (!card) {
const sourceOwner = game.players.find(row => row.id !== 'h' && row.hand.some(item => item.effect === 'karama'));
const source = sourceOwner?.hand ?? game.deck;
const index = source.findIndex(item => item.effect === 'karama'); assert.ok(index >= 0);
[card] = source.splice(index, 1); harkonnen.hand.push(card);
}
const karama = card.id;
game = act(game,'e',{type:'ship',territory:'habbanya_ridge_sietch',sector:17,amount:1});
assert.equal(game.decision?.kind,'moritaniTerror');
game = act(game,'m',{type:'decision',reveal:true});
assert.equal(game.moritaniTerror!.tokens.find(row => row.id === token.id)?.status,'extortion');
const beforeAward = seat(game,'m').spice;
game.phase = 7; game.ready = [];
for (const player of game.players) game = act(game,player.id,{type:'ready'});
assert.equal(game.phase,8);
while (game.phaseOpening) {
const step = nextStrongholdFactionsNativeStep(game); assert.ok(step);
game = act(game,step.actor,step.action);
}
assert.equal(game.decision?.kind,'moritaniPlacement');
game = act(game,'m',{type:'decision',decline:true});
assert.equal(seat(game,'m').spice,beforeAward + 5);
assert.equal(game.decision?.kind,'moritaniExtortion');
assert.equal(game.decision.player,'e');
game = act(game,'e',payment(game,false));
assert.equal(game.decision?.kind,'moritaniExtortion');
assert.equal(game.decision.player,'a');
assert.equal(viewGame(game,'a').extortion.pending?.canPay,false);
return {game, token:token.id, karama, beforeAward};
}
function original(game: Game) {
return {decision:copy(game).decision, obligation:copy(game).moritaniExtortion,
order:[...game.order], turn:game.turn, phase:game.phase};
}
for (const native of ['ixians','tleilaxu'] as const) {
void test(`Basic mixed ${native} Emperor gift enables the original Extortion payment`, () => {
const f = pending(native); let game = f.game;
const before = original(game), event = (game.decision as Extract<NonNullable<Game['decision']>,{kind:'moritaniExtortion'}>).event;
assert.deepEqual(viewGame(game,'e').emperorExtortionGift,{recipient:'a',maximum:10});
for (const id of ['a','h','i','m']) assert.equal(viewGame(game,id).emperorExtortionGift,null);
game = act(game,'e',{type:'emperorGift',amount:1});
assert.equal(game.response?.kind,'emperorGift');
game = passResponse(game);
assert.deepEqual(original(game),before);
assert.equal(seat(game,'e').spice,9); assert.equal(seat(game,'a').spice,3);
assert.equal(viewGame(game,'a').extortion.pending?.canPay,true);
game = act(game,'a',{type:'decision',event,pay:true});
assert.equal(seat(game,'a').spice,0); assert.equal(seat(game,'m').spice,f.beforeAward + 8);
assert.equal(game.moritaniTerror!.tokens.find(row => row.id === f.token)?.status,'removed');
assert.equal(game.moritaniExtortion,undefined);
});
void test(`Basic mixed ${native} Karama cancels the gift and preserves the payer`, () => {
const f = pending(native); let game = f.game;
const before = original(game);
game = act(game,'e',{type:'emperorGift',amount:1});
game = act(game,'h',{type:'card',mode:'cancel',card:f.karama});
assert.equal(game.response,null); assert.deepEqual(original(game),before);
assert.equal(seat(game,'e').spice,10); assert.equal(seat(game,'a').spice,2);
assert.equal(viewGame(game,'a').extortion.pending?.canPay,false);
assert.equal(game.discard.filter(card => card.id === f.karama).length,1);
const unchanged = copy(game); assert.throws(() => act(game,'a',payment(game,true)));
assert.deepEqual(game,unchanged);
game = act(game,'a',payment(game,false));
assert.equal(seat(game,'m').spice,f.beforeAward + 5);
});
}
void test('all four minimal bot profiles can respond to a mixed Basic Extortion gift', () => {
let {game} = pending('ixians');
game = act(game,'e',{type:'emperorGift',amount:1});
for (const difficulty of DIFFICULTIES) {
const view = viewGame(game,'h'); view.players.find(player => player.id === 'h')!.bot = difficulty;
const actions = botActions(view); assert.ok(actions.length > 0);
assert.doesNotThrow(() => act(copy(game),'h',actions[0]));
}
});
for (const native of ['ixians','tleilaxu'] as const) void test(
`Basic mixed ${native} bot ally waits for the human Emperor gift and Continue`, () => {
const initial = pending(native);
for (const difficulty of DIFFICULTIES) {
let game = copy(initial.game);
seat(game,'a').bot = difficulty;
assert.ok(game.decision?.kind === 'moritaniExtortion');
const event = game.decision.event;
assert.deepEqual(viewGame(game,'e').emperorExtortionPriority,{event,recipient:'a',canContinue:true});
assert.deepEqual(viewGame(game,'a').emperorExtortionPriority,{event,recipient:'a',canContinue:false});
assert.ok(!botActions(viewGame(game,'a')).some(action => action.type === 'decision' && action.event === event));
let waiting = runBots(game,5);
assert.equal(waiting.decision?.kind,'moritaniExtortion');
assert.equal(waiting.decision.event,event);
assert.equal(seat(waiting,'a').spice,2);
game = act(game,'e',{type:'emperorGift',amount:1});
game = passResponse(game);
assert.equal(seat(game,'a').spice,3);
assert.deepEqual(viewGame(game,'e').emperorExtortionPriority,{event,recipient:'a',canContinue:true});
waiting = runBots(game,5);
assert.equal(waiting.decision?.kind,'moritaniExtortion');
assert.equal(waiting.decision.event,event);
game = act(game,'e',{type:'emperorExtortionContinue',event});
const action = botActions(viewGame(game,'a')).find(row => row.type === 'decision' && row.event === event);
assert.ok(action, `${difficulty} must have a legal original Extortion response`);
assert.doesNotThrow(() => act(copy(game),'a',action));
game = runBots(game,1);
assert.ok(game.decision?.kind !== 'moritaniExtortion' || game.decision.event !== event);
}
});