import assert from 'node:assert/strict'; import test from 'node:test'; import type { GameView } from '../game/engine'; import { HomeworldCustodyError, homeworldForceGroups, type HomeworldCustody } from '../game/homeworld-custody'; import { homeworldPopulations } from '../game/homeworld-population'; import { richeseHomeworldMarkerChoice } from '../game/homeworld-shipment-options'; import type { HomeworldShipmentContext } from '../game/homeworld-shipment'; import { createRicheseNoField } from '../game/richese-no-field'; import { quoteRicheseHomeworldMarkerShipment as quote, type RicheseHomeworldMarkerShipmentIntent } from '../game/richese-homeworld-marker-shipment'; function fixture() { const context: HomeworldShipmentContext = {advanced: false, players: [ {id: 'r', faction: 'richese', reserves: 12, eliteReserves: 0, ally: null}, {id: 'a', faction: 'atreides', reserves: 17, eliteReserves: 0, ally: null}, {id: 'g', faction: 'guild', reserves: 20, eliteReserves: 0, ally: null}, ]}; const custody: HomeworldCustody = {salusa: null, visitors: { 'homeworld:atreides': {r: {normal: 2, elite: 0}}, }}; const noField = createRicheseNoField(['zero', 'three', 'five']); const intent: RicheseHomeworldMarkerShipmentIntent = {player: 'r', destination: 'homeworld:atreides', tokenId: 'zero', forces: {normal: 3, elite: 0}}; return {context, custody, noField, intent}; } void test('fresh marker and paid physical companions form one detached shipment in both rulesets', () => { for (const advanced of [false, true]) for (const tokenId of ['zero', 'three', 'five']) { const f = fixture(); f.context.advanced = advanced; f.intent.tokenId = tokenId; const before = structuredClone(f); const result = quote(f.context, f.custody, f.intent, f.noField); assert.equal(result.amount, 4); assert.equal(result.cost, 4); assert.equal(result.elite, 0); assert.deepEqual(result.noField.deployed, {tokenId, controller: 'r', location: {homeworld: 'homeworld:atreides'}}); assert.equal(result.players?.find(player => player.id === 'r')?.reserves, 9); assert.deepEqual(result.state?.visitors['homeworld:atreides'].r, {normal: 5, elite: 0}); assert.deepEqual(result.sources?.[0], {homeworld: 'homeworld:richese', player: 'r', before: {normal: 12, elite: 0}, after: {normal: 9, elite: 0}}); assert.deepEqual(f, before); result.state!.visitors['homeworld:atreides'].r.normal = 0; assert.deepEqual(f, before); } }); void test('whole mixed invoice rounds once and zero companions keep the legacy quote', () => { const f = fixture(); const legacy = {...f.intent}; delete legacy.forces; assert.deepEqual(quote(f.context, f.custody, {...legacy, forces: {normal: 0, elite: 0}}, f.noField), quote(f.context, f.custody, legacy, f.noField)); f.context.purchasedGuildRate = true; for (const count of [1, 2, 3, 11, 12]) { f.intent.forces = {normal: count, elite: 0}; const result = quote(f.context, f.custody, f.intent, f.noField); assert.equal(result.amount, count + 1); assert.equal(result.cost, Math.ceil((count + 1) / 2)); } }); void test('invalid companion quantities, unavailable reserves and extra sources reject without mutation', () => { for (const forces of [ {normal: 13, elite: 0}, {normal: 1, elite: 1}, {normal: -1, elite: 0}, {normal: 0.5, elite: 0}, {normal: 1}, {normal: 1, elite: 0, extra: 0}, null, ]) { const f = fixture(); f.intent = {...f.intent, forces} as unknown as RicheseHomeworldMarkerShipmentIntent; const before = structuredClone(f); assert.throws(() => quote(f.context, f.custody, f.intent, f.noField), HomeworldCustodyError); assert.deepEqual(f, before); } const f = fixture(); f.intent = {...f.intent, source: 'homeworld:guild'} as RicheseHomeworldMarkerShipmentIntent; const before = structuredClone(f); assert.throws(() => quote(f.context, f.custody, f.intent, f.noField), HomeworldCustodyError); assert.deepEqual(f, before); }); void test('owned controls quote fresh reserve companions, exact source, affordability and rate', () => { const f = fixture(), populations = homeworldPopulations(f.context, f.custody); const view = { me: 'r', active: 'r', advanced: false, status: 'playing', phase: 5, homeworldShipment: {event: 'shipment', blocked: null}, aid: {available: 0, pledged: 0}, players: f.context.players.map(player => ({...player, name: player.faction, spice: player.id === 'r' ? 6 : undefined, shipped: false, elites: {reserves: 0}})), homeworlds: {worlds: homeworldForceGroups(f.context, f.custody).map(world => ({...world, ...populations.find(population => population.location === world.id)!}))}, richeseNoField: {owner: 'r', event: 'marker', private: f.noField, canShip: true}, } as unknown as GameView; const before = structuredClone(view); const chosen = richeseHomeworldMarkerChoice(view, 'homeworld:atreides', 'three', 0, undefined, {normal: 3, elite: 0}); assert.equal(chosen.amount, 4); assert.equal(chosen.cost, 4); assert.deepEqual(chosen.action, {type: 'homeworldShip', event: 'shipment', destination: 'homeworld:atreides', sources: {'homeworld:richese': {normal: 3, elite: 0}}, noField: 'three', noFieldEvent: 'marker', allyPayment: 0}); assert.deepEqual(view, before); const discount = {...view, karamaShipping: {player: 'r'}} as GameView; assert.equal(richeseHomeworldMarkerChoice(discount, 'homeworld:atreides', 'three', 0, undefined, {normal: 2, elite: 0}).cost, 2); view.players[0].spice = 2; assert.equal(richeseHomeworldMarkerChoice(view, 'homeworld:atreides', 'three', 0, undefined, {normal: 3, elite: 0}).action, null); assert.equal(richeseHomeworldMarkerChoice(view, 'homeworld:atreides', 'three', 0, undefined, {normal: 13, elite: 0}).action, null); });