100 lines
5.7 KiB
TypeScript
100 lines
5.7 KiB
TypeScript
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);
|
|
});
|