import assert from 'node:assert/strict'; import test from 'node:test'; import { baseDeck } from '../game/cards'; import { runBots } from '../game/bots'; import { coalitionOf, emptyCoalitionState } from '../game/coalition-alliance'; import { applyAction, createGame, newPlayer, viewGame, type Action, type Game } from '../game/engine'; import { createRevivalOrder } from '../game/revival-order'; const act = (game: Game, actor: string, action: Action) => applyAction(game, actor, action); const seat = (game: Game, id: string) => { const found = game.players.find(player => player.id === id); assert.ok(found); return found; }; /** Advanced group is built by genuine pair and group Nexus actions. The Nexus * itself, original public order and subsequent Revival phase are staged, not a * native full game. Atreides and Harkonnen each move four of their own ordinary * reserves to Tanks; the queue is made with the production order helper, and * Fremen actually finishes his opportunity before the first ally acts. */ function fixture() { let game = createGame('ORDEREDGROUPREVIVAL', newPlayer('f', 'Fremen', 'fremen'), true); game.players.push(newPlayer('a', 'Atreides', 'atreides'), newPlayer('h', 'Harkonnen', 'harkonnen'), newPlayer('g', 'Guild', 'guild')); Object.assign(game, {status: 'playing', phase: 1, turn: 2, nexus: true, order: ['f', 'a', 'h', 'g'], ready: [], storm: 18, deck: baseDeck(), response: null, decision: null, phaseOpening: null}); game.coalitionState = emptyCoalitionState(); game = act(game, 'f', {type: 'alliance', target: 'a'}); game = act(game, 'a', {type: 'alliance', target: 'f'}); const group = coalitionOf(game.players, game.coalitionState, 'f'); assert.ok(group); game = act(game, 'f', {type: 'coalitionPropose', groupId: group.id, entrant: 'h', expectedRevision: game.coalitionState!.revision}); for (const id of ['a', 'h']) { const proposal = game.coalitionState!.proposal; assert.ok(proposal); game = act(game, id, {type: 'coalitionAnswer', event: proposal.event, nexus: proposal.nexus, turn: proposal.turn, accept: true, expectedRevision: game.coalitionState!.revision}); } assert.deepEqual(coalitionOf(game.players, game.coalitionState, 'f')!.members, ['a', 'f', 'h']); Object.assign(game, {phase: 4, nexus: false, ready: [], response: null, decision: null, active: 'f', phaseOpening: null}); for (const player of game.players) { player.forces = {}; player.reserves = 20; player.tanks = 0; player.spice = 10; player.hand = []; player.traitors = []; player.traitorChoices = []; } for (const id of ['a', 'h']) {seat(game, id).reserves -= 4; seat(game, id).tanks += 4;} game.revivalOrder = createRevivalOrder({turn: game.turn, order: game.order, ready: game.ready}); game = act(game, 'f', {type: 'ready'}); game = act(game, 'a', {type: 'setAutopilot', difficulty: 'Easy'}); game = act(game, 'h', {type: 'setAutopilot', difficulty: 'Hard'}); assert.equal(game.revivalOrder?.remaining[0], 'a'); return game; } void test('Advanced group release is recipient-specific across consecutive automated Revival turns', () => { let game = fixture(); const original = structuredClone(game.revivalOrder); game = runBots(game, 12); assert.deepEqual(game.revivalOrder, original); assert.equal(seat(game, 'a').revived, 0); assert.equal(seat(game, 'h').revived, 0); game = act(game, 'f', {type: 'releaseFremenRevivalWait', target: 'a'}); const projection = viewGame(game, 'f').fremenGroupRevivalGrant; assert.equal(projection?.find(row => row.recipient === 'a')?.waitReleased, true); assert.ok(!projection?.find(row => row.recipient === 'h')?.waitReleased); game = runBots(game, 30); assert.ok(seat(game, 'a').revived > 0); assert.ok(game.revivalOrder?.completed.includes('a')); assert.equal(game.revivalOrder?.remaining[0], 'h'); assert.equal(seat(game, 'h').revived, 0); assert.equal(seat(game, 'h').tanks, 4); assert.equal(game.freeRevival.includes('a'), false); assert.equal(game.freeRevival.includes('h'), false); });