Resolve Basic Ambassador child arrivals during Terror
This commit is contained in:
1 parent
a45d81aa34
commit
fbdf11c789
15 files changed
+817
-16
No files matched your search
@@ -0,0 +1,45 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { placeAmbassador } from '../game/ecaz-ambassadors';
|
||||
import { guildAmbassadorShipments } from '../game/guild-ambassador';
|
||||
import { placeTerror } from '../game/moritani-terror';
|
||||
import { createBasicNativeOverlapFixture, player, type First } from './fixture-basic-native-overlap';
|
||||
|
||||
/** Real public Basic setup and native Tleilaxu's real turn from the shared
|
||||
* fixture. Controlled supplements select Guild as one original Ambassador in
|
||||
* the five-token cohort and legally place it at the original destination.
|
||||
* For distinct destinations an additional original Terror token is placed by
|
||||
* a controlled custody-preserving edit (not a claim of natural Mentat history).
|
||||
* No purse, forces, phase, player order, pending decision or receipt is staged. */
|
||||
export function createGuildOverlapChildFixture(first: First, same: boolean) {
|
||||
const f = createBasicNativeOverlapFixture('tleilaxu', first, 'native');
|
||||
const game = structuredClone(f.beforeMarkers);
|
||||
const state = game.ecazAmbassadors!;
|
||||
const guild = state.tokens.find(token => token.effect === 'guild'); assert.ok(guild);
|
||||
if (guild.zone === 'pool') {
|
||||
const replaced = state.tokens.find(token => token.zone === 'supply' && token.effect !== 'ecaz');
|
||||
assert.ok(replaced);
|
||||
const index = state.cohort.indexOf(replaced.id); assert.ok(index >= 0);
|
||||
state.cohort[index] = guild.id; replaced.zone = 'pool'; guild.zone = 'supply';
|
||||
}
|
||||
assert.equal(guild.zone, 'supply');
|
||||
game.ecazAmbassadors = placeAmbassador(state, guild.id, {
|
||||
turn:game.turn, availableSpice:player(game,'ecaz').spice,
|
||||
destination:{id:f.destination,stronghold:true,inStorm:false,allowed:true},
|
||||
}).state;
|
||||
const choices = guildAmbassadorShipments(game, 'ecaz').destinations;
|
||||
const separate = choices.find(choice => choice.territory !== f.destination &&
|
||||
['arrakeen','carthag','sietch_tabr','tueks_sietch'].includes(choice.territory));
|
||||
const target = same ? choices.find(choice => choice.territory === f.destination && choice.sector === f.sector) : separate;
|
||||
assert.ok(target, 'The free shipment must have an available selected stronghold destination');
|
||||
const terror = game.moritaniTerror!;
|
||||
const original = terror.tokens.find(token => token.kind === (same ? 'robbery' : 'sabotage')); assert.ok(original);
|
||||
game.moritaniTerror = placeTerror(terror, original.id, f.destination, game.turn);
|
||||
let child = original;
|
||||
if (!same) {
|
||||
const second = game.moritaniTerror.tokens.find(token => token.kind === 'robbery'); assert.ok(second);
|
||||
second.status = 'placed'; second.location = target.territory; child = second;
|
||||
}
|
||||
return { ...f, game, ambassador:guild.id, originalTerror:original.id,
|
||||
childTerror:child.id, childDestination:target.territory, childSector:target.sector,
|
||||
staging:'Public native setup and actual Tleilaxu shipment. Original Guild token/cohort and Terror placements are controlled; the distinct child Terror is a physical token placed without staging a Mentat turn.' };
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
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);
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import type { Action, Game } from '../game/engine';
|
||||
import { validateAmbassadors } from '../game/ecaz-ambassadors';
|
||||
import { fremenAmbassadorMovement } from '../game/fremen-ambassador';
|
||||
import { actSemutaArrivalOverlap as act, createSemutaArrivalOverlapFixture,
|
||||
inventory, player } from './fixture-semuta-arrival-overlap';
|
||||
|
||||
export { act, inventory, player };
|
||||
|
||||
/** Native Basic public setup, original paid Semuta auction and a real turn-one
|
||||
* Terror placement come from the shared fixture. At the clean turn-two source,
|
||||
* swap the placed original CHOAM Ambassador with the original Fremen Ambassador
|
||||
* already in the same cohort. The physical tokens and paid location remain;
|
||||
* this supplement is controlled, not a played Fremen placement. The inherited
|
||||
* paid Semuta is also transferred to Richese as the shared fixture documents. */
|
||||
export function createSemutaOverlapChildFixture() {
|
||||
const f = createSemutaArrivalOverlapFixture({ effect: 'choam', first: 'ambassador', terror: 'sabotage' });
|
||||
const game = structuredClone(f.game), ambassadors = game.ecazAmbassadors!;
|
||||
const existing = ambassadors.tokens.find(token => token.id === f.token);
|
||||
const fremen = ambassadors.tokens.find(token => token.effect === 'fremen');
|
||||
assert.ok(existing && fremen);
|
||||
assert.equal(existing.zone, 'placed');
|
||||
assert.equal(existing.location, f.territory);
|
||||
assert.equal(fremen.zone, 'supply');
|
||||
assert.ok(ambassadors.cohort.includes(fremen.id));
|
||||
existing.zone = 'supply'; existing.location = null;
|
||||
fremen.zone = 'placed'; fremen.location = f.territory;
|
||||
validateAmbassadors(ambassadors);
|
||||
assert.ok(player(game, f.owner).hand.length > 0, 'The physical child entrant can suffer Sabotage');
|
||||
assert.ok(player(game, f.claimant).hand.some(card => card.id === f.semuta));
|
||||
assert.deepEqual(inventory(game), f.originalInventory);
|
||||
return { ...f, game, fremenToken: fremen.id,
|
||||
staging: [...f.staging, 'At the clean turn-two source, swap the placed original CHOAM Ambassador with the original Fremen cohort token. Both retain native custody; no force, wallet, turn or response is installed.'] };
|
||||
}
|
||||
export type SemutaOverlapChildFixture = ReturnType<typeof createSemutaOverlapChildFixture>;
|
||||
|
||||
/** The real paid Guild arrival opens both original opportunities; the Fremen
|
||||
* Ambassador then moves one existing Ecaz force into that same stronghold. */
|
||||
export function openSemutaOverlapChild(f: SemutaOverlapChildFixture): Game {
|
||||
let game = act(f.game, f.arrivalStep.actor, f.arrivalStep.action);
|
||||
assert.equal(game.pendingArrivalOverlap?.first, 'ambassador');
|
||||
const entry = game.pendingAmbassador; assert.ok(entry && entry.stage === 'offer');
|
||||
assert.equal(entry.token, f.fremenToken);
|
||||
game = act(game, f.owner, { type: 'decision', event: entry.event, trigger: true, beneficiary: f.owner });
|
||||
assert.equal(game.pendingAmbassador?.effect, 'fremen');
|
||||
assert.equal(game.pendingAmbassador?.stage, 'move');
|
||||
const sources = fremenAmbassadorMovement(game, f.owner).sources;
|
||||
const source = sources.find(row => row.territory !== f.territory && row.sectors.some(sector => sector.forces > 0) &&
|
||||
row.destinations.some(destination => destination.territory === f.territory && destination.sector === f.sector));
|
||||
assert.ok(source);
|
||||
const sector = source.sectors.find(row => row.forces > 0); assert.ok(sector);
|
||||
const action: Action = { type: 'decision', event: entry.event, forces: { [sector.key]: 1 },
|
||||
territory: f.territory, sector: f.sector };
|
||||
game = act(game, f.owner, action);
|
||||
assert.equal(game.pendingTerrorEntry?.cause, 'ambassador');
|
||||
assert.equal(game.pendingTerrorEntry?.entrant, f.owner);
|
||||
assert.equal(game.pendingTerrorEntry?.token, f.terrorToken);
|
||||
return game;
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { viewGame } from '../game/engine';
|
||||
import { act, player, type First } from './fixture-basic-native-overlap';
|
||||
import { createGuildOverlapChildFixture } from './fixture-guild-overlap-child';
|
||||
|
||||
for (const first of ['ambassador','terror'] as const satisfies readonly First[])
|
||||
for (const same of [true,false])
|
||||
for (const reveal of [false,true]) {
|
||||
void test(`Basic Guild child ${same?'same':'different'} stronghold, ${first} first, child ${reveal?'reveals':'declines'}`,()=>{
|
||||
const f=createGuildOverlapChildFixture(first,same), source=f.game;
|
||||
const originalSpice=player(source,f.entrant).spice;
|
||||
const beneficiary=player(source,f.owner);
|
||||
const reserve=beneficiary.reserves,spice=beneficiary.spice;
|
||||
let game=act(source,f.entrant,f.shipment);
|
||||
assert.equal(game.pendingArrivalOverlap?.first,first);
|
||||
const overlapEvent=game.pendingArrivalOverlap!.event;
|
||||
assert.equal(player(game,f.entrant).spice,originalSpice-1);
|
||||
assert.equal(player(game,f.entrant).forces[`${f.destination}:${f.sector}`],1);
|
||||
if(first==='terror') {
|
||||
assert.equal(game.pendingTerrorEntry?.entrant,f.entrant);
|
||||
game=act(game,f.moritani,{type:'decision',decline:true});
|
||||
}
|
||||
const entry=game.pendingAmbassador; assert.ok(entry);
|
||||
assert.equal(entry.token,f.ambassador);
|
||||
const projection=viewGame(game,f.owner).ambassadorEntry; assert.ok(projection);
|
||||
game=act(game,f.owner,{type:'decision',event:entry.event,trigger:true,beneficiary:f.owner});
|
||||
assert.equal(game.pendingAmbassador?.stage,'ship');
|
||||
assert.equal(game.decision?.kind,'ecazAmbassador');
|
||||
game=act(game,f.owner,{type:'decision',event:entry.event,amount:1,elite:0,
|
||||
territory:f.childDestination,sector:f.childSector});
|
||||
assert.equal(player(game,f.owner).reserves,reserve-1);
|
||||
assert.equal(player(game,f.owner).forces[`${f.childDestination}:${f.childSector}`],1);
|
||||
assert.equal(player(game,f.owner).spice,spice,'The independent Guild shipment is free');
|
||||
assert.equal(game.pendingTerrorEntry?.entrant,f.owner);
|
||||
assert.equal(game.pendingTerrorEntry?.token,f.childTerror);
|
||||
assert.equal(game.pendingTerrorEntry?.resume,'ambassador');
|
||||
assert.equal(game.pendingArrivalOverlap?.event,overlapEvent);
|
||||
assert.equal(game.pendingArrivalOverlap?.ambassadorEvent,entry.event);
|
||||
if(reveal) {
|
||||
game=act(game,f.moritani,{type:'decision',reveal:true});
|
||||
assert.equal(game.pendingTerrorEntry?.stage,'robbery');
|
||||
game=act(game,f.moritani,{type:'decision',choice:'spice'});
|
||||
assert.equal(player(game,f.owner).spice,Math.floor(spice/2));
|
||||
assert.equal(game.moritaniTerror?.tokens.find(token=>token.id===f.childTerror)?.status,'removed');
|
||||
} else game=act(game,f.moritani,{type:'decision',decline:true});
|
||||
if(first==='ambassador' && (!same || !reveal)) {
|
||||
assert.equal(game.pendingTerrorEntry?.entrant,f.entrant,'The original live token keeps its own prior opportunity');
|
||||
assert.equal(game.pendingTerrorEntry?.token,f.originalTerror);
|
||||
game=act(game,f.moritani,{type:'decision',decline:true});
|
||||
}
|
||||
assert.equal(game.pendingArrivalOverlap,null);
|
||||
assert.equal(game.pendingAmbassador,null);
|
||||
assert.equal(game.pendingTerrorEntry,null);
|
||||
assert.equal(game.ecazAmbassadors?.tokens.find(token=>token.id===f.ambassador)?.zone,'used');
|
||||
assert.equal(player(game,f.entrant).forces[`${f.destination}:${f.sector}`],1);
|
||||
assert.equal(player(game,f.entrant).spice,originalSpice-1);
|
||||
assert.equal(player(game,f.owner).forces[`${f.childDestination}:${f.childSector}`],1);
|
||||
assert.equal(player(game,f.owner).reserves,reserve-1);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { applyAction, createGame, newPlayer, viewGame, type Game } from '../game/engine';
|
||||
import { baseDeck } from '../game/cards';
|
||||
import { createAmbassadors, placeAmbassador } from '../game/ecaz-ambassadors';
|
||||
import { createTerrorState, placeTerror } from '../game/moritani-terror';
|
||||
|
||||
/** Controlled Advanced Ecaz/Moritani table with original physical markers.
|
||||
* It isolates the pre-existing Advanced overlap gate, not a played setup or
|
||||
* an assurance of every optional-module combination. */
|
||||
function advancedTable(effect: 'fremen' | 'guild'): Game {
|
||||
const game = createGame('OVERLAPADVANCEDGATE', newPlayer('ec','Ecaz','ecaz'), true, ['ecaz']);
|
||||
game.players.push(newPlayer('m','Moritani','moritani'),newPlayer('in','Entrant','harkonnen'));
|
||||
Object.assign(game, {status:'playing',phase:5,turn:3,storm:18,active:'in',
|
||||
order:['ec','m','in'],movementRemaining:['ec','m','in'],deck:baseDeck()});
|
||||
for (const player of game.players)
|
||||
Object.assign(player,{hand:[],spice:20,forces:{},reserves:20,shipped:false,moved:0});
|
||||
const ambassadors = createAmbassadors(() => 0.2);
|
||||
const selected = ambassadors.tokens.find(token=>token.effect===effect); assert.ok(selected);
|
||||
ambassadors.cohort = [selected.id,...ambassadors.tokens.filter(token=>token.effect!=='ecaz' &&
|
||||
token.id!==selected.id).slice(0,4).map(token=>token.id)];
|
||||
for (const token of ambassadors.tokens) {
|
||||
token.zone = token.effect==='ecaz' || ambassadors.cohort.includes(token.id) ? 'supply' : 'pool';
|
||||
token.location = null;
|
||||
}
|
||||
game.ecazAmbassadors = placeAmbassador(ambassadors,selected.id,{turn:2,availableSpice:20,
|
||||
destination:{id:'arrakeen',stronghold:true,inStorm:false,allowed:true}}).state;
|
||||
const terror = createTerrorState(()=>0.2), token = terror.tokens.find(row=>row.kind==='robbery'); assert.ok(token);
|
||||
game.moritaniTerror = placeTerror(terror,token.id,'arrakeen',2);
|
||||
return game;
|
||||
}
|
||||
|
||||
for (const effect of ['fremen','guild'] as const) {
|
||||
void test(`Advanced ${effect} Ambassador child stays gated during overlap`,()=>{
|
||||
const game = applyAction(advancedTable(effect),'in',{type:'ship',territory:'arrakeen',sector:10,amount:2});
|
||||
const entry = game.pendingAmbassador; assert.ok(entry);
|
||||
assert.equal(game.pendingArrivalOverlap?.first,'ambassador');
|
||||
const blocked = viewGame(game,'ec').ambassadorEntry?.beneficiaries[0].blocked;
|
||||
assert.match(blocked ?? '',/bare Basic faction preview/);
|
||||
const before = structuredClone(game);
|
||||
assert.throws(()=>applyAction(game,'ec',{type:'decision',event:entry.event,trigger:true,beneficiary:'ec'}),
|
||||
/bare Basic faction preview/);
|
||||
assert.deepEqual(game,before);
|
||||
let settled = applyAction(game,'ec',{type:'decision',event:entry.event,decline:true});
|
||||
assert.equal(settled.pendingTerrorEntry?.entrant,'in');
|
||||
settled = applyAction(settled,'m',{type:'decision',decline:true});
|
||||
assert.equal(settled.pendingArrivalOverlap,null);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { registerHooks } from 'node:module';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { createElement } from 'react';
|
||||
import { renderToStaticMarkup } from 'react-dom/server';
|
||||
import { viewGame, type GameView } from '../game/engine';
|
||||
import { act, createFremenOverlapFixture, triggerFremen } from './fixture-overlap-fremen-native';
|
||||
import { createGuildOverlapChildFixture } from './fixture-guild-overlap-child';
|
||||
|
||||
const aliases = registerHooks({
|
||||
resolve(specifier, context, next) {
|
||||
return next(specifier === 'next/image' ? 'vinext/shims/image' : specifier === 'next/link' ? 'vinext/shims/link' : specifier, context);
|
||||
},
|
||||
load(url, context, next) {
|
||||
if (!url.endsWith('.module.css')) return next(url, context);
|
||||
const css = readFileSync(new URL(url), 'utf8');
|
||||
const classes = Object.fromEntries([...css.matchAll(/\.([a-zA-Z_][\w-]*)/g)].map(match => [match[1],match[1]]));
|
||||
return {format:'module',source:`export default ${JSON.stringify(classes)}`,shortCircuit:true};
|
||||
},
|
||||
});
|
||||
const { EcazEntry } = await import('../components/ecaz-entry');
|
||||
aliases.deregister();
|
||||
|
||||
const htmlFor = (game: GameView, busy = false) => renderToStaticMarkup(createElement(EcazEntry,
|
||||
{ game, busy, act() { assert.fail('Rendering cannot submit an action'); } }));
|
||||
function triggerButton(html: string) {
|
||||
const buttons = [...html.matchAll(/<button\b([^>]*)>([\s\S]*?)<\/button>/g)];
|
||||
const match = buttons.find(([, , text]) => text.replace(/<[^>]*>/g, '').replace(/\s+/g, ' ').includes('Trigger for yourself'));
|
||||
assert.ok(match, 'The current Ecaz player sees its projected trigger');
|
||||
return match[1];
|
||||
}
|
||||
|
||||
void test('Fremen overlap exposes the enabled real Ambassador trigger and relocation controls', () => {
|
||||
const f = createFremenOverlapFixture('ambassador');
|
||||
const offered = act(f.game, f.entrant, f.shipment);
|
||||
const view = viewGame(offered, 'ecaz');
|
||||
assert.equal(view.ambassadorEntry?.beneficiaries[0].blocked, null);
|
||||
const html = htmlFor(view);
|
||||
assert.match(html, /Fremen Ambassador/);
|
||||
assert.doesNotMatch(triggerButton(html), /\bdisabled(?:=|\s|>)/);
|
||||
assert.match(triggerButton(htmlFor(view, true)), /\bdisabled/);
|
||||
const selected = triggerFremen(offered);
|
||||
const movement = viewGame(selected, 'ecaz');
|
||||
assert.ok(movement.ambassadorEntry?.movement?.sources.length);
|
||||
assert.match(htmlFor(movement), /aria-label="Ambassador force relocation"/);
|
||||
assert.match(htmlFor(movement), /Relocate selected forces/);
|
||||
});
|
||||
|
||||
void test('Guild overlap exposes the enabled real Ambassador trigger and shipment controls', () => {
|
||||
const f = createGuildOverlapChildFixture('ambassador', true);
|
||||
const offered = act(f.game, f.entrant, f.shipment);
|
||||
const view = viewGame(offered, 'ecaz');
|
||||
assert.equal(view.ambassadorEntry?.beneficiaries[0].blocked, null);
|
||||
const html = htmlFor(view);
|
||||
assert.match(html, /Guild Ambassador/);
|
||||
assert.doesNotMatch(triggerButton(html), /\bdisabled(?:=|\s|>)/);
|
||||
assert.match(triggerButton(htmlFor(view, true)), /\bdisabled/);
|
||||
const entry = offered.pendingAmbassador; assert.ok(entry);
|
||||
const selected = act(offered, 'ecaz', { type:'decision', event:entry.event, trigger:true, beneficiary:'ecaz' });
|
||||
const shipment = viewGame(selected, 'ecaz');
|
||||
assert.ok(shipment.ambassadorEntry?.shipment?.destinations.length);
|
||||
const selectedHtml = htmlFor(shipment);
|
||||
assert.match(selectedHtml, /aria-label="Ambassador reserve shipment"/);
|
||||
assert.match(selectedHtml, /Ship selected forces for free/);
|
||||
});
|
||||
@@ -0,0 +1,163 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { viewGame, type Game } from '../game/engine';
|
||||
import { botActions } from '../game/bots';
|
||||
import { DIFFICULTIES } from '../game/bot-profiles';
|
||||
import { act, player, createFremenOverlapFixture, triggerFremen, relocate } from './fixture-overlap-fremen-native';
|
||||
import type { First } from './fixture-basic-native-overlap';
|
||||
|
||||
function entered(first: First, otherTerror = false, withBG = false) {
|
||||
const f = createFremenOverlapFixture(first, otherTerror, withBG);
|
||||
const spice = player(f.game, f.entrant).spice;
|
||||
let game = act(f.game, f.entrant, f.shipment);
|
||||
assert.equal(player(game, f.entrant).spice, spice - 1);
|
||||
// A real Bene Gesserit seat may accompany the original shipment before
|
||||
// the queued Ambassador/Terror entry is opened.
|
||||
if (game.decision?.kind === 'advisor')
|
||||
game = act(game, 'beneGesserit', { type: 'decision', accept: false });
|
||||
assert.equal(game.pendingArrivalOverlap?.first, first);
|
||||
if (first === 'terror') {
|
||||
assert.equal(game.pendingTerrorEntry?.entrant, f.entrant);
|
||||
game = act(game, 'moritani', { type: 'decision', decline: true });
|
||||
}
|
||||
assert.equal(game.pendingAmbassador?.token, f.fremen);
|
||||
assert.equal(viewGame(game, 'ecaz').ambassadorEntry?.beneficiaries[0].blocked, null);
|
||||
game = triggerFremen(game);
|
||||
return { f, game, spice };
|
||||
}
|
||||
function decline(game: Game) { return act(game, 'moritani', { type: 'decision', decline: true }); }
|
||||
function rob(game: Game) {
|
||||
game = act(game, 'moritani', { type: 'decision', reveal: true });
|
||||
assert.equal(game.pendingTerrorEntry?.stage, 'robbery');
|
||||
return act(game, 'moritani', { type: 'decision', choice: 'spice' });
|
||||
}
|
||||
|
||||
void test('Ambassador-first child decline preserves the original Terror opportunity', () => {
|
||||
const { f, game: open, spice } = entered('ambassador');
|
||||
let game = relocate(open, f.destination, f.sector);
|
||||
assert.equal(game.pendingTerrorEntry?.cause, 'ambassador');
|
||||
assert.equal(game.pendingTerrorEntry?.entrant, 'ecaz');
|
||||
assert.equal(game.pendingTerrorEntry?.token, f.terror);
|
||||
game = decline(game);
|
||||
assert.equal(game.pendingArrivalOverlap?.stage, 'second');
|
||||
assert.equal(game.pendingTerrorEntry?.cause, 'shipment');
|
||||
assert.equal(game.pendingTerrorEntry?.entrant, f.entrant);
|
||||
assert.equal(game.pendingTerrorEntry?.token, f.terror);
|
||||
game = decline(game);
|
||||
assert.equal(game.pendingArrivalOverlap, null);
|
||||
assert.equal(game.pendingAmbassador, null);
|
||||
assert.equal(game.pendingTerrorEntry, null);
|
||||
assert.equal(player(game, f.entrant).spice, spice - 1);
|
||||
assert.equal(player(game, f.entrant).forces[`${f.destination}:${f.sector}`], 1);
|
||||
assert.equal(player(game, 'ecaz').forces[`${f.destination}:${f.sector}`], 1);
|
||||
assert.equal(game.moritaniTerror!.tokens.find(t => t.id === f.terror)?.status, 'placed');
|
||||
});
|
||||
|
||||
void test('Ambassador-first child reveal consumes the shared token without replaying the original entry', () => {
|
||||
const { f, game: open, spice } = entered('ambassador');
|
||||
let game = relocate(open, f.destination, f.sector);
|
||||
assert.equal(game.pendingTerrorEntry?.entrant, 'ecaz');
|
||||
game = rob(game);
|
||||
assert.equal(game.moritaniTerror!.tokens.find(t => t.id === f.terror)?.status, 'removed');
|
||||
assert.equal(game.pendingArrivalOverlap, null);
|
||||
assert.equal(game.pendingAmbassador, null);
|
||||
assert.equal(game.pendingTerrorEntry, null);
|
||||
assert.equal(player(game, f.entrant).spice, spice - 1);
|
||||
assert.equal(player(game, f.entrant).forces[`${f.destination}:${f.sector}`], 1);
|
||||
assert.equal(player(game, 'ecaz').forces[`${f.destination}:${f.sector}`], 1);
|
||||
});
|
||||
|
||||
void test('Terror-first original decline keeps the token available for a Fremen child entry', () => {
|
||||
const { f, game: open } = entered('terror');
|
||||
let game = relocate(open, f.destination, f.sector);
|
||||
assert.equal(game.pendingTerrorEntry?.cause, 'ambassador');
|
||||
assert.equal(game.pendingTerrorEntry?.entrant, 'ecaz');
|
||||
assert.equal(game.pendingTerrorEntry?.token, f.terror);
|
||||
game = rob(game);
|
||||
assert.equal(game.moritaniTerror!.tokens.find(t => t.id === f.terror)?.status, 'removed');
|
||||
assert.equal(game.pendingArrivalOverlap, null);
|
||||
assert.equal(game.pendingAmbassador, null);
|
||||
assert.equal(game.pendingTerrorEntry, null);
|
||||
assert.equal(player(game, f.entrant).forces[`${f.destination}:${f.sector}`], 1);
|
||||
assert.equal(player(game, 'ecaz').forces[`${f.destination}:${f.sector}`], 1);
|
||||
});
|
||||
|
||||
void test('a separate child Terror resolves before the still-eligible original Terror', () => {
|
||||
const { f, game: open } = entered('ambassador', true);
|
||||
const at = f.alternate.destination;
|
||||
let game = relocate(open, at.territory, at.sector);
|
||||
assert.equal(game.pendingTerrorEntry?.entrant, 'ecaz');
|
||||
assert.equal(game.pendingTerrorEntry?.token, f.childTerror);
|
||||
game = act(game, 'moritani', { type: 'decision', reveal: true });
|
||||
assert.equal(game.moritaniTerror!.tokens.find(t => t.id === f.childTerror)?.status, 'extortion');
|
||||
assert.equal(game.pendingTerrorEntry?.entrant, f.entrant);
|
||||
assert.equal(game.pendingTerrorEntry?.token, f.terror);
|
||||
game = decline(game);
|
||||
assert.equal(game.pendingArrivalOverlap, null);
|
||||
assert.equal(player(game, 'ecaz').forces[`${at.territory}:${at.sector}`], 1);
|
||||
assert.equal(player(game, f.entrant).forces[`${f.destination}:${f.sector}`], 1);
|
||||
});
|
||||
|
||||
void test('Terror-first original reveal leaves no token for the subsequent same-stronghold child', () => {
|
||||
const f = createFremenOverlapFixture('terror');
|
||||
let game = act(f.game, f.entrant, f.shipment);
|
||||
assert.equal(game.pendingTerrorEntry?.entrant, f.entrant);
|
||||
game = rob(game);
|
||||
assert.equal(game.moritaniTerror!.tokens.find(t => t.id === f.terror)?.status, 'removed');
|
||||
assert.equal(game.pendingAmbassador?.token, f.fremen);
|
||||
game = triggerFremen(game);
|
||||
game = relocate(game, f.destination, f.sector);
|
||||
assert.equal(game.pendingArrivalOverlap, null);
|
||||
assert.equal(game.pendingAmbassador, null);
|
||||
assert.equal(game.pendingTerrorEntry, null);
|
||||
assert.equal(player(game, f.entrant).forces[`${f.destination}:${f.sector}`], 1);
|
||||
assert.equal(player(game, 'ecaz').forces[`${f.destination}:${f.sector}`], 1);
|
||||
});
|
||||
|
||||
void test('a child Sneak Attack queues its Bene Gesserit accompaniment after the original Terror', () => {
|
||||
const { f, game: open } = entered('ambassador', true, true);
|
||||
const at = f.alternate.destination;
|
||||
const moritaniReserves = player(open, 'moritani').reserves;
|
||||
let game = relocate(open, at.territory, at.sector);
|
||||
assert.equal(game.pendingTerrorEntry?.token, f.childTerror);
|
||||
game = act(game, 'moritani', { type: 'decision', reveal: true });
|
||||
assert.equal(game.pendingTerrorEntry?.stage, 'sneakAttack');
|
||||
game = act(game, 'moritani', { type: 'decision', amount: 1 });
|
||||
assert.equal(player(game, 'moritani').reserves, moritaniReserves - 1);
|
||||
assert.equal(player(game, 'moritani').forces[`${at.territory}:${at.sector}`], 1);
|
||||
assert.equal(game.pendingArrivalOverlap?.stage, 'second');
|
||||
assert.equal(game.pendingTerrorEntry?.token, f.terror);
|
||||
assert.equal(game.pendingTerrorEntry?.entrant, f.entrant);
|
||||
assert.equal(game.decision?.kind, 'moritaniTerror', 'The original Terror controls cannot be overwritten');
|
||||
game = decline(game);
|
||||
assert.equal(game.pendingArrivalOverlap, null);
|
||||
assert.equal(game.decision?.kind, 'advisor');
|
||||
assert.equal(game.decision?.player, 'beneGesserit');
|
||||
game = act(game, 'beneGesserit', { type: 'decision', accept: false });
|
||||
assert.equal(game.pendingArrivalOverlap, null);
|
||||
assert.equal(game.pendingTerrorEntry, null);
|
||||
assert.equal(player(game, f.entrant).forces[`${f.destination}:${f.sector}`], 1);
|
||||
assert.equal(player(game, 'moritani').forces[`${at.territory}:${at.sector}`], 1);
|
||||
});
|
||||
|
||||
void test('all minimal bot profiles choose a legal Fremen Ambassador action in the overlap', () => {
|
||||
const f = createFremenOverlapFixture('ambassador');
|
||||
const offered = act(f.game, f.entrant, f.shipment);
|
||||
assert.equal(offered.pendingAmbassador?.stage, 'offer');
|
||||
for (const difficulty of DIFFICULTIES) {
|
||||
const view = viewGame(offered, 'ecaz');
|
||||
view.players.find(p => p.id === 'ecaz')!.bot = difficulty;
|
||||
const action = botActions(view)[0]; assert.ok(action, difficulty);
|
||||
let game = act(offered, 'ecaz', action);
|
||||
if (game.pendingAmbassador?.stage === 'move') {
|
||||
game = relocate(game, f.destination, f.sector);
|
||||
assert.equal(game.pendingTerrorEntry?.cause, 'ambassador');
|
||||
game = decline(game);
|
||||
}
|
||||
assert.equal(game.pendingArrivalOverlap?.stage, 'second');
|
||||
assert.equal(game.pendingTerrorEntry?.entrant, f.entrant);
|
||||
game = decline(game);
|
||||
assert.equal(game.pendingArrivalOverlap, null);
|
||||
assert.equal(player(game, f.entrant).forces[`${f.destination}:${f.sector}`], 1);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { viewGame } from '../game/engine';
|
||||
import { claimOverlapSemuta, passCurrentSemuta } from './fixture-semuta-arrival-overlap';
|
||||
import { act, inventory, player, createSemutaOverlapChildFixture, openSemutaOverlapChild } from './fixture-semuta-overlap-child';
|
||||
|
||||
for (const claim of [false, true]) {
|
||||
void test(`Richese may ${claim ? 'claim' : 'pass'} a child Sabotage discard before the original overlap continues`, () => {
|
||||
const f = createSemutaOverlapChildFixture();
|
||||
let game = openSemutaOverlapChild(f);
|
||||
const before = structuredClone(game), hand = player(game, f.owner).hand.map(card => card.id);
|
||||
assert.equal(game.pendingArrivalOverlap?.stage, 'first');
|
||||
assert.equal(game.pendingTerrorEntry?.entrant, f.owner);
|
||||
game = act(game, f.moritani, { type: 'decision', reveal: true });
|
||||
const pending = game.pendingTreacheryDiscard; assert.ok(pending);
|
||||
assert.equal(pending.reaction?.stage, 'offer');
|
||||
assert.equal(pending.continuation.kind, 'terrorDiscard');
|
||||
assert.equal(pending.batch.entries.length, 1);
|
||||
const entry = pending.batch.entries[0], card = entry.card.id, event = pending.batch.event;
|
||||
assert.equal(entry.discardedBy, f.owner);
|
||||
assert.equal(entry.publicFace, false);
|
||||
assert.ok(hand.includes(card), 'Sabotage discards an original held physical card');
|
||||
assert.equal(player(game, f.owner).hand.length, hand.length - 1);
|
||||
assert.ok(game.pendingArrivalOverlap, 'The original overlap remains suspended');
|
||||
assert.deepEqual(viewGame(game, f.claimant).semutaReaction?.candidates, []);
|
||||
assert.equal(player(game, f.entrant).forces[f.location], 1);
|
||||
assert.equal(player(game, f.entrant).spice, player(before, f.entrant).spice);
|
||||
assert.deepEqual(inventory(game), f.originalInventory);
|
||||
|
||||
game = claim ? claimOverlapSemuta(game, f.claimant, card) : passCurrentSemuta(game);
|
||||
assert.notEqual(game.pendingTreacheryDiscard?.batch.event, event);
|
||||
assert.equal(player(game, f.claimant).hand.some(held => held.id === card), claim);
|
||||
assert.equal(player(game, f.claimant).hand.some(held => held.id === f.semuta), !claim);
|
||||
assert.equal(game.discard.some(discarded => discarded.id === card), !claim);
|
||||
assert.equal(game.discard.some(discarded => discarded.id === f.semuta), claim);
|
||||
assert.equal(game.moritaniTerror?.tokens.find(token => token.id === f.terrorToken)?.status, 'removed');
|
||||
if (game.pendingTerrorEntry?.stage === 'gift')
|
||||
game = act(game, f.moritani, { type: 'decision', decline: true });
|
||||
assert.equal(game.pendingArrivalOverlap, null, 'A consumed shared Terror is not offered again');
|
||||
assert.equal(game.pendingAmbassador, null);
|
||||
assert.equal(game.pendingTerrorEntry, null);
|
||||
assert.equal(player(game, f.entrant).forces[f.location], 1);
|
||||
assert.equal(player(game, f.owner).forces[f.location], 1);
|
||||
assert.equal(player(game, f.entrant).spice, player(before, f.entrant).spice);
|
||||
assert.deepEqual(inventory(game), f.originalInventory);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { act, player, createFremenOverlapFixture } from './fixture-overlap-fremen-native';
|
||||
|
||||
void test('Terror-first Sneak waits for its own BG choice and then earns a retained Ambassador', () => {
|
||||
const f = createFremenOverlapFixture('terror', false, true);
|
||||
const source = structuredClone(f.game);
|
||||
// Controlled physical-token selection in the original public fixture: return
|
||||
// its staged Robbery token to supply and put the existing Sneak token in the
|
||||
// same stronghold. No force, money, pending event or turn is installed.
|
||||
const robbery = source.moritaniTerror!.tokens.find(token => token.id === f.terror);
|
||||
const sneak = source.moritaniTerror!.tokens.find(token => token.kind === 'sneakAttack');
|
||||
assert.ok(robbery && sneak && sneak.status === 'available');
|
||||
robbery.status = 'available'; robbery.location = null;
|
||||
sneak.status = 'placed'; sneak.location = f.destination;
|
||||
const originalSpice = player(source, f.entrant).spice;
|
||||
const reserves = player(source, 'moritani').reserves;
|
||||
let game = act(source, f.entrant, f.shipment);
|
||||
// The first accompaniment belongs to the real original shipment.
|
||||
if (game.decision?.kind === 'advisor')
|
||||
game = act(game, 'beneGesserit', { type: 'decision', accept: false });
|
||||
assert.equal(game.pendingArrivalOverlap?.first, 'terror');
|
||||
assert.equal(game.pendingTerrorEntry?.token, sneak.id);
|
||||
game = act(game, 'moritani', { type: 'decision', reveal: true });
|
||||
assert.equal(game.pendingTerrorEntry?.stage, 'sneakAttack');
|
||||
game = act(game, 'moritani', { type: 'decision', amount: 1 });
|
||||
assert.equal(player(game, 'moritani').reserves, reserves - 1);
|
||||
assert.equal(player(game, 'moritani').forces[`${f.destination}:${f.sector}`], 1);
|
||||
assert.equal(game.pendingArrivalOverlap?.stage, 'second');
|
||||
const original = game.pendingAmbassador; assert.ok(original);
|
||||
assert.equal(original.entrant, f.entrant);
|
||||
game = act(game, 'ecaz', { type: 'decision', event: original.event, decline: true });
|
||||
assert.equal(game.pendingArrivalOverlap, null);
|
||||
// Sneak is its own off-planet arrival: its BG opportunity precedes any
|
||||
// additional Ambassador created by that same committed group.
|
||||
assert.equal(game.decision?.kind, 'advisor');
|
||||
assert.equal(game.decision?.player, 'beneGesserit');
|
||||
game = act(game, 'beneGesserit', { type: 'decision', accept: false });
|
||||
const later = game.pendingAmbassador; assert.ok(later, 'The same retained Ambassador also responds to the Sneak group');
|
||||
assert.equal(later.token, f.fremen);
|
||||
assert.equal(later.entrant, 'moritani');
|
||||
assert.notEqual(later.event, original.event);
|
||||
game = act(game, 'ecaz', { type: 'decision', event: later.event, decline: true });
|
||||
assert.equal(game.pendingAmbassador, null);
|
||||
assert.equal(game.pendingTerrorEntry, null);
|
||||
assert.equal(game.pendingArrivalOverlap, null);
|
||||
assert.equal(game.pendingArrivalReaction ?? null, null);
|
||||
assert.equal(game.ecazAmbassadors?.tokens.find(token => token.id === f.fremen)?.zone, 'placed');
|
||||
assert.equal(player(game, f.entrant).spice, originalSpice - 1);
|
||||
assert.equal(player(game, f.entrant).forces[`${f.destination}:${f.sector}`], 1);
|
||||
assert.equal(player(game, 'moritani').forces[`${f.destination}:${f.sector}`], 1);
|
||||
});
|
||||
|
||||
void test('a direct Sneak arrival receives BG timing and triggers an eligible Ambassador', () => {
|
||||
const f = createFremenOverlapFixture('terror', false, true);
|
||||
const source = structuredClone(f.game);
|
||||
const inventory = source.ecazAmbassadors!;
|
||||
const old = inventory.tokens.find(token => token.id === f.fremen);
|
||||
const guild = inventory.tokens.find(token => token.effect === 'guild');
|
||||
assert.ok(old && guild && old.zone === 'placed');
|
||||
// Controlled custody-preserving token selection: use the original Guild
|
||||
// Ambassador in the same cohort and location. The original Guild entrant
|
||||
// cannot trigger its own faction's token, while the later Moritani entrant can.
|
||||
old.zone = 'supply'; old.location = null;
|
||||
if (guild.zone === 'pool') {
|
||||
const index = inventory.cohort.indexOf(old.id); assert.ok(index >= 0);
|
||||
inventory.cohort[index] = guild.id;
|
||||
old.zone = 'pool'; guild.zone = 'supply';
|
||||
}
|
||||
assert.equal(guild.zone, 'supply');
|
||||
guild.zone = 'placed'; guild.location = f.destination;
|
||||
const robbery = source.moritaniTerror!.tokens.find(token => token.id === f.terror);
|
||||
const sneak = source.moritaniTerror!.tokens.find(token => token.kind === 'sneakAttack');
|
||||
assert.ok(robbery && sneak && sneak.status === 'available');
|
||||
robbery.status = 'available'; robbery.location = null;
|
||||
sneak.status = 'placed'; sneak.location = f.destination;
|
||||
const originalSpice = player(source, f.entrant).spice;
|
||||
const reserves = player(source, 'moritani').reserves;
|
||||
let game = act(source, f.entrant, f.shipment);
|
||||
if (game.decision?.kind === 'advisor')
|
||||
game = act(game, 'beneGesserit', { type: 'decision', accept: false });
|
||||
assert.ok(!game.pendingArrivalOverlap, 'The Guild entrant does not trigger the Guild Ambassador');
|
||||
assert.equal(game.pendingTerrorEntry?.token, sneak.id);
|
||||
game = act(game, 'moritani', { type: 'decision', reveal: true });
|
||||
assert.equal(game.pendingTerrorEntry?.stage, 'sneakAttack');
|
||||
game = act(game, 'moritani', { type: 'decision', amount: 1 });
|
||||
assert.equal(player(game, 'moritani').reserves, reserves - 1);
|
||||
assert.equal(game.decision?.kind, 'advisor');
|
||||
assert.equal(game.decision?.player, 'beneGesserit');
|
||||
game = act(game, 'beneGesserit', { type: 'decision', accept: false });
|
||||
const later = game.pendingAmbassador; assert.ok(later);
|
||||
assert.equal(later.token, guild.id);
|
||||
assert.equal(later.entrant, 'moritani');
|
||||
game = act(game, 'ecaz', { type: 'decision', event: later.event, decline: true });
|
||||
assert.equal(game.pendingAmbassador, null);
|
||||
assert.equal(game.pendingTerrorEntry, null);
|
||||
assert.equal(game.pendingArrivalOverlap ?? null, null);
|
||||
assert.equal(game.pendingArrivalReaction ?? null, null);
|
||||
assert.equal(player(game, f.entrant).spice, originalSpice - 1);
|
||||
assert.equal(player(game, f.entrant).forces[`${f.destination}:${f.sector}`], 1);
|
||||
assert.equal(player(game, 'moritani').forces[`${f.destination}:${f.sector}`], 1);
|
||||
});
|
||||
Reference in new issue
Block a user