import assert from 'node:assert/strict'; import test from 'node:test'; import { applyAction, viewGame, type Action, type Game } from '../game/engine'; import { botActions } from '../game/bots'; import { DIFFICULTIES } from '../game/bot-profiles'; import { harassCustody, harassWithdrawGame, takeHarassCard } from './fixture-harass-withdraw'; const card = 'ecaz-reinforcements'; /** Genuine Advanced setup and native battle admission; the shared fixture * stages physical board forces and card custody, preserving every counter. */ function fixture(): Game { const game = harassWithdrawGame({ advanced: true, factions: ['emperor','atreides','harkonnen'] }); takeHarassCard(game, 'a', card); harassCustody(game); return game; } function plan(elite?: number, useCard = true): Action { return { type: 'battlePlan', dial: 1, support: 1, leader: 'emperor-0', weapon: useCard ? card : null, defense: null, ...(elite === undefined ? {} : { reinforcementsElite: elite }), } as Action; } function second(game: Game): Game { return applyAction(game, 'd', { type:'battlePlan', dial:1, support:1, leader:'atreides-0', weapon:null, defense:null }); } function finish(game: Game): Game { let next = applyAction(game, 'a', {type:'traitorCall',call:false}); next = applyAction(next, 'd', {type:'traitorCall',call:false}); if (next.decision?.kind === 'moritaniAssassinate') next = applyAction(next, next.decision.player, {type:'decision', event:next.decision.event, decline:true}); harassCustody(next); return next; } for (const selected of [0,1,2,3] as const) { void test(`Reinforcements commits the selected ${selected} elite reserve cost`, () => { const start = fixture(), initial = structuredClone(start); const owner = start.players.find(row => row.id === 'a')!; assert.ok(owner.reserves - owner.elites!.reserves >= 3); assert.ok(owner.elites!.reserves >= 3); const one = applyAction(start, 'a', plan(selected)); assert.deepEqual(start, initial); assert.equal(one.players[0].reserves, owner.reserves, 'Sealing does not spend the cost'); assert.equal(one.players[0].elites!.reserves, owner.elites!.reserves); assert.deepEqual(viewGame(one, 'd').battle?.plans, {}, 'A sealed plan remains private'); const resolved = finish(second(one)); const after = resolved.players.find(row => row.id === 'a')!; assert.equal(after.reserves, owner.reserves - 3); assert.equal(after.elites!.reserves, owner.elites!.reserves - selected); assert.equal(after.elites!.tanks, owner.elites!.tanks + selected); assert.equal(resolved.discard.filter(row => row.id === card).length, 1); assert.equal(resolved.log.filter(row => row.text.includes('used Reinforcements: three reserve forces')).length, 1); }); } void test('legacy omitted selection preserves normal-first reserve costs', () => { const initial = fixture(); const owner = initial.players[0]; const done = finish(second(applyAction(initial, 'a', plan()))); assert.equal(done.players[0].reserves, owner.reserves - 3); assert.equal(done.players[0].elites!.reserves, owner.elites!.reserves); }); void test('invalid or irrelevant elite-cost selections reject without mutating the game', () => { const game = fixture(); for (const action of [plan(-1), plan(4), plan(1.5), plan(Number.NaN), plan(1, false)]) { const before = structuredClone(game); assert.throws(() => applyAction(game, 'a', action)); assert.deepEqual(game, before); } const restricted = fixture(); restricted.players[0].elites!.reserves = 2; restricted.players[0].elites!.tanks = 3; restricted.players[0].reserves = 3; restricted.players[0].tanks = 12; harassCustody(restricted); for (const invalid of [0,3]) { const before = structuredClone(restricted); assert.throws(() => applyAction(restricted, 'a', plan(invalid))); assert.deepEqual(restricted, before); } assert.doesNotThrow(() => applyAction(restricted, 'a', plan(2))); }); void test('all bot profiles retain a legal battle plan at an eligible typed-cost opportunity', () => { const game = fixture(); for (const difficulty of DIFFICULTIES) { const view = viewGame(game, 'a'); view.players.find(row => row.id === 'a')!.bot = difficulty; const actions = botActions(view); const battle = actions.find(action => action.type === 'battlePlan'); assert.ok(battle, `${difficulty} must have a legal battle plan`); assert.doesNotThrow(() => applyAction(game, 'a', battle)); } });