import test from 'node:test'; import assert from 'node:assert/strict'; import { applyAction, createGame, joinGame, newPlayer, viewGame, type Game, } from '../game/engine'; import { baseDeck } from '../game/cards'; function ready(state: Game) { let g = state; for (const p of g.players) g = applyAction(g, p.id, { type: 'ready' }); return g; } function allow(state: Game) { let g = state; while (g.response) g = applyAction( g, g.players.find((p) => !g.response!.passed.includes(p.id))!.id, { type: 'passResponse' }, ); return g; } function fixture() { let g = createGame('GUILDTE2', newPlayer('e', 'Emperor', 'emperor')); joinGame(g, newPlayer('g', 'Guild', 'guild')); joinGame(g, newPlayer('h', 'Harkonnen', 'harkonnen')); joinGame(g, newPlayer('f', 'Fremen', 'fremen')); g.players.forEach((p) => (p.ready = true)); g = applyAction(g, 'e', { type: 'start' }); for (const p of g.players) if (p.traitorChoices.length) g = applyAction(g, p.id, { type: 'traitor', leader: p.traitorChoices[0], }); g = applyAction(g, 'f', { type: 'fremenSetup', placements: { sietch_tabr: 10 }, }); g.advanced = true; g.players.forEach((p) => (p.hand = [])); g.phase = 4; g.order = ['e', 'g', 'h', 'f']; return ready(g); } function choose(g: Game, take: boolean) { return applyAction(g, 'g', { type: 'decision', take }); } function end(g: Game) { return applyAction(g, g.active!, { type: 'endMovement' }); } function cancel(g: Game) { const karama = baseDeck().find((c) => c.effect === 'karama')!; g.players[2].hand = [karama]; return applyAction(g, 'h', { type: 'card', card: karama.id, mode: 'cancel' }); } void test('Guild can act first while the other factions retain their storm order and each gets one turn', () => { let g = fixture(); assert.equal(g.active, null); assert.equal(g.decision?.kind, 'guildTiming'); assert.equal(viewGame(g, 'g').decision?.player, 'g'); assert.throws( () => applyAction(g, 'e', { type: 'endMovement' }), /pending decision/, ); g = choose(g, true); assert.equal(g.response, null); g = allow(g); const acted = []; while (g.phase === 5) { acted.push(g.active); g = end(g); } assert.deepEqual(acted, ['g', 'e', 'h', 'f']); assert.deepEqual(g.order, ['e', 'g', 'h', 'f']); assert.deepEqual(g.movementRemaining, []); assert.throws(() => applyAction(g, 'g', { type: 'endMovement' }), /Wait/); }); void test('Guild can wait beyond its normal slot and then act between later players without revealing future intent', () => { let g = choose(fixture(), false); assert.equal(g.response, null); assert.equal(g.active, 'e'); assert.equal(viewGame(g, 'h').decision, null); g = end(g); assert.equal(g.decision?.kind, 'guildTiming'); assert.equal(g.decision?.kind === 'guildTiming' && g.decision.next, 'g'); g = choose(g, false); assert.equal(g.response, null); g = allow(g); assert.equal(g.active, 'h'); g = end(g); assert.equal(g.decision?.kind, 'guildTiming'); g = choose(g, true); assert.equal(g.active, 'g'); assert.equal(g.response, null); g = end(g); assert.equal(g.active, 'f'); assert.deepEqual(g.movementRemaining, ['f']); }); void test('Guild waiting until last is activated automatically and cannot interrupt another faction mid-turn', () => { let g = choose(fixture(), false); assert.throws(() => choose(g, true), /not available/); g = end(g); g = allow(choose(g, false)); assert.equal(g.active, 'h'); g = end(g); g = choose(g, false); assert.equal(g.active, 'f'); g = end(g); assert.equal(g.active, 'g'); assert.equal(g.decision, null); g = applyAction(g, 'g', { type: 'ship', territory: 'polar_sink', sector: 0, amount: 1, }); assert.equal(g.active, 'g'); assert.deepEqual(g.movementRemaining, ['g']); g = applyAction(g, 'g', { type: 'decision', allow: true }); g = end(g); assert.notEqual(g.phase, 5); }); void test('Karama cancels an early Guild turn and restores its normal position for this phase only', () => { const initial = fixture(); initial.players[2].hand = [baseDeck().find((c) => c.effect === 'karama')!]; let g = cancel(choose(initial, true)); assert.equal(g.active, 'e'); assert.equal(g.guildTimingLocked, true); assert.equal(viewGame(g, 'g').guildTimingLocked, true); g = end(g); assert.equal(g.active, 'g'); assert.equal(g.decision, null); g = end(g); assert.equal(g.active, 'h'); g = end(end(g)); assert.notEqual(g.phase, 5); g.phase = 4; g.ready = []; g = ready(g); assert.equal(g.guildTimingLocked, false); assert.equal(g.guildTimingGranted, false); assert.equal(g.decision?.kind, 'guildTiming'); }); void test('Karama can stop Guild delaying its normal turn, but taking that normal turn needs no power response', () => { let g = end(choose(fixture(), false)); const normal = choose(g, true); assert.equal(normal.active, 'g'); assert.equal(normal.response, null); g.players[2].hand = [baseDeck().find((c) => c.effect === 'karama')!]; g = cancel(choose(g, false)); assert.equal(g.active, 'g'); assert.deepEqual(g.movementRemaining, ['g', 'h', 'f']); assert.equal(g.guildTimingLocked, true); }); void test('allied co-occupation loses only the ending faction’s fighters when Guild changes order', () => { for (const guildFirst of [true, false]) { let g = fixture(); g.players[0].ally = 'g'; g.players[1].ally = 'e'; g.players[0].forces = { 'arrakeen:10': 1 }; g.players[0].reserves = 19; g.players[1].forces = { 'arrakeen:10': 5 }; g.players[1].reserves = 15; g = allow(choose(g, guildFirst)); const first = guildFirst ? 'g' : 'e'; const survivor = guildFirst ? 'e' : 'g'; const lost = guildFirst ? 5 : 1; const retained = guildFirst ? 1 : 5; assert.equal(g.active, first); g = end(g); assert.deepEqual(g.players.find((p) => p.id === first)!.forces, {}); assert.equal(g.players.find((p) => p.id === first)!.tanks, lost); assert.deepEqual(g.players.find((p) => p.id === survivor)!.forces, { 'arrakeen:10': retained, }); assert.equal(g.players.find((p) => p.id === survivor)!.tanks, 0); const acted = [first]; while (g.phase === 5) { if (g.decision?.kind === 'guildTiming') g = allow(choose(g, false)); acted.push(g.active!); g = end(g); } assert.deepEqual( acted, guildFirst ? ['g', 'e', 'h', 'f'] : ['e', 'h', 'f', 'g'], ); assert.deepEqual(g.players.find((p) => p.id === survivor)!.forces, { 'arrakeen:10': retained, }); assert.equal(g.players.find((p) => p.id === survivor)!.tanks, 0); for (const id of ['e', 'g']) { const p = g.players.find((p) => p.id === id)!; assert.equal(p.reserves, id === 'e' ? 19 : 15); assert.equal( p.reserves + p.tanks + Object.values(p.forces).reduce((sum, n) => sum + n, 0), 20, ); } } });