137 lines
7.5 KiB
TypeScript
137 lines
7.5 KiB
TypeScript
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);
|
||
});
|