Files
Dune/tests/fixture-ambassador-repeat-leaders.ts
T
Quintine 152039be5b Support cohort-eligible Ambassador leader and KH repeats
Replace lifetime first-death caps with the selected provisional composition: first returns remain immediate, while later native leaders and KH must satisfy their existing ordinary death/cohort conditions. Extract one phase/slot/price-independent cohort quote for ordinary Revival and the independent Ambassador. Preserve ordinary timing, payment, prevention, native early service and one-leader controls.

Snapshot an opened cohort before a free return makes a leader alive, preserving remaining ordinary returns. Keep Ecaz identity tracking and the shared Duke permission separate. KH retains activation/loss history and five native discs; update its marker above the actual opened owner cycle and old marker. Independent grants retain ordinary allowance and income policies. Explain eligibility in persistent controls and source-labelled reference.

Verify related rules/controls, types/lint/build, controlled native/copied free-return-to-next-ordinary-Revival programmes layered on original public Basic paid-entry sources, controlled Advanced KH actual entry/return, income allow/cancel and twelve minimal legal policy continuations. Assigned repeat/cohort facts are explicitly controlled, not claimed original battle histories. Full modes, wider custody/module combinations, comprehensive assurance and deployed acceptance remain unfinished. No strategic calibration, saved-room operation, reset, restart or deployment.
2026-10-24 13:43:11 +11:00

41 lines
3.0 KiB
TypeScript

import assert from 'node:assert/strict';
import { createGame, newPlayer, type Game } from '../game/engine';
import { baseDeck } from '../game/cards';
import { createAmbassadors, validateAmbassadors } from '../game/ecaz-ambassadors';
import { createDukeVidal } from '../game/duke-vidal';
import { actDukeAmbassador as act, type DukeAmbassadorStep } from './fixture-duke-ambassador';
const player = (g: Game, id: string) => g.players.find(p => p.id === id)!;
/** Controlled Advanced source, with the original actual shipment/trigger path.
* These assigned cohort/loss facts are not earned gameplay or start proof. */
export function createControlledAmbassadorKhRepeatFixture(marker = 2, live = false) {
const actions: DukeAmbassadorStep[] = [];
const step = (game: Game, actor: string, action: Parameters<typeof act>[2]) => act(game, actor, action, actions);
let game = createGame('AMBASSADORKHREPEAT', newPlayer('ecaz', 'Ecaz', 'ecaz'), true, ['ecaz']);
game.players.push(newPlayer('atreides', 'Atreides', 'atreides'), newPlayer('guild', 'Guild', 'guild'));
Object.assign(game, { status: 'playing', turn: 2, phase: 5, storm: 18, active: 'guild',
order: ['guild', 'ecaz', 'atreides'], movementRemaining: ['guild', 'ecaz', 'atreides'], deck: baseDeck() });
for (const p of game.players) { p.hand = []; p.forces = {}; p.reserves = 20; p.spice = 12; }
game.dukeVidal = createDukeVidal();
const state = createAmbassadors(() => 0);
const cohort = ['tleilaxu', ...state.tokens.map(t => t.effect).filter(e => e !== 'tleilaxu' && e !== 'ecaz')].slice(0, 5);
state.cohort = state.tokens.filter(t => cohort.includes(t.effect)).map(t => t.id);
for (const token of state.tokens) { token.zone = token.effect === 'ecaz' || state.cohort.includes(token.id) ? 'supply' : 'pool'; token.location = null; }
const token = state.tokens.find(t => t.effect === 'tleilaxu')!; token.zone = 'placed'; token.location = 'arrakeen';
validateAmbassadors(state); game.ecazAmbassadors = state;
const p = player(game, 'atreides'); p.ally = 'ecaz'; player(game, 'ecaz').ally = p.id;
p.revivalCycle = live ? 3 : 2; p.leaderRevived = true; p.battleLosses = 10;
p.kwisatz = { dead: true, revivalCycle: marker, usedAt: 'imperial_basin' };
for (const leader of p.leaders) { leader.dead = !live; leader.deaths = 3; }
game = step(game, 'guild', { type: 'ship', amount: 1, territory: 'arrakeen', sector: 10 });
while (game.response) game = step(game, game.players.find(p => !game.response!.passed.includes(p.id))!.id, { type: 'passResponse' });
if (game.decision?.kind === 'guildShipment') {
game = step(game, game.decision.player, { type: 'decision', allow: true });
while (game.response) game = step(game, game.players.find(p => !game.response!.passed.includes(p.id))!.id, { type: 'passResponse' });
}
assert.equal(game.pendingAmbassador?.stage, 'offer');
const event = game.pendingAmbassador!.event;
game = step(game, 'ecaz', { type: 'decision', event, trigger: true, beneficiary: 'atreides', choice: 'effect' });
return { game, event, actions, sourceActionCount: actions.length };
}