109 lines
5.7 KiB
TypeScript
109 lines
5.7 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
import { viewGame, type Game } from '../game/engine';
|
|
import { guildAmbassadorShipments } from '../game/guild-ambassador';
|
|
import { reserveShipmentCost } from '../game/shipment-price';
|
|
import { botActions } from '../game/bots';
|
|
import { DIFFICULTIES } from '../game/bot-profiles';
|
|
import { nextBasicSemutaAmbassadorStep } from './fixture-basic-semuta-ambassador';
|
|
import { act, player } from './fixture-advanced-overlap-child';
|
|
import { advancedDirectSneakFixture } from './fixture-advanced-sneak-direct';
|
|
|
|
function advanceTo(game: Game, until: (state: Game) => boolean): Game {
|
|
for (let i = 0; i < 60 && !until(game); i++) {
|
|
if (game.decision?.kind === 'advisor') {
|
|
game = act(game, game.decision.player, {type:'decision', accept:false});
|
|
continue;
|
|
}
|
|
if (game.decision?.kind === 'guildShipment') {
|
|
game = act(game, game.decision.player, {type:'decision', allow:true});
|
|
continue;
|
|
}
|
|
const next = nextBasicSemutaAmbassadorStep(game);
|
|
assert.ok(next, `The original arrival must continue: ${game.decision?.kind ?? game.response?.kind}`);
|
|
game = act(game, next.actor, next.action);
|
|
}
|
|
assert.ok(until(game), 'The requested native arrival reaction is reached');
|
|
return game;
|
|
}
|
|
|
|
function revealSneak(withBG: boolean) {
|
|
const f = advancedDirectSneakFixture(withBG);
|
|
const originalSpice = player(f.game, f.entrant).spice;
|
|
const price = reserveShipmentCost({faction:'guild', halfRate:true}, 'stronghold', 1);
|
|
let game = act(f.game, f.entrant, f.shipment);
|
|
game = advanceTo(game, state => !!state.pendingTerrorEntry);
|
|
assert.equal(game.pendingArrivalOverlap ?? null, null, 'Guild cannot trigger its own Ambassador');
|
|
assert.equal(game.pendingTerrorEntry?.token, f.sneak);
|
|
assert.equal(game.pendingTerrorEntry?.entrant, f.entrant);
|
|
assert.equal(player(game, f.entrant).spice, originalSpice - price);
|
|
game = act(game, f.moritani, {type:'decision', reveal:true});
|
|
assert.equal(game.pendingTerrorEntry?.stage, 'sneakAttack');
|
|
return {f, game, originalSpice, price};
|
|
}
|
|
|
|
for (const withBG of [false, true]) void test(
|
|
`Advanced direct Sneak opens the Moritani Ambassador entry ${withBG ? 'after BG accompaniment' : 'without BG'}`, () => {
|
|
const {f, game: before, originalSpice, price} = revealSneak(withBG);
|
|
const reserves = player(before, f.moritani).reserves;
|
|
let game = act(before, f.moritani, {type:'decision', amount:1});
|
|
if (withBG) {
|
|
assert.equal(game.decision?.kind, 'advisor');
|
|
assert.equal(game.decision?.player, 'beneGesserit');
|
|
game = act(game, 'beneGesserit', {type:'decision', accept:false});
|
|
}
|
|
game = advanceTo(game, state => !!state.pendingAmbassador);
|
|
const entry = game.pendingAmbassador; assert.ok(entry);
|
|
assert.equal(entry.token, f.ambassador);
|
|
assert.equal(entry.entrant, f.moritani);
|
|
assert.equal(player(game, f.moritani).reserves, reserves - 1);
|
|
assert.equal(player(game, f.moritani).forces[`${f.destination}:${f.sector}`], 1);
|
|
assert.equal(player(game, f.entrant).forces[`${f.destination}:${f.sector}`], 1);
|
|
assert.equal(player(game, f.entrant).spice, originalSpice - price);
|
|
for (const difficulty of DIFFICULTIES) {
|
|
const view = viewGame(game, 'ecaz');
|
|
view.players.find(p => p.id === 'ecaz')!.bot = difficulty;
|
|
const action = botActions(view)[0]; assert.ok(action, difficulty);
|
|
assert.doesNotThrow(() => act(game, 'ecaz', action), difficulty);
|
|
}
|
|
game = act(game, 'ecaz', {type:'decision', event:entry.event, decline:true});
|
|
assert.equal(game.pendingAmbassador, null);
|
|
assert.equal(game.pendingTerrorEntry, null);
|
|
assert.equal(game.pendingArrivalOverlap ?? null, null);
|
|
assert.equal(player(game, f.moritani).reserves, reserves - 1);
|
|
assert.equal(player(game, f.entrant).spice, originalSpice - price);
|
|
});
|
|
|
|
void test('Advanced direct Sneak sending zero opens no separate entry or accompaniment', () => {
|
|
const {f, game: before, originalSpice, price} = revealSneak(true);
|
|
const reserves = player(before, f.moritani).reserves;
|
|
const game = act(before, f.moritani, {type:'decision', amount:0});
|
|
assert.equal(player(game, f.moritani).reserves, reserves);
|
|
assert.equal(game.pendingAmbassador ?? null, null);
|
|
assert.equal(game.pendingTerrorEntry, null);
|
|
assert.notEqual(game.decision?.kind, 'advisor');
|
|
assert.equal(player(game, f.entrant).spice, originalSpice - price);
|
|
});
|
|
|
|
void test('Advanced direct Sneak can trigger the retained Guild Ambassador exactly once', () => {
|
|
const {f, game: before, originalSpice, price} = revealSneak(false);
|
|
const reserves = player(before, f.owner).reserves, spice = player(before, f.owner).spice;
|
|
let game = act(before, f.moritani, {type:'decision', amount:1});
|
|
game = advanceTo(game, state => !!state.pendingAmbassador);
|
|
const entry = game.pendingAmbassador; assert.ok(entry);
|
|
game = act(game, f.owner, {type:'decision', event:entry.event, trigger:true, beneficiary:f.owner});
|
|
assert.equal(game.pendingAmbassador?.stage, 'ship');
|
|
const target = guildAmbassadorShipments(game, f.owner).destinations.find(row => row.territory === 'polar_sink');
|
|
assert.ok(target);
|
|
game = act(game, f.owner, {type:'decision', event:entry.event, amount:1, elite:0,
|
|
territory:target.territory, sector:target.sector});
|
|
game = advanceTo(game, state => !state.pendingAmbassador);
|
|
assert.equal(game.pendingAmbassador, null);
|
|
assert.equal(game.pendingTerrorEntry, null);
|
|
assert.equal(player(game, f.owner).reserves, reserves - 1);
|
|
assert.equal(player(game, f.owner).spice, spice);
|
|
assert.equal(player(game, f.entrant).spice, originalSpice - price);
|
|
assert.equal(player(game, f.entrant).forces[`${f.destination}:${f.sector}`], 1);
|
|
assert.equal(game.ecazAmbassadors?.tokens.find(token => token.id === f.ambassador)?.zone, 'used');
|
|
});
|