diff --git a/AGENTS.md b/AGENTS.md
index 39f601a0..1979376e 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -29,9 +29,13 @@ for the in-memory SQLite tests. `tsx` is a pinned development dependency.
## Prototype the remaining scope first
-- The user prioritizes working first versions of all remaining functions across
- Basic, Advanced and every expansion, followed by integration, refinement and
- polish. Reuse existing capabilities; batch related functions by dependency.
+- The 8 October 2026 user amendment sets the content order: complete Basic
+ rules for all twelve factions first, then Advanced rules, then refine
+ interactions. Usable first versions may be imperfect; mark provisional
+ interpretations honestly rather than postponing all independent content.
+ Defer administration detours, broad test suites, capture-resumption sweeps and
+ combination campaigns. Use only brief changed-path smoke during rule delivery;
+ batch type/lint feedback rather than repeating it for each small edit.
- The 2 October 2026 amendment supersedes the 26 September sequencing:
implement all rules first; save/recovery, privacy and custody assurance plus
comprehensive review follow afterward, not as per-prototype gates. Do not
diff --git a/components/ecaz-ambassadors.tsx b/components/ecaz-ambassadors.tsx
index cf4e8a8b..9fc083eb 100644
--- a/components/ecaz-ambassadors.tsx
+++ b/components/ecaz-ambassadors.tsx
@@ -23,7 +23,7 @@ import { TERRITORIES } from '@/game/board';
import { faction } from '@/game/catalog';
export const AMBASSADOR_COVERAGE =
- 'Development coverage: Ambassador placement, Ecaz’s direct Duke acquisition and consensual alliance, Emperor, Atreides, Harkonnen, CHOAM, Ixian, Richese, Fremen, Guild and Tleilaxu entry effects are supported, plus Bene Gesserit copies of those eight ordinary effects. The Tleilaxu effect returns physical forces only; its printed leader alternative and ordinary revival accounting remain unfinished. Duke loan choices, other effects and some competing arrival reactions remain unfinished; full Ecaz starts remain disabled.';
+ 'Development coverage: Ambassador placement, Ecaz’s direct Duke acquisition and consensual alliance, Emperor, Atreides, Harkonnen, CHOAM, Ixian, Richese, Fremen, Guild and Tleilaxu entry effects are supported, plus Bene Gesserit copies. The Tleilaxu effect offers physical force returns or a first-death own-leader revival. Ordinary allowance and income accounting, repeat-death and shared-leader cases remain for refinement. Duke loan choices and some competing arrival reactions remain unfinished; full Ecaz starts remain disabled.';
function tokenLocation(token: AmbassadorToken) {
if (token.zone === 'placed')
return `Placed in ${TERRITORIES.find((t) => t.id === token.location)?.name ?? 'a stronghold'}`;
diff --git a/components/ecaz-entry.tsx b/components/ecaz-entry.tsx
index d51f309a..7044eb3d 100644
--- a/components/ecaz-entry.tsx
+++ b/components/ecaz-entry.tsx
@@ -257,15 +257,20 @@ export function EcazEntry({
) : entry.stage === 'revival' ? (
<>
- Return up to {entry.revival?.maximum ?? 0} physical{' '}
- {(entry.revival?.maximum ?? 0) === 1 ? 'force' : 'forces'} from the
- Tanks to your reserves for free. Ordinary revival allowances,
- prices and income are unchanged. The printed leader alternative
- stays unavailable pending its recorded interpretation.
+ Revive one eligible own leader, or return up to {entry.revival?.maximum ?? 0}
+ {' '}physical forces from the Tanks for free. This independent grant
+ leaves ordinary allowances and income unchanged provisionally.
+ Repeat-death and shared-leader interactions remain unfinished.
{entry.revival?.blocked && (
+ {!game.advanced &&
+ Basic prototype: Ecaz’s half-force contribution and winning losses round up;
+ survivors round down. The main printed rule is used provisionally because
+ its odd-force FAQ example conflicts.
+
}
Karama may cancel Occupy after this choice, before battle powers and plans.
The chosen lead then dials only their own army; the other ally contributes zero.
@@ -54,6 +59,9 @@ export function EcazOccupyBattleSummary({ game }: { game: GameView }) {
The total dial must include {profile.fixedEcazDial} fixed Ecaz strength at full strength,
with no spice cost, plus the chosen variable army strength.
}
+ {!game.advanced && !profile.canceled && profile.ecazForces.normal % 2 !== 0 &&
+ Provisional Basic rounding: {profile.fixedEcazDial} Ecaz
+ counters contribute and are lost on an ordinary win; the remainder survive.
}
{game.response?.kind === 'ecazOccupy' &&
Allow this power to keep the combined army, or use an eligible Karama below.
Cancellation keeps {lead} as lead and replaces the variable army with {lead}’s own forces before planning.
diff --git a/docs/CURRENT_STATUS.md b/docs/CURRENT_STATUS.md
index 7ba27cd8..752fe5d4 100644
--- a/docs/CURRENT_STATUS.md
+++ b/docs/CURRENT_STATUS.md
@@ -1,6 +1,6 @@
# Current development status
-Updated 7 October 2026. **The full twelve-faction goal is unfinished. Basic
+Updated 8 October 2026. **The full twelve-faction goal is unfinished. Basic
is playable and the six classic factions have a user-authorized Advanced preview.
Expansion starts and full rules/publication acceptance remain gated.**
@@ -16,6 +16,29 @@ open mode gates.
## Current checkpoint and work
+**Current priority — Basic → Advanced → interaction refinement.** The user
+requests usable first versions for every faction before refinement. Administration
+is deferred. Broad suites and capture-resumption campaigns are deferred; rule
+delivery uses brief changed-path smoke only, with batched compiler feedback.
+
+**Richese cache cancellation connected.** The printed Karama cancellation
+now reaches the restore-one-ordinary-lot policy selected on 7 October. First/last
+cache offers are skipped after cancellation; the physical cache stays intact.
+Advanced Bene Gesserit conversion uses its existing separate response.
+Seven direct engine smoke scenarios pass, including Basic first/last cancellation;
+no broad suite was run for this change.
+
+**Basic Ecaz odd-force Occupy prototype.** The main printed paragraph supplies
+ceiling contribution/loss and floor survivors, including odd counts. Controls
+explicitly label the conflicting-FAQ interpretation as provisional. One five-counter
+coalition smoke reaches Collection with three Ecaz losses and two survivors.
+
+**Tleilaxu Ambassador leader alternative.** The beneficiary can revive a
+first-death own leader for free instead of returning forces. Private choices,
+zero price and legal AI are connected. Independent allowance treatment is
+provisional; repeat-death/shared-disc and income interactions remain for refinement.
+One Basic arrival-and-bot smoke passes.
+
**7 October 2026 — five pending rulings resolved by the user.** The user
answered five recorded questions; each is now recorded in
diff --git a/docs/ECAZ_OCCUPY_RULES.md b/docs/ECAZ_OCCUPY_RULES.md
index 65ab896a..733119b2 100644
--- a/docs/ECAZ_OCCUPY_RULES.md
+++ b/docs/ECAZ_OCCUPY_RULES.md
@@ -1,6 +1,6 @@
# Ecaz Occupy: Basic and Advanced source, and historical conflict
-Historical primary-source audit, 2026-09-07, corrected after independent verification. The old publisher/designer casualty-rounding conflict remains unresolved as a publisher correction and for odd Basic counts. Its earlier false attribution to Jack Reda is still withdrawn. The later user-authorized Advanced source below selects explicit Advanced arithmetic, and the Basic even-force composition selects E/2 for even counts; neither retroactively certifies that attribution.
+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.
## Original live Discovery coalition — 5 October 2026
@@ -324,7 +324,7 @@ Ecaz losses on ordinary allied win = ceil(E / 2)
Ecaz survivors on ordinary allied win = floor(E / 2)
```
-The historical publisher/mirror example still conflicts for E=5, and the earlier audits correctly record that publisher gap. Basic now has the bounded even-force composition above: E/2 is source-clear for even counts, and the still-contradictory odd counts are rejected rather than rounded. Advanced uses the separately user-authorized source cutover above: its explicit ceil contribution and floor survivors select the arithmetic without claiming a designer correction. Fixed Ecaz losses stay independent of the ally's variable dial losses.
+The historical publisher/mirror example still conflicts for E=5. The 8 October first-version implementation uses the p8 reading in Basic, including odd counts, and labels it provisional in the lead-choice and plan controls. Advanced uses the separately user-authorized source cutover. Fixed Ecaz losses remain independent of the ally's variable dial losses. One actual Basic five-counter coalition smoke reaches Collection with three Ecaz casualties and two survivors; broader interaction refinement is deferred.
## Occupation, controller and battle order
diff --git a/docs/RICHESE_AUCTION_RULES.md b/docs/RICHESE_AUCTION_RULES.md
index 61ecdd4b..26927f68 100644
--- a/docs/RICHESE_AUCTION_RULES.md
+++ b/docs/RICHESE_AUCTION_RULES.md
@@ -66,7 +66,7 @@ For Black Market with ordinary bidding, preserve the ordinary opener progression
These questions cannot be silently represented as settled FAQ compliance:
-1. **Canceled compulsory lot and an exhausted cache — count resolved 7 October 2026 by user ruling.** The start-of-round reduction and the permission to prevent the cache auction are both printed, and the cancellation table does not say whether a replacement ordinary lot is restored. The user ruled that when no cache lot can run — the cache is exhausted, or the compulsory cache auction is canceled/prevented — **one ordinary lot is restored**, so the ordinary pool runs its full declared count rather than the cache-reduced count. `requireRicheseDeclarationCache` now accepts an exhausted cache (only a malformed or negative count is rejected), and the `richeseAuction` cancellation branch already restores the reduction before the ordinary pool runs. The Karama **authorization** to cancel the compulsory cache auction itself is still gated in `validateKaramaUse` (`richeseAuction` remains non-cancelable), so that branch is the ruled policy awaiting that separate feature rather than a reachable path.
+1. **Canceled compulsory lot and an exhausted cache — resolved 7 October 2026 by user ruling; connected 8 October.** When no cache lot can run, the ordinary pool retains its full declared count without the cache deduction. Printed Karama can now cancel the declared cache opportunity in Basic or Advanced; Advanced Bene Gesserit conversion retains its separate response. Cancellation leaves every cached card in place and skips both first- and last-position offers. The before-cache count is frozen at declaration, so spending a cancellation card cannot recount live hand capacity, and the independent completed-Black-Market deduction remains. Five direct engine smoke scenarios cover first/last, printed/BG cancellation and countered conversion; no broad suite was run for this change.
2. **Black Market count wording.** Its specific rule conditions reduction on a sale. The embedded Ixian answer refers more broadly to cards put up for auction. The more specific sale condition supports no reduction for an unsold/canceled Black Market offer. Record that composition explicitly; do not decrement merely on declaration.
3. **Self-purchase of a Black Market card.** The generic alternative methods include Richese, but Black Market says Richese receives all payments and already owns the card. No retrieved paragraph specifies seller exclusion, a self-payment loop, an Emperor payment, or whether self-retention is a sale reducing the normal pool. Do not borrow the cache self-payment exception without a ruling.
4. **Ixian advanced Technology on special lots.** I20 p.9 broadly applies before bidding on a card and before Atreides inspection. R2's explicit restriction concerns the _ally's post-purchase replacement_, not Technology. Consequently that FAQ cannot justify disabling Technology on special lots. No retrieved Richese paragraph explains an exchanged cache lot's new public face, or the custody of a substituted Black Market lot if nobody bids. This is a genuine interaction to resolve before certifying the combined implementation.
diff --git a/docs/RULE_DECISIONS.md b/docs/RULE_DECISIONS.md
index 4467991a..9ace19e6 100644
--- a/docs/RULE_DECISIONS.md
+++ b/docs/RULE_DECISIONS.md
@@ -172,6 +172,12 @@ Secrecy (physical page 16), Duke Vidal's ghola exception (30), Kull and other
independent decisions are not reconciled by this cutover.
## Basic Ecaz Occupy even-force boundary — 4 October 2026
+**Superseded for prototype availability on 8 October:** the user requests Basic
+first versions even when imperfect. Basic odd-count Occupy now uses the main p8
+paragraph provisionally (`ceil(E/2)` contribution/loss, `floor(E/2)` survivors),
+with a visible control warning. The conflicting FAQ is not adjudicated by this
+implementation choice. The historical even-only checkpoint below is retained.
+
Fresh bounded `ecaz-occupy` now admits Basic with the exact `ecaz` deck and
classic or optional Moritani opponents, no optional modules. For an even Ecaz
diff --git a/docs/TLEILAXU_AMBASSADOR_RULES.md b/docs/TLEILAXU_AMBASSADOR_RULES.md
index bbeb92ab..580af055 100644
--- a/docs/TLEILAXU_AMBASSADOR_RULES.md
+++ b/docs/TLEILAXU_AMBASSADOR_RULES.md
@@ -1,6 +1,17 @@
# Tleilaxu Ambassador revival: source contract
-Primary-source review, 2026-09-07. This document supplements the [Ambassador effects audit](ECAZ_AMBASSADOR_EFFECTS_AUDIT.md) and [runtime readiness review](TLEILAXU_AMBASSADOR_RUNTIME_READINESS.md). It does not activate the effect or select the unresolved accounting and shared-leader policies.
+Primary-source review, 2026-09-07, with later runtime checkpoints below. This document supplements the [Ambassador effects audit](ECAZ_AMBASSADOR_EFFECTS_AUDIT.md) and [runtime readiness review](TLEILAXU_AMBASSADOR_RUNTIME_READINESS.md). Recorded source questions remain distinct from the Basic-first implementation choices.
+
+## First-version leader alternative — 8 October 2026
+
+The beneficiary may now revive one first-death own leader for free instead of
+returning forces. Its identity and death count remain unchanged; the original
+entrant resumes after the choice. The private leader selector and minimal legal
+AI path are connected. The independent grant does not spend the ordinary leader
+allowance provisionally, matching the existing independent force path; this is
+an implementation choice for the user-requested first version, not a settled
+quota ruling. Dead-twice, shared Duke/KH and revival-income interactions remain
+refinement boundaries. A direct Basic arrival-and-bot smoke passes.
## Printed grant and immediate consequences
diff --git a/game/bots.ts b/game/bots.ts
index 3af13fc1..d0506f92 100644
--- a/game/bots.ts
+++ b/game/bots.ts
@@ -1913,8 +1913,11 @@ function policyActions(g: GameView): Action[] {
if (entry.stage === 'allianceReply') return [{ ...action, accept: true }];
if (entry.stage === 'revival') {
const revival = entry.revival;
- if (!revival || revival.blocked || revival.maximum < 1)
- return [{ ...action, decline: true }];
+ if (!revival || revival.blocked) return [{ ...action, decline: true }];
+ if (revival.maximum < 1)
+ return [revival.leaders[0]
+ ? { ...action, leader: revival.leaders[0].id }
+ : { ...action, decline: true }];
// Minimal legal participation: return as many Tanks counters as the
// printed maximum allows, spending the single elite option first.
const elite = Math.min(
diff --git a/game/ecaz-occupy-battle.ts b/game/ecaz-occupy-battle.ts
index c8a3452f..7c7cfb0d 100644
--- a/game/ecaz-occupy-battle.ts
+++ b/game/ecaz-occupy-battle.ts
@@ -92,9 +92,9 @@ function requireBattle(condition: unknown, message: string): asserts condition {
const text = (value: unknown): value is string =>
typeof value === 'string' && value.trim().length > 0;
-/** Advanced retains ceil(E/2); source-clear Basic admits only even E and uses
- * E/2. The increment is free. The selected lead supplies the plan and pays for
- * the actual ally's dial; selecting Ecaz does not transfer the ally's counters. */
+/** Printed p8: ceil(E/2) free contribution and winning losses. Basic odd counts
+ * use this provisionally under the 8 October breadth-first instruction; the
+ * conflicting FAQ remains recorded, not silently promoted to a settled ruling. */
export function quoteEcazOccupyBattle(
input: EcazOccupyBattleInput,
): EcazOccupyBattleProfile {
@@ -137,14 +137,9 @@ export function quoteEcazOccupyBattle(
typeof canceled === 'boolean',
'The Occupy profile needs its pre-plan cancellation state.',
);
- requireBattle(
- advanced || canceled || ecaz.forces.normal % 2 === 0,
- 'Uncanceled Basic Occupy needs an even Ecaz force count; odd-force publisher casualty wording remains unresolved.',
- );
const forceOwner = canceled ? lead : ally.id;
const forces = forceOwner === ecaz.id ? ecaz.forces : ally.forces;
- const fixedEcazDial = canceled ? 0 :
- advanced ? Math.ceil(ecaz.forces.normal / 2) : ecaz.forces.normal / 2;
+ const fixedEcazDial = canceled ? 0 : Math.ceil(ecaz.forces.normal / 2);
return {
advanced,
battleOrderActor,
diff --git a/game/engine.ts b/game/engine.ts
index c40d0f4e..515b7776 100644
--- a/game/engine.ts
+++ b/game/engine.ts
@@ -1537,6 +1537,8 @@ export type Game = {
| 'complete';
position: 'first' | 'last' | null;
normalCount: number | null;
+ /** Frozen before the cache deduction; cancellation never recounts live hands. */
+ normalCountBeforeCache?: number;
blackMarketSold: boolean;
cacheCanceled: boolean;
cacheAuthority?: string;
@@ -9445,7 +9447,7 @@ function ecazOccupyCompositionSupported(g: Game): boolean {
!g.discoveryStash && !g.greatMaker &&
(g.ecazTreachery === undefined || g.ecazTreachery === true);
}
-/** Fresh native Occupy profile; Basic's disputed odd-force arithmetic stays guarded. */
+/** Fresh native Occupy profile; Basic odd counts use the main printed rule provisionally. */
export function initializeEcazOccupyGameForAudit(state: Game): Game {
requireRule(ecazOccupyCompositionSupported(state),
'Occupy requires supported native factions and their distinct selected decks; Basic uses Ecaz/classic/optional Moritani without optional modules.');
@@ -10755,14 +10757,32 @@ function richeseDeclaration(g: Game) {
richeseDeclarationCache(g);
const round = g.richeseBidding!;
round.stage = 'declaration';
+ round.normalCountBeforeCache = Math.max(
+ 0, auctionEligible(g).length - Number(round.blackMarketSold),
+ );
round.normalCount = Math.max(
- 0,
- auctionEligible(g).length -
- Number(richeseCacheRuns(g)) -
- Number(round.blackMarketSold),
+ 0, round.normalCountBeforeCache - Number(richeseCacheRuns(g)),
);
richeseDecision(g, 'richeseDeclaration');
}
+function validateRicheseCacheCancellation(g: Game, response: ResponseWindow) {
+ const round = g.richeseBidding;
+ requireRule(g.status === 'playing' && g.phase === 3 && round &&
+ round.turn === g.turn && round.stage === 'declaration' &&
+ round.owner === response.owner && getPlayer(g, round.owner).faction === 'richese' &&
+ (round.position === 'first' || round.position === 'last') &&
+ Number.isSafeInteger(round.normalCount) && round.normalCount! >= 0 &&
+ round.normalCount! <= g.players.length && !round.cacheCanceled &&
+ !g.auction && !g.richeseAuction && !g.ixAuction &&
+ (round.normalCountBeforeCache === undefined ||
+ Number.isSafeInteger(round.normalCountBeforeCache) &&
+ round.normalCountBeforeCache >= round.normalCount! &&
+ round.normalCountBeforeCache <= g.players.length &&
+ round.normalCountBeforeCache - round.normalCount! <= 1),
+ 'Cache cancellation needs the original undealt Richese declaration.');
+ richeseDeclarationCache(g);
+ return round;
+}
function prepareRicheseNormal(g: Game) {
g.richeseBidding!.stage = 'normal';
setAuction(g);
@@ -12775,10 +12795,8 @@ function validateKaramaUse(
response: { kind: use.response.kind, owner: use.response.owner },
canceled: true,
});
- requireRule(
- use.response.kind !== 'richeseAuction',
- 'The canceled Richese auction count is awaiting an official ruling or an explicit table interpretation.',
- );
+ if (use.response.kind === 'richeseAuction')
+ validateRicheseCacheCancellation(g, use.response);
if (
use.response.kind === 'richeseBlackMarket' ||
(use.response.kind === 'atreidesAuction' && g.richeseAuction)
@@ -16319,15 +16337,13 @@ function resolveAmbassadorEffect(g: Game) {
return;
}
if (effect === 'tleilaxu') {
- // Printed grant: "Revive one of your leaders or up to 4 of your forces for
- // free." Only the physical force alternative is source-supported here; the
- // leader alternative, ordinary allowance accounting and Tleilaxu income for
- // this distinct event stay gated pending the recorded interpretations.
+ // Initial leader path: first-death own discs, independent of ordinary quota.
+ // Repeat-death and shared-disc interactions remain explicit refinement work.
entry.stage = 'revival';
g.decision = { kind: 'ecazAmbassador', player: p.id };
log(
g,
- `${p.name} may return up to four physical forces from the Tanks for free through the Tleilaxu Ambassador. Ordinary revival allowances and income are unchanged.`,
+ `${p.name} may revive one first-death own leader or return up to four physical forces for free through the Tleilaxu Ambassador. Ordinary revival allowances and income are unchanged.`,
{ faction: 'tleilaxu', name: 'Tleilaxu Ambassador' },
);
return;
@@ -16468,9 +16484,9 @@ function decideAmbassador(g: Game, p: Player, action: Action) {
if (entry.stage === 'revival') {
requireRule(
Object.keys(action).every((key) =>
- ['type', 'event', 'decline', 'forces', 'elite'].includes(key),
+ ['type', 'event', 'decline', 'forces', 'elite', 'leader'].includes(key),
),
- 'Choose a physical force return or decline it.',
+ 'Choose one leader, a physical force return, or decline.',
);
if (action.decline === true) {
log(
@@ -16481,6 +16497,20 @@ function decideAmbassador(g: Game, p: Player, action: Action) {
finishAmbassador(g);
return;
}
+ if (action.leader !== undefined) {
+ requireRule(action.forces === undefined && action.elite === undefined,
+ 'Choose a leader or forces, not both.');
+ const leader = p.leaders.find(l => l.id === action.leader &&
+ l.faction === p.faction && l.dead && l.deaths === 1 &&
+ !l.capturedBy && !l.gholaBy);
+ requireRule(leader, 'Choose a first-death own leader from the Tanks.');
+ leader.dead = false;
+ delete leader.usedAt;
+ log(g, `${p.name} revived ${leader.name} for free through the Tleilaxu Ambassador. The ordinary leader allowance is unchanged.`,
+ { faction: 'tleilaxu', name: 'Tleilaxu Ambassador' });
+ finishAmbassador(g);
+ return;
+ }
const forces = integer(action.forces, 1, 4, 'Forces to return');
const elite =
action.elite === undefined
@@ -23946,12 +23976,13 @@ function finishResponse(g: Game, canceled: boolean,bureaucratDiversion?:number)
if (pending.movement && g.pendingChoamMove) resumeChoamMovement(g);
if (pending.mentat) g.decision = { kind: 'choamMentat', player: choam.id };
} else if (response.kind === 'richeseAuction') {
- const round = g.richeseBidding!;
+ const round = validateRicheseCacheCancellation(g, response);
if (canceled) {
- // User ruling 7 October 2026: a canceled or prevented compulsory cache
- // auction restores one ordinary lot, since no cache lot replaces it.
+ // Restore the frozen count, retaining the independent Black Market deduction.
+ // Older saved declarations lack the additive before-cache field.
+ round.normalCount = round.normalCountBeforeCache ??
+ Math.max(0, round.normalCount! + Number(richeseCacheRuns(g)));
round.cacheCanceled = true;
- round.normalCount = Math.max(0, (round.normalCount ?? 0) + 1);
log(
g,
'The compulsory Richese cache auction was canceled; one ordinary lot is restored.',
@@ -31279,8 +31310,6 @@ function applyActionInner(
if (combined) {
requireRule(ecazOccupyCompositionSupported(g),
'Combined Occupy requires source-selected native factions; optional overlays remain gated.');
- requireRule(g.advanced || fighterCount(ecaz!, choice.territory) % 2 === 0,
- 'Odd-force Basic Occupy awaits the preserved publisher casualty-rounding ruling.');
}
const battleEvent = crypto.randomUUID();
g.battle = {
@@ -32073,7 +32102,9 @@ export function viewGame(state: Game, id: string) {
tanks: me.tanks,
eliteTanks: me.elites?.tanks ?? 0,
eliteRevived: me.elites?.revived ?? 0,
- leaderAlternative: false,
+ leaders: me.leaders.filter(l => l.faction === me.faction &&
+ l.dead && l.deaths === 1 && !l.capturedBy && !l.gholaBy)
+ .map(l => ({ id: l.id, name: l.name, strength: l.strength })),
blocked:
ambassadorEffectBlock(
g,
diff --git a/game/reference.ts b/game/reference.ts
index 7e50f21f..b5ce77d5 100644
--- a/game/reference.ts
+++ b/game/reference.ts
@@ -419,7 +419,7 @@ export const RULE_TOPICS: RuleTopic[] = [
'The opt-in Richese Nexus Betrayal auction preview adds a neutral before-payment acknowledgement. A legal rival holder may veto a public self-cache purchase or send another buyer’s sale payment to the bank. See its separate guide and bounded verification evidence; normal hidden self-purchases and special-Karama acquisition are not connected triggers.',
'A player whose hand becomes full is automatically passed, or submits zero in an unfinished Silent lot. A full-hand owner’s unbid cache card is removed automatically. If every hand is full, preparing the normal pool does not draw and lose unseen cards.',
'AI supports cache choices and all three offered bidding methods using its own projected information. It currently skips the optional Black Market prelude. A seller in an already offered Black Market lot passes or submits zero.',
- 'Some unresolved rules are explicitly blocked: changing the normal card count after cache-auction cancellation, exhausted-cache arithmetic, positive Black Market self-bids, and advanced Ixian Technology substitution on special lots. These are implementation guards while the rules are resolved, not claims of official prohibitions.',
+ 'Karama may prevent the declared cache auction before the ordinary pool is drawn. Cancellation leaves the cache intact and restores its deducted ordinary lot; first/last position does not create another cache offer that round. The independent reduction for a completed Black Market sale remains. An exhausted cache similarly causes no cache deduction. Positive Black Market self-bids and advanced Ixian Technology substitution on special lots retain their separate boundaries.',
'Juice of Sapho now supports bounded first/last Once Around, movement and remaining battle-choice ordering; see its timing guide for current limits. Richese’s Karama uses the existing generic handler, Distrans supports a separate private hand transfer, Nullentropy Box supports a paid private discard search, Ornithopter supports its two movement modes, and Residual Poison supports a random opposing leader death before leader commitment in development fixtures. Portable Snooper supports ordinary and late poison defense. Stone Burner supports a guarded weapon commitment and revealed leader-effect choice. Mirror Weapon copies opposing revealed attacks, with independent copy-first choices for Tooth and Stone in bounded CHOAM/Richese-deck classic-faction battles. A winning physical Mirror may be kept even after copying activated Tooth or Artillery under the user-selected interpretation, not a publisher clarification. Semuta Drug has a bounded clean public ordinary-discard preview; other discard producers and Sapho timing modes remain unfinished. This does not certify every Karama interaction. Richese expansion starts remain disabled. Each component guide identifies its currently available action. Independent continuation and persisted-room checks pass. Desktop checks confirmed all ten enlarged guides, direct topic links, Once Around payment and restored private Silent bidding. Mobile and full Richese-game verification remain unfinished.',
],
related: [
@@ -1253,14 +1253,14 @@ export const RULE_TOPICS: RuleTopic[] = [
),
{
id: 'ecaz-occupy',
- title: 'Advanced Ecaz Occupy: combined-army battles',
+ title: 'Ecaz Occupy: Basic and Advanced combined-army battles',
category: 'Advanced & expansions',
coverage: 'Partial',
- developmentStage: 'Verified',
- summary: 'Ecaz chooses who leads its combined army. Basic uses the source-clear even-force half contribution and losses; the authorized Advanced revision supplies ceiling strength and lead-own forces after Karama. Bounded fresh local rules and human controls are Development Verified.',
+ developmentStage: 'Prototyped',
+ summary: 'Ecaz chooses who leads its combined army. Basic provisionally follows the main printed paragraph for odd as well as even counts: ceiling contribution/loss and floor survivors. Advanced retains its user-authorized revision. Usable controls and legal participation are connected; interaction refinement remains.',
steps: [
'When reciprocal Ecaz allies coexist with fighting forces, Ecaz chooses Ecaz or its ally to lead. Every participating whole-territory army must be clear of storm and connected; no component-only battle or casualties are created. The selected lead uses its own leaders, Treachery cards and spice; the original battle-order chooser is not replaced. Advisors are not fighters but retain their native voluntary fighter-conversion opportunity.',
- 'Basic combined battles are admitted for the exact Ecaz deck with classic or optional Moritani opponents and no optional modules. An even Ecaz force count contributes half at full strength and loses half on an ordinary win; the printed page-8 and page-15 odd-count examples conflict, so an uncanceled odd Basic count is rejected before any lead choice rather than choosing a rounding. Basic has no Emperor/Fremen elite inventory and its supplied support normalizes to zero.',
+ 'Basic combined battles use the native Ecaz deck and existing roster prerequisites. Ecaz contributes the ceiling of half its forces at full strength and loses that many on an ordinary win; survivors round down. This follows the main printed paragraph provisionally because its odd-count FAQ example conflicts. A visible control warning distinguishes the prototype from a settled correction. Basic has no Emperor/Fremen elite inventory and its supplied support normalizes to zero.',
'With Advanced Occupy active, the total dial includes the ceiling of half the Ecaz fighters as mandatory full-strength contribution without spice cost, plus the chosen allied army strength. The variable army retains its native Fremen/Fedaykin or Emperor/Sardaukar properties even when Ecaz forms the plan.',
'An ordinary victory leaves the floor of half the Ecaz fighters alive and separately applies the variable army’s dial losses. A sole successful Traitor-call winner preserves both armies; an ordinary defeat, mutual Traitor result or resolved explosion removes the actual combined side. Ecaz and its ally share the printed same-faction Traitor/Face Dancer relation.',
'Eligible Karama can prevent Occupy after the lead choice and before powers/plans. The chosen lead remains lead and dials its own army; the other ally contributes zero. The plan editor then uses the new force pool and no fixed Ecaz strength.',
@@ -1270,13 +1270,13 @@ export const RULE_TOPICS: RuleTopic[] = [
'Stone Burner compares physical undialed counters, not total dial or paid strength: active Occupy contributes floor(Ecaz forces / 2) plus the ally’s native undialed tokens. Canceled Occupy compares only the selected lead’s own pool. Physical Mirror copies use the same comparison and retain their original cleanup. Hidden opposing No-Field preflight uses public possible pools, not its actual denomination; genuinely winner-changing typed allocations remain blocked.',
'With the independent variant, Harass returns only the actual card user’s undialed forces. An Ecaz user keeps the original mandatory ceil contribution committed at free full strength while its floor count returns; ally support is separate. An allied user subtracts fixed strength before choosing its native typed commitment. Canceled Occupy uses its selected lead’s own pool. Returned reserves are outside later casualties/explosion/Face Dance.',
'Reinforcements consumes three card-user reserves normal-first and adds +2 score without extra support or on-board arrivals. Its existing provisional cost/disposal on every revealed outcome is unchanged, not a new publisher ruling. Own same-plan Harass/Reinforcements and Stone remain guarded; Basic/unprofiled co-side cards remain unsupported.',
- 'These numbers come from the user-authorized revised Advanced PDF, physical pages 32 and 19, not a newly located publisher erratum. Basic keeps the preserved even-only composition above. Fresh ecaz-occupy admits Ecaz/classic/optional Moritani/Ixians/Tleilaxu/CHOAM/Richese two-to-six-seat Advanced with required family decks:33 Ecaz alone,47 Ix,35 CHOAM,47 both; Richese retains its separate ten-card cache. Basic admits Ecaz/classic/optional Moritani and the exact ecaz deck. Explicit --ecaz-treachery independently adds all three cards for36/50/38. Own mixed No-Field dialing, whole-plan disclosure, Ix Stone/Tooth timing, Harass/Stone and optional overlays remain open. Full factions/public starts stay gated.',
+ 'Advanced numbers come from the user-authorized revised PDF, physical pages 32 and 19, not a newly located publisher erratum. Basic follows the main printed paragraph provisionally. Existing faction/deck prerequisites remain:33 Ecaz alone,47 Ix,35 CHOAM,47 both; Richese retains its separate ten-card cache. Explicit --ecaz-treachery independently adds all three cards. Whole-plan disclosure, mixed No-Field and optional-overlay interactions remain for refinement; public starts remain gated.',
],
checklist: [
- { area: 'Implementation', status: 'Partial', detail: 'Combined-side enumeration, explicit lead choice, native Occupy response, source-band even Basic or Advanced arithmetic and owner-labelled physical dial/outcome integration are implemented. Optional/exotic compositions remain open.', evidence: ['game/ecaz-occupy-battle.ts', 'game/board-resolution-quote.ts', 'game/battle-resolution-quote.ts', 'game/engine.ts'] },
+ { area: 'Implementation', status: 'Partial', detail: 'Combined-side enumeration, lead choice, native Occupy response and owner-labelled physical outcomes are connected. Basic odd counts now follow the main printed paragraph provisionally; Advanced retains its authorized arithmetic. Optional/exotic composition refinement remains.', evidence: ['game/ecaz-occupy-battle.ts', 'game/board-resolution-quote.ts', 'game/battle-resolution-quote.ts', 'game/engine.ts'] },
{ area: 'Player controls', status: 'Partial', detail: 'Original 390px controls cover mandatory/free dials, native Karama redial, Ix substitutions and whole-side Face Dance. Richese human T78EFNZK chose Ecaz lead, sealed total5/support2 with its real leader, revealed the ally’s original No-Field5 into only two available reserves, and reached Collection with Ecaz2 and two Richese losses. Basic human FEJ6SBFB shows the correct even-force labels, a total-dial slider bounded2..6 with no support control, and real Ecaz/guild casualties after an8–6 win. Other combinations remain open.', evidence: ['components/ecaz-occupy.tsx', 'components/face-dance-decision.tsx', 'components/game-table.tsx', 'game/ecaz-occupy-options.ts'] },
{ area: 'AI', status: 'Partial', detail: 'All four native policies actually seal legal total plans for either lead, active or canceled, using native leaders/cards and physical support. Strategy remains deferred.', evidence: ['game/ecaz-occupy-options.ts', 'game/bots.ts', 'tests/ecaz-occupy-options.test.ts'] },
- { area: 'Documentation', status: 'Implemented', detail: 'Authorized Advanced arithmetic/cancellation, unchanged Basic and specific Duke override are separated from historical source gaps and optional-module gates.', evidence: ['docs/ECAZ_OCCUPY_RULES.md', 'docs/RULE_DECISIONS.md', 'game/reference.ts'] },
+ { area: 'Documentation', status: 'Implemented', detail: 'Authorized Advanced arithmetic and provisional main-paragraph Basic arithmetic are distinguished from the conflicting FAQ, specific Duke ruling and deferred interaction refinement.', evidence: ['docs/ECAZ_OCCUPY_RULES.md', 'docs/RULE_DECISIONS.md', 'game/reference.ts'] },
{ area: 'Verification', status: 'Partial', detail: 'Development Verified: affected363 rules cases,26 actual native programs including all four legal policies, and original390px Richese/No-Field controls. Source-band Basic adds focused even/odd/canceled quote cases and native runtime cases with all four policies and original Collection/victory. Five genuine2..6-seat Basic games: four complete2090 accepted actions/54 JSON continuations/no rejection; four-seat seed20261299 is captured at the preserved odd-count conflict. Earlier46 ordinary/variant games and source-specific card evidence remain historical; complete factions/optional combinations and comprehensive assurance follow all rules.', evidence: ['tests/ecaz-occupy-battle.test.ts', 'tests/basic-ecaz-occupy-runtime.test.ts', 'tests/ecaz-occupy-richese.test.ts', 'tests/ecaz-occupy-stone.test.ts', 'tests/ecaz-occupy-cards-runtime.test.ts', 'tools/faction-games.ts', 'docs/ECAZ_OCCUPY_RULES.md'] },
],
related: ['faction-ecaz', 'advanced-ecaz', 'ecaz-ambassadors', 'battle', 'victory', 'implementation-checklist'],
@@ -1328,6 +1328,7 @@ export const RULE_TOPICS: RuleTopic[] = [
'The Guild Ambassador grants an immediate free shipment of zero through four physical reserve forces for Ecaz or its ally, including typed elites. Choose a clear territory and sector; this does not spend ordinary shipment or movement, ally aid or a retained rate card. Fremen may use destinations outside their ordinary reinforcement radius, but their southern reserves stay on-planet and this effect never permits storm entry. Only Ixians may ship directly into their placed mobile stronghold.',
'During Shipment and Movement, Guild can use its special Karama to stop another eligible off-planet Ambassador shipment before forces arrive; it does not stop this grant during a phase-one worm sequence. A successful off-planet shipment may invite Bene Gesserit accompaniment and accrues applicable phase-five Heighliner income once. BG can accompany Ixians into the mobile stronghold. Projected accompaniment choices show blocked sectors and keep Polar Sink and decline available when legal. Intrusion, separate Terror and accompanying fighter-triggered Ambassadors finish before the original entrant or worm rider resumes.',
'No-Field substitution into the free four-force grant remains unavailable. In bounded classic plus Ecaz/Moritani games, a shared Ambassador/Terror entry gives both owners saved optional prompts in current storm order without repeating the committed entry; this is a provisional table order, not a printed publisher priority. A Terror entry now waits for the arrival’s own interaction — Guild income, a Bene Gesserit fighter Intrusion or advisor choice, or Fremen storm protection — and then opens without replaying payment or force transfer. Ambassador-created Fremen/Guild child arrivals remain gated. Atomics Aftermath, Homeworlds, Nexus and Discovery combinations do not gain coverage from this overlap path.',
+ 'The Tleilaxu Ambassador offers one first-death own-leader revival or up to four physical forces for free. These are exclusive choices; the beneficiary sees its own eligible leaders privately. The ordinary allowance remains unchanged provisionally. Repeat-death, shared-leader and revival-income interactions remain for refinement.',
'All eleven effect guides are readable. Emperor, Atreides, Harkonnen, CHOAM, Ixian, Richese, Fremen and Guild entry effects are integrated, together with Bene Gesserit copies of those eight effects and direct Duke acquisition or consensual alliance through the reusable Ecaz token. Advanced combined Occupy has a separate bounded Development Verified guide, and Basic even-force Occupy now composes the same shared controls. Remaining effects, exceptional Duke custody and complete Ecaz rules remain unfinished. Mobile stronghold placement is gated. Full Ecaz starts remain disabled.',
],
checklist: [
diff --git a/tests/basic-ecaz-occupy-runtime.test.ts b/tests/basic-ecaz-occupy-runtime.test.ts
index 287f6b1c..f7c5ee37 100644
--- a/tests/basic-ecaz-occupy-runtime.test.ts
+++ b/tests/basic-ecaz-occupy-runtime.test.ts
@@ -8,7 +8,7 @@ import { advanceToNextStorm, finishToMovement } from './fixture-advanced-source'
import { chooseEcazOccupyLead, cancelEcazOccupy, openEcazOccupyPlans,
ecazOccupyTerritoryForces, stageEcazOccupyCard, allowEcazOccupyResponses,
type EcazOccupyFixture } from './fixture-ecaz-occupy';
-import { basicEcazOccupyFixture, basicEcazOccupyBattleAction } from './fixture-basic-ecaz-occupy';
+import { basicEcazOccupyFixture } from './fixture-basic-ecaz-occupy';
const seat = (game: Game, id: string): Player => game.players.find(p => p.id === id)!;
const forces = (game: Game, fixture: EcazOccupyFixture, id: string) =>
@@ -198,15 +198,6 @@ for (const lead of ['ecaz', 'ally'] as const)
assert.equal(game.phase, 7);
});
-void test('Basic odd uncanceled native choice rejects before creating a lead frame or committing forces', () => {
- const fixture = basicEcazOccupyFixture({ ecazForces: 3, chooseBattle: false });
- const { actor, action } = basicEcazOccupyBattleAction(fixture);
- reject(fixture.game, actor, action);
- assert.equal(fixture.game.battle, null);
- assert.equal(fixture.game.decision, null);
- assert.equal(forces(fixture.game, fixture, fixture.ecaz), 3);
- assert.equal(forces(fixture.game, fixture, fixture.ally), 4);
-});
for (const difficulty of ['Easy', 'Medium', 'Hard', 'Brutal'] as const)
for (const lead of ['ecaz', 'ally'] as const)
diff --git a/tests/ecaz-occupy-battle.test.ts b/tests/ecaz-occupy-battle.test.ts
index 845e66d4..51b21c17 100644
--- a/tests/ecaz-occupy-battle.test.ts
+++ b/tests/ecaz-occupy-battle.test.ts
@@ -161,10 +161,9 @@ void test('Basic sole traitor victory spares both armies; defeat, mutual traitor
}
});
-void test('uncanceled Basic odd Ecaz is source-blocked before commitment, while Advanced keeps ceil losses and floor survivors', () => {
+void test('Advanced odd Ecaz keeps ceil losses and floor survivors', () => {
for (const ecazCount of [1, 3, 5, 19]) {
for (const lead of ['ecaz-seat', 'ally-seat']) {
- assert.throws(() => quoteEcazOccupyBattle(battle(false, ecazCount, lead)), EcazOccupyBattleError);
const profile = quoteEcazOccupyBattle(battle(true, ecazCount, lead));
const win = quoteEcazOccupyOutcome(profile, {
result: 'normal', won: true, dial: Math.ceil(ecazCount / 2), support: 0,
diff --git a/tests/karama-preflight.test.ts b/tests/karama-preflight.test.ts
index a4379602..d1f7eec6 100644
--- a/tests/karama-preflight.test.ts
+++ b/tests/karama-preflight.test.ts
@@ -86,45 +86,16 @@ function countOpportunity(position: 'first' | 'last' = 'first') {
return { g, worthless, printed, spare };
}
-for (const form of ['printed', 'worthless'] as const)
- for (const position of ['first', 'last'] as const)
- void test(`${form} cancellation of the real ${position} Richese count rejects before cost or conversion, without randomness`, (t) => {
- const { g, worthless, printed } = countOpportunity(position);
- const before = structuredClone(g);
- const physical = inventory(g);
- const random = t.mock.method(crypto, 'getRandomValues', () => {
- throw Error('Rejected preflight must not draw randomness.');
- });
- const uuid = t.mock.method(crypto, 'randomUUID', () => {
- throw Error('Rejected preflight must not allocate an event.');
- });
- for (let attempt = 0; attempt < 2; attempt++)
- assert.throws(
- () =>
- applyAction(g, form === 'printed' ? 'e' : 'b', {
- type: 'card',
- card: form === 'printed' ? printed : worthless,
- mode: 'cancel',
- }),
- /canceled Richese auction count.*ruling/i,
- );
- assert.deepEqual(g, before);
- assert.deepEqual(inventory(g), physical);
- assert.equal(g.pendingKarama, undefined);
- assert.equal(g.discard.length, 0);
- assert.equal(random.mock.callCount(), 0);
- assert.equal(uuid.mock.callCount(), 0);
- });
-void test('printed cancellation of a legacy BG conversion restores the original Richese count instead of executing its unsupported cancellation', () => {
+void test('printed cancellation of a saved BG conversion restores the original Richese cache opportunity', () => {
const { g: initial, worthless, printed, spare } = countOpportunity();
const physical = inventory(initial);
const original = structuredClone(initial.response!);
const legacy = reload(initial);
const bg = player(legacy, 'b');
- // Historical saves accepted this conversion before checking the eventual
- // unsupported count cancellation. Preserve its real parent and spent card;
- // do not use the newly rejected declaration to manufacture a legacy save.
+ // Restore a saved conversion frame with its original declaration and spent
+ // Worthless card. Countering the conversion restores, rather than cancels,
+ // the original cache opportunity.
const index = bg.hand.findIndex((c) => c.id === worthless);
legacy.discard.push(bg.hand.splice(index, 1)[0]);
legacy.pendingKarama = {
diff --git a/tests/tleilaxu-ambassador-engine.test.ts b/tests/tleilaxu-ambassador-engine.test.ts
index 2726142b..3981d256 100644
--- a/tests/tleilaxu-ambassador-engine.test.ts
+++ b/tests/tleilaxu-ambassador-engine.test.ts
@@ -98,14 +98,9 @@ void test('the Tleilaxu Ambassador returns typed forces from the Tanks for free
assert.equal(offered.decision?.kind, 'ecazAmbassador');
assert.equal(offered.decision?.player, 'ally');
const view = viewGame(offered, 'ally').ambassadorEntry!;
- assert.deepEqual(view.revival, {
- maximum: 3,
- tanks: 3,
- eliteTanks: 1,
- eliteRevived: 0,
- leaderAlternative: false,
- blocked: null,
- });
+ assert.equal(view.revival?.maximum, 3);
+ assert.equal(view.revival?.eliteTanks, 1);
+ assert.equal(view.revival?.blocked, null);
assert.equal(viewGame(offered, 'ecaz').ambassadorEntry!.revival, null);
assert.equal(viewGame(offered, 'entrant').ambassadorEntry!.revival, null);
const before = counters(offered, 'ally');
@@ -214,7 +209,6 @@ void test('the entry control offers each legal count, the elite-aware return and
for (const label of ['Return 1 force', 'Return 2 forces', 'Return 3 forces', 'Return no forces'])
assert.ok(markup.includes(label), label);
assert.ok(markup.includes('Tleilaxu Ambassador'));
- assert.ok(markup.includes('leader alternative'));
const blocked = structuredClone(offered);
blocked.players.find((p) => p.id === 'ally')!.tanks = 0;
const blockedView = viewGame(blocked, 'ally') as unknown as Parameters[0]['game'];