Connect native Homeworld Ecaz Arrakis coalition battles

This commit is contained in:
Quintine committed 2026-10-17 18:24:11 +11:00
1 parent 210aa04811
commit 7ef3377218
10 files changed
+310 -22

No files matched your search

+3 -2
View File
@@ -48,8 +48,9 @@ The 17 October lobby-access pass opens expansion factions and supported optional
modules as previews; it does not remove their existing composition guards.
Skills and the independent Ecaz cards use lobby-only selections consumed during
start; the other modules retain their existing undealt lobby shape. Every
selected module must be honoured or the start rejected. Ecaz with Homeworlds
alone remains unavailable because its combined Occupy battle is not supported.
selected module must be honoured or the start rejected. Ecaz selections must
also support their mandatory allied Arrakis Occupy battle; fresh Ecaz/classic
Homeworlds now compose that path, with or without the supported Skills entry.
Special development profiles remain separate. No pending rules ruling or
complete-mode/publication gate is resolved by opening these controls; see the
[decision index](RULE_DECISIONS.md).
+14 -2
View File
@@ -29,13 +29,25 @@ optional skill bands and combinations remain open.
Rules implementation continues; comprehensive assurance and administration
remain deferred until all rules are implemented.
**Native World Ecaz Arrakis Occupy — 17 October.** Fresh Ecaz/classic
Homeworld games no longer need Nexus Cards for their ordinary Arrakis
coalition, including the supported native Skills entry. Basic setup now marks
Ecaz participation before shared movement/battle enumeration. Actual World
shipments, four Ecaz/four Imperial counters, selected lead, fixed losses,
Sardaukar/support and Karama use the existing physical rule paths. World
locations themselves do not gain Occupy or Intrusion. Seven new cases and the
affected91/91 union, types/lint and build pass; four native-action smokes reach
Collection. Actual390px Ecaz World+Skills lobby entry has no browser errors or
overflow; coalition arithmetic is engine evidence, not a browser-played battle.
[Source and limits](ECAZ_OCCUPY_RULES.md#native-world-reserves-with-arrakis-occupy--17-october-2026).
**Optional modules from the normal lobby — 17 October (user-directed speed pass).**
The host can now tick Leader Skills, Ecaz Treachery Cards, Nexus Cards,
Discoveries and Homeworlds as labelled previews alongside Tech Tokens and
Stronghold Cards. A normal start routes to the existing genuine setup for that
selection; each module's own admission still decides what combinations are
supported, and an unsupported choice is rejected at start with its reason. Ecaz
with Homeworlds alone stays unavailable (no combined Occupy composition yet).
supported, and an unsupported choice is rejected at start with its reason.
Ecaz's native World Arrakis coalition is connected in the follow-up above.
Lobby choices for Skills and the independent cards live in `lobbyOptions` and
are consumed at start. A 240-start sweep (Basic/Advanced × classic/Ix/CHOAM/Ecaz
× ten selections, repeated three times) exercised 183 all-AI games to turn 3 or
+30
View File
@@ -2,6 +2,36 @@
Historical primary-source audit, 2026-09-07, corrected after independent verification. The publisher/designer odd-force conflict remains unresolved as a publisher correction. Under the 8 October Basic-first prototype instruction, Basic now provisionally uses the main p8 paragraph: ceiling contribution/loss and floor survivors, with a visible control warning. This is not a numerical ruling by the user or designer. Advanced retains its separately authorized source. The earlier false attribution to Jack Reda remains withdrawn.
## Native World reserves with Arrakis Occupy — 17 October 2026
Fresh Ecaz/classic Homeworld games now retain their ordinary Arrakis coalition
without needing Nexus Cards. The original Homeworld occupation entry marks
Ecaz participation in Basic as well as Advanced; the supported native
Homeworld/Leader Skills entry keeps the same lead, skill posture and
owner-labelled army path. This composes the supplied revision's physical
pages21–23 optional-module permission with the existing printed/authorized
Occupy arithmetic, not a new combat formula.
Homeworlds change the source of shipped reserves and native benefits, not the
identity of an Arrakis army. A real worm alliance and native shipments produce
four Ecaz and four Emperor fighters (two Sardaukar) against two Guild fighters.
Both lead choices preserve fixed Ecaz losses and the allied typed commitment;
Advanced Salusa's unpaid full-strength Sardaukar still compose with paid
ordinary support. Karama keeps the selected lead and spares the excluded
co-side. Arrakis casualties never remove another copy of those forces from a
Homeworld pool. Actual World battles remain ordinary separate-army battles:
this does not add Occupy or Intrusion to Homeworld locations.
Seven new cases cover both modes/leads, explicit typed losses, cancellation
and four legal Skills policies. The affected eight-file union91/91, types/lint
and build pass; four standalone original-action smokes reach Collection.
Real390px normal-lobby Ecaz + Homeworlds + Skills setup reaches its offered
skill controls with no app errors or horizontal overflow. That is entry/control
proof, not a browser-played coalition battle or complete combination acceptance.
Unopened Spice order is controlled in the fixture; armies, wallets, alliances,
World custody and phases follow native actions. Other E3/mixed module envelopes,
World-location coalition rules and final assurance remain separate.
## Advanced Ecaz special Karama first version — 8 October 2026
The authorized supplied revision, physical **p30, Ecaz G**, permits Karama
+25
View File
@@ -1,5 +1,30 @@
# Dune implementation status
## 17 October 2026 — Native World Ecaz Arrakis coalition
The existing native World occupation and World/Skills setups could deal Ecaz
but still rejected the mandatory allied Arrakis battle. Fresh Ecaz/classic
World compositions now reuse the ordinary coalition instead of requiring Nexus
Cards; the original World occupation entry also marks Basic participation.
World locations remain outside Occupy/Intrusion. No army merger, new support
rule, World loss adapter, played-game conversion or numerical ruling is added.
Genuine setup, original worm alliance and native paid shipments put Ecaz4,
Emperor4 (two Sardaukar) and Guild2 into a clear Arrakis stronghold. Both leads
retain fixed Ecaz losses, exact allied normal/starred losses and unchanged
native World custody. Advanced Salusa and paid ordinary support compose through
the original physical quote; native Karama keeps the lead and excludes its
co-side. The existing Skills disclosure/lead controls and four legal policies
reach original Collection. Only unopened Spice order is controlled.
The affected eight-file union **91/91**, types/lint and build pass; four separate
runtime smokes reach Collection without rejected actions. Actual390px Ecaz
World+Skills normal-lobby start reaches the real skill offers with no app errors
or overflow. Coalition arithmetic is engine proof, not a browser-played battle.
The broader9866-case pass belongs to preceding lobby checkpoint `210aa04`, not
this follow-up. Other roster/module compositions and final assurance remain
open. [Source and detailed boundaries](ECAZ_OCCUPY_RULES.md#native-world-reserves-with-arrakis-occupy--17-october-2026).
## 17 October 2026 — Normal-lobby module previews and legal continuation
Hosts can select Leader Skills, the independent three-card Ecaz variant,
+5 -3
View File
@@ -1,6 +1,6 @@
# Rule decision index
Updated 15 October 2026. This is the authoritative navigation index for existing decisions, not a replacement rulebook or a new ruling. Detailed linked contracts retain their source citations and exact supported boundaries. Runtime follow-ups supersede historical absence claims; passing tests establish software behavior, not publisher authority.
Updated 17 October 2026. This is the authoritative navigation index for existing decisions, not a replacement rulebook or a new ruling. Detailed linked contracts retain their source citations and exact supported boundaries. Runtime follow-ups supersede historical absence claims; passing tests establish software behavior, not publisher authority.
## Physical Traitor roles — 15 October 2026
@@ -616,13 +616,15 @@ Discovery, Tech and Strongholds together/separately irrespective of faction
selection. The bounded fresh native composition therefore reuses supported
E1/E2 and standalone Ecaz OR Moritani/classic setup predicates, not a new
rule model. Original skill-before-faction ordering applies to typed Suk and
residual Cyborg substitution; Homeworlds remain outside Arrakis Occupy and
Intrusion. Existing CHOAM low-opening-income, Basic odd Ecaz and exhausted
residual Cyborg substitution. The 17 October follow-up connects original
Homeworld reserves to Ecaz's Arrakis Occupy; World locations themselves do not
gain Occupy or Intrusion. Existing CHOAM low-opening-income, Basic odd Ecaz and exhausted
Richese-cache guards remain visible in original game captures. The external
Imperial Suk split is still an implementation interpretation, not ordinary
revival placement or a new user/publisher ruling. Native Face Dance,
Diplomat retreat and other pending interpretations remain separate.
[Exact scope and evidence](HOMEWORLD_RUNTIME_IMPLEMENTATION.md#native-optional-module-compositions--6-october-2026).
[Native World Arrakis coalition follow-up](ECAZ_OCCUPY_RULES.md#native-world-reserves-with-arrakis-occupy--17-october-2026).
## Advanced Homeworld occupation source cutover — 5 October 2026
+13 -1
View File
@@ -9588,7 +9588,10 @@ export function initializeHomeworldOccupationGameForAudit(state: Game, leaderSki
g.homeworldTupileState = createOccupiedTupileState();
g.pendingHomeworldTupileCleanup = null;
}
if (byFaction(g, 'ecaz')) g.homeworldEcazVictoryPreview = true;
if (byFaction(g, 'ecaz')) {
g.homeworldEcazVictoryPreview = true;
g.ecazOccupyPreview = true;
}
return g;
}
/** Shared classic/supported-native Nexus setup; existing composition guards apply. */
@@ -10523,6 +10526,15 @@ function ecazOccupyCompositionSupported(g: Game): boolean {
g.players.some(p => p.faction === 'ecaz')) return true;
if (g.discoveries && nativeDiscoveryLeaderSkillsProfile(g) &&
g.players.some(p => p.faction === 'ecaz')) return true;
// Native World reserve/threshold rules do not replace the ordinary Arrakis
// coalition. Reuse that battle only within the original Ecaz/classic setups.
if (g.homeworlds && g.expansions.length === 1 && g.expansions[0] === 'ecaz' &&
g.players.some(p => p.faction === 'ecaz') &&
g.players.every(p => p.faction === 'ecaz' || CLASSIC_FACTIONS[p.faction]) &&
((g.homeworldOccupationPreview === true && !g.leaderSkills &&
!g.nexusCards && !g.techTokens && !g.strongholdCards && !g.discoveryEnabled) ||
(g.ecazOccupyPreview === true && !!g.leaderSkills && homeworldLeaderSkillsProfile(g))))
return true;
return typeof g.advanced === 'boolean' && Array.isArray(g.expansions) &&
g.expansions.includes('ecaz') &&
g.expansions.every(id => ['ecaz', 'ix', 'choam'].includes(id)) &&
+1 -1
View File
@@ -2176,7 +2176,7 @@ export const RULE_TOPICS: RuleTopic[] = [
area: 'Implementation',
status: 'Partial',
detail:
'Base setup enforces prediction, traitors, forces and automatic dealing in order, with private choices and preserved saved tables. Advanced starting advisors and elite forces share the same setup logic. The normal lobby now starts selected expansions and the Leader Skills, Ecaz Treachery Card, Nexus, Discovery, Homeworld, Tech and Stronghold modules through their existing genuine setups as labelled previews; each module still rejects unsupported combinations at start, and Ecaz with Homeworlds alone remains unavailable.',
'Base setup enforces prediction, traitors, forces and automatic dealing in order, with private choices and preserved saved tables. Advanced starting advisors and elite forces share the same setup logic. The normal lobby now starts selected expansions and the Leader Skills, Ecaz Treachery Card, Nexus, Discovery, Homeworld, Tech and Stronghold modules through their existing genuine setups as labelled previews; unsupported combinations still reject at start. Fresh native Ecaz/classic World games retain ordinary Arrakis Occupy, with or without supported Skills.',
},
{
area: 'Player controls',
-13
View File
@@ -132,19 +132,6 @@ void test('incompatible selected modules reject the whole start instead of silen
}
});
void test('an Ecaz Homeworld Skills selection cannot start without supported combined Occupy battles', () => {
let game = createGame('ECAZLOBBYMODULES', newPlayer('ecaz', 'Ecaz', 'ecaz'), true, ['ecaz']);
joinGame(game, newPlayer('atreides', 'Atreides', 'atreides'));
joinGame(game, newPlayer('guild', 'Guild', 'guild'));
game = act(game, game.host, { type: 'homeworlds', enabled: true });
game = act(game, game.host, { type: 'moduleOption', module: 'leaderSkills', enabled: true });
game = ready(game);
reject(game, game.host, preview);
assert.equal(game.status, 'lobby');
assert.equal(game.homeworlds!.custody, null);
assert.equal(game.leaderSkills, undefined);
});
void test('Advanced starts require the exact host opt-in and cannot dispatch audit initializers or hidden bypass options', () => {
const game = ready(lobby(BASE, true));
reject(game, game.host, { type: 'start' });
+108
View File
@@ -0,0 +1,108 @@
import assert from 'node:assert/strict';
import { applyAction, createGame, joinGame, newPlayer, viewGame, type Action, type Game } from '../game/engine';
import { botActions } from '../game/bots';
import { territory } from '../game/board';
import { finishEcazOccupySetup, type EcazOccupyFixture } from './fixture-ecaz-occupy';
export type HomeworldEcazStep = { actor: string; action: Action };
export type HomeworldEcazOccupyFixture = EcazOccupyFixture & { actions: HomeworldEcazStep[] };
export const homeworldEcazClean = (game: Game) =>
!game.phaseOpening && !game.response && !game.decision && !game.pendingTreacheryDiscard;
/** Original choices only: leave optional Ambassadors unplaced and pass auctions. */
export function nextHomeworldEcazStep(game: Game): HomeworldEcazStep | null {
if (game.response) return { actor: game.players.find(p => !game.response!.passed.includes(p.id))!.id,
action: { type: 'passResponse' } };
if (game.decision?.kind === 'ecazPlacement')
return { actor: game.decision.player, action: { type: 'decision', decline: true } };
if (!game.phaseOpening && !game.decision && game.phase === 0 && game.stormPending === null) {
const actor = game.stormDialers.find(id => game.stormDials[id] === undefined);
if (actor) return { actor, action: { type: 'stormDial', amount: game.turn === 1 ? 0 : 1 } };
}
if (!game.phaseOpening && !game.decision && game.phase === 3 && game.auction)
return { actor: game.auction.active, action: { type: 'passBid' } };
for (const player of game.players) {
const view = viewGame(game, player.id);
view.players.find(p => p.id === player.id)!.bot = 'Easy';
const actions = botActions(view);
const action = actions.find(a => a.type === 'ready') ?? actions[0];
if (action) return { actor: player.id, action };
}
return null;
}
/** Fresh native World setup, a real worm alliance and paid Arrakis shipments.
* Only the unopened Spice order is staged; no armies, wallets, phases, alliances
* or World custody are assigned. The selected skill offers remain the real deal. */
export function createHomeworldEcazOccupyFixture(options: { advanced?: boolean; skills?: boolean } = {}): HomeworldEcazOccupyFixture {
const advanced = options.advanced ?? true;
let game = createGame('WORLDECAZOCCUPY', newPlayer('ecaz', 'Ecaz', 'ecaz'), advanced, ['ecaz']);
joinGame(game, newPlayer('emperor', 'Emperor', 'emperor'));
joinGame(game, newPlayer('guild', 'Guild', 'guild'));
const actions: HomeworldEcazStep[] = [];
const act = (actor: string, action: Action) => {
actions.push({ actor, action: structuredClone(action) });
game = applyAction(game, actor, action);
};
act('ecaz', { type: 'homeworlds', enabled: true });
if (options.skills) act('ecaz', { type: 'moduleOption', module: 'leaderSkills', enabled: true });
for (const player of game.players) act(player.id, { type: 'ready' });
act('ecaz', advanced ? { type: 'start', advancedPreview: true } : { type: 'start' });
const initial = structuredClone(game);
game = finishEcazOccupySetup(game);
const afterSetup = structuredClone(game);
const step = () => {
const next = nextHomeworldEcazStep(game);
assert.ok(next, `A native World action must exist at ${game.turn}/${game.phase}/${game.decision?.kind}`);
act(next.actor, next.action);
};
const reach = (until: (state: Game) => boolean) => {
for (let n = 0; n < 800 && !until(game); n++) step();
assert.ok(until(game), 'The original World programme must reach its next native boundary.');
};
const orderBlow = (worm: boolean, position: number) => {
const at = game.spiceDeck.findIndex((card, i) => i >= position &&
(worm ? 'worm' in card && !card.greatMaker && !card.suppressed :
'territory' in card && !['arrakeen', 'sietch_tabr', 'habbanya_ridge_sietch'].includes(card.territory)));
assert.ok(at >= position);
game.spiceDeck.splice(position, 0, game.spiceDeck.splice(at, 1)[0]);
};
orderBlow(false, 0);
if (advanced) orderBlow(false, 1);
reach(g => g.phase === 5 && homeworldEcazClean(g));
while (game.phase === 5) {
if (homeworldEcazClean(game)) act(game.active!, { type: 'endMovement' }); else step();
}
reach(g => g.turn === 2 && g.phase === 0 && !g.phaseOpening);
orderBlow(true, 0);
orderBlow(false, 1);
if (advanced) orderBlow(false, 2);
reach(g => g.phase === 1 && g.nexus && !g.spiceWindow && !g.spiceResolution && homeworldEcazClean(g));
act('ecaz', { type: 'alliance', target: 'emperor' });
act('emperor', { type: 'alliance', target: 'ecaz' });
reach(g => g.phase === 5 && homeworldEcazClean(g));
const battleTerritory = ['arrakeen', 'sietch_tabr', 'habbanya_ridge_sietch']
.find(id => territory(id).sectors[0] !== game.storm)!;
const sector = territory(battleTerritory).sectors[0];
const location = `${battleTerritory}:${sector}`;
const shipped = new Set<string>();
while (game.phase === 5) {
if (!homeworldEcazClean(game)) { step(); continue; }
const actor = game.active!;
if (!shipped.has(actor)) {
act(actor, { type: 'ship', territory: battleTerritory, sector,
amount: actor === 'guild' ? 2 : 4, elite: actor === 'emperor' ? 2 : 0, allyPayment: 0 });
shipped.add(actor);
reach(homeworldEcazClean);
}
act(actor, { type: 'endMovement' });
}
reach(homeworldEcazClean);
const choice = viewGame(game, game.active!).battleChoices.find(c => c.territory === battleTerritory);
assert.ok(choice, 'The actually shipped coalition must have a mandatory Arrakis battle.');
act(choice.chooser, { type: 'chooseBattle', territory: battleTerritory,
target: choice.attacker === choice.chooser ? choice.defender : choice.attacker });
if (!options.skills) assert.equal(game.decision?.kind, 'ecazBattleLead');
return { game, initial, afterSetup, actions, ecaz: 'ecaz', ally: 'emperor', opponent: 'guild',
territory: battleTerritory, location, ecazForces: 4 };
}
+111
View File
@@ -0,0 +1,111 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { applyAction, viewGame, type Game } from '../game/engine';
import { botActions } from '../game/bots';
import { DIFFICULTIES } from '../game/bot-profiles';
import { homeworldGameIntegrity } from '../game/homeworld-game';
import { chooseEcazOccupyLead, openEcazOccupyPlans, stageEcazOccupyCard } from './fixture-ecaz-occupy';
import { createHomeworldEcazOccupyFixture, homeworldEcazClean, nextHomeworldEcazStep } from './fixture-homeworld-ecaz-occupy';
const player = (game: Game, id: string) => game.players.find(p => p.id === id)!;
const leader = (game: Game, id: string, strongest: boolean) => player(game, id).leaders
.filter(l => !l.dead && !l.usedAt).sort((a, b) => strongest ? b.strength - a.strength : a.strength - b.strength)[0].id;
function revealAndFinish(state: Game, mixedLoss = false): Game {
let game = state;
while (game.battle) {
const view = viewGame(game, game.battle.attacker).battle!;
const voter = view.traitorVoters.find(id => !view.traitorSubmitted.includes(id));
assert.ok(voter);
game = applyAction(game, voter, { type: 'traitorCall', call: false });
}
if (mixedLoss) {
const decision = game.decision;
assert.ok(decision?.kind === 'battleLosses');
const choice = decision.options.findIndex(loss => loss.normal === 1 && loss.elite === 1);
assert.ok(choice >= 0);
game = applyAction(game, decision.player, { type: 'decision', choice });
}
for (let n = 0; n < 100 && (game.phase === 6 || game.phase === 7 && !homeworldEcazClean(game)); n++) {
const next = nextHomeworldEcazStep(game); assert.ok(next);
game = applyAction(game, next.actor, next.action);
}
assert.equal(game.phase, 7);
homeworldGameIntegrity(game);
return game;
}
for (const advanced of [false, true]) for (const lead of ['ecaz', 'ally'] as const)
void test(`${advanced ? 'Advanced' : 'Basic'} native Worlds ${lead} lead keeps separate typed Arrakis Occupy losses and original World custody`, () => {
const f = createHomeworldEcazOccupyFixture({ advanced });
const actor = lead === 'ecaz' ? f.ecaz : f.ally;
let game = openEcazOccupyPlans(chooseEcazOccupyLead(f, lead));
const before = structuredClone(game);
const profile = viewGame(game, actor).battle!.ecazOccupy!.profile!;
assert.equal(profile.forceOwner, f.ally);
assert.equal(profile.fixedEcazDial, 2);
// Advanced Salusa retains full-strength unpaid Sardaukar. One paid ordinary
// plus one unpaid starred counter makes variable strength 3, not three pieces.
game = applyAction(game, actor, { type: 'battlePlan', dial: advanced ? 5 : 4,
support: advanced ? 1 : 0, leader: leader(game, actor, true) });
game = applyAction(game, f.opponent, { type: 'battlePlan', dial: 0, leader: leader(game, f.opponent, false) });
game = revealAndFinish(game, !advanced);
assert.equal(game.lastBattleContext!.winner, actor);
assert.equal(game.lastBattleContext!.result, 'normal');
assert.equal(player(game, f.ecaz).forces[f.location], 2);
assert.equal(player(game, f.ecaz).tanks - player(before, f.ecaz).tanks, 2);
assert.equal(player(game, f.ally).forces[f.location], 2);
assert.equal(player(game, f.ally).elites!.forces[f.location], 1);
assert.equal(player(game, f.ally).tanks - player(before, f.ally).tanks, 2);
assert.equal(player(game, f.ally).elites!.tanks - player(before, f.ally).elites!.tanks, 1);
assert.equal(player(game, f.opponent).forces[f.location], undefined);
assert.deepEqual(game.homeworlds!.custody, before.homeworlds!.custody);
});
for (const lead of ['ecaz', 'ally'] as const)
void test(`native World Karama prevention keeps ${lead} as lead and spares the excluded allied army`, () => {
const f = createHomeworldEcazOccupyFixture();
const actor = lead === 'ecaz' ? f.ecaz : f.ally;
const excluded = actor === f.ecaz ? f.ally : f.ecaz;
const karama = stageEcazOccupyCard(f.game, f.opponent, card => card.effect === 'karama');
let game = chooseEcazOccupyLead(f, lead);
assert.equal(game.response?.kind, 'ecazOccupy');
game = applyAction(game, f.opponent, { type: 'card', card: karama, mode: 'cancel' });
game = openEcazOccupyPlans(game);
const before = structuredClone(game);
const profile = viewGame(game, actor).battle!.ecazOccupy!.profile!;
assert.equal(profile.forceOwner, actor);
assert.equal(profile.fixedEcazDial, 0);
game = applyAction(game, actor, { type: 'battlePlan', dial: actor === f.ecaz ? 2 : 3,
support: actor === f.ecaz ? 2 : 1, leader: leader(game, actor, true) });
game = applyAction(game, f.opponent, { type: 'battlePlan', dial: 0, leader: leader(game, f.opponent, false) });
game = revealAndFinish(game);
assert.equal(game.lastBattleContext!.winner, actor);
assert.equal(player(game, actor).forces[f.location], 2);
assert.equal(player(game, actor).tanks - player(before, actor).tanks, 2);
assert.deepEqual(player(game, excluded).forces, player(before, excluded).forces);
assert.equal(player(game, excluded).tanks, player(before, excluded).tanks);
assert.equal(game.discard.filter(card => card.id === karama).length, 1);
assert.deepEqual(game.homeworlds!.custody, before.homeworlds!.custody);
});
void test('all four legal profiles complete the native World Skills coalition and retain physical source integrity', () => {
for (const difficulty of DIFFICULTIES) {
const f = createHomeworldEcazOccupyFixture({ skills: true });
let game = f.game;
for (let n = 0; n < 150 && game.phase === 6; n++) {
let next;
for (const p of game.players) {
const view = viewGame(game, p.id);
view.players.find(seat => seat.id === p.id)!.bot = difficulty;
const action = botActions(view)[0];
if (action) { next = { actor: p.id, action }; break; }
}
assert.ok(next, `${difficulty} must retain a legal coalition actor`);
game = applyAction(game, next.actor, next.action);
}
assert.equal(game.phase, 7, difficulty);
assert.ok(game.lastBattleContext!.combatants.includes(f.opponent));
assert.equal(game.battle, null);
homeworldGameIntegrity(game);
}
});