Files
Dune/tests/truthtrance-choam-promises-engine.test.ts

182 lines
13 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 {
createTruthtranceChoamPromisesFixture as fixture, actTruthtranceChoamPromises as act,
advanceTruthtranceChoamPromises as advance, allowTruthtranceChoamPromisesIncome as allowIncome,
finishTruthtranceChoamPromises as finish, truthtranceChoamPromisesPlayer as player,
truthtranceChoamPromisesInventory as inventory, truthtranceChoamPromisesPhysical as physical,
type TruthtranceChoamPromisesFixture,
} from './fixture-truthtrance-choam-promises';
const variants = [
{ advanced: false, kind: 'shipment' as const }, { advanced: true, kind: 'shipment' as const },
{ advanced: false, kind: 'movement' as const }, { advanced: true, kind: 'movement' as const },
];
const promise = (game: Game, f: TruthtranceChoamPromisesFixture) =>
f.kind === 'shipment' ? game.shipmentPromises?.[0] : game.movementPromises?.[0];
function reject(game: Game, actor: string, action: Action) {
const before = structuredClone(game); assert.throws(() => applyAction(game, actor, action)); assert.deepEqual(game, before);
}
function nativeSource(f: TruthtranceChoamPromisesFixture) {
assert.equal(f.setup.status, 'setup'); assert.equal(f.setup.advanced, f.advanced);
assert.deepEqual(f.setup.expansions, ['choam']);
assert.deepEqual(f.game.players.map(player => player.faction), ['atreides', 'choam', 'emperor', 'harkonnen']);
assert.ok(!f.setup.homeworlds && !f.setup.nexusCards && !f.setup.leaderSkills && !f.setup.techTokens &&
!f.setup.strongholdCards && !f.setup.discoveryEnabled && !f.setup.ecazTreachery);
assert.ok(f.actions.some(step => step.action.type === 'start' && (!f.advanced || step.action.advancedPreview === true)));
assert.ok(f.beforeStartingDeal.players.every(player => player.hand.length === 0));
assert.deepEqual(f.afterSetup.players.flatMap(player => player.hand.map(card => card.id)), f.deal);
assert.ok(player(f.afterSetup, f.asker).hand.some(card => card.id === f.card && card.effect === 'truthtrance'));
assert.ok(player(f.afterSetup, f.counterHolder).hand.some(card => card.id === f.karama && card.effect === 'karama'));
assert.equal(player(f.afterSetup, f.target).hand.length, 1);
assert.ok(player(f.afterSetup, f.target).hand.every(card => card.kind === 'projectile' && !card.effect));
assert.equal(f.game.phase, 0); assert.equal(f.game.turn, 1); assert.equal(f.game.truthtrance ?? null, null);
assert.equal(player(f.game, f.target).spice, 2); assert.equal(player(f.game, f.target).reserves, 20);
assert.deepEqual(player(f.game, f.target).forces, {}); assert.equal(player(f.game, f.target).tanks, 0);
assert.equal(f.game.choamCharity, undefined); assert.equal(f.incomeAmount, 8); assert.equal(f.allowedCash, 10);
assert.equal(f.originalInventory.length, 35); assert.deepEqual(inventory(f.game), f.originalInventory);
assert.deepEqual(physical(f.game), f.originalPhysical); assert.equal(f.sourceActionCount, f.actions.length);
}
function asked(f: TruthtranceChoamPromisesFixture) {
let game = act(structuredClone(f.game), f.declarationStep.actor, f.declarationStep.action);
game = advance(game, state => state.truthtrance?.stage === 'ask');
game = act(game, f.askStep.actor, f.askStep.action);
const answers = f.kind === 'shipment' ? viewGame(game, f.target).truthShipmentAnswers : viewGame(game, f.target).truthMovementAnswers;
assert.ok(answers?.includes('yes'), 'Native owned opening income supplies an allowed-outcome route');
assert.ok(answers?.includes('no')); return game;
}
function bound(f: TruthtranceChoamPromisesFixture, answer: 'yes' | 'no' = 'yes') {
const game = act(asked(f), f.target, { type: 'truthAnswer', answer });
assert.equal(game.truthtrance ?? null, null); assert.equal(promise(game, f)?.answer, answer === 'yes');
assert.equal(promise(game, f)?.released, undefined); assert.equal(promise(game, f)?.fulfilled, undefined);
assert.equal(player(game, f.target).spice, 2); assert.equal(game.choamCharity, undefined);
assert.deepEqual(physical(game), f.originalPhysical); assert.deepEqual(inventory(game), f.originalInventory);
const view = viewGame(game, f.target); assert.equal(view.shipmentCompletion, null);
assert.ok(!(view.movementPromisePolicy ?? []).some(action => action.type === 'ship' || action.type === 'move'));
reject(game, f.target, f.shipment); reject(game, f.target, f.move); return game;
}
function incomePending(game: Game, f: TruthtranceChoamPromisesFixture) {
const actions: { actor: string; action: Action }[] = [];
const before = structuredClone(game);
game = advance(game, state => state.phase === 2 && state.response?.kind === 'choamCharity', actions);
assert.equal(game.response?.owner, f.target); assert.equal(player(game, f.target).spice, 2);
assert.equal(game.choamCharity, undefined); assert.equal(promise(game, f)?.released, undefined);
assert.ok(viewGame(game, f.counterHolder).responseControls?.cancelCards.includes(f.karama));
reject(game, f.target, { type: 'ready' }); reject(game, f.target, { type: 'charity' });
reject(game, f.target, f.shipment); return { game, before, actions };
}
for (const variant of variants) void test(
`${variant.advanced ? 'Advanced' : 'Basic'} native CHOAM ${variant.kind} promise awaits real opening income then completes its funded native route`, () => {
const f = fixture(variant); nativeSource(f);
const pending = incomePending(bound(f), f), originalResponse = structuredClone(pending.game.response);
assert.equal(promise(pending.game, f)?.fulfilled, undefined);
const incomeActions: { actor: string; action: Action }[] = [];
const allowed = allowIncome(pending.game, incomeActions);
assert.equal(player(allowed, f.target).spice, 10);
assert.deepEqual(allowed.choamCharity, { turn: 1, canceled: false });
assert.equal(originalResponse?.kind, 'choamCharity'); assert.ok(incomeActions.some(step => step.action.type === 'passResponse'));
assert.equal(promise(allowed, f)?.released, undefined);
reject(allowed, f.target, f.blockedBribe);
const completed = finish(f, allowed);
reject(completed.beforeBid, f.target, f.blockedBid);
assert.equal(completed.afterShipment.choamCharity?.canceled, false);
assert.equal(player(completed.delivered, f.target).forces[f.location], f.minimum);
assert.equal(promise(completed.delivered, f)?.fulfilled, true);
assert.equal(promise(completed.delivered, f)?.released, undefined);
if (f.kind === 'shipment') {
assert.equal(completed.purchase, null); assert.equal(completed.move, null);
assert.equal(player(completed.delivered, f.target).spice, 0);
assert.equal(player(completed.delivered, f.target).reserves, 10);
assert.equal(player(completed.delivered, f.target).moved, 0);
} else {
assert.ok(completed.purchase && completed.move);
assert.equal(player(completed.purchase.after, f.target).spice, 8);
assert.ok(player(completed.purchase.after, f.target).hand.some(card => card.id === f.purchaseCard));
assert.equal(player(completed.afterShipment, f.target).forces['arrakeen:10'], 6);
assert.equal(promise(completed.afterShipment, f)?.fulfilled, undefined, 'The reserve shipment is not the promised ground event');
assert.equal(player(completed.delivered, f.target).forces['arrakeen:10'] ?? 0, 0);
assert.equal(player(completed.delivered, f.target).reserves, 14);
assert.equal(player(completed.delivered, f.target).spice, 2);
assert.equal(player(completed.delivered, f.target).moved, 1);
}
assert.equal(player(completed.delivered, f.target).shipped, true);
assert.ok(player(completed.delivered, f.counterHolder).hand.some(card => card.id === f.karama));
assert.equal(completed.delivered.discard.filter(card => card.id === f.card).length, 1);
assert.deepEqual(inventory(completed.game), f.originalInventory);
});
for (const variant of variants) void test(
`${variant.advanced ? 'Advanced' : 'Basic'} native CHOAM ${variant.kind} opening cancellation spends the real Karama and releases only the unfunded obligation`, () => {
const f = fixture(variant); nativeSource(f); const pending = incomePending(bound(f), f);
const game = act(pending.game, f.counterHolder, f.cancelIncome);
assert.equal(game.response ?? null, null); assert.equal(game.pendingTreacheryDiscard ?? null, null);
assert.deepEqual(game.choamCharity, { turn: 1, canceled: true });
assert.equal(player(game, f.target).spice, 2); assert.equal(player(game, f.target).reserves, 20);
assert.deepEqual(player(game, f.target).forces, {});
assert.equal(promise(game, f)?.released, true); assert.equal(promise(game, f)?.fulfilled, undefined);
assert.equal(player(game, f.counterHolder).hand.some(card => card.id === f.karama), false);
assert.equal(game.discard.filter(card => card.id === f.karama).length, 1);
reject(game, f.counterHolder, f.cancelIncome); reject(game, f.target, { type: 'passResponse' });
assert.deepEqual(inventory(game), f.originalInventory);
});
for (const variant of variants) void test(
`${variant.advanced ? 'Advanced' : 'Basic'} native CHOAM ${variant.kind} No retains a real smaller alternative without reserving Yes funds`, () => {
const f = fixture(variant), pending = incomePending(bound(f, 'no'), f), allowed = allowIncome(pending.game);
const spent = act(structuredClone(allowed), f.target, { type: 'bribe', target: f.asker, amount: 1 });
assert.equal(player(spent, f.target).spice, 9); assert.equal(promise(spent, f)?.released, undefined);
const completed = finish(f, allowed, { answer: 'no' });
assert.equal(player(completed.delivered, f.target).forces[f.location], f.minimum - 1);
assert.equal(promise(completed.delivered, f)?.released, undefined);
if (f.kind === 'shipment') {
assert.equal(promise(completed.delivered, f)?.fulfilled, true);
assert.equal(player(completed.delivered, f.target).spice, 1); assert.equal(completed.turnEnd, null);
} else {
assert.equal(promise(completed.delivered, f)?.fulfilled, undefined, 'A later move could still match until this actual turn ends');
assert.equal(player(completed.delivered, f.target).forces['arrakeen:10'], 1);
assert.ok(completed.turnEnd);
assert.deepEqual(completed.turnEnd.steps, [{ actor: f.target, action: { type: 'endMovement' } }]);
assert.equal(promise(completed.finished, f)?.fulfilled, true);
assert.ok(!completed.finished.movementRemaining?.includes(f.target));
assert.deepEqual(player(completed.finished, f.target).forces, player(completed.delivered, f.target).forces);
assert.equal(player(completed.finished, f.target).spice, player(completed.delivered, f.target).spice);
assert.equal(player(completed.finished, f.target).reserves, player(completed.delivered, f.target).reserves);
}
assert.deepEqual(inventory(completed.game), f.originalInventory);
});
void test('native CHOAM promise questions reject foreign and underlying replies while raw cash stays uncredited', () => {
for (const kind of ['shipment', 'movement'] as const) {
const f = fixture({ advanced: true, kind }), game = asked(f);
reject(game, f.asker, { type: 'truthAnswer', answer: 'yes' });
reject(game, f.target, { type: 'ready' }); reject(game, f.target, f.shipment);
reject(game, f.asker, f.askStep.action);
assert.equal(player(game, f.target).spice, 2); assert.equal(game.choamCharity, undefined);
const answered = act(game, f.target, { type: 'truthAnswer', answer: 'yes' });
reject(answered, f.target, { type: 'truthAnswer', answer: 'yes' });
const pending = incomePending(answered, f);
assert.equal(player(pending.game, f.target).spice, 2); assert.equal(promise(pending.game, f)?.released, undefined);
}
});
void test('all four minimal policies preserve native CHOAM promise funding during actual Bidding', () => {
for (const variant of variants) {
const f = fixture(variant), pending = incomePending(bound(f), f), allowed = allowIncome(pending.game);
const game = advance(allowed, state => state.phase === 3 && !state.response && !state.phaseOpening &&
!state.decision && state.auction?.active === f.target);
for (const difficulty of DIFFICULTIES) {
const view = viewGame(game, f.target); view.players.find(player => player.id === f.target)!.bot = difficulty;
const before = structuredClone(view), action = botActions(view)[0]; assert.ok(action, difficulty);
assert.deepEqual(view, before);
const done = act(structuredClone(game), f.target, action);
assert.equal(promise(done, f)?.released, undefined);
assert.ok(player(done, f.target).spice >= (f.kind === 'shipment' ? 10 : 6));
assert.deepEqual(inventory(done), f.originalInventory);
}
}
});