Combine current battle decisions with turn commitments
This commit is contained in:
1 parent
1a79fd3131
commit
f76c8dfb93
15 files changed
+1380
-66
No files matched your search
@@ -0,0 +1,168 @@
|
||||
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 { MixedBattleActionClaim } from '../game/mixed-battle-question';
|
||||
import type { TruthFact, TruthQuestion } from '../game/truthtrance';
|
||||
import { nextStrongholdFactionsNativeStep } from './fixture-stronghold-factions';
|
||||
import { createTruthMixedBattleFixture } from './fixture-truth-mixed-battle';
|
||||
import { createTruthMixedTraitorFixture, createTruthMixedTraitorHarkonnenFixture } from './fixture-truth-mixed-traitor';
|
||||
import { truthTraitorDecisionTraitors } from './fixture-truth-traitor-decision';
|
||||
import {
|
||||
actTruthTurnAction, truthTurnActionInventory, truthTurnActionPlayer, type TruthTurnActionStep,
|
||||
} from './fixture-truth-turn-action';
|
||||
import type { TruthTurnAllianceLeaf } from './fixture-truth-turn-alliance';
|
||||
|
||||
export type TruthTurnCombatLeaf = TruthTurnAllianceLeaf |
|
||||
{ kind: 'battlePlan'; battle: string; territory: string; claim: MixedBattleActionClaim } |
|
||||
{ kind: 'traitorCall'; battle: string; territory: string; call: boolean };
|
||||
export type TruthTurnCombatExpression = MixedActionExpression<TruthTurnCombatLeaf>;
|
||||
export type TruthTurnCombatStep = TruthTurnActionStep;
|
||||
export const actTruthTurnCombat = actTruthTurnAction;
|
||||
export const truthTurnCombatPlayer = truthTurnActionPlayer;
|
||||
export const truthTurnCombatInventory = truthTurnActionInventory;
|
||||
export const truthTurnCombatTraitors = truthTraitorDecisionTraitors;
|
||||
export const truthTurnCombatClean = (game: Game) => !game.truthtrance && !game.response && !game.decision &&
|
||||
!game.phaseOpening && !game.pendingTreacheryDiscard && !game.pendingShipment && !game.pendingRevival;
|
||||
export const truthTurnCombatFact = (fact: TruthFact): TruthTurnCombatExpression => ({ kind: 'fact', fact });
|
||||
export const truthTurnCombatPayment = (recipient: string, minimum = 2): TruthTurnCombatExpression =>
|
||||
({ kind: 'action', action: { kind: 'payment', recipient, minimum } });
|
||||
export const truthTurnCombatPlan = (battle: string, territory: string, claim: MixedBattleActionClaim): TruthTurnCombatExpression =>
|
||||
({ kind: 'action', action: { kind: 'battlePlan', battle, territory, claim: structuredClone(claim) } });
|
||||
export const truthTurnCombatCall = (battle: string, territory: string, call = true): TruthTurnCombatExpression =>
|
||||
({ kind: 'action', action: { kind: 'traitorCall', battle, territory, call } });
|
||||
export const truthTurnCombatGroup = (kind: 'and' | 'or', ...terms: TruthTurnCombatExpression[]): TruthTurnCombatExpression =>
|
||||
({ kind, terms });
|
||||
|
||||
/** Original Basic24 beforePlans, original Basic26 unsubmitted Traitor voter,
|
||||
* or original remote allied Harkonnen72. The no-match variant actually seals
|
||||
* another living original Atreides disc; it never exchanges a Traitor or
|
||||
* installs plans. Optional-module Cunning stays outside the native envelope. */
|
||||
export function createTruthTurnCombatFixture(options: {
|
||||
source?: 'plan' | 'traitor' | 'noMatch' | 'harkonnen'; mixed?: TruthTurnCombatExpression;
|
||||
} = {}) {
|
||||
const sourceKind = options.source ?? 'plan';
|
||||
const planSource = sourceKind === 'plan' ? createTruthMixedBattleFixture() : null;
|
||||
const traitorSource = sourceKind === 'traitor' || sourceKind === 'noMatch' ? createTruthMixedTraitorFixture() : null;
|
||||
const harkonnenSource = sourceKind === 'harkonnen' ? createTruthMixedTraitorHarkonnenFixture({ kind: 'ally' }) : null;
|
||||
const original = planSource ?? traitorSource ?? harkonnenSource!;
|
||||
let game = structuredClone(original.game), ownPlan = structuredClone(original.ownPlan);
|
||||
let actions = structuredClone(original.sourceActions);
|
||||
if (sourceKind === 'noMatch') {
|
||||
assert.ok(traitorSource);
|
||||
game = structuredClone(traitorSource.beforePlans);
|
||||
actions = structuredClone(traitorSource.sourceActions.slice(0, -traitorSource.planSteps.length));
|
||||
const other = truthTurnCombatPlayer(game, traitorSource.asker).leaders.find(disc =>
|
||||
!disc.dead && !disc.usedAt && disc.id !== traitorSource.traitorHeld); assert.ok(other);
|
||||
ownPlan = { ...traitorSource.ownPlan, leader: other.id };
|
||||
game = actTruthTurnCombat(game, traitorSource.asker, ownPlan, actions);
|
||||
game = actTruthTurnCombat(game, traitorSource.target, traitorSource.targetPlan, actions);
|
||||
assert.ok(game.battle?.revealed);
|
||||
assert.deepEqual(viewGame(game, traitorSource.target).battle!.traitorCallChoices, [false]);
|
||||
}
|
||||
assert.ok(game.battle?.event);
|
||||
const event = game.battle.event, territory = game.battle.territory;
|
||||
const asker = original.asker, target = original.target, recipient = asker;
|
||||
const weaponClaim: MixedBattleActionClaim = planSource?.weaponClaim ?? { kind: 'weapon', name: null };
|
||||
const mixed = structuredClone(options.mixed ?? truthTurnCombatGroup('and', planSource
|
||||
? truthTurnCombatPlan(event, territory, weaponClaim) : truthTurnCombatCall(event, territory), truthTurnCombatPayment(recipient)));
|
||||
const question = { kind: 'turnActions', target, turn: game.turn, mixed } satisfies TruthQuestion;
|
||||
const spiceFact: TruthFact = { kind: 'spice', compare: 'eq', value: truthTurnCombatPlayer(game, target).spice };
|
||||
const traitorHeld = planSource?.base.base.traitorHeld ?? traitorSource?.traitorHeld ?? harkonnenSource!.traitorHeld;
|
||||
assert.ok(truthTurnCombatPlayer(game, asker).hand.some(card => card.id === original.truth));
|
||||
assert.equal(truthTurnCombatPlayer(game, target).ally === recipient, false);
|
||||
assert.deepEqual(truthTurnCombatInventory(game), original.originalCards);
|
||||
assert.deepEqual(truthTurnCombatTraitors(game), original.originalTraitors);
|
||||
assert.equal(game.turnActionPromises?.length ?? 0, 0);
|
||||
return {
|
||||
game, source: structuredClone(game), sourceKind, planSource, traitorSource, harkonnenSource,
|
||||
asker, target, recipient, truth: original.truth, event, battle: event, territory, mixed, question,
|
||||
initial: original.initial, setup: original.setup, beforeStartingDeal: original.beforeStartingDeal,
|
||||
afterSetup: original.afterSetup, originalCards: original.originalCards, originalTraitors: original.originalTraitors,
|
||||
originalSpice: original.originalSpice, actions, sourceActions: actions, sourceActionCount: actions.length,
|
||||
sourceCounts: { inherited: original.sourceActionCount, selected: actions.length,
|
||||
originalPlanSeals: sourceKind === 'plan' ? 0 : 2 },
|
||||
weaponClaim, spiceFact, traitorHeld, ownPlan, targetPlan: structuredClone(original.targetPlan),
|
||||
projectilePlan: planSource ? structuredClone(planSource.projectilePlan) : null,
|
||||
ordinary: planSource?.ordinary ?? traitorSource?.ordinary ?? null, karama: harkonnenSource?.karama ?? null,
|
||||
beneficiary: harkonnenSource?.beneficiary ?? target,
|
||||
payment: { type: 'bribe', target: recipient, amount: 2 } satisfies Action,
|
||||
reveal: { type: 'traitorCall', call: true } satisfies Action,
|
||||
decline: { type: 'traitorCall', call: false } satisfies Action,
|
||||
sourceDisclosure: `${original.sourceDisclosure} This adapter changes only the raw combined current-battle question. ` +
|
||||
(sourceKind === 'noMatch' ? 'The original beforePlans prefix is followed by two actual seals using another living original Atreides disc; no original Traitor card is moved. ' : '') +
|
||||
'Only actual plan reveal and native Traitor declaration supply future receipts. No hand, cash, physical card, force, death, battle, plan, vote, clock or promise is installed.',
|
||||
};
|
||||
}
|
||||
export type TruthTurnCombatFixture = ReturnType<typeof createTruthTurnCombatFixture>;
|
||||
|
||||
export function declareTruthTurnCombat(f: TruthTurnCombatFixture, source = f.game, actions: TruthTurnCombatStep[] = []) {
|
||||
const before = structuredClone(source), from = actions.length;
|
||||
let game = actTruthTurnCombat(source, f.asker, { type: 'card', card: f.truth }, actions);
|
||||
while (game.truthtrance?.stage === 'priority') {
|
||||
const actor = game.players.find(candidate => !game.truthtrance!.passed.includes(candidate.id)); assert.ok(actor);
|
||||
game = actTruthTurnCombat(game, actor.id, { type: 'truthPass' }, actions);
|
||||
}
|
||||
assert.equal(game.truthtrance?.stage, 'ask');
|
||||
return { game, before, actions, actionCount: actions.length - from };
|
||||
}
|
||||
export function askTruthTurnCombat(game: Game, asker: string, target: string, mixed: TruthTurnCombatExpression,
|
||||
actions: TruthTurnCombatStep[] = []): Game {
|
||||
return actTruthTurnCombat(game, asker, { type: 'truthAsk', question: {
|
||||
kind: 'turnActions', target, turn: game.turn, mixed: structuredClone(mixed),
|
||||
} }, actions);
|
||||
}
|
||||
export function answerTruthTurnCombat(game: Game, target: string, answer: 'yes' | 'no' | 'unknown',
|
||||
actions: TruthTurnCombatStep[] = []): Game {
|
||||
return actTruthTurnCombat(game, target, { type: 'truthAnswer', answer }, actions);
|
||||
}
|
||||
export function bindTruthTurnCombat(f: TruthTurnCombatFixture, answer: 'yes' | 'no' = 'yes', mixed = f.mixed,
|
||||
source = f.game, actions: TruthTurnCombatStep[] = []) {
|
||||
const from = actions.length, declared = declareTruthTurnCombat(f, source, actions);
|
||||
const asked = askTruthTurnCombat(declared.game, f.asker, f.target, mixed, actions);
|
||||
const game = answerTruthTurnCombat(asked, f.target, answer, actions);
|
||||
return { game, declared, asked, actions, actionCount: actions.length - from };
|
||||
}
|
||||
export function truthTurnCombatPromise(game: Game, target = game.turnActionPromises?.[0]?.player ?? game.players[0].id) {
|
||||
const row = viewGame(game, target).turnActionPromises.find(candidate => candidate.player === target); assert.ok(row); return row;
|
||||
}
|
||||
export function revealTruthTurnCombat(game: Game, f: TruthTurnCombatFixture, plan: Action = f.projectilePlan!,
|
||||
actions: TruthTurnCombatStep[] = []) {
|
||||
assert.ok(f.planSource, 'Only the original unsealed-plan source supplies both actual plan controls');
|
||||
game = actTruthTurnCombat(game, f.target, plan, actions); const afterTargetPlan = structuredClone(game);
|
||||
game = actTruthTurnCombat(game, f.asker, f.ownPlan, actions); assert.ok(game.battle?.revealed);
|
||||
return { game, afterTargetPlan, actions };
|
||||
}
|
||||
export function nextTruthTurnCombatNative(game: Game): TruthTurnCombatStep | null {
|
||||
return nextStrongholdFactionsNativeStep(game);
|
||||
}
|
||||
export function nextTruthTurnCombat(game: Game): TruthTurnCombatStep | 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 nextTruthTurnCombatNative(game);
|
||||
}
|
||||
export function advanceTruthTurnCombat(game: Game, until: (state: Game) => boolean,
|
||||
actions: TruthTurnCombatStep[] = [], nativeOnly = false): Game {
|
||||
for (let count = 0; count < 1200; count++) {
|
||||
if (until(game)) return game;
|
||||
assert.notEqual(game.status, 'finished', 'The original combat source ended before its requested native boundary');
|
||||
const next = nativeOnly ? nextTruthTurnCombatNative(game) : nextTruthTurnCombat(game);
|
||||
assert.ok(next, 'Supply the actual original private plan or current native vote before continuing');
|
||||
game = actTruthTurnCombat(game, next.actor, next.action, actions);
|
||||
}
|
||||
throw Error('The genuine combat source did not reach its finite boundary');
|
||||
}
|
||||
export function settleTruthTurnCombat(game: Game, actions: TruthTurnCombatStep[] = []): Game {
|
||||
return advanceTruthTurnCombat(game, truthTurnCombatClean, actions);
|
||||
}
|
||||
export function finishTruthTurnCombatBattle(game: Game, f: TruthTurnCombatFixture, actions: TruthTurnCombatStep[] = []): Game {
|
||||
assert.ok(game.battle?.revealed, 'Actually reveal both original plans before cleanup');
|
||||
if (!f.planSource) assert.notEqual(game.battle.traitorCalls[f.target], undefined, 'Actually declare the bound native Traitor choice');
|
||||
return advanceTruthTurnCombat(game, state => !state.battle || state.battle.event !== f.event, actions, true);
|
||||
}
|
||||
export function finishTruthTurnCombat(game: Game, actions: TruthTurnCombatStep[] = []): Game {
|
||||
const turn = game.turn;
|
||||
return advanceTruthTurnCombat(game, state => state.turn === turn + 1 && state.phase === 0 && truthTurnCombatClean(state), actions);
|
||||
}
|
||||
Reference in new issue
Block a user