Files
Dune/tests/guild-transport-atomics.test.ts
T
Quintine 0f8e0b4153
Verify and publish checkpoint container / container (push) Successful in 10m7s
Enforce Atomics shipment ban across transport
2026-10-29 03:03:18 +11:00

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