Files
Dune/tests/fixture-ecaz-duke-skills.ts

157 lines
8.3 KiB
TypeScript

import assert from 'node:assert/strict';
import { applyAction, viewGame, type Action, type Game } from '../game/engine';
import { leaders, treacheryDeck, type Card } from '../game/cards';
import { traitorDeck } from '../game/traitors';
import { DUKE_VIDAL_ID } from '../game/duke-vidal';
import { validateAmbassadors } from '../game/ecaz-ambassadors';
import {
advancedNativePlayer as player, advancedNativeStep, assertAdvancedNativeCustody,
advanceAdvancedNativeSkillsToPhase, completedAdvancedNativeSkillsGame,
stageAdvancedNativeSkillBattle,
} from './fixture-advanced-native-skills';
export const reloadEcazDukeSkills = (game: Game): Game => JSON.parse(JSON.stringify(game)) as Game;
export const ecazDukeSkillsAction = (game: Game, actor: string, action: Action): Game =>
applyAction(reloadEcazDukeSkills(game), actor, action);
export const ecazDukeSkillsTrainer = (game: Game) => {
const assignment = game.leaderSkills!.assignments.find(a => a.owner === player(game, 'ecaz').id)!;
assert.equal(assignment.skill, 'warmaster');
return assignment;
};
export function assertEcazDukeSkillsCustody(game: Game): void {
assertAdvancedNativeCustody(game);
const physical = [...game.deck, ...game.discard, ...game.players.flatMap(p => p.hand),
...(game.auction?.cards.slice(game.auction.index) ?? [])].map(c => c.id);
assert.deepEqual(physical.sort(), treacheryDeck(game.expansions).map(c => c.id).sort());
for (const seat of game.players)
assert.deepEqual(seat.leaders.map(l => l.id).sort(), leaders(seat.faction).map(l => l.id).sort());
assert.equal(player(game, 'ecaz').leaders.length, 5);
assert.equal(game.dukeVidal!.leader.id, DUKE_VIDAL_ID);
assert.equal(game.dukeVidal!.leader.strength, 6);
assert.equal(game.dukeVidal!.leader.faction, 'ecaz');
assert.equal(game.players.some(p => p.leaders.some(l => l.id === DUKE_VIDAL_ID)), false);
assert.equal(game.leaderSkills!.assignments.some(a => a.leader === DUKE_VIDAL_ID), false);
const traits = [...game.traitorReserve!, ...game.players.flatMap(p => p.traitors),
...(game.ecazLoyalty?.card ? [game.ecazLoyalty.card] : [])];
assert.deepEqual(traits.sort(), traitorDeck(game.players, game.expansions.includes('ix')).sort());
assert.equal(traits.includes(DUKE_VIDAL_ID), false);
validateAmbassadors(game.ecazAmbassadors!);
}
function responses(state: Game): Game {
let game = state;
for (let i = 0; game.response && i < 100; i++)
game = advancedNativeStep(reloadEcazDukeSkills(game));
assert.equal(game.response, null);
return game;
}
/** Original Ecaz setup, original end-of-Revival paid token placement, actual
* entrant shipment and owner acquisition. The only controlled board position
* returns deployed forces to their own reserves and puts the storm at sector18
* after genuine setup/phase progression. Cards, wallets, discs, traitors and
* the original random Ambassador cohort are never replaced or cherry-picked. */
export function acquireNativeEcazSkillDuke(rules: 'basic' | 'advanced' = 'advanced'): Game {
let game = completedAdvancedNativeSkillsGame({
family: 'ecaz', requestedSkill: 'warmaster', opponents: ['emperor'], rules,
});
const actor = player(game, 'ecaz').id, entrant = player(game, 'emperor').id;
const assignment = structuredClone(ecazDukeSkillsTrainer(game));
assert.equal(game.dukeVidal!.controller, null);
assertEcazDukeSkillsCustody(game);
game = advanceAdvancedNativeSkillsToPhase(game, 4);
// Explicit conserved position, not a claim of played movement history.
for (const seat of game.players) {
seat.reserves += Object.values(seat.forces).reduce((sum, n) => sum + n, 0);
seat.forces = {};
if (seat.elites) {
seat.elites.reserves += Object.values(seat.elites.forces).reduce((sum, n) => sum + n, 0);
seat.elites.forces = {};
}
}
game.storm = 18;
for (let i = 0; game.decision?.kind !== 'ecazPlacement' && i < 100; i++)
game = advancedNativeStep(reloadEcazDukeSkills(game));
assert.equal(game.decision?.kind, 'ecazPlacement');
const token = game.ecazAmbassadors!.tokens.find(t => t.effect === 'ecaz')!;
const cohort = structuredClone(game.ecazAmbassadors!.cohort);
const wallet = player(game, 'ecaz').spice;
game = ecazDukeSkillsAction(game, actor, { type: 'decision', token: token.id, territory: 'arrakeen' });
game = responses(game);
assert.equal(player(game, 'ecaz').spice, wallet - 1);
assert.equal(game.ecazAmbassadors!.tokens.find(t => t.id === token.id)!.zone, 'placed');
game = ecazDukeSkillsAction(game, actor, { type: 'decision', decline: true });
game = responses(game);
assert.equal(game.phase, 5);
for (let i = 0; game.active !== entrant && i < 10; i++) {
assert.ok(game.active);
game = responses(ecazDukeSkillsAction(game, game.active, { type: 'endMovement' }));
}
assert.equal(game.active, entrant);
const before = reloadEcazDukeSkills(game);
game = ecazDukeSkillsAction(game, entrant, { type: 'ship', territory: 'arrakeen', sector: 10, amount: 1 });
game = responses(game);
assert.equal(game.pendingAmbassador!.owner, actor);
assert.equal(game.pendingAmbassador!.entrant, entrant);
assert.equal(viewGame(game, actor).ambassadorEntry!.dukeAcquisition!.blocked, null);
assert.equal(player(game, 'emperor').reserves, player(before, 'emperor').reserves - 1);
assert.equal(player(game, 'emperor').forces['arrakeen:10'], 1);
assert.equal(player(game, 'emperor').spice, player(before, 'emperor').spice - 1);
const disc = structuredClone(game.dukeVidal!.leader);
const shippedPlayers = structuredClone(game.players);
game = ecazDukeSkillsAction(game, actor, {
type: 'decision', event: game.pendingAmbassador!.event,
trigger: true, beneficiary: actor, choice: 'duke',
});
assert.deepEqual(game.players, shippedPlayers);
assert.deepEqual(game.dukeVidal!.leader, disc);
assert.deepEqual([game.dukeVidal!.controller, game.dukeVidal!.source, game.dukeVidal!.acquiredTurn],
[actor, 'ecaz', game.turn]);
assert.equal(game.pendingAmbassador, null);
assert.equal(game.ecazAmbassadors!.tokens.find(t => t.id === token.id)!.zone, 'supply');
assert.deepEqual(game.ecazAmbassadors!.cohort, cohort);
assert.deepEqual(ecazDukeSkillsTrainer(game), assignment);
assertEcazDukeSkillsCustody(game);
return game;
}
/** Controlled subsequent native battle position after real token acquisition.
* Transfer original deck cards, not fabricated cards; original Duke custody and
* its separate normal-disc trainer survive the conserved board relocation. */
export function stageNativeEcazDukeSkillBattle(state: Game, poison = false): {
game: Game; actor: string; enemy: string; worthless: Card; weapon?: Card; defender: string;
} {
const actor = player(state, 'ecaz').id, enemy = player(state, 'emperor').id;
let game = stageAdvancedNativeSkillBattle(state, actor, enemy);
// Retain the original interrupted movement suffix; only the conserved board
// position is staged. Both armies now have a real pending native battle.
Object.assign(game, { phase: state.phase, active: state.active, order: [...state.order], ready: [...state.ready] });
for (let i = 0; game.phase === 5 && i < 20; i++) {
assert.ok(game.active);
game = responses(ecazDukeSkillsAction(game, game.active, { type: 'endMovement' }));
}
assert.equal(game.phase, 6);
assert.equal(game.dukeVidal!.controller, actor);
// Explicit controlled chooser order at the genuine Battle boundary gives
// Ecaz the attacker's printed tie-break; neither army or battle is removed.
game.order = [actor, enemy];
game.active = actor;
const take = (owner: string, kind: Card['kind']): Card => {
const index = game.deck.findIndex(c => c.kind === kind);
assert.ok(index >= 0, 'Transfer an original physical native-deck card.');
const card = game.deck.splice(index, 1)[0];
game.players.find(p => p.id === owner)!.hand.push(card);
return card;
};
const worthless = take(actor, 'worthless');
const weapon = poison ? take(enemy, 'poison') : undefined;
// The real setup attaches the Emperor's card to first-disc Fenring6,
// leaving this printed5 disc available while the native trainers stay face up.
const defender = player(game, 'emperor').leaders.find(l => l.strength === 5)!;
assert.notEqual(game.leaderSkills!.assignments.find(a => a.owner === enemy)!.leader, defender.id);
assertEcazDukeSkillsCustody(game);
return { game, actor, enemy, worthless, weapon, defender: defender.id };
}