163 lines
11 KiB
TypeScript
163 lines
11 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
import { viewGame, type Action } from '../game/engine';
|
|
import { botActions } from '../game/bots';
|
|
import { DIFFICULTIES } from '../game/bot-profiles';
|
|
import { presenceAt } from '../game/force-presence';
|
|
import { territory } from '../game/board';
|
|
import { reserveShipmentCost } from '../game/shipment-price';
|
|
import { quoteRicheseHomeworldPair } from '../game/richese-homeworld-shipment';
|
|
import { botHomeworldShipmentPaymentAllowed } from '../game/homeworld-payment-options';
|
|
import {
|
|
createRicheseHomeworldCunningFixture as fixture,
|
|
actRicheseHomeworldCunning as act,
|
|
commitRicheseHomeworldCunning as commit,
|
|
revealRicheseHomeworldCunning as reveal,
|
|
finishRicheseHomeworldCunning as finish,
|
|
richeseHomeworldCunningPlayer as player,
|
|
richeseHomeworldCunningInventory as inventory,
|
|
richeseHomeworldCunningNexusInventory as nexusInventory,
|
|
richeseHomeworldCunningPopulation as population,
|
|
} from './fixture-richese-homeworld-cunning';
|
|
|
|
for (const advanced of [false, true]) {
|
|
void test(`${advanced ? 'Advanced' : 'Basic'} native HIGH Cunning pays for eleven companions before its immediate five and retains a concealed three`, () => {
|
|
const f = fixture({ advanced }); assert.ok(f.observer && f.nexusAlliance && f.nexusDraw);
|
|
const before = structuredClone(player(f.game, f.owner));
|
|
assert.equal(f.game.turn, 2); assert.equal(f.game.active, f.owner);
|
|
assert.equal(player(f.game, f.holder).ally, f.observer);
|
|
assert.equal(player(f.game, f.observer).ally, f.holder);
|
|
assert.equal(before.ally, null); assert.equal(before.reserves, 20); assert.equal(before.spice, 12);
|
|
assert.equal(f.game.nexusCards!.cards!.hands[f.owner], 'richese');
|
|
assert.equal(f.nexusDraw.steps[0].action.type, 'nexusCardChoice');
|
|
assert.equal(f.nexusDraw.steps[0].action.choice, 'draw');
|
|
assert.equal(viewGame(f.game, f.owner).richeseNoField?.homeworldForcesEnabled, true);
|
|
assert.deepEqual(f.pairQuote, { paid: 11, physical: 16, priced: 12, revealed: 5 });
|
|
const action: Action = 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?.amount, 1);
|
|
assert.equal(result.requested.pendingShipment?.noFieldReserves, 11);
|
|
assert.equal(result.requested.pendingShipment?.richesePair?.revealedTokenId, f.revealedToken);
|
|
assert.equal(result.requested.pendingShipment?.noFieldSkillProof, undefined, 'The original pair frame owns its optional physical group');
|
|
assert.equal(result.requested.pendingShipment?.cost, 12);
|
|
assert.equal(requested.reserves, 20); assert.deepEqual(requested.forces, {}); assert.equal(requested.spice, 12);
|
|
assert.deepEqual(requested.noField, before.noField); assert.equal(requested.shipped, false);
|
|
assert.equal(result.requested.nexusCards!.cards!.hands[f.owner], null);
|
|
assert.equal(result.requested.nexusCards!.cards!.discard.filter(card => card === 'richese').length, 1);
|
|
assert.equal(result.requested.nexusRicheseCunningLast?.stage, 'pending');
|
|
assert.equal(after.reserves, 4); assert.deepEqual(after.forces, { [f.location]: 16 });
|
|
assert.equal(after.spice, 0); assert.equal(after.shipped, true); assert.equal(after.moved, 0);
|
|
assert.equal(after.noField?.deployed?.tokenId, f.concealedToken);
|
|
assert.equal(after.noField?.lastShipped, f.concealedToken);
|
|
assert.equal(presenceAt(after, f.territory), 17);
|
|
assert.equal(population(result.game).side, 'low');
|
|
assert.equal(result.game.nexusRicheseCunningLast?.stage, 'shipped');
|
|
assert.equal(result.game.nexusRicheseCunningHistory?.length, 1);
|
|
const frame = JSON.parse(result.game.nexusRicheseCunningHistory![0].frame) as { noFieldReserves?: number };
|
|
assert.equal(frame.noFieldReserves, 11);
|
|
assert.equal(viewGame(result.game, f.holder).richeseNoField?.private, null);
|
|
assert.equal(viewGame(result.game, f.holder).richeseNoField?.public.lastUsed, null);
|
|
assert.ok(player(result.game, f.holder).hand.some(card => card.id === f.karama));
|
|
const revealed = reveal(result.game, f), done = player(revealed.game, f.owner);
|
|
assert.equal(done.reserves, 1); assert.deepEqual(done.forces, { [f.location]: 19 });
|
|
assert.equal(done.noField?.deployed, null); assert.equal(done.noField?.lastShipped, f.concealedToken);
|
|
assert.equal(done.spice, 0); assert.equal(done.moved, 0);
|
|
assert.deepEqual(inventory(revealed.game), f.originalInventory);
|
|
assert.deepEqual(nexusInventory(revealed.game), f.originalNexusInventory);
|
|
assert.equal(f.sourceActionCount, f.actions.length);
|
|
});
|
|
}
|
|
|
|
void test('sixteen genuine paid companions reserve stock before the immediate five; the concealed three later reveals zero', () => {
|
|
const f = fixture({ companions: 16 });
|
|
assert.equal(f.game.turn, 3);
|
|
assert.deepEqual(f.funding.map(record => record.steps[0].action.amount), [10, 2]);
|
|
assert.equal(player(f.game, f.owner).reserves, 20);
|
|
assert.equal(player(f.game, f.owner).spice, 17);
|
|
assert.deepEqual(f.pairQuote, { paid: 16, physical: 20, priced: 17, revealed: 4 });
|
|
const result = commit(f.game, f), after = player(result.game, f.owner);
|
|
assert.equal(after.spice, 0); assert.equal(after.reserves, 0);
|
|
assert.deepEqual(after.forces, { [f.location]: 20 }); assert.equal(after.tanks, 0);
|
|
assert.equal(after.noField?.deployed?.tokenId, f.concealedToken);
|
|
const revealed = reveal(result.game, f), done = player(revealed.game, f.owner);
|
|
assert.equal(done.reserves, 0); assert.deepEqual(done.forces, after.forces);
|
|
assert.equal(done.noField?.deployed, null); assert.equal(done.noField?.lastShipped, f.concealedToken);
|
|
assert.equal(done.spice, 0); assert.equal(done.tanks, 0);
|
|
assert.equal(revealed.game.nexusCards!.cards!.discard.filter(card => card === 'richese').length, 1);
|
|
assert.deepEqual(inventory(revealed.game), f.originalInventory);
|
|
assert.deepEqual(nexusInventory(revealed.game), f.originalNexusInventory);
|
|
});
|
|
|
|
void test('the original Emperor Karama prevents the whole unpaid pair and paid group while its declared Nexus stays spent', () => {
|
|
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.nexusCards!.cards!.hands[f.owner], null);
|
|
const stopped = finish(act(pending, f.holder, f.cancel)), after = player(stopped, f.owner);
|
|
assert.equal(after.reserves, before.reserves); assert.deepEqual(after.forces, before.forces);
|
|
assert.equal(after.spice, before.spice); assert.equal(after.shipped, false); assert.equal(after.moved, 0);
|
|
assert.deepEqual(after.noField, before.noField); assert.equal(after.noFieldEvent, before.noFieldEvent);
|
|
assert.equal(stopped.pendingShipment ?? null, null);
|
|
assert.equal(stopped.nexusRicheseCunningLast?.stage, 'stopped');
|
|
assert.equal(stopped.nexusCards!.cards!.hands[f.owner], null);
|
|
assert.equal(stopped.nexusCards!.cards!.discard.filter(card => card === 'richese').length, 1);
|
|
assert.equal(stopped.discard.filter(card => card.id === f.karama).length, 1);
|
|
assert.equal(viewGame(stopped, f.owner).nexusRicheseCunning, null);
|
|
const replacement = finish(act(stopped, f.owner, f.replacement)), replaced = player(replacement, f.owner);
|
|
assert.equal(replaced.reserves, 19); assert.deepEqual(replaced.forces, { [f.location]: 1 });
|
|
assert.equal(replaced.spice, before.spice - 1); assert.equal(replaced.shipped, true);
|
|
assert.deepEqual(replaced.noField, before.noField);
|
|
assert.deepEqual(inventory(replacement), f.originalInventory);
|
|
assert.deepEqual(nexusInventory(replacement), f.originalNexusInventory);
|
|
});
|
|
|
|
void test('omitting the new reserve count preserves an original native pair priced as one with only its immediate token materialized', () => {
|
|
const f = fixture();
|
|
const legacy: Action = { type: 'ship', noField: f.concealedToken, event: f.event, revealedToken: f.revealedToken,
|
|
nexus: f.nexusEvent, territory: f.territory, sector: f.sector, allyPayment: 0 };
|
|
const result = commit(f.game, f, legacy), after = player(result.game, f.owner);
|
|
assert.equal(result.requested.pendingShipment?.noFieldReserves, undefined);
|
|
assert.equal(result.requested.pendingShipment?.noFieldSkillProof, undefined);
|
|
assert.equal(result.requested.pendingShipment?.cost, 1);
|
|
assert.equal(after.spice, player(f.game, f.owner).spice - 1);
|
|
assert.equal(after.reserves, 15); assert.deepEqual(after.forces, { [f.location]: 5 });
|
|
assert.equal(after.noField?.deployed?.tokenId, f.concealedToken);
|
|
assert.equal(after.noField?.lastShipped, f.concealedToken);
|
|
assert.equal(result.game.nexusRicheseCunningLast?.stage, 'shipped');
|
|
assert.equal(result.game.nexusCards!.cards!.discard.filter(card => card === 'richese').length, 1);
|
|
assert.deepEqual(inventory(result.game), f.originalInventory);
|
|
assert.deepEqual(nexusInventory(result.game), f.originalNexusInventory);
|
|
});
|
|
|
|
void test('all four existing profiles retain a legal positive paid-group pair from the original Cunning offer', () => {
|
|
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.revealedToken &&
|
|
action.nexus === f.nexusEvent && typeof action.noFieldReserves === 'number' && action.noFieldReserves > 0);
|
|
assert.ok(action, `${difficulty} has a minimal positive paid Cunning companion path`);
|
|
const token = player(f.game, f.owner).noField!.tokens.find(token => token.id === action.revealedToken); assert.ok(token);
|
|
const quote = quoteRicheseHomeworldPair({ enabled: true, reserves: 20, amount: Number(action.noFieldReserves), revealedValue: token.value });
|
|
const price = reserveShipmentCost({ faction: 'richese', halfRate: false }, territory(String(action.territory)).type, quote.priced);
|
|
const result = commit(structuredClone(f.game), f, action), after = player(result.game, f.owner);
|
|
assert.equal(after.reserves, 20 - quote.physical);
|
|
assert.equal(Object.values(after.forces).reduce((total, count) => total + count, 0), quote.physical);
|
|
assert.equal(after.spice, player(f.game, f.owner).spice - price);
|
|
assert.equal(after.noField?.deployed?.tokenId, action.noField);
|
|
assert.equal(after.shipped, true); assert.equal(after.moved, 0);
|
|
assert.equal(result.game.nexusCards!.cards!.discard.filter(card => card === 'richese').length, 1);
|
|
assert.equal(result.game.nexusRicheseCunningLast?.stage, 'shipped');
|
|
assert.deepEqual(inventory(result.game), f.originalInventory);
|
|
assert.deepEqual(nexusInventory(result.game), f.originalNexusInventory);
|
|
}
|
|
});
|
|
|
|
void test('the shared AI payment guard admits the actual funded pair and rejects an unfunded paid group', () => {
|
|
const f = fixture(), view = viewGame(f.game, f.owner), before = structuredClone(view);
|
|
assert.equal(botHomeworldShipmentPaymentAllowed(view, f.action), true);
|
|
assert.equal(botHomeworldShipmentPaymentAllowed(view, { ...f.action, noFieldReserves: 16 }), false,
|
|
'Twenty available counters cannot fund a seventeen-spice invoice from the actual twelve-spice wallet');
|
|
assert.deepEqual(view, before);
|
|
});
|