import test from 'node:test'; import assert from 'node:assert/strict'; import { applyAction, viewGame, normalizeAutomaticGame, type Game, } from '../game/engine'; import { casualtyOptions, maxCombatDial } from '../game/combat'; import { createNexusSardaukar, validateNexusSardaukar, } from '../game/nexus-sardaukar'; import { nexusSardaukarFixture, nexusSardaukarPlayer, nexusSardaukarInventory, prepareSardaukarBattle, beginNexusSardaukar, allowNexusSardaukar, } from './fixture-nexus-sardaukar'; import { nexusReload, nexusReject, nexusAllow } from './fixture-nexus-cards'; import { drawNexusCard } from '../game/nexus-cards'; function hold(g: Game, id: string, effect: string): string { const index = g.deck.findIndex( (card) => card.effect === effect || card.kind === effect, ); assert.ok(index >= 0); const card = g.deck.splice(index, 1)[0]; g.players.find((p) => p.id === id)!.hand.push(card); return card.id; } function activate(g: Game): Game { return allowNexusSardaukar(beginNexusSardaukar(g)); } function request(g: Game) { const p = nexusSardaukarPlayer(g); return { type: 'nexusSardaukar', event: viewGame(g, p.id).nexusSardaukar!.offer!.event, }; } function resolveOrdinary(state: Game, dial = 6, support = 3): Game { let g = prepareSardaukarBattle(state); const owner = nexusSardaukarPlayer(g), target = g.players.find((p) => p.id === g.battle!.defender)!; const leader = [...owner.leaders].sort((a, b) => b.strength - a.strength)[0]; const opposing = [...target.leaders].sort( (a, b) => a.strength - b.strength, )[0]; g = applyAction(g, owner.id, { type: 'battlePlan', dial, support, leader: leader.id, }); g = applyAction(g, target.id, { type: 'battlePlan', dial: 0, leader: opposing.id, }); for (const id of [owner.id, target.id]) g = applyAction(g, id, { type: 'traitorCall', call: false }); return g; } void test('Sardaukar receipt binds the original five-counter battle role without depending on later force custody', () => { const g = nexusSardaukarFixture(); const receipt = createNexusSardaukar( g, 'p', g.battle!.event!, 'pasty_mesa', 7, ); assert.equal(receipt.elite, 0); assert.equal(receipt.count, 5); validateNexusSardaukar({ turn: g.turn + 1, players: g.players }, receipt); for (const patch of [ { normal: 4 }, { normal: 8 }, { count: 4 }, { elite: 1 }, { owner: 'q' }, { territory: 'hagga_basin' }, { battle: 'other' }, { version: 0 }, ]) { const bad = { ...receipt, ...patch }; assert.throws(() => validateNexusSardaukar(g, bad as typeof receipt)); } assert.throws(() => createNexusSardaukar(g, 'q', 'battle', 'pasty_mesa', 7)); assert.throws(() => createNexusSardaukar(g, 'p', 'battle', 'pasty_mesa', 4)); const changed = g.players.map((p) => ({ id: p.id, faction: p.faction })); changed[1].id = 'another-seat'; assert.throws(() => validateNexusSardaukar({ turn: g.turn, players: changed }, receipt), ); }); void test('five temporary Sardaukar enhance an owned battle while every actual force and starred counter stays in place', () => { let g = nexusSardaukarFixture(); hold(g, 'q', 'karama'); const before = structuredClone(g.players); g = beginNexusSardaukar(g); assert.equal(g.response?.kind, 'nexusSardaukar'); g = allowNexusSardaukar(g); assert.deepEqual(g.players, before); const forces = viewGame(g, 'p').battle!.ownForces!; assert.equal(forces.normal, 7); assert.equal(forces.elite, 0); assert.equal(forces.temporaryElite, 5); assert.equal(maxCombatDial(forces), 12); assert.deepEqual( casualtyOptions(forces, 6, 3).map((x) => [x.normal, x.elite]), [ [3, 0], [4, 0], [5, 0], ], ); for (const p of g.players) assert.equal(viewGame(g, p.id).nexusSardaukar!.active, true); nexusSardaukarInventory(g); }); void test('temporary Sardaukar retain the ordinary Fremen exception and need support for full strength', () => { const g = activate(nexusSardaukarFixture({ opponentFaction: 'fremen' })); const forces = viewGame(g, 'p').battle!.ownForces!; assert.equal(forces.temporaryElite, 5); assert.equal(forces.eliteStrength, 1); assert.equal(forces.freeSupport, false); assert.equal(maxCombatDial(forces), 7); assert.deepEqual( casualtyOptions(forces, 5, 5).map((x) => [x.normal, x.elite]), [[5, 0]], ); assert.deepEqual(casualtyOptions(forces, 5, 0), []); }); void test('Karama stops the new role grant entirely and leaves all physical forces normal', () => { let g = nexusSardaukarFixture(); const karama = hold(g, 'q', 'karama'); const original = structuredClone(nexusSardaukarPlayer(g).elites); g = beginNexusSardaukar(g); g = applyAction(g, 'q', { type: 'card', card: karama, mode: 'cancel' }); g = allowNexusSardaukar(g); const forces = viewGame(g, 'p').battle!.ownForces!; assert.equal(forces.temporaryElite ?? 0, 0); assert.equal(maxCombatDial(forces), 7); assert.deepEqual(nexusSardaukarPlayer(g).elites, original); assert.equal(g.discard.filter((c) => c.id === karama).length, 1); assert.equal( g.nexusCards!.cards!.discard.filter((c) => c === 'emperor').length, 1, ); nexusSardaukarInventory(g); }); void test('every winning casualty choice removes ordinary counters and never invents Sardaukar in the Tanks', () => { const initial = activate(nexusSardaukarFixture()); for (const [index, losses] of [3, 4, 5].entries()) { let g = resolveOrdinary(nexusReload(initial)); assert.equal(g.decision?.kind, 'battleLosses'); g = applyAction(g, 'p', { type: 'decision', choice: index }); const p = nexusSardaukarPlayer(g); assert.equal(p.tanks, losses); assert.equal(p.forces['pasty_mesa:5'], 7 - losses); assert.equal(p.elites!.tanks, 0); assert.equal(p.elites!.reserves, 5); assert.deepEqual(p.elites!.forces, {}); nexusSardaukarInventory(g); } }); void test('the completed grant expires before a second battle in the same turn', () => { let g = resolveOrdinary(activate(nexusSardaukarFixture()), 0, 0); assert.equal(g.battle, null); assert.ok(g.decision?.kind === 'nexusChoamInspection'); g = applyAction(g, g.decision.player, { type: 'decision', event: g.decision.event, inspect: false, }); g = applyAction(g, 'p', { type: 'chooseBattle', territory: 'hagga_basin', target: 'q', }); assert.equal(viewGame(g, 'p').battle!.ownForces!.temporaryElite ?? 0, 0); assert.equal(viewGame(g, 'p').nexusSardaukar!.active, false); assert.deepEqual(normalizeAutomaticGame(nexusReload(g)), nexusReload(g)); }); void test('Basic, short groups, actual Sardaukar, foreign actors and stale or repeated declarations reject unchanged', () => { for (const options of [{ advanced: false }, { normal: 4 }, { starred: 1 }]) { const g = nexusSardaukarFixture(options); assert.ok(viewGame(g, 'p').nexusSardaukar!.offer!.blocked); nexusReject(g, 'p', request(g), /Basic|Advanced|five|5|Sardaukar|ruling/i); } const g = nexusSardaukarFixture(), action = request(g); nexusReject(g, 'q', action, /Sardaukar|Cunning/); nexusReject(g, 'p', { ...action, event: 'old' }, /Sardaukar|Cunning/); nexusReject(g, 'p', { ...action, count: 4 }, /Sardaukar|Cunning/); nexusReject(activate(g), 'p', action, /Sardaukar|Cunning/); const ready = prepareSardaukarBattle(g); const sealed = applyAction(ready, 'p', { type: 'battlePlan', dial: 0, leader: ready.players[0].leaders[0].id, }); nexusReject(sealed, 'p', action, /submitted|Sardaukar|Cunning/); }); void test('saved declaration restores its public pending effect and only settles once', () => { let g = nexusSardaukarFixture({ seatIds: ['emperor-seat', 'guild-seat', 'fremen-seat'], }); hold(g, 'guild-seat', 'karama'); const action = request(g); g = beginNexusSardaukar(g); const saved = nexusReload(g); for (const p of saved.players) { const view = viewGame(saved, p.id); assert.equal(view.nexusSardaukar!.pending!.owner, 'emperor-seat'); for (const other of view.players) if (other.id !== p.id) assert.equal(other.hand, undefined); } assert.deepEqual(normalizeAutomaticGame(saved), saved); g = allowNexusSardaukar(nexusReload(saved)); assert.deepEqual(normalizeAutomaticGame(nexusReload(g)), nexusReload(g)); nexusReject(g, 'emperor-seat', action, /Sardaukar|Cunning/); nexusSardaukarInventory(g); }); void test('native Prescience accepts a future Cunning dial and keeps its answer binding after activation', () => { let g = nexusSardaukarFixture({ opponentFaction: 'atreides' }); g = applyAction(g, 'q', { type: 'prescience', field: 'dial' }); g = nexusAllow(g); g = applyAction(g, 'p', { type: 'prescienceAnswer', value: 10 }); assert.equal(g.nexusCards!.cards!.hands.p, 'emperor'); g = activate(prepareSardaukarBattle(g)); g = applyAction(g, 'p', { type: 'battlePlan', dial: 10, support: 5, leader: g.players[0].leaders[0].id, }); assert.equal(g.battle!.plans.p.dial, 10); assert.equal(g.battle!.prescience!.value, 10); }); void test('canceling Cunning reopens an impossible native dial answer without granting a new inspection', () => { let g = nexusSardaukarFixture({ opponentFaction: 'atreides' }); const karama = hold(g, 'q', 'karama'); g = applyAction(g, 'q', { type: 'prescience', field: 'dial' }); g = nexusAllow(g); g = applyAction(g, 'p', { type: 'prescienceAnswer', value: 10 }); const battle = g.battle!.event; g = beginNexusSardaukar(prepareSardaukarBattle(g)); g = applyAction(g, 'q', { type: 'card', card: karama, mode: 'cancel' }); assert.equal(g.battle!.event, battle); assert.equal(g.battle!.prescience!.player, 'q'); assert.equal(g.battle!.prescience!.field, 'dial'); assert.equal(g.battle!.preparation?.kind, 'prescienceAnswer'); g = applyAction(nexusReload(g), 'p', { type: 'prescienceAnswer', value: 5 }); assert.equal(g.battle!.prescience!.value, 5); g = prepareSardaukarBattle(g); g = applyAction(g, 'p', { type: 'battlePlan', dial: 5, support: 5, leader: g.players[0].leaders[0].id, }); assert.equal(g.battle!.plans.p.dial, 5); assert.equal( g.nexusCards!.cards!.discard.filter((card) => card === 'emperor').length, 1, ); assert.equal(g.discard.filter((card) => card.id === karama).length, 1); nexusSardaukarInventory(g); }); void test('canceled Sardaukar reopen the affected Cunning inspection while preserving the other answered element', () => { for (const [nativeDial, extraLeader] of [ [true, false], [false, false], [true, true], ]) { let g = nexusSardaukarFixture({ opponentFaction: 'atreides' }); const leader = g.players[0].leaders[0].id; const unchangedField = extraLeader ? 'leader' : nativeDial ? 'defense' : 'weapon'; const unchangedValue = extraLeader ? leader : null; const karama = hold(g, 'q', 'karama'); const cards = g.nexusCards!.cards!; cards.deck = [ 'atreides', ...cards.deck.filter((card) => card !== 'atreides'), ]; g.nexusCards!.cards = drawNexusCard(cards, 'q', g.players, () => 0); g = applyAction(g, 'q', { type: 'prescience', field: nativeDial ? 'dial' : 'weapon', }); g = nexusAllow(g); g = applyAction(g, 'p', { type: 'prescienceAnswer', value: nativeDial ? 10 : null, }); g = prepareSardaukarBattle(g); g = applyAction(g, 'q', { type: 'nexusAtreides', event: g.battle!.event, mode: 'cunning', field: extraLeader ? 'leader' : nativeDial ? 'defense' : 'dial', }); g = nexusAllow(g); g = applyAction(g, 'p', { type: 'nexusPrescienceAnswer', event: g.battle!.event, value: nativeDial ? unchangedValue : 10, }); g = beginNexusSardaukar(prepareSardaukarBattle(g)); g = applyAction(g, 'q', { type: 'card', card: karama, mode: 'cancel' }); assert.equal( g.battle!.preparation?.kind, nativeDial ? 'prescienceAnswer' : 'nexusPrescienceAnswer', ); assert.deepEqual(viewGame(g, 'r').battle!.nexusInsights, []); g = applyAction( nexusReload(g), 'p', nativeDial ? { type: 'prescienceAnswer', value: 5 } : { type: 'nexusPrescienceAnswer', event: g.battle!.event, value: 5 }, ); const commitments = viewGame(g, 'p').battle!.ownCommitments; assert.ok(commitments.some((c) => c.field === 'dial' && c.value === 5)); assert.ok( commitments.some( (c) => c.field === unchangedField && c.value === unchangedValue, ), ); g = prepareSardaukarBattle(g); if (extraLeader) nexusReject( g, 'p', { type: 'battlePlan', dial: 5, support: 5, leader: g.players[0].leaders.find( (candidate) => candidate.id !== leader, )!.id, }, /Every element revealed by a battle inspection must remain unchanged/, ); g = applyAction(g, 'p', { type: 'battlePlan', dial: 5, support: 5, leader, }); assert.equal(g.battle!.plans.p.dial, 5); nexusSardaukarInventory(g); } }); void test('Secret Ally inspection retains private history when canceled Sardaukar require a revised dial', () => { let g = nexusSardaukarFixture(); const karama = hold(g, 'q', 'karama'); const cards = g.nexusCards!.cards!; cards.deck = [ 'atreides', ...cards.deck.filter((card) => card !== 'atreides'), ]; g.nexusCards!.cards = drawNexusCard(cards, 'q', g.players, () => 0); g = applyAction(g, 'q', { type: 'nexusAtreides', event: g.battle!.event, mode: 'secretAlly', field: 'dial', }); g = applyAction(g, 'p', { type: 'nexusPrescienceAnswer', event: g.battle!.event, value: 10, }); g = beginNexusSardaukar(prepareSardaukarBattle(g)); g = applyAction(g, 'q', { type: 'card', card: karama, mode: 'cancel' }); assert.equal(g.battle!.preparation?.kind, 'nexusPrescienceAnswer'); assert.deepEqual(viewGame(g, 'r').battle!.nexusInsights, []); assert.ok( viewGame(g, 'q').battle!.nexusInsights.some( (insight) => insight.value === 10 && !insight.active, ), ); g = applyAction(nexusReload(g), 'p', { type: 'nexusPrescienceAnswer', event: g.battle!.event, value: 5, }); const answers = viewGame(g, 'q').battle!.nexusInsights; assert.ok(answers.some((insight) => insight.value === 10 && !insight.active)); assert.ok(answers.some((insight) => insight.value === 5 && insight.active)); g = prepareSardaukarBattle(g); g = applyAction(g, 'p', { type: 'battlePlan', dial: 5, support: 5, leader: g.players[0].leaders[0].id, }); nexusSardaukarInventory(g); });