Files
Dune/tests/bribe-pending-basic-controls.test.tsx
T
Quintine 72f6ec2b12
Verify and publish checkpoint container / container (push) Canceled after 0s
Allow Basic bribes during selected pending decisions
2026-10-29 06:42:50 +11:00

137 lines
7.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import assert from 'node:assert/strict';
import test from 'node:test';
import { createElement } from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
import { registerHooks } from 'node:module';
import { applyAction, createGame, joinGame, newPlayer, viewGame, type Game } from '../game/engine';
import { bribeAction } from '../game/bribe-options';
import { territory } from '../game/board';
import { createTruthTurnAmalFixture } from './fixture-truth-turn-amal';
import { nextStrongholdFactionsNativeStep } from './fixture-stronghold-factions';
import { placeTerror } from '../game/moritani-terror';
const aliases = registerHooks({ resolve(specifier, context, next) {
return next(specifier === 'next/link' ? 'vinext/shims/link' : specifier, context);
} });
const { Bribes } = await import('../components/bribes');
aliases.deregister();
const act = (game: Game, actor: string, action: Parameters<typeof applyAction>[2]) => 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 render = (game: Game, actor: string, busy = false) =>
renderToStaticMarkup(createElement(Bribes, { game: viewGame(game, actor), busy, act() {} }));
void test('actual Basic Ix Mentat opening offers a bribe for the upcoming initialization', () => {
const f = createTruthTurnAmalFixture({phase:8, incoming:4});
let game = f.game;
assert.deepEqual(game.phaseOpening, {passed:[], initialize:true});
const quote = viewGame(game, 'harkonnen').bribeOptions;
assert.equal(quote.receiptTiming, 'upcoming');
assert.equal(quote.blocked, null);
assert.ok(quote.targets.some(target => target.id === 'emperor' && target.maximum >= 1));
const html = render(game, 'harkonnen');
assert.match(html, /This payment will be collected at the upcoming Mentat Pause\./);
assert.match(html, /<select(?![^>]*disabled)[^>]*>/);
assert.match(html, /Emperor/);
assert.match(html, /<button\b[^>]*disabled=""[^>]*>Pay bribe<\/button>/);
assert.match(render(game, 'harkonnen', true), /<select\b[^>]*disabled=""/);
const action = bribeAction(quote, 'emperor', 1);
assert.deepEqual(action, {type:'bribe', target:'emperor', amount:1});
const before = {cash:seat(game,'emperor').spice, incoming:seat(game,'emperor').bribes};
game = act(game, 'harkonnen', action!);
assert.equal(seat(game,'emperor').spice, before.cash);
assert.equal(seat(game,'emperor').bribes, before.incoming + 1);
assert.deepEqual(game.phaseOpening, {passed:[], initialize:true});
for (const id of ['harkonnen','atreides','emperor']) game = act(game, id, {type:'ready'});
assert.equal(game.phaseOpening, null);
assert.equal(seat(game,'emperor').bribes, 0);
assert.equal(seat(game,'emperor').spice, before.cash + before.incoming + 1);
const late = viewGame(game, 'harkonnen').bribeOptions;
assert.equal(late.receiptTiming, 'nextTurn');
assert.equal(late.blocked, null);
assert.match(render(game,'harkonnen'), /This payment will be collected at the start of next turn’s Mentat Pause\./);
});
/** Public E3 setup and actual native controls reach Moritani's first Mentat
* placement; no pending decision or funds are manufactured by the test. */
function moritaniPlacement(): Game {
let game = createGame('BRIBENATIVE', newPlayer('moritani','Moritani','moritani'), false, ['ecaz']);
joinGame(game, newPlayer('atreides','Atreides','atreides'));
joinGame(game, newPlayer('emperor','Emperor','emperor'));
for (const p of game.players) game = act(game,p.id,{type:'ready'});
game = act(game,'moritani',{type:'start'});
for (let i = 0; i < 1800; i++) {
if (game.phase === 8 && game.decision?.kind === 'moritaniPlacement') return game;
assert.notEqual(game.status,'finished');
const next = game.decision?.kind === 'moritaniSetup'
? {actor:'moritani', action:{type:'decision' as const, territory:'sietch_tabr', sector:territory('sietch_tabr').sectors[0]}}
: nextStrongholdFactionsNativeStep(game);
assert.ok(next, 'Native setup and turns must reach the original Mentat placement');
game = act(game,next.actor,next.action);
}
throw Error('Native Mentat placement did not open');
}
void test('a different pending Mentat decision stays blocked with next-turn receipt timing', () => {
const game = moritaniPlacement();
assert.equal(game.decision?.kind, 'moritaniPlacement');
const quote = viewGame(game, 'atreides').bribeOptions;
assert.equal(quote.receiptTiming,'nextTurn');
assert.ok(quote.blocked);
assert.equal(bribeAction(quote,'emperor',1),null);
const html = render(game,'atreides');
assert.match(html,/This payment will be collected at the start of next turn’s Mentat Pause\./);
assert.match(html,/<select\b[^>]*disabled=""/);
});
/** Public E3 setup and real shipment/Extortion reveal. The later clean movement
* boundary and prior physical token placement are explicitly staged. */
function moritaniExtortion(): Game {
let game = createGame('BRIBEEXTORTION', newPlayer('moritani','Moritani','moritani'), false, ['ecaz']);
for (const faction of ['emperor','atreides','harkonnen'] as const)
joinGame(game,newPlayer(faction,faction,faction));
for (const p of game.players) game = act(game,p.id,{type:'ready'});
game = act(game,'moritani',{type:'start'});
for (let i = 0; i < 100 && game.status === 'setup'; i++) {
const next = game.decision?.kind === 'moritaniSetup'
? {actor:'moritani', action:{type:'decision' as const, territory:'sietch_tabr', sector:territory('sietch_tabr').sectors[0]}}
: nextStrongholdFactionsNativeStep(game);
assert.ok(next); game = act(game,next.actor,next.action);
}
assert.equal(game.status,'playing');
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:['emperor','atreides','harkonnen','moritani'],
active:'emperor',movementRemaining:['emperor','atreides','harkonnen','moritani'],
response:null,decision:null,phaseOpening:null,ready:[]});
game = act(game,'emperor',{type:'ship',territory:'habbanya_ridge_sietch',sector:17,amount:1});
assert.equal(game.decision?.kind,'moritaniTerror');
game = act(game,'moritani',{type:'decision',reveal:true});
game.phase = 7; game.ready = [];
for (const p of game.players) game = act(game,p.id,{type:'ready'});
assert.equal(game.decision?.kind,'moritaniPlacement');
game = act(game,'moritani',{type:'decision',decline:true});
assert.equal(game.decision?.kind,'moritaniExtortion');
return game;
}
void test('an actual Basic Extortion decision has its own next-turn bribe quote', () => {
const game = moritaniExtortion();
assert.equal(game.phase,8);
assert.equal(game.decision?.kind,'moritaniExtortion');
const quote = viewGame(game,'emperor').bribeOptions;
assert.equal(quote.receiptTiming,'nextTurn');
assert.equal(quote.blocked,null);
const target = quote.targets.find(candidate => candidate.maximum >= 1); assert.ok(target);
assert.deepEqual(bribeAction(quote,target.id,1), {type:'bribe',target:target.id,amount:1});
const html = render(game,'emperor');
assert.match(html,/This payment will be collected at the start of next turn’s Mentat Pause\./);
assert.match(html,/<select(?![^>]*disabled)[^>]*>/);
assert.match(render(game,'emperor',true),/<select\b[^>]*disabled=""/);
const paid = act(game,'emperor',bribeAction(quote,target.id,1)!);
assert.deepEqual(paid.decision,game.decision);
assert.equal(seat(paid,target.id).bribes,seat(game,target.id).bribes + 1);
});