import test from 'node:test'; import assert from 'node:assert/strict'; import { NativeReserveError, type NativeReserveSelections } from '../game/homeworld-native-reserves'; import { type HomeworldCustody, type HomeworldCustodyContext } from '../game/homeworld-custody'; import { quoteReinforcementsHomeworldCost as quote, reinforcementsHomeworldSources as sources, } from '../game/reinforcements-homeworld'; import { ReinforcementsError } from '../game/reinforcements'; import { combatArmy } from '../game/combat-location'; import { assertReinforcementsHomeworldCustody, createReinforcementsHomeworldFixture, finishReinforcementsHomeworldBattle, reinforcementsHomeworldPlayer as player, reinforcementsHomeworldPool as pool, sealReinforcementsHomeworldPlan, } from './fixture-reinforcements-homeworld'; const kaitain = 'homeworld:emperor'; const salusa = 'homeworld:emperor:salusa'; const ix = 'homeworld:ixians'; const units = (normal: number, elite = 0) => ({ normal, elite }); function fixture(): { context: HomeworldCustodyContext; custody: HomeworldCustody } { return { context: { advanced: true, players: [ { id: 'e', faction: 'emperor', reserves: 10, eliteReserves: 3 }, { id: 'i', faction: 'ixians', reserves: 10, eliteReserves: 2 }, ] }, custody: { salusa: units(3, 2), visitors: { [kaitain]: { i: units(2) }, [ix]: { e: units(1, 1) }, } }, }; } void test('Homeworld options and costs honor the selected typed fee and preserve normal-first defaults', () => { const { context, custody } = fixture(); const before = structuredClone({ context, custody }); const defaults = sources(context, custody, 'e'); assert.deepEqual(defaults, sources(context, custody, 'e', 0)); assert.equal(defaults.length, 4); const mixed = sources(context, custody, 'e', 2); assert.equal(mixed.length, 4); for (const allocation of mixed) { assert.equal(allocation[kaitain].normal + allocation[salusa].normal, 1); assert.equal(allocation[kaitain].elite + allocation[salusa].elite, 2); const result = quote(context, custody, 'e', allocation, 2); assert.deepEqual({ normal: result.normal, elite: result.elite, bonus: result.bonus }, { normal: 1, elite: 2, bonus: 2 }); assert.deepEqual(result.sources, allocation); assert.equal(result.native.players[0].reserves, 7); assert.equal(result.native.players[0].eliteReserves, 1); } assert.throws(() => quote(context, custody, 'e', undefined, 2), NativeReserveError); const defaultResult = quote(context, custody, 'e', defaults[0]); assert.deepEqual({ normal: defaultResult.normal, elite: defaultResult.elite }, { normal: 3, elite: 0 }); assert.deepEqual({ context, custody }, before); }); void test('single native Homeworld can automatically pay an explicit elite choice', () => { const { context, custody } = fixture(); assert.deepEqual(sources(context, custody, 'i', 2), [{ [ix]: units(1, 2) }]); const result = quote(context, custody, 'i', undefined, 2); assert.deepEqual(result.sources, { [ix]: units(1, 2) }); assert.deepEqual({ normal: result.normal, elite: result.elite, bonus: result.bonus }, { normal: 1, elite: 2, bonus: 2 }); assert.equal(result.native.players[1].eliteReserves, 0); }); void test('selected types and explicit native allocations are both validated', () => { const { context, custody } = fixture(); const before = structuredClone({ context, custody }); for (const choice of [-1, 4, 1.5, NaN, Infinity]) { assert.throws(() => sources(context, custody, 'e', choice), ReinforcementsError); assert.throws(() => quote(context, custody, 'e', undefined, choice), ReinforcementsError); } for (const allocation of [ { [kaitain]: units(3) }, // valid default fee, wrong for the chosen mix { [ix]: units(1, 2) }, // visitor forces cannot pay the fee { [kaitain]: units(1, 2) }, // Kaitain has only one elite { [kaitain]: units(1, 1) }, // does not sum to the chosen fee { [kaitain]: { normal: 1, elite: 2, extra: 0 } }, ]) { assert.throws(() => quote(context, custody, 'e', allocation as unknown as NativeReserveSelections, 2), NativeReserveError); } assert.deepEqual({ context, custody }, before); }); void test('Advanced Emperor seals and pays a mixed Homeworld fee from the selected Worlds', () => { const allocation = { [kaitain]: units(1), [salusa]: units(1, 1) }; const f = createReinforcementsHomeworldFixture({ advanced: true, sources: allocation }); const before = player(f.game, f.owner); assert.deepEqual(pool(f.game, f.owner, kaitain), units(10)); assert.deepEqual(pool(f.game, f.owner, salusa), units(2, 1)); assert.deepEqual(combatArmy(f.game, f.owner, f.location), units(3, 4)); const action = { ...f.planActions[0].action, reinforcementsElite: 1 }; let game = sealReinforcementsHomeworldPlan(f.game, { actor: f.owner, action }); assert.deepEqual(game.homeworlds, f.game.homeworlds, 'Sealing does not pay the fee'); assert.equal(player(game, f.owner).elites!.reserves, before.elites!.reserves); game = sealReinforcementsHomeworldPlan(JSON.parse(JSON.stringify(game)), f.planActions[1]); const done = finishReinforcementsHomeworldBattle(game); const after = player(done, f.owner); assert.equal(done.lastBattleContext!.winner, f.opponent); assert.deepEqual(pool(done, f.owner, kaitain), units(9)); assert.deepEqual(pool(done, f.owner, salusa), units(1)); assert.deepEqual(combatArmy(done, f.owner, f.location), units(0)); assert.equal(after.reserves, before.reserves - 3); assert.equal(after.elites!.reserves, before.elites!.reserves - 1); assert.equal(after.tanks, before.tanks + 10, 'Three fee forces and seven defeated invaders'); assert.equal(after.elites!.tanks, before.elites!.tanks + 5, 'One fee elite and four defeated elites'); assert.equal(after.battleLosses, before.battleLosses + 10); assertReinforcementsHomeworldCustody(f.game); assertReinforcementsHomeworldCustody(done); });