Files
Dune/tests/fixture-truth-shipment-expression.ts
T

152 lines
9.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import assert from 'node:assert/strict';
import { viewGame, type Game } from '../game/engine';
import { TERRITORIES } from '../game/board';
import type { ShipmentExpression } from '../game/shipment-promises';
import type { TruthQuestion } from '../game/truthtrance';
import {
createTruthTraitorDecisionFixture,
actTruthTraitorDecision,
advanceTruthTraitorDecision,
truthTraitorDecisionClean,
truthTraitorDecisionInventory,
truthTraitorDecisionPlayer,
type TruthTraitorDecisionStep,
} from './fixture-truth-traitor-decision';
export type TruthShipmentExpressionStep = TruthTraitorDecisionStep;
export type TruthShipmentExpressionScenario = {
id: string;
expression: ShipmentExpression;
answer: 'yes' | 'no';
};
export const actTruthShipmentExpression = actTruthTraitorDecision;
export const truthShipmentExpressionClean = truthTraitorDecisionClean;
export const truthShipmentExpressionInventory = truthTraitorDecisionInventory;
export const truthShipmentExpressionPlayer = truthTraitorDecisionPlayer;
const leaf = (territory = 'arrakeen', minimum = 1): ShipmentExpression => ({ territory, minimum });
/** The original one-force Arrakeen shipment determines every selected answer.
* AND No retains true earlier clauses; OR Yes needs the final destination. */
export function truthShipmentExpressionScenario(
op: 'and' | 'or' = 'and', count = 3, answer: 'yes' | 'no' = 'yes',
): TruthShipmentExpressionScenario {
assert.ok(Number.isInteger(count) && count >= 3 && count <= 16);
const alternatives = TERRITORIES.filter(place => place.id !== 'arrakeen');
const terms = op === 'and'
? Array.from({ length: count }, (_, index) => leaf('arrakeen', answer === 'no' && index === count - 1 ? 2 : 1))
: [...alternatives.slice(0, count - 1).map(place => leaf(place.id)), leaf('arrakeen', answer === 'yes' ? 1 : 2)];
return { id: `${count}-${op}-${answer}`, expression: { op, terms }, answer };
}
export function truthShipmentExpressionNestedScenario(
op: 'and' | 'or' = 'and', answer: 'yes' | 'no' = 'yes',
): TruthShipmentExpressionScenario {
const decisive = leaf('arrakeen', answer === 'yes' ? 1 : 2);
const expression: ShipmentExpression = op === 'and'
? { op, terms: [leaf(), { op: 'or', terms: [leaf('carthag'), leaf('sietch_tabr'), decisive] }, leaf()] }
: { op, terms: [leaf('carthag'), { op: 'and', terms: [leaf(), leaf(), decisive] }, leaf('sietch_tabr')] };
return { id: `nested-${op}-${answer}`, expression, answer };
}
export function truthShipmentExpressionDepthScenario(depth = 4): TruthShipmentExpressionScenario {
let expression = leaf();
for (let level = 0; level < depth; level++) expression = level % 2 === 0
? { op: 'and', terms: [leaf(), expression] }
: { op: 'or', terms: [leaf('carthag'), expression] };
return { id: `depth-${depth}`, expression, answer: 'yes' };
}
/** Reuse the genuine two-seat Basic source only through its beforeShipment
* boundary. The prefix excludes that original shipment and every later battle
* action. No postdeal hand, wallet, army, clock or promise is installed. */
export function createTruthShipmentExpressionFixture() {
const base = createTruthTraitorDecisionFixture();
const shipmentIndex = base.actions.findIndex(step => step.actor === base.shipmentStep.actor &&
JSON.stringify(step.action) === JSON.stringify(base.shipmentStep.action));
assert.ok(shipmentIndex >= 0);
const actions = structuredClone(base.actions.slice(0, shipmentIndex));
const game = structuredClone(base.beforeShipment);
assert.equal(game.phase, 5); assert.equal(game.active, base.target);
assert.ok(truthShipmentExpressionClean(game)); assert.equal(game.pendingShipment ?? null, null);
assert.equal(truthShipmentExpressionPlayer(game, base.target).shipped, false);
assert.equal(truthShipmentExpressionPlayer(game, base.target).reserves, 20);
assert.ok(truthShipmentExpressionPlayer(game, base.asker).hand.some(card => card.id === base.card));
assert.deepEqual(truthShipmentExpressionInventory(game), base.originalCards);
const scenario = truthShipmentExpressionScenario();
const question: Extract<TruthQuestion, { kind: 'shipment' }> = {
kind: 'shipment', target: base.target, claim: scenario.expression,
};
const declarationStep: TruthShipmentExpressionStep = { actor: base.asker, action: { type: 'card', card: base.card } };
const askStep: TruthShipmentExpressionStep = { actor: base.asker, action: { type: 'truthAsk', question } };
const answerStep: TruthShipmentExpressionStep = { actor: base.target, action: { type: 'truthAnswer', answer: scenario.answer } };
const continuationStep: TruthShipmentExpressionStep = { actor: base.target, action: { type: 'endMovement' } };
return {
game, source: structuredClone(game), base, beforeShipment: structuredClone(game),
lobby: base.lobby, setup: base.setup, beforeStartingDeal: base.beforeStartingDeal,
afterSetup: base.afterSetup, initial: base.initial, deal: base.deal, dealOrder: base.dealOrder,
spiceOrder: base.spiceOrder, originalSpice: base.originalSpice, originalCards: base.originalCards,
originalInventory: base.originalCards, asker: base.asker, target: base.target,
card: base.card, truth: base.card, ordinary: base.ordinary, location: base.location,
scenario, expression: scenario.expression, question, declarationStep, askStep, answerStep,
shipmentStep: structuredClone(base.shipmentStep), continuationStep,
actions, steps: actions, sourceActionCount: actions.length,
sourceCounts: { shipment: shipmentIndex, completeBase: base.sourceActionCount },
staging: [...base.staging.slice(0, 2),
'Only the original action prefix through Emperor’s unused active reserve-shipment turn is retained. Atreides has genuinely ended Movement. Truthtrance declaration, priority, the full pure shipment question, answer, original paid one-force Arrakeen delivery and Emperor endMovement remain caller actions. No later source shipment or battle action is counted as prefix evidence.'],
};
}
export type TruthShipmentExpressionFixture = ReturnType<typeof createTruthShipmentExpressionFixture>;
export function declareTruthShipmentExpression(fixture: TruthShipmentExpressionFixture) {
const before = structuredClone(fixture.game), actions: TruthShipmentExpressionStep[] = [];
const declaration = structuredClone(fixture.declarationStep);
const game = actTruthShipmentExpression(before, declaration.actor, declaration.action, actions);
assert.equal(game.truthtrance?.stage, 'priority');
return { game, before, declaration, opened: structuredClone(game), actions };
}
export function passTruthShipmentExpressionPriority(game: Game, actions: TruthShipmentExpressionStep[] = []): Game {
for (let count = 0; game.truthtrance?.stage === 'priority' && count < game.players.length; count++) {
const actor = game.players.find(player => !game.truthtrance!.passed.includes(player.id)); assert.ok(actor);
game = actTruthShipmentExpression(game, actor.id, { type: 'truthPass' }, actions);
}
assert.equal(game.truthtrance?.stage, 'ask');
return game;
}
export function askTruthShipmentExpression(fixture: TruthShipmentExpressionFixture, expression = fixture.expression) {
const declared = declareTruthShipmentExpression(fixture), actions = declared.actions;
let game = passTruthShipmentExpressionPriority(declared.game, actions);
const beforeQuestion = structuredClone(game);
const question: TruthShipmentExpressionStep = { actor: fixture.asker, action: { type: 'truthAsk', question: {
kind: 'shipment', target: fixture.target, claim: structuredClone(expression),
} } };
game = actTruthShipmentExpression(game, question.actor, question.action, actions);
assert.equal(game.truthtrance?.stage, 'answer');
return { ...declared, game, beforeQuestion, question, actions };
}
export function answerTruthShipmentExpression(game: Game, answer: 'yes' | 'no', actions: TruthShipmentExpressionStep[] = []) {
const target = game.truthtrance?.question?.target; assert.ok(target);
assert.ok(viewGame(game, target).truthShipmentAnswers!.includes(answer));
return actTruthShipmentExpression(game, target, { type: 'truthAnswer', answer }, actions);
}
/** Settle original shipment responses only; actual delivery remains explicit. */
export function settleTruthShipmentExpression(game: Game, actions: TruthShipmentExpressionStep[] = []) {
return advanceTruthTraitorDecision(game, state => truthShipmentExpressionClean(state) && !state.pendingShipment, actions);
}
export function finishTruthShipmentExpression(
fixture: TruthShipmentExpressionFixture, scenario: TruthShipmentExpressionScenario = fixture.scenario,
) {
const asked = askTruthShipmentExpression(fixture, scenario.expression), actions = asked.actions;
let game = answerTruthShipmentExpression(asked.game, scenario.answer, actions);
const answerStep = actions.at(-1)!, bound = structuredClone(game);
const shipmentStep = structuredClone(fixture.shipmentStep);
game = actTruthShipmentExpression(game, shipmentStep.actor, shipmentStep.action, actions);
const declaredShipment = structuredClone(game);
game = settleTruthShipmentExpression(game, actions);
return { ...asked, asked: asked.game, game, answerStep, bound, shipmentStep, declaredShipment, actions };
}