119 lines
7.1 KiB
TypeScript
119 lines
7.1 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
import { applyAction, createGame, joinGame, newPlayer, viewGame, type Action, type Game } from '../game/engine';
|
|
import { placeAmbassador } from '../game/ecaz-ambassadors';
|
|
import { botActions } from '../game/bots';
|
|
import { DIFFICULTIES } from '../game/bot-profiles';
|
|
import { SEMUTA_DRUG_ID } from '../game/semuta-drug';
|
|
import { basicSemutaAmbassadorInventory as inventory, nextBasicSemutaAmbassadorStep } from './fixture-basic-semuta-ambassador';
|
|
|
|
const seat = (game: Game, id: string) => {
|
|
const found = game.players.find(player => player.id === id); assert.ok(found); return found;
|
|
};
|
|
const act = (game: Game, id: string, action: Action) => applyAction(game, id, action);
|
|
|
|
/** Original public selected-family setup and physical component inventory.
|
|
* The later board, wallets, selected Ambassador cohort/token and physical hand
|
|
* transfers are staged; shipment, trigger, discard and reactions are actions. */
|
|
function fixture(effect: 'choam' | 'ixians') {
|
|
let game = createGame('SEMUTAINDAMBASSADOR', newPlayer('ecaz','Ecaz','ecaz'), false,
|
|
['choam','ix','ecaz']);
|
|
for (const faction of ['richese','guild','emperor'] as const)
|
|
joinGame(game,newPlayer(faction,faction,faction));
|
|
game = act(game,'ecaz',{type:'moduleOption',module:'ecazTreachery',enabled:true});
|
|
for (const player of game.players) game = act(game,player.id,{type:'ready'});
|
|
game = act(game,'ecaz',{type:'start'});
|
|
assert.equal(game.status,'setup'); assert.equal(game.semutaRecruitsPreview,true);
|
|
for (let count = 0; game.status === 'setup' && count < 150; count++) {
|
|
const step = nextBasicSemutaAmbassadorStep(game); assert.ok(step);
|
|
game = act(game,step.actor,step.action);
|
|
}
|
|
assert.equal(game.status,'playing');
|
|
const original = inventory(game);
|
|
for (const player of game.players) {
|
|
game.deck.push(...player.hand); player.hand = [];
|
|
player.reserves += player.tanks + Object.values(player.forces).reduce((sum,n) => sum+n,0);
|
|
player.forces = {}; player.tanks = 0;
|
|
if (player.elites) {
|
|
player.elites.reserves += player.elites.tanks + Object.values(player.elites.forces).reduce((sum,n) => sum+n,0);
|
|
player.elites.forces = {}; player.elites.tanks = 0;
|
|
}
|
|
Object.assign(player,{spice:20,shipped:false,moved:0});
|
|
}
|
|
const take = (predicate: (card: Game['deck'][number]) => boolean) => {
|
|
for (const zone of [game.deck,game.discard,game.richeseCache!]) {
|
|
const index = zone.findIndex(predicate);
|
|
if (index >= 0) return zone.splice(index,1)[0];
|
|
}
|
|
throw Error('The original physical card is unavailable.');
|
|
};
|
|
seat(game,'guild').hand.push(take(card => card.id === SEMUTA_DRUG_ID));
|
|
seat(game,'ecaz').hand.push(take(card => card.kind === 'projectile'),take(card => card.kind === 'shield'));
|
|
const state = structuredClone(game.ecazAmbassadors!);
|
|
const token = state.tokens.find(item => item.effect === effect); assert.ok(token);
|
|
if (!state.cohort.includes(token.id)) {
|
|
const replaced = state.tokens.find(item => item.id === state.cohort[0]); assert.ok(replaced);
|
|
replaced.zone = 'pool'; token.zone = 'supply'; state.cohort[0] = token.id;
|
|
}
|
|
game.ecazAmbassadors = placeAmbassador(state,token.id,{turn:1,availableSpice:20,
|
|
destination:{id:'arrakeen',stronghold:true,inStorm:false,allowed:true}}).state;
|
|
Object.assign(game,{turn:2,phase:5,storm:18,active:'emperor',order:['emperor','ecaz','richese','guild'],
|
|
movementRemaining:['emperor','ecaz','richese','guild'],ready:[],response:null,decision:null,
|
|
phaseOpening:null,stormPending:null,nexus:false});
|
|
assert.deepEqual(inventory(game),original);
|
|
game = act(game,'emperor',{type:'ship',territory:'arrakeen',sector:10,amount:1,elite:0,allyPayment:0});
|
|
for (let count=0;game.response && count<20;count++) {
|
|
const actor = game.players.find(player => !game.response!.passed.includes(player.id)); assert.ok(actor);
|
|
game = act(game,actor.id,{type:'passResponse'});
|
|
}
|
|
assert.equal(game.pendingAmbassador?.stage,'offer');
|
|
const event = game.pendingAmbassador!.event;
|
|
game = act(game,'ecaz',{type:'decision',event,trigger:true,beneficiary:'ecaz',choice:'effect'});
|
|
assert.equal(game.pendingAmbassador?.stage,'cards');
|
|
const chosen = seat(game,'ecaz').hand.slice(0,effect === 'choam' ? 2 : 1).map(card => card.id);
|
|
assert.equal(chosen.length,effect === 'choam' ? 2 : 1);
|
|
const deckTop = game.deck[0]?.id; assert.ok(deckTop);
|
|
return {game,event,chosen,deckTop,original};
|
|
}
|
|
|
|
for (const effect of ['choam','ixians'] as const) for (const claim of [false,true]) {
|
|
void test(`Basic independent cards: ${effect} Ambassador discard offers Semuta ${claim ? 'claim' : 'pass'}`, () => {
|
|
const f = fixture(effect), before = structuredClone(f.game), spice = seat(f.game,'ecaz').spice;
|
|
let game = act(f.game,'ecaz',{type:'decision',event:f.event,cards:f.chosen});
|
|
assert.deepEqual(f.game,before);
|
|
const pending = game.pendingTreacheryDiscard; assert.ok(pending?.reaction);
|
|
assert.equal(pending.continuation.kind,'ambassador');
|
|
assert.deepEqual(pending.batch.entries.map(entry => entry.card.id),f.chosen);
|
|
assert.ok(pending.batch.entries.every(entry => !entry.publicFace && entry.discardedBy === 'ecaz'));
|
|
assert.equal(seat(game,'ecaz').spice,spice + (effect === 'choam' ? 3*f.chosen.length : 0));
|
|
assert.deepEqual(game.deck,before.deck);
|
|
for (const player of game.players) assert.deepEqual(viewGame(game,player.id).semutaReaction?.candidates,[]);
|
|
for (const difficulty of DIFFICULTIES) {
|
|
const view = viewGame(game,'guild');
|
|
const owner = view.players.find(player => player.id === 'guild'); assert.ok(owner); owner.bot = difficulty;
|
|
assert.ok(botActions(view).some(action => action.type === 'semutaCommit' || action.type === 'semutaPass'));
|
|
}
|
|
const event = pending.batch.event;
|
|
if (claim) {
|
|
game = act(game,'guild',{type:'semutaCommit',event});
|
|
if (game.pendingTreacheryDiscard?.reaction?.stage === 'select') {
|
|
assert.deepEqual(viewGame(game,'guild').semutaReaction!.candidates.map(card => card.id),f.chosen);
|
|
assert.deepEqual(viewGame(game,'ecaz').semutaReaction!.candidates,[]);
|
|
game = act(game,'guild',{type:'semutaSelect',event,card:f.chosen[0]});
|
|
}
|
|
} else for (const player of game.players) game = act(game,player.id,{type:'semutaPass',event});
|
|
assert.equal(game.pendingTreacheryDiscard,null); assert.equal(game.pendingAmbassador,null);
|
|
assert.equal(seat(game,'ecaz').spice,spice + (effect === 'choam' ? 3*f.chosen.length : 0));
|
|
assert.equal(seat(game,'guild').hand.some(card => card.id === f.chosen[0]),claim);
|
|
if (effect === 'ixians') {
|
|
assert.ok(seat(game,'ecaz').hand.some(card => card.id === f.deckTop));
|
|
assert.deepEqual(game.deck.map(card => card.id),before.deck.slice(1).map(card => card.id));
|
|
} else assert.deepEqual(game.deck,before.deck);
|
|
assert.equal(game.discard.some(card => card.id === SEMUTA_DRUG_ID),claim);
|
|
assert.deepEqual(inventory(game),f.original);
|
|
assert.throws(() => act(game,'guild',{type:'semutaCommit',event}));
|
|
game = act(game,'emperor',{type:'endMovement'});
|
|
assert.ok(!game.movementRemaining?.includes('emperor'));
|
|
});
|
|
}
|