Files
Dune/tests/richese-homeworld-shipment-runtime.test.ts
T

156 lines
9.8 KiB
TypeScript

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 { presenceAt } from '../game/force-presence';
import { reserveShipmentCost } from '../game/shipment-price';
import { territory } from '../game/board';
import { quoteRicheseHomeworldForces } from '../game/richese-homeworld-shipment';
import {
createRicheseHomeworldShipmentFixture as fixture,
actRicheseHomeworldShipment as act,
commitRicheseHomeworldShipment as commit,
revealRicheseHomeworldShipment as reveal,
finishRicheseHomeworldShipment as finish,
richeseHomeworldShipmentPlayer as player,
richeseHomeworldShipmentPopulation as population,
richeseHomeworldShipmentInventory as inventory,
} from './fixture-richese-homeworld-shipment';
function reject(game: Game, actor: string, action: Action, reason?: RegExp) {
const before = structuredClone(game);
if (reason) assert.throws(() => applyAction(game, actor, action), reason);
else assert.throws(() => applyAction(game, actor, action));
assert.deepEqual(game, before);
}
for (const advanced of [false, true]) {
void test(`${advanced ? 'Advanced' : 'Basic'} native HIGH Richese commits eleven paid reserves and one concealed marker, then may reveal after crossing LOW`, () => {
const f = fixture({ advanced }), before = structuredClone(player(f.game, f.owner));
assert.equal(f.game.turn, 2); assert.equal(f.game.phase, 5); assert.equal(f.game.active, f.owner);
assert.equal(f.funding.length, 1);
assert.deepEqual(f.funding[0].steps, [{ actor: f.holder, action: { type: 'bribe', target: f.owner, amount: 7 } }]);
assert.equal(before.spice, 12); assert.equal(before.bribes, 0); assert.equal(before.reserves, 20);
assert.equal(before.shipped, false); assert.equal(before.moved, 0);
assert.equal(population(f.game, f.owner).side, 'high');
assert.equal(viewGame(f.game, f.owner).richeseNoField?.homeworldForcesEnabled, true);
assert.deepEqual(quoteRicheseHomeworldForces({ enabled: true, reserves: before.reserves, amount: f.companions }),
{ paid: 11, physical: 11, priced: 12 });
const action: Action = advanced ? { ...f.action, amount: 1 } : f.action;
const result = commit(f.game, f, action), requested = player(result.requested, f.owner), after = player(result.game, f.owner);
assert.equal(result.requested.response?.kind, 'richeseNoField');
assert.equal(result.requested.pendingShipment?.noFieldReserves, 11);
assert.equal(result.requested.pendingShipment?.cost, 12);
assert.ok(result.requested.pendingShipment?.noFieldSkillProof);
assert.equal(requested.reserves, before.reserves); assert.equal(requested.spice, before.spice);
assert.equal(requested.shipped, false); assert.deepEqual(requested.forces, {});
assert.deepEqual(requested.noField, before.noField); assert.equal(requested.noFieldEvent, before.noFieldEvent);
assert.equal(after.reserves, 9); assert.deepEqual(after.forces, { [f.location]: 11 });
assert.equal(after.spice, 0); assert.equal(after.shipped, true); assert.equal(after.moved, 0);
assert.equal(after.noField?.deployed?.tokenId, f.token);
assert.deepEqual(after.noField?.deployed?.location, { territory: f.territory, sector: f.sector });
assert.equal(after.noField?.lastShipped, f.token);
assert.notEqual(after.noFieldEvent, before.noFieldEvent);
assert.equal(presenceAt(after, f.territory), 12, 'The marker counts once beside eleven physical counters');
assert.equal(population(result.game, f.owner).side, 'low');
assert.equal(viewGame(result.game, f.owner).richeseNoField?.homeworldForcesEnabled, false);
const opponent = viewGame(result.game, f.holder);
assert.equal(opponent.richeseNoField?.private, null);
assert.equal(opponent.richeseNoField?.public.deployed?.effectiveForces, 1);
assert.equal(opponent.richeseNoField?.public.lastUsed, null);
assert.ok(player(result.game, f.holder).hand.some(card => card.id === f.karama));
assert.deepEqual(inventory(result.game), f.originalInventory);
const revealed = reveal(result.game, f), done = player(revealed.game, f.owner);
assert.equal(done.noField?.deployed, null); assert.equal(done.noField?.lastShipped, f.token);
assert.equal(done.reserves, 4); assert.deepEqual(done.forces, { [f.location]: 16 });
assert.equal(done.spice, after.spice); assert.equal(done.moved, 0); assert.equal(done.shipped, true);
reject(revealed.game, f.owner, revealed.action, /No concealed/);
assert.deepEqual(inventory(revealed.game), f.originalInventory);
assert.equal(f.sourceActionCount, f.actions.length);
});
}
void test('the original single-marker omission still pays one without withdrawing HIGH physical reserves', () => {
const f = fixture(), before = structuredClone(player(f.game, f.owner));
const action: Action = { type: 'ship', noField: f.token, event: f.event,
territory: f.territory, sector: f.sector, allyPayment: 0 };
const result = commit(f.game, f, action), after = player(result.game, f.owner);
assert.equal(result.requested.pendingShipment?.noFieldReserves, undefined);
assert.equal(after.spice, before.spice - 1);
assert.equal(after.reserves, 20); assert.deepEqual(after.forces, {});
assert.equal(after.noField?.deployed?.tokenId, f.token);
assert.equal(presenceAt(after, f.territory), 1);
assert.equal(population(result.game, f.owner).side, 'high');
assert.deepEqual(inventory(result.game), f.originalInventory);
});
void test('sixteen genuine paid companions leave only four native reserves for a printed five marker to reveal', () => {
const f = fixture({ companions: 16 }), before = structuredClone(player(f.game, f.owner));
assert.equal(f.game.turn, 3);
assert.deepEqual(f.funding.map(record => record.steps[0].action.amount), [10, 2]);
assert.ok(f.actions.some(step => step.actor === f.holder && step.action.type === 'charity'));
assert.equal(before.spice, 17); assert.equal(before.reserves, 20);
const result = commit(f.game, f), after = player(result.game, f.owner);
assert.equal(after.reserves, 4); assert.deepEqual(after.forces, { [f.location]: 16 });
assert.equal(before.spice - after.spice, 17);
assert.equal(after.noField?.deployed?.tokenId, f.token);
const revealed = reveal(result.game, f), done = player(revealed.game, f.owner);
assert.equal(done.reserves, 0); assert.deepEqual(done.forces, { [f.location]: 20 });
assert.equal(done.tanks, 0); assert.equal(done.noField?.deployed, null);
assert.equal(done.noField?.lastShipped, f.token);
assert.deepEqual(inventory(revealed.game), f.originalInventory);
});
void test('the genuinely dealt opposing Karama prevents the entire unpaid mixed declaration and preserves ordinary replacement shipment', () => {
const f = fixture(), before = structuredClone(player(f.game, f.owner));
const pending = act(f.game, f.owner, f.action);
assert.equal(pending.response?.kind, 'richeseNoField');
assert.equal(pending.pendingShipment?.noFieldReserves, 11);
const prevented = finish(act(pending, f.holder, f.cancel)), after = player(prevented, f.owner);
assert.equal(after.reserves, before.reserves); assert.equal(after.spice, before.spice);
assert.deepEqual(after.forces, before.forces); assert.deepEqual(after.noField, before.noField);
assert.equal(after.noFieldEvent, before.noFieldEvent);
assert.equal(after.shipped, false); assert.equal(after.moved, 0);
assert.equal(prevented.pendingShipment ?? null, null);
assert.equal(prevented.discard.filter(card => card.id === f.karama).length, 1);
reject(prevented, f.owner, f.action, /prevented No-Field/);
const replaced = finish(act(prevented, f.owner, f.replacement)), replacement = player(replaced, f.owner);
assert.equal(replacement.reserves, before.reserves - 1);
assert.deepEqual(replacement.forces, { [f.location]: 1 });
assert.equal(replacement.spice, before.spice - 1);
assert.equal(replacement.shipped, true); assert.equal(replacement.moved, 0);
assert.deepEqual(replacement.noField, before.noField);
assert.deepEqual(inventory(replaced), f.originalInventory);
});
void test('mixed payloads preserve the one-marker amount and require positive available physical reserves', () => {
const f = fixture();
reject(f.game, f.owner, { ...f.action, amount: 2 }, /one No-Field/);
reject(f.game, f.owner, { ...f.action, noFieldReserves: 0 });
reject(f.game, f.owner, { ...f.action, noFieldReserves: 21 });
reject(f.game, f.owner, { ...f.replacement, noFieldReserves: 1 });
assert.deepEqual(inventory(f.game), f.originalInventory);
});
void test('all four existing profiles propose a legal positive companion shipment through the native response and payment', () => {
const f = fixture();
for (const difficulty of DIFFICULTIES) {
const view = viewGame(f.game, f.owner); view.players.find(player => player.id === f.owner)!.bot = difficulty;
const action = botActions(view).find(action => action.type === 'ship' && action.noField &&
typeof action.noFieldReserves === 'number' && action.noFieldReserves > 0);
assert.ok(action, `${difficulty} retains a minimal positive HIGH companion path`);
const companions = Number(action.noFieldReserves);
const priced = 1 + companions;
const expectedCost = reserveShipmentCost({ faction: 'richese', halfRate: false },
territory(String(action.territory)).type, priced);
const result = commit(structuredClone(f.game), f, action), after = player(result.game, f.owner);
assert.equal(after.reserves, 20 - companions);
assert.equal(Object.values(after.forces).reduce((total, count) => total + count, 0), companions);
assert.equal(after.spice, player(f.game, f.owner).spice - expectedCost);
assert.equal(after.shipped, true); assert.equal(after.moved, 0);
assert.ok(after.noField?.deployed);
assert.deepEqual(inventory(result.game), f.originalInventory);
}
});