Connect bare Basic native Ecaz coalitions
Admit fresh public Basic Ecaz starts with selected native E1/E2 families while retaining roster, inventory, seat and optional-profile guards. Keep actual coalition battle assertions. Include allied marker-only and mixed Richese presence, zero and capped materialization after both seals, and owner-specific casualties. Extend the explicitly provisional additive mixed-marker interpretation to Basic without new arithmetic or FAQ ruling. Use the current physical force pool for battle-wheel half steps so Ecaz can commit Ixian Suboids; cancellation restores Ecaz-only whole steps. Preserve Basic free support, zero funding/income, native Ix substitution, lead-owned cards and original Collection. Verify related Basic/Advanced rules, types/lint/build, nine actual-control/rendered-table programmes after genuine public Basic setup, explicit controlled board/budget/card staging and four existing profiles with legal lead/plan/native-substitution proposals. Full modes, wider optional configurations, comprehensive assurance and deployed acceptance remain unfinished. No strategic calibration, saved-room operation, reset, restart or deployment.
This commit is contained in:
1 parent
152039be5b
commit
1f3e71fe03
12 files changed
+570
-28
No files matched your search
@@ -0,0 +1,29 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { applyAction, createGame, joinGame, newPlayer } from '../game/engine';
|
||||
import { faction, type FactionId } from '../game/catalog';
|
||||
|
||||
function lobby(roster: FactionId[]) {
|
||||
const expansions = [...new Set(roster.map(id => faction(id).expansion).filter(id => id !== 'base'))];
|
||||
let game = createGame('BASICECAZNATIVEENTRY', newPlayer(roster[0], roster[0], roster[0]), false, expansions);
|
||||
for (const id of roster.slice(1)) joinGame(game, newPlayer(id, id, id));
|
||||
for (const p of game.players) game = applyAction(game, p.id, { type: 'ready' });
|
||||
return game;
|
||||
}
|
||||
for (const native of ['ixians', 'choam', 'richese'] as const)
|
||||
void test(`a fresh public Basic Ecaz/${native} lobby starts with its actual native selected family`, () => {
|
||||
const source = lobby(['ecaz', native, 'guild']), before = structuredClone(source);
|
||||
const game = applyAction(source, source.host, { type: 'start' });
|
||||
assert.equal(game.status, 'setup'); assert.equal(game.advanced, false);
|
||||
assert.equal(game.ecazOccupyPreview, true);
|
||||
assert.deepEqual(game.expansions, source.expansions);
|
||||
assert.deepEqual(game.players.map(p => p.faction), source.players.map(p => p.faction));
|
||||
assert.equal(game.leaderSkills, undefined); assert.equal(game.ecazTreachery, undefined);
|
||||
assert.deepEqual(source, before);
|
||||
});
|
||||
void test('the fresh six-native Basic lobby preserves all three selected families through public start', () => {
|
||||
const source = lobby(['ecaz', 'ixians', 'choam', 'richese', 'moritani', 'tleilaxu']);
|
||||
const game = applyAction(source, source.host, { type: 'start' });
|
||||
assert.equal(game.status, 'setup'); assert.equal(game.advanced, false);
|
||||
assert.equal(game.players.length, 6); assert.deepEqual(new Set(game.expansions), new Set(['ecaz', 'ix', 'choam']));
|
||||
});
|
||||
@@ -0,0 +1,184 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { viewGame, type Game } from '../game/engine';
|
||||
import { DIFFICULTIES } from '../game/bot-profiles';
|
||||
import { treacheryDeck } from '../game/cards';
|
||||
import { richeseCards } from '../game/richese-cards';
|
||||
import { ecazOccupyPlanControl } from '../game/ecaz-occupy-options';
|
||||
import { occupyRicheseSeat as player } from './fixture-ecaz-occupy-richese';
|
||||
import { basicEcazNativeFixture, resolveBasicEcazNative } from './fixture-basic-ecaz-native';
|
||||
|
||||
const cards = (game: Game) => [...game.deck, ...game.discard, ...(game.ixSetupCards ?? []),
|
||||
...game.players.flatMap(p => p.hand)].map(card => card.id).sort();
|
||||
const allCards = (game: Game) => [...cards(game), ...(game.richeseCache ?? []).map(card => card.id),
|
||||
...(game.richeseRemoved ?? []).map(card => card.id)].sort();
|
||||
function conserved(game: Game) {
|
||||
for (const p of game.players) {
|
||||
assert.equal(p.reserves + p.tanks + Object.values(p.forces).reduce((sum, n) => sum + n, 0), 20,
|
||||
`${p.faction} retains its original physical counters.`);
|
||||
if (p.elites) assert.equal(p.elites.reserves + p.elites.tanks +
|
||||
Object.values(p.elites.forces).reduce((sum, n) => sum + n, 0), p.faction === 'ixians' ? 7 : 5);
|
||||
}
|
||||
}
|
||||
function originalBasic(fixture: ReturnType<typeof basicEcazNativeFixture>) {
|
||||
assert.equal(fixture.source.status, 'setup');
|
||||
assert.equal(fixture.source.turn, 1);
|
||||
assert.equal(fixture.source.advanced, false);
|
||||
assert.equal(fixture.afterSetup.advanced, false);
|
||||
assert.equal(fixture.afterSetup.status, 'playing');
|
||||
assert.deepEqual(cards(fixture.source), treacheryDeck(fixture.source.expansions).map(card => card.id).sort());
|
||||
assert.deepEqual(fixture.source.richeseCache, richeseCards());
|
||||
const choam = fixture.afterSetup.players.find(p => p.faction === 'choam');
|
||||
if (choam) assert.equal(choam.leaders.length, 5, 'Original Basic CHOAM adds no Advanced Auditor disc.');
|
||||
assert.ok(fixture.afterSetup.players.every(p => p.leaders.length > 0 &&
|
||||
(p.faction === 'tleilaxu' ? p.faceDancers!.length > 0 : p.traitors.length > 0)));
|
||||
assert.equal(player(fixture.game, fixture.ecaz).ally, fixture.ally);
|
||||
assert.equal(player(fixture.game, fixture.ally).ally, fixture.ecaz);
|
||||
assert.ok(fixture.staging.some(line => line.includes('thirty-spice')));
|
||||
conserved(fixture.game);
|
||||
}
|
||||
function nativeWin(result: ReturnType<typeof resolveBasicEcazNative>) {
|
||||
const { fixture, actor, before, game, profile } = result;
|
||||
assert.equal(profile.planOwner, actor);
|
||||
assert.equal(profile.forceOwner, fixture.ally);
|
||||
assert.equal(profile.fixedEcazDial, 3, 'Basic odd-five policy provisionally uses main p8 ceil/floor.');
|
||||
assert.equal(profile.maxSupport, 0);
|
||||
assert.equal(ecazOccupyPlanControl(viewGame(before, actor), Number(result.plan.dial), 0)?.blocked, null);
|
||||
assert.ok(ecazOccupyPlanControl(viewGame(before, actor), Number(result.plan.dial), 1)?.blocked);
|
||||
assert.equal(player(game, fixture.ecaz).forces[fixture.location], 2);
|
||||
assert.equal(player(game, fixture.ecaz).tanks - player(before, fixture.ecaz).tanks, 3);
|
||||
assert.equal(player(game, fixture.opponent).forces[fixture.location] ?? 0, 0);
|
||||
assert.equal(player(game, fixture.opponent).tanks - player(before, fixture.opponent).tanks, 8);
|
||||
for (const id of [fixture.ecaz, fixture.ally]) assert.equal(player(game, id).spice, player(before, id).spice,
|
||||
'Basic support and shared Carthag occupancy produce no Advanced spice debit or city income.');
|
||||
assert.equal(game.lastBattleContext?.event, before.battle?.event);
|
||||
assert.equal(game.lastBattleContext?.winner, actor);
|
||||
assert.equal(game.phase, 7);
|
||||
assert.deepEqual(allCards(game), allCards(fixture.source));
|
||||
conserved(game);
|
||||
}
|
||||
|
||||
for (const allyFaction of ['ixians', 'choam', 'richese'] as const)
|
||||
for (const lead of ['ecaz', 'ally'] as const)
|
||||
void test(`Public Basic native ${allyFaction} coalition with ${lead} lead resolves free actual-owner losses`, () => {
|
||||
const fixture = basicEcazNativeFixture({ allyFaction,
|
||||
opponentFaction: allyFaction === 'ixians' ? 'choam' : 'guild' });
|
||||
originalBasic(fixture);
|
||||
if (allyFaction === 'ixians') assert.deepEqual(fixture.source.players.map(p => p.faction),
|
||||
['ecaz', 'ixians', 'choam', 'richese', 'moritani', 'tleilaxu']);
|
||||
const result = resolveBasicEcazNative(fixture, lead);
|
||||
nativeWin(result);
|
||||
const loss = allyFaction === 'ixians' ? 4 : 2;
|
||||
assert.equal(player(result.game, fixture.ally).forces[fixture.location] ?? 0, 4 - loss);
|
||||
assert.equal(player(result.game, fixture.ally).tanks - player(result.before, fixture.ally).tanks, loss);
|
||||
assert.equal(result.profile.forces.normalFixedHalf, allyFaction === 'ixians');
|
||||
});
|
||||
|
||||
for (const lead of ['ecaz', 'ally'] as const)
|
||||
void test(`Public Basic mixed Ix with ${lead} lead owns its actual typed choice and Suboid substitution`, () => {
|
||||
const fixture = basicEcazNativeFixture({ allyFaction: 'ixians', opponentFaction: 'choam', mixedIxians: true });
|
||||
originalBasic(fixture);
|
||||
const result = resolveBasicEcazNative(fixture, lead, { losses: { normal: 0, elite: 1 }, substitute: true });
|
||||
nativeWin(result);
|
||||
assert.ok(result.typedDecision?.decision?.kind === 'battleLosses');
|
||||
const decision = result.typedDecision.decision;
|
||||
assert.equal(decision.player, result.actor);
|
||||
assert.equal(decision.forceOwner, fixture.ally);
|
||||
assert.ok(decision.options.some(loss => loss.normal === 4 && loss.elite === 0));
|
||||
assert.ok(decision.options.some(loss => loss.normal === 0 && loss.elite === 1));
|
||||
assert.ok(result.afterLosses && result.substitutionDecision && result.afterSubstitution);
|
||||
assert.equal(player(result.afterLosses, fixture.ally).elites!.forces[fixture.location], 1);
|
||||
assert.equal(player(result.afterLosses, fixture.ally).elites!.tanks - player(result.before, fixture.ally).elites!.tanks, 1);
|
||||
assert.equal(result.substitutionDecision.decision?.player, fixture.ally);
|
||||
assert.equal(player(result.afterSubstitution, fixture.ally).elites!.forces[fixture.location], 2);
|
||||
assert.equal(player(result.game, fixture.ally).elites!.tanks, player(result.before, fixture.ally).elites!.tanks);
|
||||
assert.equal(player(result.game, fixture.ally).forces[fixture.location], 7);
|
||||
assert.equal(player(result.game, fixture.ally).tanks - player(result.before, fixture.ally).tanks, 1,
|
||||
'The actual surviving Suboid funds the recovered Cyborg without consuming Ecaz counters.');
|
||||
});
|
||||
|
||||
for (const marker of [0, 3] as const) for (const lead of ['ecaz', 'ally'] as const)
|
||||
void test(`Public Basic concealed Richese ${marker} with ${lead} lead materializes only after both real seals`, () => {
|
||||
const fixture = basicEcazNativeFixture({ marker, richeseReserves: 2 });
|
||||
originalBasic(fixture);
|
||||
const result = resolveBasicEcazNative(fixture, lead);
|
||||
nativeWin(result);
|
||||
assert.ok(player(result.before, fixture.richese).noField!.deployed);
|
||||
assert.deepEqual(player(result.firstSeal, fixture.richese).noField!.deployed,
|
||||
player(result.before, fixture.richese).noField!.deployed);
|
||||
assert.equal(player(result.firstSeal, fixture.richese).reserves, player(result.before, fixture.richese).reserves);
|
||||
assert.equal(player(result.revealed, fixture.richese).noField!.deployed, null);
|
||||
const materialized = Math.min(marker, 2);
|
||||
assert.equal(player(result.before, fixture.richese).reserves - player(result.revealed, fixture.richese).reserves,
|
||||
materialized);
|
||||
assert.equal(player(result.revealed, fixture.richese).forces[fixture.location] ?? 0, materialized);
|
||||
assert.equal(player(result.game, fixture.richese).tanks - player(result.before, fixture.richese).tanks, materialized);
|
||||
assert.equal(player(result.game, fixture.richese).forces[fixture.location] ?? 0, 0);
|
||||
assert.equal(result.profile.allyNoFieldPresence, true, 'A native zero marker remains real coalition presence.');
|
||||
});
|
||||
|
||||
void test('Public Basic Ecaz-led Ix total dial keeps a legal half-step from its native Suboid owner', () => {
|
||||
const fixture = basicEcazNativeFixture({ allyFaction: 'ixians', opponentFaction: 'choam' });
|
||||
const result = resolveBasicEcazNative(fixture, 'ecaz', { variableDial: 0.5 });
|
||||
nativeWin(result);
|
||||
assert.equal(result.plan.dial, 3.5);
|
||||
assert.equal(result.profile.forces.normalFixedHalf, true);
|
||||
assert.equal(player(result.game, fixture.ally).forces[fixture.location], 3);
|
||||
assert.equal(player(result.game, fixture.ally).tanks - player(result.before, fixture.ally).tanks, 1);
|
||||
});
|
||||
|
||||
void test('Public Basic canceled Ecaz-led mixed Ix uses only its own free ordinary pool', () => {
|
||||
const fixture = basicEcazNativeFixture({ allyFaction: 'ixians', opponentFaction: 'choam', mixedIxians: true });
|
||||
const result = resolveBasicEcazNative(fixture, 'ecaz', { canceled: true });
|
||||
assert.equal(result.profile.canceled, true);
|
||||
assert.equal(result.profile.fixedEcazDial, 0);
|
||||
assert.equal(result.profile.forceOwner, fixture.ecaz);
|
||||
assert.equal(result.profile.forces.normalFixedHalf, false);
|
||||
assert.equal(result.profile.maxSupport, 0);
|
||||
assert.equal(result.plan.dial, 2);
|
||||
assert.equal(player(result.game, fixture.ecaz).forces[fixture.location], 3);
|
||||
assert.equal(player(result.game, fixture.ecaz).tanks - player(result.before, fixture.ecaz).tanks, 2);
|
||||
assert.deepEqual(player(result.game, fixture.ally).forces, player(result.before, fixture.ally).forces);
|
||||
assert.deepEqual(player(result.game, fixture.ally).elites, player(result.before, fixture.ally).elites);
|
||||
assert.equal(player(result.game, fixture.ally).tanks, player(result.before, fixture.ally).tanks);
|
||||
assert.equal(player(result.game, fixture.ecaz).spice, player(result.before, fixture.ecaz).spice);
|
||||
assert.equal(player(result.game, fixture.ally).spice, player(result.before, fixture.ally).spice);
|
||||
assert.equal(result.substitutionDecision, null);
|
||||
assert.equal(result.game.phase, 7);
|
||||
conserved(result.game);
|
||||
});
|
||||
|
||||
for (const lead of ['ecaz', 'ally'] as const)
|
||||
void test(`Public Basic mixed Richese ordinary2 plus marker5 with ${lead} lead retains separate materialization and losses`, () => {
|
||||
const fixture = basicEcazNativeFixture({ marker: 5, richeseReserves: 2, mixedRicheseForces: 2 });
|
||||
originalBasic(fixture);
|
||||
const result = resolveBasicEcazNative(fixture, lead);
|
||||
nativeWin(result);
|
||||
assert.equal(player(result.before, fixture.richese).forces[fixture.location], 2);
|
||||
assert.equal(player(result.firstSeal, fixture.richese).forces[fixture.location], 2);
|
||||
assert.equal(player(result.firstSeal, fixture.richese).reserves, 2);
|
||||
assert.ok(player(result.firstSeal, fixture.richese).noField!.deployed);
|
||||
assert.equal(player(result.revealed, fixture.richese).noField!.deployed, null);
|
||||
assert.equal(player(result.revealed, fixture.richese).reserves, 0);
|
||||
assert.equal(player(result.revealed, fixture.richese).forces[fixture.location], 4,
|
||||
'Two original ordinary counters and two reserve-limited marker counters materialize at the real second seal.');
|
||||
assert.equal(result.profile.forces.normal, 4);
|
||||
assert.equal(player(result.game, fixture.richese).forces[fixture.location], 2);
|
||||
assert.equal(player(result.game, fixture.richese).tanks - player(result.before, fixture.richese).tanks, 2,
|
||||
'The variable dial loses two physical Richese counters independently of the three fixed Ecaz casualties.');
|
||||
});
|
||||
|
||||
void test('All four Basic policies select and seal a legal changed native coalition position through ordinary continuation', () => {
|
||||
const fixture = basicEcazNativeFixture({ allyFaction: 'ixians', opponentFaction: 'choam', mixedIxians: true });
|
||||
for (const [index, difficulty] of DIFFICULTIES.entries()) {
|
||||
const lead = index % 2 === 0 ? 'ecaz' : 'ally';
|
||||
const result = resolveBasicEcazNative(fixture, lead,
|
||||
{ difficulty, losses: { normal: 0, elite: 1 }, substitute: true });
|
||||
nativeWin(result);
|
||||
assert.equal(result.selectedLead.lead, result.actor);
|
||||
assert.equal(result.plan.type, 'battlePlan');
|
||||
assert.equal(Number(result.plan.support ?? 0), 0);
|
||||
assert.ok(result.actions.some(step => step.action.type === 'battlePlan' && step.actor === result.actor));
|
||||
assert.ok(result.actions.some(step => step.action.type === 'decision' && step.action.sources));
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,184 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { applyAction, viewGame, type Action, type Game } from '../game/engine';
|
||||
import { botActions } from '../game/bots';
|
||||
import type { Difficulty } from '../game/bot-profiles';
|
||||
import { quoteEcazOccupyDial } from '../game/ecaz-occupy-battle';
|
||||
import { ecazOccupyRicheseFixture, occupyRicheseSeat,
|
||||
type EcazOccupyRicheseFixture, type EcazOccupyRicheseOptions } from './fixture-ecaz-occupy-richese';
|
||||
import { nextRicheseStrongholdsNativeStep } from './fixture-richese-strongholds';
|
||||
|
||||
export type BasicEcazNativeStep = { actor: string; action: Action };
|
||||
export type BasicEcazNativeLead = 'ecaz' | 'ally';
|
||||
export type BasicEcazNativeOptions = Omit<EcazOccupyRicheseOptions,
|
||||
'advanced' | 'publicStart' | 'omitPreview'> & {
|
||||
/** Labelled conserved eight-counter Ix army, including two original Cyborgs. */
|
||||
mixedIxians?: boolean;
|
||||
};
|
||||
export type BasicEcazNativeFixture = EcazOccupyRicheseFixture & { source: Game };
|
||||
export const basicEcazNativeClean = (game: Game) =>
|
||||
!game.response && !game.phaseOpening && !game.decision;
|
||||
|
||||
/** Live native windows only; plans remain an explicit action by their real owner. */
|
||||
export function nextBasicEcazNativeStep(game: Game): BasicEcazNativeStep | null {
|
||||
if (!game.response && !game.phaseOpening && game.decision?.kind === 'richeseAllyOpportunity')
|
||||
return { actor: game.decision.player, action: { type: 'decision', decline: true } };
|
||||
if (!game.response && !game.phaseOpening && game.decision?.kind === 'faceDance')
|
||||
return { actor: game.decision.player, action: { type: 'decision', reveal: false } };
|
||||
return nextRicheseStrongholdsNativeStep(game);
|
||||
}
|
||||
|
||||
/** Genuine public Basic ready/start/setup, original native components, real Nexus
|
||||
* alliance and actual No-Field shipment from the shared fixture. Its conserved
|
||||
* board, thirty-spice budget and Karama staging are explicit controlled inputs;
|
||||
* this is not an unstaged game or a played Advanced game with a changed flag. */
|
||||
export function basicEcazNativeFixture(options: BasicEcazNativeOptions = {}): BasicEcazNativeFixture {
|
||||
const { mixedIxians, ...nativeOptions } = options;
|
||||
const fixture = ecazOccupyRicheseFixture({ ...nativeOptions, advanced: false, publicStart: true });
|
||||
assert.equal(fixture.initial.advanced, false);
|
||||
assert.equal(fixture.initial.status, 'setup');
|
||||
assert.equal(fixture.initial.ecazOccupyPreview, true);
|
||||
assert.ok(!fixture.initial.ecazTreachery && !fixture.initial.homeworlds &&
|
||||
!fixture.initial.nexusCards && !fixture.initial.leaderSkills &&
|
||||
!fixture.initial.strongholdCards && !fixture.initial.techTokens &&
|
||||
!fixture.initial.discoveryEnabled && !fixture.initial.greatMaker);
|
||||
if (mixedIxians) {
|
||||
// Stage before admission, so the original chooseBattle sees actual native
|
||||
// types. The first shared-fixture admission is discarded with its branch.
|
||||
let game = structuredClone(fixture.beforeBattle);
|
||||
const ix = occupyRicheseSeat(game, fixture.ally);
|
||||
assert.equal(ix.faction, 'ixians');
|
||||
assert.ok(ix.elites && ix.elites.reserves >= 2 && ix.reserves >= 4);
|
||||
assert.equal(ix.forces[fixture.location], 4);
|
||||
ix.reserves -= 4;
|
||||
ix.forces[fixture.location] += 4;
|
||||
ix.elites.reserves -= 2;
|
||||
ix.elites.forces[fixture.location] = 2;
|
||||
fixture.staging.push('Before real battle admission: conserve eight actual Ixian counters at Carthag, including two original reserve Cyborgs; no typed casualty or substitution is installed.');
|
||||
fixture.beforeBattle = structuredClone(game);
|
||||
const choice = viewGame(game, game.active!).battleChoices.find(b => b.territory === fixture.territory);
|
||||
assert.ok(choice);
|
||||
game = applyAction(game, choice.chooser, { type: 'chooseBattle', territory: fixture.territory,
|
||||
target: choice.attacker === choice.chooser ? choice.defender : choice.attacker });
|
||||
fixture.game = game;
|
||||
}
|
||||
return { ...fixture, source: fixture.initial };
|
||||
}
|
||||
|
||||
export type BasicEcazNativeResolveOptions = {
|
||||
/** Choose the live allocation; omitted cases consume its first legal option. */
|
||||
losses?: { normal: number; elite: number };
|
||||
substitute?: boolean;
|
||||
canceled?: boolean;
|
||||
variableDial?: number;
|
||||
/** Uses the existing projected legal policy for the lead and plain plan. */
|
||||
difficulty?: Difficulty;
|
||||
};
|
||||
|
||||
function leader(game: Game, actor: string, strongest: boolean): string {
|
||||
const available = occupyRicheseSeat(game, actor).leaders.filter(l => !l.dead && !l.usedAt)
|
||||
.sort((a, b) => strongest ? b.strength - a.strength : a.strength - b.strength);
|
||||
assert.ok(available[0]);
|
||||
return available[0].id;
|
||||
}
|
||||
function policyActions(game: Game, actor: string, difficulty: Difficulty): Action[] {
|
||||
const view = viewGame(game, actor);
|
||||
view.players.find(p => p.id === actor)!.bot = difficulty;
|
||||
return botActions(view);
|
||||
}
|
||||
|
||||
/** Select, seal, reveal and settle one original Basic native coalition battle.
|
||||
* Captured stages and actions are available for bounded runtime/control proof. */
|
||||
export function resolveBasicEcazNative(fixture: BasicEcazNativeFixture, lead: BasicEcazNativeLead,
|
||||
options: BasicEcazNativeResolveOptions = {}) {
|
||||
const actor = lead === 'ecaz' ? fixture.ecaz : fixture.ally;
|
||||
let game = structuredClone(fixture.game);
|
||||
const actions: BasicEcazNativeStep[] = [];
|
||||
const act = (player: string, action: Action) => {
|
||||
game = applyAction(game, player, action);
|
||||
actions.push({ actor: player, action: structuredClone(action) });
|
||||
};
|
||||
const next = () => {
|
||||
const step = nextBasicEcazNativeStep(game);
|
||||
assert.ok(step, 'The actual native continuation must retain a legal action.');
|
||||
act(step.actor, step.action);
|
||||
};
|
||||
assert.equal(game.decision?.kind, 'ecazBattleLead');
|
||||
const selectedLead = options.difficulty
|
||||
? policyActions(game, fixture.ecaz, options.difficulty).find(a => a.type === 'decision' && a.lead === actor)
|
||||
: { type: 'decision', event: game.decision!.event, lead: actor } satisfies Action;
|
||||
assert.ok(selectedLead, 'The policy must retain this actual native lead choice.');
|
||||
act(fixture.ecaz, selectedLead);
|
||||
if (options.canceled) {
|
||||
assert.equal(game.response?.kind, 'ecazOccupy');
|
||||
const card = occupyRicheseSeat(game, fixture.opponent).hand.find(c => c.effect === 'karama');
|
||||
assert.ok(card);
|
||||
act(fixture.opponent, { type: 'card', mode: 'cancel', card: card.id });
|
||||
}
|
||||
for (let count = 0; count < 100; count++) {
|
||||
if (game.battle && !game.battle.preparation &&
|
||||
(!game.battle.preLeader || game.battle.preLeader.closed) && basicEcazNativeClean(game)) break;
|
||||
next();
|
||||
}
|
||||
assert.ok(game.battle && basicEcazNativeClean(game));
|
||||
const before = structuredClone(game);
|
||||
const profile = viewGame(game, actor).battle!.ecazOccupy!.profile!;
|
||||
assert.ok(profile);
|
||||
assert.equal(profile.advanced, false);
|
||||
assert.equal(profile.maxSupport, 0);
|
||||
const variable = options.variableDial ?? Math.min(2, profile.maxDial - profile.fixedEcazDial);
|
||||
const dial = profile.fixedEcazDial + variable;
|
||||
quoteEcazOccupyDial(profile, dial, 0);
|
||||
const plan = options.difficulty
|
||||
? policyActions(game, actor, options.difficulty).find(a => a.type === 'battlePlan' &&
|
||||
a.dial === dial && Number(a.support ?? 0) === 0 && !a.weapon && !a.defense &&
|
||||
a.leader === leader(game, actor, true))
|
||||
: { type: 'battlePlan', dial, support: 0, leader: leader(game, actor, true) } satisfies Action;
|
||||
assert.ok(plan, 'Every profile must offer a legal plain actual-owner plan.');
|
||||
const concealed = structuredClone(occupyRicheseSeat(game, fixture.richese).noField!.deployed);
|
||||
act(actor, plan);
|
||||
const firstSeal = structuredClone(game);
|
||||
assert.equal(game.battle!.revealed, false);
|
||||
assert.deepEqual(occupyRicheseSeat(game, fixture.richese).noField!.deployed, concealed);
|
||||
assert.equal(viewGame(game, fixture.opponent).battle!.plans[actor], undefined);
|
||||
act(fixture.opponent, { type: 'battlePlan', dial: 0, support: 0,
|
||||
leader: leader(game, fixture.opponent, false) });
|
||||
assert.equal(game.battle!.revealed, true);
|
||||
const revealed = structuredClone(game);
|
||||
let typedDecision: Game | null = null;
|
||||
let afterLosses: Game | null = null;
|
||||
let substitutionDecision: Game | null = null;
|
||||
let afterSubstitution: Game | null = null;
|
||||
for (let count = 0; count < 200; count++) {
|
||||
if (!game.battle && basicEcazNativeClean(game) && !game.pendingTreacheryDiscard) break;
|
||||
// Actions replace this game inside the local closure. Read each current
|
||||
// decision afresh instead of retaining the initial lead-choice narrowing.
|
||||
const decision = (game as Game).decision;
|
||||
if (!game.response && !game.phaseOpening && decision?.kind === 'battleLosses') {
|
||||
typedDecision = structuredClone(game);
|
||||
const choice = options.losses ? decision.options.findIndex(loss =>
|
||||
loss.normal === options.losses!.normal && loss.elite === options.losses!.elite) : 0;
|
||||
assert.ok(choice >= 0, 'The original native decision must offer the requested typed loss.');
|
||||
act(decision.player, { type: 'decision', choice });
|
||||
afterLosses = structuredClone(game);
|
||||
} else if (!game.response && !game.phaseOpening && decision?.kind === 'ixSubstitution') {
|
||||
substitutionDecision = structuredClone(game);
|
||||
assert.equal(decision.player, fixture.ally, 'The physical Ixian owns substitution.');
|
||||
if (options.substitute) {
|
||||
const lost = decision.losses[fixture.location];
|
||||
assert.ok(lost > 0);
|
||||
act(decision.player, { type: 'decision', sources: { [fixture.location]: lost },
|
||||
recover: { [fixture.location]: lost } });
|
||||
} else act(decision.player, { type: 'decision', decline: true });
|
||||
while (game.response) next();
|
||||
afterSubstitution = structuredClone(game);
|
||||
} else next();
|
||||
}
|
||||
assert.equal(game.battle, null);
|
||||
assert.equal(game.decision, null);
|
||||
assert.equal(game.lastBattleContext?.result, 'normal');
|
||||
assert.equal(game.lastBattleContext?.winner, actor);
|
||||
assert.equal(game.phase, 7, 'The original coalition battle continues to native Basic Collection.');
|
||||
return { fixture, source: fixture.source, actor, selectedLead, before, firstSeal, revealed,
|
||||
typedDecision, afterLosses, substitutionDecision, afterSubstitution,
|
||||
profile, variable, plan, actions, staging: fixture.staging, result: game, game };
|
||||
}
|
||||
@@ -8,7 +8,9 @@ import { finishEcazOccupySetup, stageEcazOccupyCard } from './fixture-ecaz-occup
|
||||
import { nextRicheseStrongholdsNativeStep } from './fixture-richese-strongholds';
|
||||
|
||||
export type EcazOccupyRicheseOptions = {
|
||||
allyFaction?: 'richese' | 'ixians' | 'guild';
|
||||
allyFaction?: 'richese' | 'ixians' | 'choam' | 'guild';
|
||||
advanced?: boolean;
|
||||
publicStart?: boolean;
|
||||
opponentFaction?: 'richese' | 'choam' | 'atreides' | 'guild';
|
||||
marker?: NoFieldValue;
|
||||
richeseReserves?: number;
|
||||
@@ -72,11 +74,12 @@ function place(game: Game, owner: string, location: string, count: number): void
|
||||
if (count) player.forces[location] = (player.forces[location] ?? 0) + count;
|
||||
}
|
||||
|
||||
/** Real fresh Advanced family deal and natural Nexus alliance. Explicitly
|
||||
/** Real fresh selected Basic/Advanced family deal and natural Nexus alliance. Explicitly
|
||||
* controlled conserved board/bank/card staging follows original setup; only
|
||||
* native shipment creates the concealed marker, and actual chooseBattle admits
|
||||
* the coalition. No phase, faction, alliance, plan or marker is assigned. */
|
||||
export function ecazOccupyRicheseFixture(options: EcazOccupyRicheseOptions = {}): EcazOccupyRicheseFixture {
|
||||
const advanced = options.advanced ?? true;
|
||||
const allyFaction = options.allyFaction ?? 'richese';
|
||||
const opponentFaction = options.opponentFaction ?? 'guild';
|
||||
assert.notEqual(allyFaction, opponentFaction);
|
||||
@@ -89,12 +92,12 @@ export function ecazOccupyRicheseFixture(options: EcazOccupyRicheseOptions = {})
|
||||
game = structuredClone(options.initial);
|
||||
assert.equal(game.status, 'setup');
|
||||
assert.equal(game.turn, 1);
|
||||
assert.equal(game.advanced, true);
|
||||
assert.equal(game.advanced, advanced);
|
||||
assert.equal(game.ecazOccupyPreview, true);
|
||||
assert.deepEqual(game.players.map(p => p.faction), roster);
|
||||
assert.deepEqual(game.expansions, expansions);
|
||||
} else {
|
||||
game = createGame('ECAZOCCUPYRICHESE', newPlayer(roster[0], roster[0], roster[0]), true, expansions);
|
||||
game = createGame('ECAZOCCUPYRICHESE', newPlayer(roster[0], roster[0], roster[0]), advanced, expansions);
|
||||
for (const faction of roster.slice(1)) joinGame(game, newPlayer(faction, faction, faction));
|
||||
for (let i = roster.length - 1; i >= 0; i--) {
|
||||
const position = i + 1;
|
||||
@@ -102,7 +105,9 @@ export function ecazOccupyRicheseFixture(options: EcazOccupyRicheseOptions = {})
|
||||
game = applyAction(game, roster[i], { type: 'seatPosition', position });
|
||||
}
|
||||
for (const player of game.players) game = applyAction(game, player.id, { type: 'ready' });
|
||||
game = initializeEcazOccupyGameForAudit(game);
|
||||
game = options.publicStart
|
||||
? applyAction(game, game.host, { type: 'start', ...(advanced ? { advancedPreview: true } : {}) })
|
||||
: initializeEcazOccupyGameForAudit(game);
|
||||
}
|
||||
const initial = structuredClone(game);
|
||||
game = finishEcazOccupySetup(game);
|
||||
|
||||
Reference in new issue
Block a user