94 lines
6.1 KiB
TypeScript
94 lines
6.1 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import { createGame, joinGame, newPlayer, type Action, type Game } from '../game/engine';
|
|
import { placeAmbassador, validateAmbassadors } from '../game/ecaz-ambassadors';
|
|
import { placeTerror } from '../game/moritani-terror';
|
|
import { fremenAmbassadorMovement } from '../game/fremen-ambassador';
|
|
import { act, player, createBasicNativeOverlapFixture, type First } from './fixture-basic-native-overlap';
|
|
import { advanceBasicSemutaAmbassador, basicSemutaAmbassadorClean } from './fixture-basic-semuta-ambassador';
|
|
|
|
export { act, player };
|
|
|
|
/** Public Basic native setup and actual Guild shipment. The Fremen cohort and
|
|
* two original physical tokens are selected and placed in controlled setup;
|
|
* the second token is explicitly staged because two placements cannot occur
|
|
* during one real turn. All later moves, payments and reactions are native. */
|
|
function createWithBg(first: First) {
|
|
let game = createGame('OVERLAPFREMENBG',newPlayer('ecaz','Ecaz','ecaz'),false,['ecaz','choam','ix']);
|
|
// Arrange five occupied circles while the sixth is still free, then join
|
|
// Guild in the remaining circle. Public seat selection never swaps seats.
|
|
for (const faction of ['choam','tleilaxu','beneGesserit','moritani'] as const)
|
|
joinGame(game,newPlayer(faction,faction,faction));
|
|
for (const [actor,position] of [['ecaz',6],['choam',1],['tleilaxu',2],['beneGesserit',3]] as const)
|
|
game = act(game,actor,{type:'seatPosition',position});
|
|
if (first === 'terror') game = act(game,'moritani',{type:'seatPosition',position:4});
|
|
game = act(game,'ecaz',{type:'seatPosition',position:first === 'ambassador' ? 4 : 5});
|
|
joinGame(game,newPlayer('guild','guild','guild'));
|
|
for (const p of game.players) game = act(game,p.id,{type:'ready'});
|
|
game = act(game,'ecaz',{type:'start'});
|
|
game = advanceBasicSemutaAmbassador(game, g => g.status === 'playing');
|
|
const draw = game.spiceDeck.findIndex(card => 'territory' in card && !card.discovery); assert.ok(draw >= 0);
|
|
game.spiceDeck.unshift(game.spiceDeck.splice(draw,1)[0]);
|
|
game = advanceBasicSemutaAmbassador(game, g => g.turn === 1 && g.phase === 5 &&
|
|
g.active === 'guild' && basicSemutaAmbassadorClean(g));
|
|
assert.equal(game.order.indexOf('ecaz') < game.order.indexOf('moritani'),first === 'ambassador');
|
|
return { beforeMarkers:structuredClone(game), entrant:'guild', destination:'habbanya_ridge_sietch', sector:17,
|
|
shipment:{type:'ship',territory:'habbanya_ridge_sietch',sector:17,amount:1,elite:0,allyPayment:0} as Action };
|
|
}
|
|
export function createFremenOverlapFixture(first: First, otherTerror = false, withBG = false) {
|
|
const f = withBG ? createWithBg(first) : createBasicNativeOverlapFixture('tleilaxu', first);
|
|
const game = structuredClone(f.beforeMarkers);
|
|
const ambassadors = game.ecazAmbassadors!;
|
|
const fremen = ambassadors.tokens.find(token => token.effect === 'fremen'); assert.ok(fremen);
|
|
if (fremen.zone === 'pool') {
|
|
const index = ambassadors.cohort.findIndex(id => ambassadors.tokens.some(token => token.id === id && token.zone === 'supply'));
|
|
assert.ok(index >= 0);
|
|
const old = ambassadors.tokens.find(token => token.id === ambassadors.cohort[index])!;
|
|
old.zone = 'pool'; fremen.zone = 'supply'; ambassadors.cohort[index] = fremen.id;
|
|
}
|
|
assert.equal(fremen.zone, 'supply');
|
|
validateAmbassadors(ambassadors);
|
|
game.ecazAmbassadors = placeAmbassador(ambassadors, fremen.id, {
|
|
turn: game.turn, availableSpice: player(game, 'ecaz').spice,
|
|
destination: { id: f.destination, stronghold: true, inStorm: false, allowed: true },
|
|
}).state;
|
|
const terror = game.moritaniTerror!.tokens.find(token => token.kind === 'robbery' && token.status === 'available');
|
|
assert.ok(terror);
|
|
game.moritaniTerror = placeTerror(game.moritaniTerror!, terror.id, f.destination, game.turn);
|
|
const options = fremenAmbassadorMovement(game, 'ecaz').sources.flatMap(source =>
|
|
source.destinations.filter(destination => source.territory !== destination.territory &&
|
|
source.sectors.some(sector => sector.forces > 0)).map(destination => ({ source, destination })));
|
|
const alternate = options.find(option => option.destination.territory !== f.destination &&
|
|
['arrakeen','carthag','sietch_tabr','habbanya_ridge_sietch','tuek_sietch'].includes(option.destination.territory));
|
|
assert.ok(alternate, 'A real Ecaz group can enter a second ordinary stronghold');
|
|
let childTerror: string | undefined;
|
|
if (otherTerror) {
|
|
const token = game.moritaniTerror.tokens.find(row => row.kind === (withBG ? 'sneakAttack' : 'extortion') && row.status === 'available');
|
|
assert.ok(token);
|
|
// Controlled additional original token placement. It preserves the native
|
|
// inventory and current placement-turn history; no force or spice is added.
|
|
token.status = 'placed'; token.location = alternate.destination.territory;
|
|
childTerror = token.id;
|
|
}
|
|
return { ...f, game, fremen: fremen.id, terror: terror.id, alternate, childTerror,
|
|
staging: 'Public native setup and shipment; selected Fremen cohort and original Terror placements are controlled. An optional second physical Terror placement is staged without changing force or spice.' };
|
|
}
|
|
|
|
export function triggerFremen(game: Game): Game {
|
|
const entry = game.pendingAmbassador; assert.ok(entry && entry.stage === 'offer');
|
|
const next = act(game, 'ecaz', { type: 'decision', event: entry.event, trigger: true, beneficiary: 'ecaz' });
|
|
assert.equal(next.pendingAmbassador?.effect, 'fremen');
|
|
assert.equal(next.pendingAmbassador?.stage, 'move');
|
|
return next;
|
|
}
|
|
|
|
export function relocate(game: Game, territory: string, sector: number): Game {
|
|
const sources = fremenAmbassadorMovement(game, 'ecaz').sources;
|
|
const source = sources.find(row => row.territory !== territory && row.sectors.some(s => s.forces > 0) &&
|
|
row.destinations.some(d => d.territory === territory && d.sector === sector));
|
|
assert.ok(source, `A native Ecaz group can reach ${territory}:${sector}`);
|
|
const position = source.sectors.find(row => row.forces > 0); assert.ok(position);
|
|
const action: Action = { type: 'decision', event: game.pendingAmbassador!.event,
|
|
forces: { [position.key]: 1 }, territory, sector };
|
|
return act(game, 'ecaz', action);
|
|
}
|