99 lines
5.6 KiB
TypeScript
99 lines
5.6 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
import { botActions } from '../game/bots';
|
|
import { botArrivalBlock } from '../game/bot-arrival';
|
|
import { DIFFICULTIES } from '../game/bot-profiles';
|
|
import { applyAction, createGame, newPlayer, normalizeAutomaticGame, viewGame, type Action, type Game } from '../game/engine';
|
|
import { createRicheseNoField, deployRicheseNoField } from '../game/richese-no-field';
|
|
import { guildTransportQuote } from '../game/transport-quote';
|
|
import { atomicsFixtureGame } from './fixture-moritani-atomics';
|
|
|
|
const p = (g: Game, id: string) => g.players.find(player => player.id === id)!;
|
|
const act = (g: Game, id: string, action: Action) => applyAction(g,id,action);
|
|
const cross: Action = {type:'guildShip',forces:{'imperial_basin:10':2},
|
|
territory:'arrakeen',sector:10,allyPayment:0};
|
|
|
|
/** The physical Atomics token is genuinely revealed by the original entrant.
|
|
* Later turn ownership, a conserved board group and marker custody are staged
|
|
* to reach the particular transportation quotes without replaying a game. */
|
|
function aftermath(advanced = false) {
|
|
let game = createGame('GUILDATOMICS',newPlayer('m','Moritani','moritani'),advanced,['ecaz','choam']);
|
|
game.players.push(newPlayer('e','Emperor','emperor'),newPlayer('g','Guild','guild'),
|
|
newPlayer('f','Fremen','fremen'),newPlayer('r','Richese','richese'));
|
|
atomicsFixtureGame(game,{moritani:'m',entrant:'e',ally:'g'},{advanced,allied:false});
|
|
game.order=['e','m','g','f','r']; game.movementRemaining=[...game.order];
|
|
game=act(game,'e',{type:'ship',territory:'arrakeen',sector:10,amount:2});
|
|
if (game.decision?.kind==='guildShipment')
|
|
game=act(game,game.decision.player,{type:'decision',allow:true});
|
|
game=allow(game);
|
|
assert.equal(game.pendingTerrorEntry?.stage,'offer');
|
|
game=normalizeAutomaticGame(act(game,'m',{type:'decision',reveal:true}));
|
|
assert.equal(game.moritaniAtomics?.territory,'arrakeen');
|
|
assert.equal(game.pendingTerrorEntry,null);
|
|
return game;
|
|
}
|
|
function prepare(g: Game, actor: 'g'|'f'|'r') {
|
|
g.active=actor; g.movementRemaining=[actor,...g.order.filter(id=>id!==actor)];
|
|
p(g,actor).shipped=false; p(g,actor).moved=0; p(g,actor).spice=20;
|
|
return g;
|
|
}
|
|
function reject(g: Game, actor: string, action: Action) {
|
|
const before=structuredClone(g);
|
|
assert.throws(()=>act(g,actor,action),/Atomics Aftermath/);
|
|
assert.deepEqual(g,before);
|
|
}
|
|
function allow(g: Game) {
|
|
for (let i=0;g.response && i<30;i++) {
|
|
const actor=g.players.find(player=>!g.response!.passed.includes(player.id)); assert.ok(actor);
|
|
g=act(g,actor.id,{type:'passResponse'});
|
|
}
|
|
return g;
|
|
}
|
|
|
|
for (const advanced of [false,true]) {
|
|
void test(`${advanced?'Advanced':'Basic'} Guild cannot cross-ship into Aftermath, but may move in or return to reserves`,()=>{
|
|
const game=prepare(aftermath(advanced),'g');
|
|
p(game,'g').forces={'imperial_basin:10':2}; p(game,'g').reserves=18;
|
|
const view=viewGame(game,'g');
|
|
assert.ok(guildTransportQuote(view,cross).unavailableReasons.some(reason=>reason.includes('Atomics Aftermath')));
|
|
assert.match(botArrivalBlock(view,cross)??'',/Atomics Aftermath/);
|
|
reject(game,'g',cross);
|
|
for (const difficulty of DIFFICULTIES) {
|
|
const botView=viewGame(game,'g'); botView.players.find(player=>player.id==='g')!.bot=difficulty;
|
|
assert.ok(!botActions(botView).some(action=>action.type==='guildShip' && action.territory==='arrakeen'));
|
|
}
|
|
const movement: Action={type:'move',forces:{'imperial_basin:10':2},territory:'arrakeen',sector:10};
|
|
const moved=act(structuredClone(game),'g',movement);
|
|
assert.equal(p(moved,'g').forces['arrakeen:10'],2);
|
|
const back: Action={type:'guildShip',forces:{'imperial_basin:10':2},territory:'reserves',sector:0,allyPayment:0};
|
|
assert.ok(!guildTransportQuote(view,back).unavailableReasons.some(reason=>reason.includes('Atomics')));
|
|
const returned=allow(act(structuredClone(game),'g',back));
|
|
assert.equal(p(returned,'g').forces['imperial_basin:10']??0,0);
|
|
assert.equal(p(returned,'g').reserves,20);
|
|
});
|
|
void test(`${advanced?'Advanced':'Basic'} Guild-allied Fremen cannot cross-ship southern reserves into Aftermath`,()=>{
|
|
const game=prepare(aftermath(advanced),'f'); p(game,'g').ally='f'; p(game,'f').ally='g';
|
|
const shipment: Action={type:'guildShip',from:'reserves',amount:2,territory:'arrakeen',sector:10,allyPayment:0};
|
|
const view=viewGame(game,'f');
|
|
assert.ok(guildTransportQuote(view,shipment).unavailableReasons.some(reason=>reason.includes('Atomics Aftermath')));
|
|
assert.match(botArrivalBlock(view,shipment)??'',/Atomics Aftermath/);
|
|
reject(game,'f',shipment);
|
|
});
|
|
}
|
|
|
|
void test('Guild-allied Richese cannot cross-ship a concealed No-Field into Aftermath',()=>{
|
|
const game=prepare(aftermath(true),'r'); p(game,'g').ally='r'; p(game,'r').ally='g';
|
|
const stock=createRicheseNoField(['controlled-zero','controlled-three','controlled-five']);
|
|
const marker=stock.tokens.find(token=>token.value===0)!;
|
|
p(game,'r').noField=deployRicheseNoField(stock,{tokenId:marker.id,controller:'r',
|
|
location:{territory:'imperial_basin',sector:10}});
|
|
p(game,'r').noFieldEvent='controlled-marker-event';
|
|
const shipment: Action={type:'guildShip',from:'imperial_basin:10',forces:{},noField:marker.id,
|
|
event:p(game,'r').noFieldEvent,territory:'arrakeen',sector:10,allyPayment:0};
|
|
const view=viewGame(game,'r');
|
|
assert.ok(guildTransportQuote(view,shipment).unavailableReasons.some(reason=>reason.includes('Atomics Aftermath')));
|
|
assert.match(botArrivalBlock(view,shipment)??'',/Atomics Aftermath/);
|
|
reject(game,'r',shipment);
|
|
assert.equal(p(game,'r').noField?.deployed?.tokenId,marker.id);
|
|
});
|