53 lines
3.3 KiB
TypeScript
53 lines
3.3 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import { applyAction, createGame, newPlayer, type Action, type Game } from '../game/engine';
|
|
import { baseDeck } from '../game/cards';
|
|
import { createAmbassadors, placeAmbassador, type AmbassadorEffect } from '../game/ecaz-ambassadors';
|
|
import { createTerrorState, placeTerror } from '../game/moritani-terror';
|
|
|
|
export type WormSneakKind = 'direct' | 'overlap' | 'ambassador';
|
|
export type WormOrder = 'terror' | 'ambassador';
|
|
export const player = (game: Game, id: string) => {
|
|
const result = game.players.find(p => p.id === id); assert.ok(result); return result;
|
|
};
|
|
export const act = (game: Game, id: string, action: Action) => applyAction(structuredClone(game), id, action);
|
|
|
|
/** Controlled Advanced worm position based on the existing overlap-worm
|
|
* fixture. Seats, forces, card deck, physical markers and the first and second
|
|
* rider decisions are staged; the accepted first ride and every subsequent
|
|
* force placement and decision are actual engine actions. */
|
|
export function advancedSneakWormFixture(kind: WormSneakKind, first: WormOrder = 'terror', withBG = false) {
|
|
const game = createGame('ADVANCEDSNEAKWORM', newPlayer('ec', 'Ecaz', 'ecaz'), true, ['ecaz']);
|
|
game.players.push(newPlayer('m', 'Moritani', 'moritani'), newPlayer('in', 'Rider', 'fremen'));
|
|
if (withBG) game.players.push(newPlayer('bg', 'Bene Gesserit', 'beneGesserit'));
|
|
const order = first === 'terror' ? ['m','ec','in'] : ['ec','m','in'];
|
|
if (withBG) order.push('bg');
|
|
Object.assign(game, {status:'playing', phase:1, turn:3, storm:18, active:null,
|
|
order, movementRemaining:[...order], deck:baseDeck()});
|
|
for (const seat of game.players)
|
|
Object.assign(seat, {hand:[], spice:20, forces:{}, reserves:20});
|
|
player(game,'in').forces = {'imperial_basin:10':3, 'hagga_basin:12':1};
|
|
player(game,'in').reserves = 16;
|
|
const ambassadors = createAmbassadors(() => 0.2);
|
|
const effect: AmbassadorEffect = kind === 'direct' ? 'fremen' : kind === 'overlap' ? 'emperor' : 'guild';
|
|
const token = ambassadors.tokens.find(row => row.effect === effect); assert.ok(token);
|
|
ambassadors.cohort = [token.id,...ambassadors.tokens.filter(row => row.effect !== 'ecaz' && row.id !== token.id)
|
|
.slice(0,4).map(row => row.id)];
|
|
for (const row of ambassadors.tokens) {
|
|
row.zone = row.effect === 'ecaz' || ambassadors.cohort.includes(row.id) ? 'supply' : 'pool';
|
|
row.location = null;
|
|
}
|
|
game.ecazAmbassadors = placeAmbassador(ambassadors, token.id, {turn:2, availableSpice:20,
|
|
destination:{id:'arrakeen',stronghold:true,inStorm:false,allowed:true}}).state;
|
|
const terror = createTerrorState(() => 0.2), sneak = terror.tokens.find(row => row.kind === 'sneakAttack');
|
|
assert.ok(sneak);
|
|
const childTerritory = kind === 'ambassador' ? 'carthag' : 'arrakeen';
|
|
const childSector = kind === 'ambassador' ? 11 : 10;
|
|
game.moritaniTerror = placeTerror(terror, sneak.id, childTerritory, 1);
|
|
game.decision = {kind:'wormRide', player:'in', territory:'imperial_basin'};
|
|
game.wormRides = ['hagga_basin'];
|
|
const entry = act(game, 'in', {type:'decision', accept:true, territory:'arrakeen', sector:10,
|
|
forces:{'imperial_basin:10':2}});
|
|
return {game:entry, token:token.id, sneak:sneak.id, kind, first, withBG, childTerritory, childSector,
|
|
nextRide:{kind:'wormRide',player:'in',territory:'hagga_basin'} as const};
|
|
}
|