diff --git a/components/ornithopter-movement.tsx b/components/ornithopter-movement.tsx index b805fecf..5e654cf9 100644 --- a/components/ornithopter-movement.tsx +++ b/components/ornithopter-movement.tsx @@ -194,6 +194,13 @@ export function OrnithopterMovement({

)} + {me.faction === 'choam' && game.choamMovementBonus > 0 && ( +

+ Provisional Kulon combination: the one-group card move stays capped + at three territories. Kulon still extends each normal-range move in + the two-group mode. +

+ )} {reason && (

{reason} diff --git a/docs/CURRENT_STATUS.md b/docs/CURRENT_STATUS.md index 5014bd8b..179913fe 100644 --- a/docs/CURRENT_STATUS.md +++ b/docs/CURRENT_STATUS.md @@ -29,6 +29,16 @@ optional skill bands, combinations and public expansion starts remain open. Rules implementation continues; comprehensive assurance and administration remain deferred until all rules are implemented. +**Kulon/Ornithopter coexistence prototype — 9 October.** Active Kulon no +longer blocks the card's fixed-range mode in either rules mode. The labeled +provisional policy retains its three-territory maximum and extends both +normal-range groups. Real Kulon/card activation, boundary rejection and +four-profile legal-candidate smoke pass in both modes. Isolated Chromium +controls complete the three-territory move against the production engine; +no saved room or deployed application was changed. Hajr/prior moves and +advisor composition remain guarded. See the +[movement contract](RICHESE_ORNITHOPTER_ENGINE_AUDIT.md#provisional-kulon-coexistence--9-october-2026). + **Richese cache cancellation connected.** The printed Karama cancellation now reaches the restore-one-ordinary-lot policy selected on 7 October. First/last diff --git a/docs/IMPLEMENTATION_STATUS.md b/docs/IMPLEMENTATION_STATUS.md index a4833913..19e2b88b 100644 --- a/docs/IMPLEMENTATION_STATUS.md +++ b/docs/IMPLEMENTATION_STATUS.md @@ -1,5 +1,21 @@ # Dune implementation status +## 9 October 2026 — provisional Kulon/Ornithopter coexistence + +Active Kulon no longer blocks either mode of the Richese movement card. +The labeled prototype keeps the fixed three-territory cap and applies +Kulon to both normal-range groups. The source distinction and remaining +Hajr/advisor boundaries are in the [movement contract](RICHESE_ORNITHOPTER_ENGINE_AUDIT.md#provisional-kulon-coexistence--9-october-2026). + +`check:quick` passes; 39 affected engine, bot and discard-continuation cases +pass. A throwaway runtime script plays actual Kulon, moves with the card, +rejects four territories, finishes both extended normal groups after JSON, +and executes fixed-card candidates from all four profiles in both modes. +An isolated Chromium surface mounts the production control and commits +the three-territory move through the real engine, leaving three forces in +Red Chasm and two in Imperial Basin with Kulon/Ornithopter in discard. +This is not full-table/deployed acceptance. QA scaffolds were removed. + ## 9 October 2026 — gated captures resumed under the Basic occupation and odd-force Occupy prototypes The 14 captures that remained gated on 7 October were resumed in place with diff --git a/docs/RICHESE_ORNITHOPTER_ENGINE_AUDIT.md b/docs/RICHESE_ORNITHOPTER_ENGINE_AUDIT.md index 8df28c6d..7bcd3388 100644 --- a/docs/RICHESE_ORNITHOPTER_ENGINE_AUDIT.md +++ b/docs/RICHESE_ORNITHOPTER_ENGINE_AUDIT.md @@ -2,6 +2,31 @@ Audit: 2026-09-06. Document-only source and engine review. The canonical component is `richese-ornithopter`; this is distinct from ordinary Arrakeen/Carthag ornithopter access and the later Ecaz discovery token. No runtime effect or complete expansion compliance is claimed here. +## Provisional Kulon coexistence — 9 October 2026 + +Both modes now remain usable after Kulon. The one-group card move retains +its explicit maximum of three territories; Kulon extends the normal range +of each distinct group in the two-group mode. This applies in Basic and +Advanced without changing movement quotas, cohort identity, storm/entry +rules or physical card disposal. The owned control labels the policy. + +This is a content-first composition choice, **not a publisher ruling**. +The photographed card supplies “up to three”; R2 p7 supplies Kulon's +extra territory. The authorized supplied Advanced PDF repeats “one extra +territory during your Force Movement” on physical p29, but does not resolve +the separately named Richese card's maximum. Choosing the explicit card +cap avoids inventing a four-territory exception. Hajr/prior-move and +Advanced advisor combinations remain guarded. + +Changed-path proof: real Kulon activation followed by a three-territory +card move, four-territory rejection, both normal groups moving two +territories, and legal candidates from all four profiles in both modes. +An isolated Chromium surface mounted the production movement component +against the real engine: the enabled control moved two forces from Red +Chasm to Imperial Basin and discarded Ornithopter once. It used in-memory +QA state, not a saved room or deployed application. + + ## Authority The photographed GF9 card face was independently inspected at `/tmp/dune-rules/choam-ornithopter-reading.png`, derived from the publisher components shown in [the product gallery](https://www.tabletopfinder.eu/en/boardgame/32692/dune-choam-richese). Full photo provenance and limits are recorded in [RICHESE_ACQUISITION_RULES.md](RICHESE_ACQUISITION_RULES.md) and [RICHESE_COMPONENTS.md](RICHESE_COMPONENTS.md). It is a Special / Movement card. During the holder's movement, it offers either one group moving up to three territories or two different groups using normal movement. It is discarded after use. No spice payment, faction restriction, alliance transfer of the effect, reserve shipment or Karama cancellation is printed. @@ -27,7 +52,7 @@ The card changes movement allowance, not geography. Keep storm paths, stronghold Genuine source interactions not settled by retrieved card-specific authority: 1. **Hajr and prior moves:** does the two-group option replace a normal movement action, increase the turn total to two, or compose into three/four moves with Hajr? Can an earlier completed move be counted retroactively as the first group? Existing Hajr implementation alone is not evidence. A bounded initial adapter may require an unused ordinary move and exclude concurrent Hajr with an explicit unfinished-combination reason, but cannot label that an official prohibition. -2. **Kulon:** its additive territory text and this card's explicit maximum three need a deliberate composition decision. Existing code adds Kulon to ordinary stronghold access. That does not itself prove a fixed-card range becomes four, or how a turn-scoped Kulon interacts with both normal-mode groups. No retrieved specific answer settles it. +2. **Kulon:** no retrieved card-specific answer settles its additive text against the fixed maximum. The provisional coexistence policy above now preserves the three-territory cap and extends both normal-range groups; this no longer blocks declaration. 3. **Advisors:** the printed general prohibition on using ornithopters must not be ignored, but whether the two-normal-group card option is also covered is not expressly resolved. Keep this named boundary rather than silently treating the advisor path flag as permission. The following are primarily implementation work, not missing rulings. “Different groups” does not say “different territories”: distinct subsets may share an origin, but no physical unit can be counted in both. A distinct-origin-only shortcut would be an incomplete implementation, not a rule. The same accounting prevents the first group's arrivals from being moved again after merging with another stack. Base rules check stronghold access at the start of a force move, so normal-mode range should be recomputed for each accepted group; freezing both at declaration is not required. The player may end optional movement without a separate acknowledgement; after any actual card use, closing that use discards the card once. An uncommitted illegal route spends no movement. Interruption handling must preserve completed movements and remaining distinct-group custody rather than inventing a rollback. Exact provenance through complicated simultaneous force-removal effects can remain a named implementation limitation without asking for a new rule. diff --git a/docs/RULE_DECISIONS.md b/docs/RULE_DECISIONS.md index 740929e6..2fbae713 100644 --- a/docs/RULE_DECISIONS.md +++ b/docs/RULE_DECISIONS.md @@ -19,6 +19,7 @@ They supersede earlier software-absence claims only within their named scope. | Guild-recipient allied No-Field | Rounded one-force Guild fee: one spice to bank, even when Richese funds it; no Guild self-shipment income. | [Allied No-Field payment](RICHESE_ALLIED_SHIPMENT_AUDIT.md) | | Already-active Karama / allied No-Field | Honor the already-played rounded one-force Guild rate and bank routing; no implicit additional card or shipment. | [Allied No-Field payment](RICHESE_ALLIED_SHIPMENT_AUDIT.md) | | Full-hand Nullentropy Box | Basic and Advanced permit an atomic retrieval/Box-disposal exchange with final hand within its actual limit; Box remains last/on top. Intermediate capacity is not adjudicated. | [Box runtime](NULLENTROPY_BOX_ENGINE_AUDIT.md) | +| Kulon with Richese Ornithopter | Both modes coexist in Basic/Advanced. The fixed one-group card maximum remains three; Kulon extends each normal-range group. Explicitly provisional, not a publisher ruling. | [Movement policy](RICHESE_ORNITHOPTER_ENGINE_AUDIT.md#provisional-kulon-coexistence--9-october-2026) | | Basic occupied Homeworld lifecycle | Current sole foreign garrison controls occupied income, Bidding effects, defenses, percentage shares and the Ecaz-victory holding; the entitlement ends immediately on departure, contest or native repopulation. A recorded source observation is still required. Basic Tupile slot leasing keeps the conservative original-epoch gate. Visibly provisional, not a publisher or numerical user ruling. | [Occupation](HOMEWORLD_OCCUPATION_RULES.md#basic-current-sole-benefit-policy--9-october-2026) | No publication or faction/mode gate is opened. The exact 7 October user diff --git a/game/engine.ts b/game/engine.ts index 30b01b23..03977f83 100644 --- a/game/engine.ts +++ b/game/engine.ts @@ -13856,7 +13856,6 @@ function emperorHomeworldMoveEvent(g: Game, p: Player) { function ornithopterBlock( g: Game, p: Player, - mode: OrnithopterMode, ): string | null { if (g.status !== 'playing' || g.phase !== 5 || g.active !== p.id) return 'Use Ornithopter during your own Shipment and Movement turn.'; @@ -13867,13 +13866,8 @@ function ornithopterBlock( if (currentGuildCunning(g,p.id)) return 'Guild Cunning combined with the physical Ornithopter card awaits its timing ruling.'; if (p.moved || g.hajr.includes(p.id)) return 'Ornithopter combined with prior moves or Hajr awaits its timing ruling.'; - if ( - mode === 'range3' && - p.faction === 'choam' && - g.choamMovement?.turn === g.turn && - g.choamMovement.bonus - ) - return 'Kulon combined with the fixed three-territory card range awaits a ruling.'; + // Provisional composition: Kulon augments normal movement, not the + // Ornithopter card's explicit maximum of three territories. return null; } function discoveryFlightIntegrity(g: Game) { @@ -13981,7 +13975,7 @@ function ornithopterView(g: Game, p: Player) { if (!card) return null; const modes = (['range3', 'twoGroups'] as const).map((mode) => ({ mode, - blocked: ornithopterBlock(g, p, mode) ?? transferCardBlock(g, p, card), + blocked: ornithopterBlock(g, p) ?? transferCardBlock(g, p, card), })); return { card, @@ -26424,7 +26418,6 @@ function choamPowerPlays(g: Game, p: Player) { else if (g.decision && !(g.decision.player === p.id && ['choamStorm','choamFreeRevival','choamMovement','choamMentat'].includes(g.decision.kind))) blocked = 'Finish the current decision before declaring this effect.'; else if (effect === 'kulon' && (g.active !== p.id || p.moved >= movesAllowed(g,p))) blocked = 'Use Kulon before an available move on your own turn.'; - else if (effect === 'kulon' && g.ornithopter?.mode === 'range3' && g.ornithopter.player === p.id) blocked = 'Kulon combined with fixed Ornithopter range awaits a ruling.'; else if (effect === 'jubba' && !(g.decision?.kind === 'choamStorm' && g.decision.player === p.id)) blocked = 'Use Jubba Cloak in your moving-storm response.'; return {source:'nexus' as const,card,effect,event:JSON.stringify(['nexusChoam',g.turn,g.phase,p.id,card.id,effect]),blocked}; }))]; @@ -26499,10 +26492,6 @@ function playChoamWorthless(g: Game, p: Player, action: Action) { let elite: number | undefined; let noFieldEvent: string | undefined; if (effectName === 'Kulon') { - requireRule( - g.ornithopter?.mode !== 'range3' || g.ornithopter.player !== p.id, - 'Kulon combined with fixed Ornithopter range awaits a ruling.', - ); requireRule( g.phase === 5 && g.active === p.id && p.moved < movesAllowed(g, p), 'Use Kulon on your movement turn before an available move.', @@ -31270,7 +31259,7 @@ function applyActionInner( mode === 'range3' || mode === 'twoGroups', 'Choose an Ornithopter movement mode.', ); - const blocked = ornithopterBlock(g, p, mode); + const blocked = ornithopterBlock(g, p); requireRule(!blocked, blocked ?? 'Ornithopter cannot be played.'); const card = p.hand.find((c) => c.id === action.movementCard); requireRule( diff --git a/game/reference.ts b/game/reference.ts index ec7251ea..87e49506 100644 --- a/game/reference.ts +++ b/game/reference.ts @@ -887,7 +887,7 @@ export const RULE_TOPICS: RuleTopic[] = [ 'A concealed No-Field may move as a group member without exposing its value. A marker moved in the first group cannot also move in the second. Revealing an unmoved marker converts its remaining entitlement to its actual placed forces; revealing a moved marker does not give those forces another move.', 'A validated declaration moves the physical card from your hand to the public played-card area. It remains there through genuine CHOAM, Ixian or other arrival decisions and is discarded once when the chosen use ends. Random hand effects act on cards still in hand. Ending your movement closes any remaining card movement without undoing a completed group.', 'The saved event binds retries and the remaining original-group data. Refresh and reconnect preserve that event. After disposal, recovery preserves the completed forces and marker event before opening arrival decisions or advancing an early-ended turn; it does not move the group again. Missing saved group data cannot be reconstructed from merged forces and blocks the damaged continuation instead of allowing reuse.', - 'Hajr and prior-move composition, Kulon against the fixed three-territory maximum, and advanced advisor use remain explicit unresolved combinations. These development guards are not printed prohibitions. Existing unfinished entry and optional-module interactions remain unfinished; full Richese starts are still disabled.', + 'Kulon and the fixed-range card now coexist under a provisional policy: the one-group move stays capped at three territories, while each normal-range group receives Kulon’s bonus. Hajr/prior-move composition and advanced advisor use remain guarded; these development guards are not printed prohibitions. Other unfinished entry and optional-module interactions remain unfinished; full Richese starts are still disabled.', 'All four AI profiles use their own projected movement choices and original-group limits. They can use a longer route, split distinct groups, finish interrupted movement and end optional movement when no legal second group remains. Complete Richese-game calibration is not yet certified.', ], related: [ @@ -901,7 +901,7 @@ export const RULE_TOPICS: RuleTopic[] = [ area: 'Implementation', status: 'Partial', detail: - 'Both movement modes, played-card custody, exact event continuation, typed original-group quotas, concealed-marker conversion and separate disposal recovery for completed moves or early endings are integrated. Three named source combinations and existing expansion interactions remain unresolved.', + 'Both movement modes, played-card custody, exact event continuation, typed original-group quotas, concealed-marker conversion and separate disposal recovery are integrated. Kulon coexistence uses a labeled provisional three-territory cap; Hajr/prior moves and advisor composition remain guarded.', }, { area: 'Player controls', @@ -1159,7 +1159,7 @@ export const RULE_TOPICS: RuleTopic[] = [ : definition.card.effect === 'nullentropyBox' ? 'Use the Nullentropy Box panel to pay two bank spice for a private discard search. Full-hand activation and provisional Guild refund claims remain guarded pending resolution.' : definition.card.effect === 'ornithopter' - ? 'Use the Ornithopter movement controls with a selected group and destination. Hajr/prior-move composition, fixed-range Kulon and advanced advisors remain explicitly unresolved.' + ? 'Use the Ornithopter movement controls with a selected group and destination. Provisionally, Kulon coexists without raising the one-group maximum of three; both normal-range groups retain its bonus. Hajr/prior-move composition and advanced advisors remain guarded.' : definition.card.effect === 'residualPoison' ? 'Use the Residual Poison panel before either combatant commits a leader. A shared preparation step provides an opportunity regardless of hand contents; advanced Harkonnen tables remain guarded pending the secret-captive ruling. In the explicit Semuta preview, a clean death with no readiness or inspection commitments pauses after public disposal for the neutral offer, then resumes the same preparation.' : definition.card.effect === 'stoneBurner' @@ -3476,7 +3476,7 @@ export const RULE_TOPICS: RuleTopic[] = [ 'Six printed effects cover range, revival, movement, storm protection, force return and Kull Wahad’s opt-in Karama reaction.', steps: [ 'These CHOAM abilities work in basic and advanced games. Choose a printed Worthless card from the phase controls. Its name and intended effect are announced, then a Karama response occurs before discard. A canceled card remains in hand and cannot be retried for its effect during that phase.', - 'On your own Shipment and Movement turn, play Kulon while a movement remains. It adds one territory to your movement range, including ornithopters. It does not add another movement action, bypass the storm or relax alliance/stronghold entry rules. The current implementation applies the bonus to both movements if Hajr is used; this combination remains under audit.', + 'On your own Shipment and Movement turn, play Kulon while a movement remains. It adds one territory to normal movement, including ordinary stronghold ornithopter access. Under the provisional Richese card policy, it does not raise the fixed one-group maximum of three, but extends both normal-range groups. It does not add a movement action or bypass storm/alliance/stronghold rules. Applying the bonus to both Hajr movements remains under audit.', 'During Revival, play La La La and select a player to prevent its free force revivals for the rest of the phase. Earlier revivals are unchanged. The prohibition also prevents a Fremen allied free allowance from providing free returns, even if granted later.', '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.', diff --git a/tests/ornithopter-engine.test.ts b/tests/ornithopter-engine.test.ts index feb4d661..cc6cd205 100644 --- a/tests/ornithopter-engine.test.ts +++ b/tests/ornithopter-engine.test.ts @@ -67,18 +67,35 @@ void test('played card escrow, owner-only remaining quotas and ended movement su assert.equal(ended.active,'q'); }); void test('invalid source, blocked route, committed card and explicitly unresolved combinations are immutable',()=>{ - for(const change of ['range','storm','prior','hajr','advisor','kulon','gift','wrongMode'] as const){ - const g=fixture(change==='advisor'?'beneGesserit':change==='kulon'?'choam':'emperor',change==='advisor'); + for(const change of ['range','storm','prior','hajr','advisor','gift','wrongMode'] as const){ + const g=fixture(change==='advisor'?'beneGesserit':'emperor',change==='advisor'); let key=destination(g,origin,3);const extra:Partial={movementCard:card,ornithopter:'range3'}; if(change==='range')key=destination(g,origin,4);if(change==='storm')g.storm=splitLocation(key).sector; if(change==='prior')g.players[0].moved=1;if(change==='hajr')g.hajr=['p']; if(change==='advisor'){g.players[0].advisors={red_chasm:{}};g.players[1].forces={[origin]:1};} - if(change==='kulon')g.choamMovement={turn:g.turn,bonus:1}; if(change==='wrongMode')extra.ornithopter='both'; if(change==='gift')g.pendingRicheseGift={event:'reserved',intent:{owner:'p',recipient:'q',cardId:card},turn:g.turn,phase:g.phase,resume:{response:null,decision:null,pendingKarama:null,phaseOpening:null}} as Game['pendingRicheseGift']; const before=structuredClone(g);assert.throws(()=>move(g,key,extra),change);assert.deepEqual(g,before); } }); +void test('Kulon coexists with a capped card move and extends both normal-range groups',()=>{ + for(const advanced of [false,true]){ + const g=fixture('choam',advanced);g.choamMovement={turn:g.turn,bonus:1}; + const key=destination(g,origin,3); + const done=move(g,key,{movementCard:card,ornithopter:'range3'}); + assert.equal(done.players[0].forces[key],2); + assert.equal(done.discard.filter(c=>c.id===card).length,1); + const before=structuredClone(g); + assert.throws(()=>move(g,destination(g,origin,4),{movementCard:card,ornithopter:'range3'})); + assert.deepEqual(g,before); + const normal=destination(g,origin,2); + let two=move(g,normal,{movementCard:card,ornithopter:'twoGroups'}); + two=move(reload(two),normal,{amount:3,ornithopterEvent:two.ornithopter!.event}); + assert.equal(two.players[0].forces[normal],5); + assert.equal(two.players[0].moved,2); + assert.equal(two.discard.filter(c=>c.id===card).length,1); + } +}); void test('Ixian fixed card range does not open a faction-speed cancellation but two-group normal range does',()=>{ const g=fixture('ixians');g.players[0].elites!.forces={[origin]:2};g.players[0].elites!.reserves-=2; const karama=g.deck.find(c=>c.effect==='karama')!;g.deck=g.deck.filter(c=>c.id!==karama.id);g.players[1].hand=[karama];