Files
Dune/tests/fixture-truth-turn-action.ts

154 lines
10 KiB
TypeScript

import assert from 'node:assert/strict';
import { viewGame, type Action, type Game } from '../game/engine';
import type { MixedActionExpression } from '../game/mixed-action-question';
import type { TruthFact, TruthQuestion } from '../game/truthtrance';
import {
actTruthBid, nextTruthBid, truthBidClean, truthBidInventory, truthBidPlayer, type TruthBidStep,
} from './fixture-truth-bid';
import { createTruthBribeExpressionFixture } from './fixture-truth-bribe-expression';
import { nextTruthBribe } from './fixture-truth-bribe';
import { createTruthTravelFixture } from './fixture-truth-travel';
import { createTruthTravelOrderFixture } from './fixture-truth-travel-order';
export type TruthTurnActionClaim = { kind: 'ship' | 'move'; territory: string; minimum: number }
| { kind: 'payment'; recipient: string; minimum: number };
export type TruthTurnActionExpression = MixedActionExpression<TruthTurnActionClaim>;
export type TruthTurnActionStep = TruthBidStep;
export const actTruthTurnAction = actTruthBid;
export const truthTurnActionClean = truthBidClean;
export const truthTurnActionInventory = truthBidInventory;
export const truthTurnActionPlayer = truthBidPlayer;
export const truthTurnActionFact = (fact: TruthFact): TruthTurnActionExpression => ({ kind: 'fact', fact });
export const truthTurnActionPayment = (recipient: string, minimum = 2): TruthTurnActionExpression =>
({ kind: 'action', action: { kind: 'payment', recipient, minimum } });
export const truthTurnActionShip = (territory = 'polar_sink', minimum = 1): TruthTurnActionExpression =>
({ kind: 'action', action: { kind: 'ship', territory, minimum } });
export const truthTurnActionMove = (territory = 'imperial_basin', minimum = 1): TruthTurnActionExpression =>
({ kind: 'action', action: { kind: 'move', territory, minimum } });
export const truthTurnActionGroup = (kind: 'and' | 'or', ...terms: TruthTurnActionExpression[]): TruthTurnActionExpression =>
({ kind, terms });
/** These adapters select existing genuine original public sources only. The
* default keeps Harkonnen's two opening Truthtrances and Atreides' unused
* movement turn; Hajr keeps its actually dealt card, and Mentat earns the late
* payment window through original native actions. No played state is set. */
export function createTruthTurnActionFixture(options: {
source?: 'twoTruths' | 'hajr' | 'mentat'; advanced?: boolean; mixed?: TruthTurnActionExpression;
} = {}) {
const sourceKind = options.source ?? 'twoTruths';
const orderSource = sourceKind === 'twoTruths' ? createTruthTravelOrderFixture() : null;
const hajrSource = sourceKind === 'hajr' ? createTruthTravelFixture() : null;
const mentatSource = sourceKind === 'mentat' ? createTruthBribeExpressionFixture({ phase: 8, advanced: options.advanced }) : null;
const original = orderSource ?? hajrSource ?? mentatSource!;
const game = structuredClone(original.game), asker = original.asker, target = original.target;
const observer = game.players.find(player => player.id !== asker && player.id !== target)?.id ?? asker;
const truths = orderSource ? structuredClone(orderSource.truths) : mentatSource
? structuredClone(mentatSource.truths) : [hajrSource!.truth];
const recipient = observer;
const mixed = structuredClone(options.mixed ?? (sourceKind === 'mentat'
? truthTurnActionGroup('or', truthTurnActionPayment(recipient), truthTurnActionMove())
: truthTurnActionGroup('and', truthTurnActionPayment(recipient), truthTurnActionShip(), truthTurnActionMove())));
const question = { kind: 'turnActions', target, turn: game.turn, mixed } satisfies TruthQuestion;
const actions = structuredClone(original.sourceActions);
const shipment = orderSource?.shipment ?? hajrSource?.shipmentAction ??
{ type: 'ship', territory: 'polar_sink', sector: 0, amount: 1, elite: 0, allyPayment: 0 } satisfies Action;
const move = orderSource?.move ?? hajrSource?.firstMove ??
{ type: 'move', from: 'arrakeen:10', territory: 'imperial_basin', sector: 9, amount: 1 } satisfies Action;
const spiceFact: TruthFact = { kind: 'spice', compare: 'gte', value: truthBidPlayer(game, target).spice };
assert.ok(truthBidClean(game)); assert.equal(game.pendingShipment ?? null, null);
assert.equal(truthBidPlayer(game, recipient).ally, null);
assert.ok(truths.every(truth => truthBidPlayer(game, asker).hand.some(card => card.id === truth)));
assert.deepEqual(truthBidInventory(game), original.originalCards);
assert.equal(game.turnActionPromises?.length ?? 0, 0);
return {
game, source: structuredClone(game), sourceKind, orderSource, hajrSource, mentatSource,
asker, target, observer, recipient, truths, truth: truths[0], mixed, question, spiceFact,
setup: original.setup, afterSetup: original.afterSetup, beforeStartingDeal: original.beforeStartingDeal,
originalCards: original.originalCards, originalSpice: original.originalSpice,
dealOrder: original.dealOrder, spiceOrder: original.spiceOrder,
actions, sourceActions: actions, sourceActionCount: actions.length, sourceCount: actions.length,
sourceCounts: { selected: actions.length,
inherited: orderSource?.sourceCounts.inheritedBidding ?? hajrSource?.sourceCounts.inherited ?? mentatSource!.sourceActionCount,
nativeContinuation: orderSource?.sourceCounts.nativeContinuation ?? 0 },
shipment: structuredClone(shipment), move: structuredClone(move), shipmentCost: 2,
payment: { type: 'bribe', target: recipient, amount: 2 } satisfies Action,
end: { type: 'endMovement' } satisfies Action,
hajr: hajrSource?.hajr ?? null, hajrAction: hajrSource?.hajrAction ?? null,
sourceDisclosure: `${original.sourceDisclosure} This turn-action adapter changes only the raw question. ` +
'Future ordinary payments, the original reserve shipment and physical movement groups retain their real native actions. ' +
'All counts derive from recorded arrays; no extra card, hand, wallet, army, clock, promise, receipt or compiled fact is installed.',
};
}
export type TruthTurnActionFixture = ReturnType<typeof createTruthTurnActionFixture>;
export function declareTruthTurnAction(f: TruthTurnActionFixture, source = f.game, copy: 0 | 1 = 0,
actions: TruthTurnActionStep[] = []) {
assert.ok(f.truths[copy], 'The source must actually hold this distinct printed Truthtrance');
const before = structuredClone(source), from = actions.length;
let game = actTruthTurnAction(source, f.asker, { type: 'card', card: f.truths[copy] }, actions);
while (game.truthtrance?.stage === 'priority') {
const actor = game.players.find(player => !game.truthtrance!.passed.includes(player.id)); assert.ok(actor);
game = actTruthTurnAction(game, actor.id, { type: 'truthPass' }, actions);
}
assert.equal(game.truthtrance?.stage, 'ask');
return { game, before, actions, steps: structuredClone(actions.slice(from)), actionCount: actions.length - from };
}
export function askTruthTurnAction(game: Game, asker: string, target: string, mixed: TruthTurnActionExpression,
actions: TruthTurnActionStep[] = []): Game {
return actTruthTurnAction(game, asker, { type: 'truthAsk', question: {
kind: 'turnActions', target, turn: game.turn, mixed: structuredClone(mixed),
} }, actions);
}
export function answerTruthTurnAction(game: Game, target: string, answer: 'yes' | 'no',
actions: TruthTurnActionStep[] = []): Game {
return actTruthTurnAction(game, target, { type: 'truthAnswer', answer }, actions);
}
export function bindTruthTurnAction(f: TruthTurnActionFixture, answer: 'yes' | 'no' = 'yes', mixed = f.mixed,
source = f.game, copy: 0 | 1 = 0, actions: TruthTurnActionStep[] = []) {
const from = actions.length, declared = declareTruthTurnAction(f, source, copy, actions);
const asked = askTruthTurnAction(declared.game, f.asker, f.target, mixed, actions);
const game = answerTruthTurnAction(asked, f.target, answer, actions);
return { game, declared, asked, actions, actionCount: actions.length - from };
}
export function truthTurnActionPromise(game: Game, target = game.turnActionPromises?.[0]?.player ?? game.players[0].id) {
const row = viewGame(game, target).turnActionPromises.find(promise => promise.player === target);
assert.ok(row, 'The respondent retains the original whole turn-action question and status'); return row;
}
export function nextTruthTurnAction(game: Game): TruthTurnActionStep | null {
if (game.truthtrance) return null;
for (const player of game.players) {
const guide = viewGame(game, player.id).turnActionGuide;
if (guide) return { actor: player.id, action: guide.action };
}
return nextTruthBribe(game);
}
export function advanceTruthTurnAction(game: Game, until: (state: Game) => boolean,
actions: TruthTurnActionStep[] = [], nativeOnly = false): Game {
for (let count = 0; count < 1200; count++) {
if (until(game)) return game;
assert.notEqual(game.status, 'finished', 'The genuine source ended before its requested native boundary');
const next = nativeOnly ? nextTruthBid(game) : nextTruthTurnAction(game);
assert.ok(next, 'Supply the actual original Truthtrance question, answer or owned payment choice');
game = actTruthTurnAction(game, next.actor, next.action, actions);
}
throw Error('The genuine turn-action source did not reach its finite boundary');
}
export function settleTruthTurnAction(game: Game, actions: TruthTurnActionStep[] = []): Game {
return advanceTruthTurnAction(game, state => truthBidClean(state) && !state.pendingShipment, actions);
}
export function completeTruthTurnAction(game: Game, target = game.turnActionPromises?.[0]?.player ?? game.players[0].id,
actions: TruthTurnActionStep[] = []): Game {
for (let count = 0; count < 24; count++) {
const promise = truthTurnActionPromise(game, target);
if (promise.fulfilled || promise.released) return game;
game = settleTruthTurnAction(game, actions);
const guide = viewGame(game, target).turnActionGuide; assert.ok(guide, 'The accepted whole answer has an actual native guide');
game = settleTruthTurnAction(actTruthTurnAction(game, target, guide.action, actions), actions);
}
throw Error('The exact native turn-action guide did not settle the accepted whole answer');
}
export function finishTruthTurnAction(game: Game, actions: TruthTurnActionStep[] = []): Game {
const turn = game.turn;
return advanceTruthTurnAction(game, state => state.turn === turn + 1 && state.phase === 0 && truthBidClean(state), actions);
}