From 6d50829c0cf102377891be1fca18f53e37a3887b Mon Sep 17 00:00:00 2001 From: Quintine <15965533+Quintine@users.noreply.github.com> Date: Tue, 27 Oct 2026 20:13:04 +1100 Subject: [PATCH] Enable Basic Kull with Ecaz and Moritani --- components/game-table.tsx | 2 +- docs/CHOAM_KULL_DESIGN.md | 27 +++++ docs/CURRENT_STATUS.md | 10 ++ docs/IMPLEMENTATION_STATUS.md | 14 +++ docs/RULE_DECISIONS.md | 8 ++ game/bribe-promises.ts | 8 +- game/engine.ts | 52 +++++++-- game/reference.ts | 8 +- tests/fixture-kull-native-e3.ts | 96 +++++++++++++++++ tests/kull-bribe-scope.test.ts | 5 +- tests/kull-ground-movement-counter.test.ts | 84 +++++++++++++++ tests/kull-ground-movement-runtime.test.ts | 89 ++++++++++++++++ tests/kull-native-e3-runtime.test.ts | 116 +++++++++++++++++++++ 13 files changed, 503 insertions(+), 16 deletions(-) create mode 100644 tests/fixture-kull-native-e3.ts create mode 100644 tests/kull-ground-movement-counter.test.ts create mode 100644 tests/kull-ground-movement-runtime.test.ts create mode 100644 tests/kull-native-e3-runtime.test.ts diff --git a/components/game-table.tsx b/components/game-table.tsx index adfef07a..b4236008 100644 --- a/components/game-table.tsx +++ b/components/game-table.tsx @@ -2210,7 +2210,7 @@ export function GameTable({ Kull Wahad · Basic development preview {g.lobbyModules?.kullPreview &&
- Requires Basic CHOAM with classic factions, Ixians or Tleilaxu, both CHOAM and Ix decks, and no other optional modules.
+ Requires Basic CHOAM with classic factions or Ixian, Tleilaxu, Ecaz or Moritani factions, the CHOAM and Ix decks plus each faction’s deck, and no other optional modules.
This preview does not support structured Truthtrance questions about bids, alliances, Revival,
or combined turn actions, and it defers unfunded non-CHOAM Karama-dependent bids. It does not represent complete
CHOAM rules. Every player must ready again after changing it.
diff --git a/docs/CHOAM_KULL_DESIGN.md b/docs/CHOAM_KULL_DESIGN.md
index 24e22652..011c9644 100644
--- a/docs/CHOAM_KULL_DESIGN.md
+++ b/docs/CHOAM_KULL_DESIGN.md
@@ -2,6 +2,33 @@
Source audit: 2026-09-06; opt-in preview contract updated 30 September 2026. This is an engineering and product-policy record, not a rules-completion claim. The source audit in [CHOAM_REMAINING_RULES.md](CHOAM_REMAINING_RULES.md) remains applicable. The user-approved counter and BG choices supersede the original proposal; publisher-source uncertainty and full expansion gates remain.
+## Basic native Ecaz and Moritani — 27 October 2026
+
+**Prototyped / Partial.** The fresh Basic printed-Kull opt-in admits Ecaz and
+Moritani with their native setup and all three expansion decks. The earlier
+Ixian, Tleilaxu and classic seats remain available; optional modules and the
+Richese/Semuta composition remain separate. Structured bribes remain supported.
+The existing [publisher Basic Kull rule](https://www.gf9games.com/dune/wp-content/uploads/2021/11/CHOAM-Rulebook-low-res.pdf#page=7)
+and selected distinct-counter policy remain unchanged. This roster extension
+does not settle broader interactions or certify a complete mode.
+
+When a ground-movement commitment needs an owned Karama shipment discount,
+the detached feasibility witness can consider CHOAM declining its fresh Kull
+offer. It neither executes nor selects the real reaction. A successful foreign
+Kull may make the route impossible and release the commitment after resolution;
+a decline or distinct counter retains the actual original effect. A distinct
+counter cannot spend the countering player's last feasible ground-movement
+route: the private quote applies its exact card cost and the resumed original
+intent before offering it. Other movement and counter compositions and fully
+integrated acceptance remain open.
+
+Frozen changed-path proof passed 101/101 tests in 13 files, type/lint and the
+production build. [Native Ecaz/Moritani tests](../tests/kull-native-e3-runtime.test.ts)
+exercise actual placements and a Charity-funded bribe; [movement tests](../tests/kull-ground-movement-runtime.test.ts)
+and [counter-cost tests](../tests/kull-ground-movement-counter.test.ts) use
+real public actions with selected unopened cards. This does not establish
+browser, recovery or deployed acceptance.
+
## Basic native Ix faction roster — 27 October 2026
**Prototyped / Partial.** The explicit fresh-game Basic printed-Kull opt-in extends
diff --git a/docs/CURRENT_STATUS.md b/docs/CURRENT_STATUS.md
index 1a9b9613..991839b3 100644
--- a/docs/CURRENT_STATUS.md
+++ b/docs/CURRENT_STATUS.md
@@ -20,6 +20,16 @@ open mode gates.
Keep identified Basic-function gaps ahead of Advanced and wider interactions.
Prototype labels and final acceptance gates remain.
+**Printed Kull with native Ecaz/Moritani and movement forecast — 27 October. Prototyped / Partial.**
+The Basic Kull opt-in includes Ecaz and Moritani with their selected deck,
+CHOAM and Ix; existing supported bribe questions remain available. A detached
+ground-movement quote can account for CHOAM declining a newly opened Kull offer
+on a needed Karama shipment discount while the real table awaits its response.
+Richese/Semuta, optional modules, wider interactions and complete modes remain
+open. [Scope](CHOAM_KULL_DESIGN.md#basic-native-ecaz-and-moritani--27-october-2026).
+Frozen changed-path checks passed 101/101 tests in 13 files, type/lint and the
+production build; browser and deployed acceptance remain open.
+
**Printed Kull with native Ix factions — 27 October. Prototyped / Partial.**
The Basic printed-Kull lobby opt-in includes Ixians and Tleilaxu alongside
CHOAM and classic factions with the CHOAM and Ix decks. Existing native setup
diff --git a/docs/IMPLEMENTATION_STATUS.md b/docs/IMPLEMENTATION_STATUS.md
index 4b310371..101465bb 100644
--- a/docs/IMPLEMENTATION_STATUS.md
+++ b/docs/IMPLEMENTATION_STATUS.md
@@ -1,5 +1,19 @@
# Dune implementation status
+## 27 October 2026 — Native Ecaz/Moritani Kull and movement forecast
+
+The Basic printed-Kull opt-in adds Ecaz and Moritani with all three selected
+expansion decks, using native setup, placement continuations and the existing
+bribe question profile. A ground-movement completion witness can quote a
+possible decline of an owned shipment Karama's Kull interruption; the real
+table retains the response and settles the chosen outcome. Richese/Semuta,
+other optional modules, wider interactions and complete modes remain open.
+The frozen batch passed 101/101 tests across 13 files, type/lint and the
+production build. Native placement, real bribe continuation and movement-cost
+regressions use selected unopened cards and actual phase actions; broader
+interaction and deployed acceptance remain open.
+[Contract](CHOAM_KULL_DESIGN.md#basic-native-ecaz-and-moritani--27-october-2026).
+
## 27 October 2026 — Printed Kull with native Ix factions
The Basic Kull opt-in admits native Ixians and Tleilaxu with CHOAM and classic
diff --git a/docs/RULE_DECISIONS.md b/docs/RULE_DECISIONS.md
index 4f47a8f2..f5cd4bb5 100644
--- a/docs/RULE_DECISIONS.md
+++ b/docs/RULE_DECISIONS.md
@@ -1,5 +1,13 @@
# Rule decision index
+## Basic printed Kull with native Ecaz and Moritani — 27 October 2026
+
+Extend the existing provisional printed-Kull protocol to Ecaz and Moritani
+with their selected deck and the CHOAM and Ix decks. A hypothetical owned
+movement completion may include a decline of its Kull offer; the actual opponent
+retains its independent choice. This is a software composition, not a new
+publisher ruling. [Scope](CHOAM_KULL_DESIGN.md#basic-native-ecaz-and-moritani--27-october-2026).
+
## Basic printed Kull with native Ix factions — 27 October 2026
Extend the existing opt-in printed Kull interpretation to native Ixians and
diff --git a/game/bribe-promises.ts b/game/bribe-promises.ts
index 27d37dad..7a7de8ec 100644
--- a/game/bribe-promises.ts
+++ b/game/bribe-promises.ts
@@ -38,7 +38,7 @@ export function parseBribeClaim(value: unknown): BribeClaim {
/** The ordinary, grouped and mixed bribe families share this admission. The
* printed Kull extension is deliberately limited to the existing fresh Basic
- * CHOAM, Ixian, Tleilaxu and classic roster; other previews keep their own composition boundaries.
+ * CHOAM, Ixian, Tleilaxu, Ecaz, Moritani and classic roster; other previews keep their own composition boundaries.
* This quote grants no start permission or future unowned funds. */
export function bribePromiseModeSupported(game: NativePromiseScope & {
semutaPreview?: unknown; kullPreview?: unknown; nexusKullPreview?: unknown;
@@ -50,12 +50,14 @@ export function bribePromiseModeSupported(game: NativePromiseScope & {
if (!nativePromiseScopeSupported(game) || game.semutaPreview || game.nexusKullPreview ||
game.richeseBetrayalPreview) return false;
if (!game.kullPreview) return true;
- return game.kullPreview === true && game.advanced === false && game.expansions.length === 2 &&
+ return game.kullPreview === true && game.advanced === false && (game.expansions.length === 2 ||
+ game.expansions.length === 3 && game.expansions.includes('ecaz')) &&
game.expansions.includes('choam') && game.expansions.includes('ix') &&
game.players.some(player => player.faction === 'choam') &&
new Set(game.players.map(player => player.faction)).size === game.players.length &&
game.players.every(player => player.faction === 'choam' || player.faction === 'ixians' ||
- player.faction === 'tleilaxu' ||
+ player.faction === 'tleilaxu' || game.expansions.includes('ecaz') &&
+ (player.faction === 'ecaz' || player.faction === 'moritani') ||
FACTIONS.some(faction => faction.id === player.faction && faction.expansion === 'base')) &&
!game.guildBetrayalPreview && !game.nexusIxianReplacementPreview &&
!game.nexusIxianBetrayalPreview && !game.nexusHarkonnenBetrayalPreview &&
diff --git a/game/engine.ts b/game/engine.ts
index e759d057..c07094e9 100644
--- a/game/engine.ts
+++ b/game/engine.ts
@@ -10017,13 +10017,15 @@ function startModules(state: Game): Game {
const g = structuredClone(state);
delete g.lobbyOptions;
if (options.kullPreview) {
- requireRule(g.advanced === false && g.expansions.length === 2 && g.expansions.includes('choam') &&
+ requireRule(g.advanced === false && (g.expansions.length === 2 ||
+ g.expansions.length === 3 && g.expansions.includes('ecaz')) && g.expansions.includes('choam') &&
g.expansions.includes('ix') && !!byFaction(g,'choam') &&
g.players.every(player => player.faction === 'choam' || player.faction === 'ixians' ||
- player.faction === 'tleilaxu' || CLASSIC_FACTIONS[player.faction] === true) &&
+ player.faction === 'tleilaxu' || g.expansions.includes('ecaz') &&
+ (player.faction === 'ecaz' || player.faction === 'moritani') || CLASSIC_FACTIONS[player.faction] === true) &&
!options.leaderSkills && !options.ecazTreachery && !g.nexusCards && !g.discoveryEnabled &&
!g.homeworlds && !g.techTokens && !g.strongholdCards,
- 'Kull preview needs Basic CHOAM, Ixian, Tleilaxu or classic factions, CHOAM and Ix decks, and no optional modules.');
+ 'Kull preview needs Basic CHOAM with classic or supported Ix/Ecaz factions, CHOAM and Ix decks, each faction’s deck, and no optional modules.');
return initializeKullGameForAudit(g);
}
if (options.ecazTreachery && options.leaderSkills) g.ecazTreachery = true;
@@ -12394,11 +12396,13 @@ export function initializeSemutaGameForAudit(state: Game, skills = false): Game
/** Opt-in printed Kull gameplay with the physical Ix deck, not a public release gate. */
export function initializeKullGameForAudit(state: Game): Game {
requireRule(!state.kullPreview && !state.nexusKullPreview && !state.pendingKull && !state.kullRestrictions &&
- state.expansions.length === 2 && state.expansions.includes('choam') &&
+ (state.expansions.length === 2 || !state.advanced && state.expansions.length === 3 &&
+ state.expansions.includes('ecaz')) && state.expansions.includes('choam') &&
state.expansions.includes('ix') && state.players.some(p => p.faction === 'choam') &&
state.players.every(p => p.faction === 'choam' || faction(p.faction).expansion === 'base' ||
- !state.advanced && (p.faction === 'ixians' || p.faction === 'tleilaxu')),
- 'Kull preview needs a fresh CHOAM, Ixian, Tleilaxu or classic-faction lobby with CHOAM and Ix decks.');
+ !state.advanced && (p.faction === 'ixians' || p.faction === 'tleilaxu' ||
+ state.expansions.includes('ecaz') && (p.faction === 'ecaz' || p.faction === 'moritani'))),
+ 'Kull preview needs a fresh CHOAM lobby with supported native factions and selected CHOAM and Ix decks.');
const g = initializeFactionExpansionsGameForAudit(state);
g.kullPreview = true;
return g;
@@ -17912,6 +17916,26 @@ function assertKaramaPromiseFeasibility(
use: KaramaUse,
costCard?: Card,
): boolean {
+ if (g.pendingKull?.stage === 'counter' && use.kind === 'cancel' &&
+ use.response.kind === 'choamWorthless' && use.response.intent === 'Kull Wahad') {
+ const moves = liveMovementPromises(g.movementPromises ?? [], p.id, g.turn);
+ const expressions = liveMovementExpressionPromises(g.movementExpressionPromises ?? [], p.id, g.turn);
+ if (moves.length || expressions.length) {
+ if (!costCard) return false;
+ const projected = structuredClone(g);
+ const owner = getPlayer(projected, p.id);
+ owner.hand = owner.hand.filter(card => card.id !== costCard.id);
+ // A successful counter resumes the original intent. Quote the actual
+ // resumed parent with this distinct counter already unavailable.
+ try {
+ resumeKullAttempt(projected, false);
+ if (!groundMovementCompletion(projected, p.id, moves, expressions)) return false;
+ } catch (error) {
+ if (error instanceof RuleError) return false;
+ throw error;
+ }
+ }
+ }
const battlePromises =
g.battle && !g.battle.revealed && !g.battle.plans[p.id]
? (g.battle.truthPromises ?? []).filter(
@@ -19696,6 +19720,12 @@ function recoverableUnsupportedShipment(shipment: PendingShipment) {
* owned movement reachability; unresolved opposing outcomes are never replayed. */
function groundMovementProbe(source: Game, actor: string): Game {
const game = structuredClone(source);
+ // A freshly interrupted owned shipment discount has a possible native
+ // continuation when CHOAM declines Kull. Quote that outcome only in this
+ // detached witness; the live table still waits for the real response.
+ if (game.pendingKull?.stage === 'offer' && game.pendingKull.owner === actor &&
+ game.pendingKull.intent.kind === 'ordinary' && game.pendingKull.intent.use.kind === 'shipment')
+ resumeKullAttempt(game, false);
projectPendingShipmentKarama(game, actor);
if (game.phase < 5) {
if (game.pendingRevival?.player === actor) projectOwnedOrdinaryRevival(game, actor);
@@ -21575,7 +21605,15 @@ function groundMovementCompletionAtMovement(game: Game, actor: string, promises:
for (const card of player.hand) {
if (!player.shipped && !state.karamaShipping && canUseAsKaramaRole(state, player, card)) {
const action: Action = { type: 'card', card: card.id, mode: 'shipment', target: owner };
- try { yield { game: groundMovementProbe(applyActionInner(state, owner, action, 'movementPreparation'), owner), actions: [action] }; }
+ try {
+ const prepared = applyActionInner(state, owner, action, 'movementPreparation');
+ // This detached witness may assume a newly opened Kull offer is
+ // declined. The real action still waits for CHOAM's decision.
+ if (!state.pendingKull && prepared.pendingKull?.stage === 'offer' &&
+ prepared.pendingKull.owner === owner && prepared.pendingKull.card === card.id)
+ resumeKullAttempt(prepared, false);
+ yield { game: groundMovementProbe(prepared, owner), actions: [action] };
+ }
catch (error) { if (!(error instanceof RuleError)) throw error; }
}
if (card.effect === 'hajr' && hajrAvailable(state, player) && !transferCardBlock(state, player, card)) {
diff --git a/game/reference.ts b/game/reference.ts
index d5fb04c7..e1415dc3 100644
--- a/game/reference.ts
+++ b/game/reference.ts
@@ -4755,7 +4755,7 @@ export const RULE_TOPICS: RuleTopic[] = [
'Whenever another faction requests a normal revival with a free portion, CHOAM receives a response decision independently of its private hand. Allow the request, or declare La La La for that requester. If the effect succeeds, the whole pending request stops without payment, force movement or quota use; a faction permitted to purchase normal revivals can submit a paid request. If the effect is canceled, the original quote resumes through any Tleilaxu and other applicable responses.',
'La La La does not prevent otherwise legal paid revivals or separate Ghola card effects. With Tleilaxu in the game, Fremen may submit a paid request within their current limit after free revival is prevented. Without Tleilaxu, Fremen’s base purchase restriction leaves no normal force return available. Emperor-funded extras remain separate. The prohibition resets when the next Revival phase begins. Card custody is checked at settlement: if CHOAM cashed in the declared card during the response, no Worthless effect occurs and the interrupted revival resumes.',
'During Shipment and Movement, use Baliset to select another player and a territory CHOAM occupies. The current implementation prevents that player from entering that territory by movement for the phase while CHOAM remains there; shipment is allowed. A declared move into a CHOAM territory pauses for CHOAM regardless of its private hand. Allow it or declare Baliset. A successful effect leaves the moving forces in place and the movement unspent; a canceled or unavailable card resumes the original move after checking its legality again.',
- 'Kulon, La La La, Baliset, Jubba Cloak and Trip to Gamont have engine, private controls and AI support. Printed Kull Wahad has an opt-in Basic lobby preview for CHOAM, Ixians, Tleilaxu and classic factions with the CHOAM and Ix decks and no other modules; the explicit Nexus Kull profile adds Cunning fuel choices at that same reaction, not in every Nexus game. See both Kull guides for distinct counters, pre-conversion Bene Gesserit custody and the deferred winning-overbid boundary. Exact cancellation scope, Hajr/ornithopter combinations, Baliset duration and destination scope, simultaneous prevention and later module exceptions remain under audit. Complete expansion play remains unfinished.',
+ 'Kulon, La La La, Baliset, Jubba Cloak and Trip to Gamont have engine, private controls and AI support. Printed Kull Wahad has an opt-in Basic lobby preview for CHOAM, Ixians, Tleilaxu, Ecaz, Moritani and classic factions with the CHOAM and Ix decks, each seated faction’s deck and no other modules; the explicit Nexus Kull profile adds Cunning fuel choices at that same reaction, not in every Nexus game. See both Kull guides for distinct counters, pre-conversion Bene Gesserit custody and the deferred winning-overbid boundary. Exact cancellation scope, Hajr/ornithopter combinations, Baliset duration and destination scope, simultaneous prevention and later module exceptions remain under audit. Complete expansion play remains unfinished.',
],
related: [
'faction-choam',
@@ -4778,7 +4778,7 @@ export const RULE_TOPICS: RuleTopic[] = [
developmentStage: 'Prototyped',
summary: 'React before a Karama is played; a distinct counter can stop Kull before its phase restriction begins.',
steps: [
- 'In the lobby, the host can opt into this Basic development preview with CHOAM, Ixians, Tleilaxu and classic factions, the CHOAM and Ix Treachery decks, and no other optional modules. It uses printed ordinary Karama. The preview withholds some structured Truthtrance question types and defers unfunded non-CHOAM Karama-dependent bids; leave it off to retain the ordinary current game. A separate Advanced development profile supports Bene Gesserit substitutions and legal special powers. Existing games are not retrofitted, and the warning is not rules-completion approval.',
+ 'In the lobby, the host can opt into this Basic development preview with CHOAM, Ixians, Tleilaxu, Ecaz, Moritani and classic factions, the CHOAM and Ix decks, each seated faction’s deck, and no other optional modules. It uses printed ordinary Karama. The preview withholds some structured Truthtrance question types and defers unfunded non-CHOAM Karama-dependent bids; leave it off to retain the ordinary current game. A separate Advanced development profile supports Bene Gesserit substitutions and legal special powers. Existing games are not retrofitted, and the warning is not rules-completion approval.',
'In the supported Basic printed Kull preview, ordinary, grouped and mixed Truthtrance bribe questions remain available. A pending Kull response temporarily holds bribe controls; its real resolution resumes the existing commitment. Other structured question types remain incomplete in this preview.',
'Another player’s legal Karama cancellation, shipment-rate use, immediate auction purchase, winning-bid payment or supported special activation pauses before its card and other costs. CHOAM sees the attempted player and purpose and may decline or choose a legal held Kull Wahad. Each Use names its printed source explicitly. The separate Nexus Kull guide covers additional Cunning costs in its new profile only. The same public opportunity appears without Kull in hand, so waiting does not disclose hidden ownership. Invalid attempts never open this window; CHOAM does not receive a self-targeted Kull offer.',
'Decline resumes the original attempt once. Declaring Kull opens the ordinary CHOAM-effect response. Kull is still pending: its target may counter with a different eligible physical Karama before any phase ban applies. The interrupted card stays reserved and cannot serve as its own counter. Other eligible responders use their existing response controls.',
@@ -4788,12 +4788,12 @@ export const RULE_TOPICS: RuleTopic[] = [
'Advanced Bene Gesserit is intercepted before Worthless conversion or discard: successful Kull keeps that original unplayed Worthless card held. If CHOAM declines or Kull is countered, the original proceeds to its separate conversion response. Ordinary cancellation of that conversion still leaves its Worthless card discarded. A different BG Worthless counter has its own conversion response; it is never the reserved original.',
'Winning overbids whose Karama payment would become unplayable remain deferred. To avoid accepting a bid that the preview cannot settle, a non-CHOAM unfunded Karama-dependent bid is refused privately before commitment, independently of CHOAM’s hidden Kull ownership. CHOAM self-activations are outside interception; normal non-preview bidding is unchanged. An already unfunded winning activation remains guarded. Holding Karama alone is not a reaction trigger. This unfinished-composition fence does not restart bidding, award the lot free, supply new funding or change numeric UI bid limits.',
'A live Truthtrance battle or shipment promise also constrains original attempts, Kull costs and counters. A cost that cannot be proved compatible is unavailable before disposal; CHOAM may decline instead. Proven supported cancellations remain available. Pending Kull cannot release a suspended promise, and its counter choices exclude unproven costs so legal AI can progress. Unsupported continuations remain an incomplete preview composition, not a publisher ban or permission to release the promise.',
- 'The different-card counter and before-conversion custody are explicitly selected development policy, not a located publisher clarification. This printed-only profile still excludes Nexus. The separate Nexus Kull profile adds one bounded Cunning interaction without changing older games; Richese, Ecaz and Moritani rosters, optional modules, unpayable-auction recovery and full CHOAM acceptance remain outside this printed preview. Nexus cards do not receive blanket immunity to Karama.',
+ 'The different-card counter and before-conversion custody are explicitly selected development policy, not a located publisher clarification. This printed-only profile still excludes Nexus. The separate Nexus Kull profile adds one bounded Cunning interaction without changing older games; Richese/Semuta, optional modules, unpayable-auction recovery and full CHOAM acceptance remain outside this printed preview. Nexus cards do not receive blanket immunity to Karama.',
],
example: 'Bene Gesserit attempts to use Baliset as Karama. CHOAM declares Kull. Baliset cannot also counter Kull; a different eligible Karama may. If Kull succeeds, Baliset stays held; in a new Advanced attempt BG cannot repeat that specific Karama effect this phase but may use another legal effect. If a distinct counter prevents Kull, the original Baliset attempt proceeds to its normal conversion response.',
related: ['choam-worthless', 'nexus-choam-kull', 'faction-choam', 'card-karama', 'advanced-beneGesserit', 'special-karama', 'bidding', 'choam-karama', 'nexus-cards', 'implementation-checklist'],
checklist: [
- { area: 'Implementation', status: 'Partial', detail: 'Opt-in Kull binds typed intents, distinct physical counters and Basic/legacy all-purpose or new Advanced semantic-effect restrictions. Different effects remain playable; phase expiry is provisional in Advanced. Basic printed Kull admits native Ixians and Tleilaxu and retains structured bribe promises through its response; deferred overbid recovery and broader module support remain excluded.', evidence: ['game/choam-kull.ts', 'game/kull-purpose.ts', 'game/bribe-promises.ts', 'game/engine.ts', 'tests/kull-native-ix-runtime.test.ts'] },
+ { area: 'Implementation', status: 'Partial', detail: 'Opt-in Kull binds typed intents, distinct physical counters and Basic/legacy all-purpose or new Advanced semantic-effect restrictions. Different effects remain playable; phase expiry is provisional in Advanced. Basic printed Kull admits native Ixians, Tleilaxu, Ecaz and Moritani and retains structured bribe promises through its response; deferred overbid recovery and broader module support remain excluded.', evidence: ['game/choam-kull.ts', 'game/kull-purpose.ts', 'game/bribe-promises.ts', 'game/engine.ts', 'tests/kull-native-ix-runtime.test.ts', 'tests/kull-native-e3-runtime.test.ts', 'tests/kull-ground-movement-runtime.test.ts'] },
{ area: 'Player controls', status: 'Partial', detail: 'Owner-only legal held costs and decline use the Kull event; counters reuse ordinary response controls. Public viewers receive purpose/actors, not private intent payloads or hidden choices.', evidence: ['components/choam-kull.tsx', 'components/game-table.tsx'] },
{ area: 'AI', status: 'Partial', detail: 'Legal preview participation uses projected reactor choices and ordinary distinct counter controls. Full strategy and difficulty calibration remain deferred.', evidence: ['game/bot-choam-kull.ts', 'game/bots.ts'] },
{ area: 'Documentation', status: 'Partial', detail: 'Pending versus active restrictions, distinct custody, decline/prevention/success, BG-before-conversion and deferred overbid limits have internal explanations. Publisher-source and full-faction acceptance remain separate.', evidence: ['docs/CHOAM_KULL_DESIGN.md', 'docs/CHOAM_KULL_SOURCE_UPDATE.md', 'docs/NEXUS_CHOAM_RULES.md'] },
diff --git a/tests/fixture-kull-native-e3.ts b/tests/fixture-kull-native-e3.ts
new file mode 100644
index 00000000..c683882d
--- /dev/null
+++ b/tests/fixture-kull-native-e3.ts
@@ -0,0 +1,96 @@
+import assert from 'node:assert/strict';
+import { createGame, joinGame, newPlayer, type Action, type Game } from '../game/engine';
+import { territory } from '../game/board';
+import { actKullPurpose, kullPurposeClean, kullPurposePlayer, nextKullPurposeStep,
+ type KullPurposeStep } from './fixture-kull-purpose';
+import { publicKullInventory } from './fixture-public-choam-kull';
+
+export const act = actKullPurpose;
+export const clean = kullPurposeClean;
+export const player = kullPurposePlayer;
+export const inventory = publicKullInventory;
+export type E3NativeRoster = 'ecaz' | 'moritani' | 'both';
+export type E3NativeTarget = 'ecaz' | 'moritani';
+
+/** Make only ordinary native decisions, declining optional placements and
+ * effects unless the caller has stopped at the requested original control. */
+function step(game: Game): KullPurposeStep | null {
+ if (!game.phaseOpening && !game.response && game.decision) {
+ const d = game.decision;
+ if (d.kind === 'moritaniSetup') return { actor: d.player,
+ action: { type: 'decision', territory: 'carthag', sector: territory('carthag').sectors[0] } };
+ if (d.kind === 'ecazPlacement' || d.kind === 'moritaniPlacement')
+ return { actor: d.player, action: { type: 'decision', decline: true } };
+ if (d.kind === 'ecazAmbassador') return { actor: d.player,
+ action: { type: 'decision', event: game.pendingAmbassador!.event, decline: true } };
+ }
+ return nextKullPurposeStep(game);
+}
+export function advance(game: Game, until: (state: Game) => boolean, actions: KullPurposeStep[] = []): Game {
+ for (let n = 0; n < 1800; n++) {
+ if (until(game)) return game;
+ assert.notEqual(game.status,'finished','The native E3 Kull programme ended before the requested window');
+ const next = step(game); assert.ok(next,`A native continuation is required at ${game.status}/${game.turn}/${game.phase}/${game.decision?.kind}`);
+ game = act(game,next.actor,next.action,actions);
+ }
+ throw Error('The native E3 Kull programme did not reach its finite boundary');
+}
+
+/** Real public Basic start; only original unopened Treachery and Spice
+ * positions are chosen before native deal. No force, wallet, token or phase is
+ * staged. Every subsequent placement and cancellation uses native actions. */
+export function createKullNativeE3Fixture(roster: E3NativeRoster, target: E3NativeTarget = roster === 'moritani' ? 'moritani' : 'ecaz') {
+ assert.ok(roster === 'both' || roster === target);
+ let game = createGame('KULLE3',newPlayer('choam','CHOAM','choam'),false,['choam','ix','ecaz']);
+ const natives = roster === 'both' ? ['ecaz','moritani'] as const : [roster] as const;
+ for (const faction of natives) joinGame(game,newPlayer(faction,faction,faction));
+ for (const faction of ['emperor','guild'] as const) joinGame(game,newPlayer(faction,faction,faction));
+ const actions: KullPurposeStep[] = [];
+ const send = (actor: string, action: Action) => { game = act(game,actor,action,actions); };
+ const reach = (predicate: (state: Game) => boolean) => { game = advance(game,predicate,actions); };
+ send('choam',{type:'moduleOption',module:'kullPreview',enabled:true});
+ for (const seat of game.players) send(seat.id,{type:'ready'});
+ const lobby = structuredClone(game);
+ send('choam',{type:'start'});
+ assert.equal(game.status,'setup'); assert.equal(game.kullPreview,true);
+ const setup = structuredClone(game), originalCards = game.deck.map(card => card.id).sort();
+ const kinds = roster === 'both' ? ['kull','truth','filler','karama','karama'] : ['kull','truth','karama','karama'];
+ const chosen: Game['deck'] = [];
+ for (const kind of kinds) {
+ const index = game.deck.findIndex(card => kind === 'kull' ? card.id === 'ix-kull-wahad' :
+ kind === 'truth' ? card.effect === 'truthtrance' : kind === 'karama' ? card.effect === 'karama' :
+ card.kind === 'worthless' && card.id !== 'ix-kull-wahad');
+ assert.ok(index >= 0); chosen.push(game.deck.splice(index,1)[0]);
+ }
+ game.deck.unshift(...chosen);
+ const beforeDeal = structuredClone(game);
+ reach(state => state.status === 'playing');
+ const afterSetup = structuredClone(game), kull = 'ix-kull-wahad';
+ const karama = player(game,'emperor').hand.find(card => card.effect === 'karama')?.id;
+ const counter = player(game,'guild').hand.find(card => card.effect === 'karama')?.id;
+ const asker = roster === 'moritani' ? 'moritani' : 'ecaz';
+ const truth = player(game,asker).hand.find(card => card.effect === 'truthtrance')?.id;
+ assert.ok(karama && counter && truth && karama !== counter);
+ assert.ok(player(game,'choam').hand.some(card => card.id === kull));
+ assert.deepEqual(inventory(game),originalCards);
+ if (roster !== 'moritani') assert.ok(game.ecazAmbassadors && game.ecazOccupyPreview);
+ if (roster !== 'ecaz') assert.ok(game.moritaniTerror);
+ const spice = game.spiceDeck.findIndex(card => 'territory' in card && !card.discovery);
+ assert.ok(spice >= 0); game.spiceDeck.unshift(game.spiceDeck.splice(spice,1)[0]);
+ reach(state => state.phase <= 1 && clean(state));
+ const early = structuredClone(game);
+ const phase = target === 'ecaz' ? 4 : 8;
+ const decision = target === 'ecaz' ? 'ecazPlacement' : 'moritaniPlacement';
+ reach(state => state.phase === phase && state.decision?.kind === decision && !state.response && !state.phaseOpening);
+ const beforePlacement = structuredClone(game);
+ const token = target === 'ecaz' ? game.ecazAmbassadors!.tokens.find(row => row.effect === 'ecaz' && row.zone === 'supply')
+ : game.moritaniTerror!.tokens.find(row => row.status === 'available');
+ assert.ok(token);
+ send(target,{type:'decision',token:token.id,territory:'carthag'});
+ assert.equal(game.response?.kind,decision);
+ assert.deepEqual(inventory(game),originalCards);
+ return { game, early, beforePlacement, lobby, setup, beforeDeal, afterSetup, actions, originalCards,
+ actor:'emperor', reactor:'choam', counterOwner:'guild', asker, target, token:token.id,
+ kull, karama, counter, truth };
+}
+export type KullNativeE3Fixture = ReturnType