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 { 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 type { RicheseCunningRateSource } from '../game/richese-cunning-rate'; import { createRicheseCunningPurchasedRateFixture as fixture, actRicheseCunningPurchasedRate as act, passRicheseCunningPurchasedRate as pass, finishRicheseCunningPurchasedRate as finish, commitRicheseCunningPurchasedRate as commit, richeseCunningPurchasedRatePlayer as player, richeseCunningPurchasedRateInventory as inventory, richeseCunningPurchasedRateNexusInventory as nexusInventory, } from './fixture-richese-cunning-purchased-rate'; function frame(game: Game) { const record = game.nexusRicheseCunningHistory?.at(-1); assert.ok(record); return JSON.parse(record.frame) as { cost: number; richesePair: { karama?: RicheseCunningRateSource } }; } 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 three-seat Cunning uses Emperor's actual purchased rate for the combined twelve-unit invoice`, () => { const f = fixture({ advanced }), before = structuredClone(player(f.game, f.owner)); assert.equal(f.guild, null); assert.ok(f.observer); assert.equal(player(f.beforePurchase, f.holder).hand.some(card => card.id === f.karama), true); assert.deepEqual(f.activation.steps[0], { actor: f.holder, action: f.activationAction }); assert.equal(f.activationAction.mode, 'shipment'); assert.equal(f.activationAction.target, f.owner); assert.deepEqual(f.purchasedRate, { player: f.owner, owner: f.holder, card: f.karama }); assert.equal(viewGame(f.game, f.owner).nexusRicheseCunning?.blocked, null); assert.equal(f.fullCost, 12); assert.equal(f.cost, 6); const result = commit(f.game, f), after = player(result.game, f.owner), saved = frame(result.game); assert.equal(saved.cost, 6); assert.deepEqual(saved.richesePair.karama, f.purchasedRate); assert.equal(Object.hasOwn(saved.richesePair.karama!, 'costClaimed'), false); assert.equal(before.spice - after.spice, 6); assert.equal(after.reserves, 4); assert.deepEqual(after.forces, { [f.location]: 16 }); assert.equal(after.shipped, true); assert.equal(after.moved, 0); assert.equal(after.noField?.deployed?.tokenId, f.concealedToken); assert.equal(result.game.karamaShipping, null); assert.equal(result.game.nexusRicheseCunningLast?.stage, 'shipped'); assert.equal(result.game.nexusCards!.cards!.discard.filter(card => card === 'richese').length, 1); assert.equal(result.game.discard.filter(card => card.id === f.karama).length, 1); assert.equal(player(result.game, f.holder).hand.some(card => card.id === f.karama), false); assert.deepEqual(inventory(result.game), f.originalInventory); assert.deepEqual(nexusInventory(result.game), f.originalNexusInventory); assert.equal(f.sourceActionCount, f.actions.length); }); } void test('a genuine fourth Guild seat permits the purchased pair but receives none of its bank-paid fee', () => { const f = fixture({ advanced: true, guild: true }); assert.ok(f.guild && f.guildKarama && f.guildNexusPass); assert.notEqual(f.guildKarama, f.karama); assert.deepEqual(f.guildNexusPass.steps[0], { actor: f.guild, action: { type: 'nexusCardChoice', turn: 2, card: null, choice: 'keep', ownRedraws: 0, } }); const guildSpice = player(f.game, f.guild).spice; const pending = act(f.game, f.owner, f.action); assert.equal(pending.response?.kind, 'richeseNoField'); assert.equal(pending.pendingShipment?.cost, 6); assert.deepEqual(pending.pendingShipment?.richesePair?.karama, f.purchasedRate); const interception = pass(pending); assert.equal(interception.decision?.kind, 'guildShipment'); assert.equal(interception.decision?.player, f.guild); const result = finish(act(interception, f.guild, { type: 'decision', allow: true })); assert.equal(player(result, f.owner).spice, player(f.game, f.owner).spice - 6); assert.equal(player(result, f.guild).spice, guildSpice); assert.ok(player(result, f.guild).hand.some(card => card.id === f.guildKarama)); assert.equal(result.karamaShipping, null); assert.equal(result.nexusRicheseCunningLast?.stage, 'shipped'); assert.notEqual(result.response?.kind, 'guildIncome'); assert.notEqual(result.response?.kind, 'guildRate'); assert.deepEqual(inventory(result), f.originalInventory); assert.deepEqual(nexusInventory(result), f.originalNexusInventory); }); void test('Guild cancelling the original No-Field retains Emperor’s grant for an ordinary bank-rate replacement', () => { const f = fixture({ advanced: true, guild: true }); assert.ok(f.guild && f.guildCancel); const before = structuredClone(player(f.game, f.owner)), guildSpice = player(f.game, f.guild).spice; const pending = act(f.game, f.owner, f.action); const stopped = finish(act(pending, f.guild, f.guildCancel)), after = player(stopped, f.owner); assert.equal(after.spice, before.spice); assert.equal(after.reserves, before.reserves); assert.deepEqual(after.forces, before.forces); assert.deepEqual(after.noField, before.noField); assert.equal(after.shipped, false); assert.equal(after.moved, 0); assert.deepEqual(stopped.karamaShipping, f.purchasedRate); 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); const replaced = finish(act(stopped, f.owner, f.replacement)); assert.equal(player(replaced, f.owner).spice, before.spice - 1); assert.equal(player(replaced, f.owner).reserves, 19); assert.deepEqual(player(replaced, f.owner).forces, { [f.location]: 1 }); assert.equal(player(replaced, f.guild).spice, guildSpice); assert.equal(replaced.karamaShipping, null); assert.equal(replaced.discard.filter(card => card.id === f.karama).length, 1); assert.deepEqual(inventory(replaced), f.originalInventory); assert.deepEqual(nexusInventory(replaced), f.originalNexusInventory); }); void test('native Guild special prevention consumes the pair opportunity and refunds only the unclaimed original activation card', () => { const f = fixture({ advanced: true, guild: true }); assert.ok(f.guild && f.guildStop && f.guildKarama); const before = structuredClone(player(f.game, f.owner)); const interception = pass(act(f.game, f.owner, f.action)); assert.equal(interception.decision?.kind, 'guildShipment'); const stopped = finish(act(interception, f.guild, f.guildStop)), after = player(stopped, f.owner); assert.equal(after.spice, before.spice); assert.equal(after.reserves, before.reserves); assert.deepEqual(after.forces, before.forces); assert.deepEqual(after.noField, before.noField); assert.equal(after.shipped, true); assert.equal(after.moved, 0); assert.equal(stopped.karamaShipping, null); assert.equal(stopped.pendingShipment ?? null, null); assert.equal(stopped.nexusRicheseCunningLast?.stage, 'stopped'); assert.equal(stopped.nexusCards!.cards!.discard.filter(card => card === 'richese').length, 1); assert.ok(player(stopped, f.holder).hand.some(card => card.id === f.karama)); assert.equal(stopped.discard.filter(card => card.id === f.karama).length, 0); assert.equal(stopped.discard.filter(card => card.id === f.guildKarama).length, 1); assert.equal(player(stopped, f.guild).specialKaramaUsed, true); assert.deepEqual(inventory(stopped), f.originalInventory); assert.deepEqual(nexusInventory(stopped), f.originalNexusInventory); }); void test('one paid companion and legacy zero-companion city pairs each cost one and still route the purchased fee to Bank', () => { for (const companions of [0, 1]) { const f = fixture({ companions, guild: true }); assert.ok(f.guild); const guildSpice = player(f.game, f.guild).spice; assert.equal(f.cost, 1); const result = commit(f.game, f), saved = frame(result.game), after = player(result.game, f.owner); assert.deepEqual(saved.richesePair.karama, f.purchasedRate); assert.equal(saved.cost, 1); assert.equal(player(f.game, f.owner).spice - after.spice, 1); assert.equal(after.reserves, 20 - (companions + 5)); assert.deepEqual(after.forces, { [f.location]: companions + 5 }); assert.equal(player(result.game, f.guild).spice, guildSpice); assert.equal(result.game.karamaShipping, null); assert.deepEqual(inventory(result.game), f.originalInventory); assert.deepEqual(nexusInventory(result.game), f.originalNexusInventory); } }); void test('CONTROLLED claimed-cost metadata preserves the actual rate identity but prevents another activation-card refund', () => { const f = fixture({ advanced: true, guild: true, costClaimed: true }); assert.ok(f.guild && f.guildStop && f.guildKarama); assert.equal(f.controlledCostClaimed, true); assert.equal(f.game.karamaShipping?.costClaimed, true); assert.ok(f.staging.some(line => line.startsWith('CONTROLLED SUPPLEMENT:'))); const pending = act(f.game, f.owner, f.action); assert.equal(pending.pendingShipment?.cost, 6); assert.deepEqual(pending.pendingShipment?.richesePair?.karama, { player: f.owner, owner: f.holder, card: f.karama }); const interception = pass(pending); assert.equal(interception.decision?.kind, 'guildShipment'); const stopped = finish(act(interception, f.guild, f.guildStop)); assert.equal(player(stopped, f.owner).reserves, 20); assert.deepEqual(player(stopped, f.owner).forces, {}); assert.equal(player(stopped, f.holder).hand.some(card => card.id === f.karama), false); assert.equal(stopped.discard.filter(card => card.id === f.karama).length, 1); assert.equal(stopped.discard.filter(card => card.id === f.guildKarama).length, 1); assert.equal(stopped.karamaShipping, null); assert.deepEqual(inventory(stopped), f.originalInventory); }); void test('CONTROLLED pending-origin changes reject even when the city invoice remains one spice', () => { const f = fixture({ companions: 0, guild: true }); assert.ok(f.guild && f.guildKarama); const pending = act(f.game, f.owner, f.action); assert.equal(pending.response?.kind, 'richeseNoField'); assert.equal(pending.pendingShipment?.cost, 1); for (const changed of [null, { ...f.purchasedRate, owner: f.guild }, { ...f.purchasedRate, card: f.guildKarama }]) { // CONTROLLED metadata edit to an actually declared native pending pair; // no card, force, phase or original fixture source is constructed here. const controlled = structuredClone(pending); controlled.karamaShipping = changed; reject(controlled, f.owner, { type: 'passResponse' }); } 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 originallyFullPrice = act(f.beforePurchase, f.owner, legacy); assert.equal(originallyFullPrice.pendingShipment?.cost, 1); assert.equal(originallyFullPrice.pendingShipment?.richesePair?.karama, undefined); // CONTROLLED later card-omitted legacy grant. The pair itself was genuinely // declared without a purchase; no physical card moves or gets duplicated. const later = structuredClone(originallyFullPrice); later.karamaShipping = { player: f.owner, owner: f.holder }; reject(later, f.owner, { type: 'passResponse' }, /original purchased Karama rate source/); }); void test('the shared payment filter accepts a real sixteen-companion group affordable only under its purchased rate', () => { const f = fixture(), action = { ...f.action, noFieldReserves: 16 }; assert.equal(player(f.game, f.owner).spice, 12); assert.equal(botHomeworldShipmentPaymentAllowed(viewGame(f.game, f.owner), action), true); assert.equal(botHomeworldShipmentPaymentAllowed(viewGame(f.beforePurchase, f.owner), action), false); const result = commit(f.game, f, action); assert.equal(player(result.game, f.owner).spice, 3); assert.equal(player(result.game, f.owner).reserves, 0); assert.deepEqual(player(result.game, f.owner).forces, { [f.location]: 20 }); assert.equal(result.game.karamaShipping, null); }); void test('all four existing profiles retain legal positive Cunning pair choices under the actual purchased rate', () => { 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} proposes a legal positive paid-group pair using the original purchased grant`); const revealed = player(f.game, f.owner).noField!.tokens.find(token => token.id === action.revealedToken); assert.ok(revealed); const quote = quoteRicheseHomeworldPair({ enabled: true, reserves: 20, amount: Number(action.noFieldReserves), revealedValue: revealed.value }); const cost = reserveShipmentCost({ faction: 'richese', halfRate: true }, territory(String(action.territory)).type, quote.priced); const result = commit(structuredClone(f.game), f, action); assert.equal(player(result.game, f.owner).spice, player(f.game, f.owner).spice - cost); assert.equal(player(result.game, f.owner).reserves, 20 - quote.physical); assert.equal(result.game.karamaShipping, null); assert.equal(result.game.nexusRicheseCunningLast?.stage, 'shipped'); assert.deepEqual(inventory(result.game), f.originalInventory); assert.deepEqual(nexusInventory(result.game), f.originalNexusInventory); } });