import assert from 'node:assert/strict'; import { test } from 'node:test'; import { baseDeck } from '../game/cards'; import { botActions } from '../game/bots'; import { coalitionOf, emptyCoalitionState } from '../game/coalition-alliance'; import { applyAction, createGame, newPlayer, viewGame, type Action, type Game } from '../game/engine'; const act = (game: Game, id: string, action: Action) => applyAction(game, id, action); const player = (game: Game, id: string) => game.players.find(seat => seat.id === id)!; const shipment: Action = {type:'guildShip', from:'reserves', territory:'carthag', sector:11, amount:3, elite:0, allyPayment:0}; /** The unexposed group mode requires a staged empty ledger and later Movement * phase; the pair and third member join using actual Nexus actions. Forces and * wallets are staged with physical totals intact, and the actual Truthtrance * card is transferred from the undealt deck. No saved room is touched. */ function fixture() { let game = createGame('GROUPFREMENSHIP', newPlayer('g', 'Guild', 'guild'), false); game.players.push(newPlayer('e', 'Emperor', 'emperor'), newPlayer('f', 'Fremen', 'fremen'), newPlayer('h', 'Harkonnen', 'harkonnen')); Object.assign(game, {status:'playing', phase:1, turn:2, nexus:true, order:['g','e','f','h'], ready:[], storm:18, deck:baseDeck(), response:null, decision:null, phaseOpening:null}); game.coalitionState = emptyCoalitionState(); game = act(game, 'g', {type:'alliance', target:'e'}); game = act(game, 'e', {type:'alliance', target:'g'}); const pair = coalitionOf(game.players, game.coalitionState, 'g'); assert.ok(pair); game = act(game, 'g', {type:'coalitionPropose', groupId:pair.id, entrant:'f', expectedRevision:game.coalitionState!.revision}); for (const id of ['e','f']) { const proposal = game.coalitionState!.proposal!; game = act(game, id, {type:'coalitionAnswer', event:proposal.event, nexus:proposal.nexus, turn:proposal.turn, accept:true, expectedRevision:game.coalitionState!.revision}); } assert.equal(player(game,'f').ally, null, 'A three-member group does not use the pair-only scalar.'); Object.assign(game, {phase:5, nexus:false, active:'f', ready:[], movementRemaining:['f','g','e','h'], guildTimingGranted:false, guildTimingLocked:false}); for (const seat of game.players) { seat.forces = {}; seat.reserves = 20; seat.tanks = 0; seat.spice = 10; seat.hand = []; seat.traitors = []; seat.traitorChoices = []; seat.shipped = false; seat.moved = 0; } const index = game.deck.findIndex(card => card.effect === 'truthtrance'); assert.ok(index >= 0); const [truth] = game.deck.splice(index,1); player(game,'h').hand.push(truth); // A distinct undealt Karama gives the outside player a real opportunity to // counter the native permission and rate; it stays held when passed. const karama = game.deck.findIndex(card => card.effect === 'karama'); assert.ok(karama >= 0); player(game,'h').hand.push(...game.deck.splice(karama,1)); return {game, truth}; } function settle(game: Game): Game { for (let n = 0; game.response && n < 32; n++) { const seat = game.players.find(candidate => !game.response!.passed.includes(candidate.id)); assert.ok(seat); game = act(game,seat.id,{type:'passResponse'}); } assert.equal(game.response ?? null,null); return game; } function ask() { const {game:before, truth} = fixture(); let game = act(before,'h',{type:'card',card:truth.id}); while (game.truthtrance?.stage === 'priority') { const seat = game.players.find(candidate => !game.truthtrance!.passed.includes(candidate.id)); assert.ok(seat); game = act(game,seat.id,{type:'truthPass'}); } game = act(game,'h',{type:'truthAsk',question:{kind:'shipment',target:'f',turn:game.turn, territory:'carthag',minimum:3}}); assert.equal(game.truthtrance?.stage,'answer'); return {before,game,truth}; } void test('group-allied Fremen can actually ship from reserves to Carthag with native Guild permission', () => { let {game} = fixture(); // Carthag is outside the Fremen southern reinforcement area. The native // group-aware Guild path is the real legal transport for this location. assert.throws(() => act(game,'f',{type:'ship',territory:'carthag',sector:11,amount:3,elite:0})); game = act(game,'f',shipment); assert.equal(game.response?.kind,'guildTransportPermission'); assert.equal(player(game,'f').reserves,20); game = settle(game); assert.equal(player(game,'f').forces['carthag:11'],3); assert.equal(player(game,'f').reserves,17); assert.equal(player(game,'f').spice,8); assert.equal(player(game,'g').spice,12); }); void test('a group-allied Fremen can answer Yes to the real Carthag reserve-shipment question and fulfill it', () => { const {game:asked,truth} = ask(); assert.ok(viewGame(asked,'f').truthShipmentAnswers?.includes('yes')); let game = act(asked,'f',{type:'truthAnswer',answer:'yes'}); assert.ok(game.shipmentPromises?.some(promise => promise.player === 'f' && promise.answer && !promise.released)); assert.equal(game.discard.filter(card => card.id === truth.id).length,1); assert.throws(() => act(game,'f',{type:'endMovement'}),/Truthtrance/i); game = act(game,'f',shipment); assert.equal(game.response?.kind,'guildTransportPermission'); game = settle(game); assert.equal(player(game,'f').forces['carthag:11'],3); assert.equal(player(game,'f').spice,8); assert.equal(player(game,'g').spice,12); assert.ok(game.shipmentPromises?.some(promise => promise.player === 'f' && promise.fulfilled && !promise.released)); }); void test('all four bot profiles can answer and follow the original group Guild shipment witness', () => { const {game:asked} = ask(); for (const difficulty of ['Easy','Medium','Hard','Brutal'] as const) { const answerView = viewGame(asked,'f'); answerView.players.find(seat => seat.id === 'f')!.bot = difficulty; const answer = botActions(answerView).find(action => action.type === 'truthAnswer'); assert.ok(answer); assert.equal(answer.answer,'yes'); let game = act(structuredClone(asked),'f',answer); const movementView = viewGame(game,'f'); movementView.players.find(seat => seat.id === 'f')!.bot = difficulty; const next = botActions(movementView)[0]; assert.equal(next?.type,'guildShip'); assert.equal(next.from,'reserves'); assert.equal(next.territory,'carthag'); game = act(game,'f',next); game = settle(game); assert.ok(game.shipmentPromises?.some(promise => promise.player === 'f' && promise.fulfilled && !promise.released)); } });