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); });