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:
Quintine committed 2026-10-24 13:54:50 +11:00
1 parent 152039be5b
commit 1f3e71fe03
12 files changed
+570 -28

No files matched your search

+29
View File
@@ -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']));
});
+184
View File
@@ -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));
}
});
+184
View File
@@ -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 };
}
+10 -5
View File
@@ -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);