Files
Dune/tests/truthtrance-early-advanced-shipment-engine.test.ts
T

159 lines
11 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 {
createTruthtranceEarlyAdvancedShipmentFixture as fixture, actTruthtranceEarlyAdvancedShipment as act,
advanceTruthtranceEarlyAdvancedShipment as advance, finishTruthtranceEarlyAdvancedShipment as finish,
truthtranceEarlyAdvancedShipmentClean as clean, truthtranceEarlyAdvancedShipmentPlayer as player,
truthtranceEarlyAdvancedShipmentInventory as inventory, truthtranceEarlyAdvancedShipmentPhysical as physical,
type TruthtranceEarlyAdvancedShipmentFixture,
} from './fixture-truthtrance-early-advanced-shipment';
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: TruthtranceEarlyAdvancedShipmentFixture) {
assert.equal(f.setup.status, 'setup'); assert.equal(f.setup.advanced, true); assert.deepEqual(f.setup.expansions, []);
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.actor === f.lobby.host && step.action.type === 'start' && 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.ok(player(f.afterSetup, f.target).hand.some(card => card.id === f.baliset && card.name === 'Baliset'));
assert.equal(player(f.game, f.target).spice, 10); 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(player(f.game, f.target).shipped, false); assert.equal(player(f.game, f.target).moved, 0);
assert.equal(f.game.turn, 1); assert.equal(f.game.phase, f.phaseNumber); assert.ok(clean(f.game));
assert.equal(f.game.truthtrance ?? null, null); assert.equal(f.game.shipmentPromises, undefined);
assert.equal(f.originalInventory.length, 33); assert.deepEqual(inventory(f.game), f.originalInventory);
assert.deepEqual(physical(f.game), f.originalPhysical); assert.equal(f.sourceActionCount, f.actions.length);
if (f.phase === 'storm') assert.deepEqual(f.game.stormDials, {});
else {
assert.equal(f.actions.filter(step => step.action.type === 'stormDial').length, 2);
assert.equal(f.game.spiceSequence ?? null, null);
assert.ok(f.game.spiceDiscard[0].some(card => 'territory' in card));
assert.ok(f.game.spiceDiscard[1].some(card => 'territory' in card));
}
}
function asked(f: TruthtranceEarlyAdvancedShipmentFixture) {
const actions: { actor: string; action: Action }[] = [];
let game = act(structuredClone(f.game), f.declarationStep.actor, f.declarationStep.action, actions);
const declared = structuredClone(game);
game = advance(game, state => state.truthtrance?.stage === 'ask', actions);
assert.ok(viewGame(game, f.asker).truthShipmentTargets.includes(f.target));
const beforeQuestion = structuredClone(game);
game = act(game, f.askStep.actor, f.askStep.action, actions);
assert.equal(game.truthtrance?.stage, 'answer');
assert.ok(viewGame(game, f.target).truthShipmentAnswers?.includes('yes'));
assert.ok(viewGame(game, f.target).truthShipmentAnswers?.includes('no'));
return { game, declared, beforeQuestion, actions };
}
function bound(f: TruthtranceEarlyAdvancedShipmentFixture, answer: 'yes' | 'no' = 'yes') {
const question = asked(f), game = act(question.game, f.target, { type: 'truthAnswer', answer }, question.actions);
assert.equal(game.truthtrance ?? null, null); assert.equal(game.shipmentPromises?.length, 1);
assert.equal(game.shipmentPromises![0].player, f.target);
assert.equal(game.shipmentPromises![0].answer, answer === 'yes');
assert.equal(game.shipmentPromises![0].released, undefined);
assert.equal(game.shipmentPromises![0].fulfilled, undefined);
assert.equal(game.phase, f.phaseNumber); assert.deepEqual(physical(game), f.originalPhysical);
assert.deepEqual(inventory(game), f.originalInventory); return game;
}
for (const phase of ['storm', 'charity', 'bidding'] as const) void test(
`public Advanced ${phase} Truthtrance binds an eight-force future shipment without granting an early action`, () => {
const f = fixture({ phase }); nativeSource(f); const game = bound(f);
const view = viewGame(game, f.target);
assert.equal(view.shipmentCompletion, null); assert.equal(view.shipmentPreparation, null);
reject(game, f.target, f.shipment); reject(game, f.target, { type: 'endMovement' });
reject(game, f.target, f.bigBribe);
assert.equal(game.shipmentPromises![0].released, undefined);
});
for (const guildTake of [false, true]) void test(
`public Advanced early shipment Yes retains purchase2 and native Guild ${guildTake ? 'TAKE' : 'WAIT'} through actual eight-force delivery`, () => {
const f = fixture(); nativeSource(f); const game = bound(f);
const completed = finish(f, game, { guildTake });
reject(completed.purchase.before, f.target, f.bigBid);
assert.equal(player(completed.purchase.before, f.target).spice, 10);
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(completed.purchase.after.shipmentPromises![0].released, undefined);
assert.ok(completed.timing.some(record => record.steps.some(step => step.actor === f.guild &&
step.action.type === 'decision' && step.action.take === guildTake)));
if (guildTake) {
assert.equal(completed.guildTurn.length, 1);
assert.deepEqual(completed.guildTurn[0].steps, [{ actor: f.guild, action: { type: 'endMovement' } }]);
} else assert.equal(completed.guildTurn.length, 0);
assert.equal(completed.beforeShipment.active, f.target);
assert.equal(viewGame(completed.beforeShipment, f.target).shipmentCompletion?.actions.at(-1)?.amount, 8);
assert.ok(completed.shipment.controls.some(control => control.decision?.kind === 'guildShipment'));
assert.ok(completed.shipment.controls.some(control => control.response?.kind === 'guildIncome'));
assert.equal(player(completed.delivered, f.guild).spice, player(completed.beforeShipment, f.guild).spice + 8);
assert.equal(player(completed.delivered, f.target).spice, 0);
assert.equal(player(completed.delivered, f.target).forces[f.location], 8);
assert.equal(player(completed.delivered, f.target).reserves, 12);
assert.equal(completed.delivered.shipmentPromises![0].fulfilled, true);
assert.equal(completed.delivered.shipmentPromises![0].released, undefined);
assert.ok(player(completed.game, f.counterHolder).hand.some(card => card.id === f.karama));
assert.equal(completed.game.discard.filter(card => card.id === f.card).length, 1);
reject(completed.delivered, f.target, f.shipment);
assert.deepEqual(inventory(completed.game), f.originalInventory);
});
void test('public Advanced early shipment No allows voluntary costs and a real smaller Carthag shipment', () => {
const f = fixture({ phase: 'bidding' }); nativeSource(f); const game = bound(f, 'no');
const voluntary = act(structuredClone(game), f.target, f.bigBribe);
assert.equal(player(voluntary, f.target).spice, 7); assert.equal(voluntary.shipmentPromises![0].released, undefined);
const completed = finish(f, game, { amount: 7, guildTake: true });
reject(completed.beforeShipment, f.target, f.shipment);
assert.equal(player(completed.delivered, f.target).forces[f.location], 7);
assert.equal(player(completed.delivered, f.target).spice, 1);
assert.equal(completed.delivered.shipmentPromises![0].fulfilled, true);
assert.equal(completed.delivered.shipmentPromises![0].released, undefined);
assert.deepEqual(inventory(completed.game), f.originalInventory);
});
void test('an actual first Storm releases an early Advanced shipment only after its current-board route becomes unavailable', () => {
const f = fixture({ phase: 'storm' }); nativeSource(f); let game = bound(f);
const initialStorm = game.storm, distance = (f.sector - initialStorm + 18) % 18;
assert.ok(distance > 0 && distance <= 20); assert.equal(game.stormDialers.length, 2);
for (const [index, actor] of game.stormDialers.entries())
game = act(game, actor, { type: 'stormDial', amount: index === 0 ? distance : 0 });
assert.equal(game.storm, initialStorm); assert.equal(game.stormPending, distance);
assert.equal(game.shipmentPromises![0].released, undefined, 'Recorded future distance is not a future-board promise prediction');
game = advance(game, state => state.phase === 2 && clean(state));
assert.equal(game.storm, f.sector); assert.equal(game.shipmentPromises![0].released, true);
const spent = act(game, f.target, f.bigBribe);
assert.equal(player(spent, f.target).spice, 7);
assert.deepEqual(inventory(spent), f.originalInventory);
});
void test('the genuine early Advanced Truth question rejects foreign and underlying replies before its native answer', () => {
const f = fixture(), question = asked(f), game = question.game;
reject(game, f.asker, { type: 'truthAnswer', answer: 'yes' });
reject(game, f.target, { type: 'ready' }); reject(game, f.target, f.shipment);
reject(game, f.target, f.bigBribe); reject(game, f.asker, f.askStep.action);
const done = act(game, f.target, { type: 'truthAnswer', answer: 'yes' });
reject(done, f.target, { type: 'truthAnswer', answer: 'yes' });
reject(done, f.asker, f.askStep.action);
assert.deepEqual(physical(done), f.originalPhysical); assert.deepEqual(inventory(done), f.originalInventory);
});
void test('all four minimal policies participate legally in the actual early Advanced bid while retaining shipment funds', () => {
const f = fixture(), boundGame = bound(f);
const game = advance(boundGame, state => state.phase === 3 && clean(state) && 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(done.shipmentPromises![0].released, undefined);
assert.ok(player(done, f.target).spice >= 8);
assert.deepEqual(inventory(done), f.originalInventory);
}
});