diff --git a/docs/COMPONENT_INVENTORY.md b/docs/COMPONENT_INVENTORY.md index d5401d45..28cf5525 100644 --- a/docs/COMPONENT_INVENTORY.md +++ b/docs/COMPONENT_INVENTORY.md @@ -306,7 +306,7 @@ This later 2026-09-06 checkpoint supersedes the preceding statements that Assass | Robbery | One opaque physical token; original source-traced definition | Choose half the entrant's available spice rounded up, or top card; full hands draw before a private chosen discard; empty deck finishes without inventing a card | Owner reveal/choice/discard controls; own card names only; revealed face publicly inspectable | Chooses card or spice using its own private view | `tests/moritani-entry.test.ts` | Combined arrival reactions and broader timing audit; no full-game acceptance | | Sabotage | One token; random discard and optional gift described separately | Random entrant discard, with generic public log; optional own-card gift or decline; exceptional recipient overflow rejected | Owner gift selector; no victim card name in opponent controls; spent face publicly inspectable | Declines the optional gift | `tests/moritani-entry.test.ts` | Exceptional gift overflow and combined reactions remain unsupported | | Assassination | One token; source rule distinguishes it from advanced Assassinate Leaders | Reveal automatically selects one living verified ordinary native leader, updates death history and awards strength in spice; Zoal is 3 | Owner sees eligibility/reason before revealing; no victim picker; spent face uses shared inspector | Uses owner-projected reveal eligibility | `tests/moritani-force-leader.test.ts` | Missing/altered native pools, empty living pool, captured/foreign/special custody blocked; advanced Harkonnen tables uniformly blocked to avoid revealing capture state | -| Sneak Attack | One token; reserve entry, storm/occupancy limits and printed Aftermath exception described | Up to min(5, own reserves), using the recorded entry sector; free, without ordinary shipment/movement usage; zero resolves even when positive entry is blocked | Private maximum/reason at offer and resolution; labeled count selector and explicit Send no forces; shared face inspector | Uses projected maximum when allowed, otherwise zero | `tests/moritani-force-leader.test.ts` | Positive entry blocked for unresolved BG arrival reactions and phase-5 Heighliners interactions; exact sector choice and other combined-entry classifications remain limited | +| Sneak Attack | One token; reserve entry, storm/occupancy limits and printed Aftermath exception described | Up to min(5, own reserves), using the recorded entry sector; free, without ordinary shipment/movement usage; zero resolves even when positive entry is blocked | Private maximum/reason at offer and resolution; labeled count selector and explicit Send no forces; shared face inspector | Uses projected maximum when allowed, otherwise zero | `tests/moritani-force-leader.test.ts` | A positive entry in Shipment and Movement now accrues the Heighliners token once for its owner under an explicitly adopted shipment classification; positive entry remains blocked for unresolved BG arrival reactions, and exact sector choice and other combined-entry classifications remain limited | The common entry record binds the original entrant, territory, sector, cause, turn, phase and continuation. Current isolated paths cover ordinary movement, shipment, Guild cross-transport, accompanying advisors and worm rides; existing Moritani or allied entry does not offer Terror. When the same arrival would already need another response/decision, the engine rejects the complete attempted entry atomically instead of silently selecting a priority. No-Field placement/reveal distinctions remain source guidance until that separate subsystem exists. Worm continuation and temporary Truthtrance overlays have dedicated coverage in `tests/moritani-entry-overlays.test.ts`; they do not establish universal reaction ordering. diff --git a/docs/CURRENT_STATUS.md b/docs/CURRENT_STATUS.md index 677d8548..5f63d27f 100644 --- a/docs/CURRENT_STATUS.md +++ b/docs/CURRENT_STATUS.md @@ -50,6 +50,16 @@ settles, without replaying payment or force transfer. A Terror entry already in flight and every Ambassador combination still reject. Selected decks/cache, shared-Duke/capture, force/payment and pending-effect guards remain. +**In progress — Sneak Attack shipment classification.** A positive Moritani +Sneak Attack in Shipment and Movement now accrues the Heighliners token once +for its owner through the ordinary off-planet-arrival helper, under an +**explicitly adopted** shipment classification rather than a retrieved ruling; +a zero-force resolution accrues nothing, and the token still enforces its +working phase, Guild exception and one accrual per turn. Bene Gesserit +spiritual-advisor accompaniment and Intrusion stay gated because their ordering +inherits the unresolved arrival-timing boundary. See +[MORITANI_ASSASSINATION_SNEAK.md](MORITANI_ASSASSINATION_SNEAK.md#adopted-shipment-classification-7-october-2026). + **In progress — Tleilaxu Ambassador force return.** The last unimplemented Ambassador effect is now wired: a placed Tleilaxu token gives its beneficiary — the Ecaz owner or its ally — a private decision to return one to four physical diff --git a/docs/IMPLEMENTATION_STATUS.md b/docs/IMPLEMENTATION_STATUS.md index b1183327..22827a49 100644 --- a/docs/IMPLEMENTATION_STATUS.md +++ b/docs/IMPLEMENTATION_STATUS.md @@ -1,5 +1,29 @@ # Dune implementation status +## 7 October 2026 — Sneak Attack adopts the shipment classification for Heighliners + +A positive Moritani Sneak Attack in Shipment and Movement now accrues the +Heighliners token once for its owner through the same `techIncome` helper as an +ordinary off-planet arrival. The token still enforces its working phase, its +printed Guild exception and one accrual per turn, and a zero-force resolution +accrues nothing; the previous phase-5 guard is removed rather than duplicated. + +This is an **explicitly adopted implementation interpretation**, not a retrieved +publisher ruling: the printed verb is “Send”, and classifying the expressly +off-planet reserve transfer as a shipment is the supported reading already +recorded in the source note. The Bene Gesserit spiritual-advisor accompaniment +and Intrusion rows remain gated because their ordering inherits the unresolved +arrival-timing boundary, and a Spice Blow worm-ride Sneak Attack stays outside +the token's working phase. + +Evidence: `tests/moritani-force-leader.test.ts` asserts the accrual, the +once-per-turn guard, the unchanged zero-force resolution and the altered-sector +rejection; the former "unsupported technology classification" pin was replaced +by the adopted behaviour rather than re-pinned. +`docs/COMPONENT_INVENTORY.md`, the source note and the pending-interpretation +registry record the reduced boundary. No game, seat, saved state or deployment +changes. + ## 7 October 2026 — Tleilaxu Ambassador force return wired The last unimplemented Ambassador effect is now reachable. A placed Tleilaxu diff --git a/docs/MORITANI_ASSASSINATION_SNEAK.md b/docs/MORITANI_ASSASSINATION_SNEAK.md index 20354c33..311ee024 100644 --- a/docs/MORITANI_ASSASSINATION_SNEAK.md +++ b/docs/MORITANI_ASSASSINATION_SNEAK.md @@ -20,6 +20,31 @@ The proposed core behavior—bounded reserve transfer, no spice charge, no ordin Therefore neither “Sneak Attack definitely bypasses all shipment reactions” nor “a specific FAQ explicitly orders all these reactions” is supported. An implementation may adopt the shipment composition explicitly, or gate affected combinations pending a ruling. It should not accidentally adopt one by calling or omitting a generic shipment helper. +## Adopted shipment classification, 7 October 2026 + +The engine now **adopts the shipment composition explicitly for the Heighliners +token**: a positive Sneak Attack in Shipment and Movement calls the same +`techIncome(g, 'heighliners', entrant)` helper used by ordinary off-planet +arrivals, so the token owner accrues its once-per-turn income and collects it +at the phase boundary. The token itself still enforces its working phase, its +printed Guild exception and one accrual per turn, and a zero-force resolution +calls nothing — the earlier phase-5 guard is removed rather than left as a +duplicate check. + +This is an **explicit implementation interpretation**, not a retrieved +publisher ruling: the printed verb is “Send”, and the classification of that +off-planet reserve transfer as a shipment is the supported reading recorded in +the table above. The Bene Gesserit spiritual-advisor accompaniment and +Intrusion rows remain **gated** (`Sneak Attack combined with Bene Gesserit +arrival reactions is still being implemented`) because their ordering inherits +the unresolved arrival-timing boundary; adopting the Heighliners row does not +adopt them. A Spice Blow worm-ride Sneak Attack stays outside the token's +working phase and accrues nothing. + +Evidence: `tests/moritani-force-leader.test.ts` asserts the accrual, the +once-per-turn guard, the unchanged zero-force resolution and the altered-sector +rejection; `docs/COMPONENT_INVENTORY.md` records the reduced boundary. + ## Assassination pool: safe ordinary support Ordinary live native discs held by the entrant are the straightforward pool. Killing a disc already in the Tanks again, or awarding its strength again, is not supported. Do not award Moritani a player-chosen victim. Zoal's explicit payout must survive any generic strength calculation. diff --git a/docs/RULE_DECISIONS.md b/docs/RULE_DECISIONS.md index dfb53f59..08e01d53 100644 --- a/docs/RULE_DECISIONS.md +++ b/docs/RULE_DECISIONS.md @@ -389,6 +389,7 @@ These are unresolved in the cited records. Do not infer an answer from elapsed t | Occupied Homeworlds | Entitlement/expiry after departure, replacement or contest; unique powers and shared income. The first-occupier/current-occupier question was already sent. | [Occupation questions](HOMEWORLD_OCCUPATION_RULES.md#questions-the-retrieved-sources-do-not-settle), [lifecycle audit](HOMEWORLD_OCCUPATION_LIFECYCLE_AUDIT.md) | | Nexus responses and composition | Betrayal private-response timing, partial revival/payment choices, Emperor fewer-than-five, fresh advisor/storm cases, Duke restoration and remaining faction-specific combinations. Tleilaxu Secret Ally force accounting, leader slot/eligibility and force-only use remain unresolved after requested publisher/designer source searches; do not infer answers from native Tleilaxu revival or Ghola. | [Common boundaries](NEXUS_CARD_RULES.md#remaining-source-questions-and-release-boundaries), [Tleilaxu source search](NEXUS_TLEILAXU_RULES.md#source-search-follow-up) and individual faction source/runtime documents | | Moritani Atomics after alliance change | The publisher's persistent −1 hand-limit text names Moritani and “your ally,” but not whether the penalty follows a later ally, remains on the original ally or affects both. Requested online source searches found no clarification; the alliance-change case remains gated. | [Terror source boundary](MORITANI_TERROR_RULES.md#material-gaps-and-ordinary-compositions) | +| Moritani Sneak Attack entry classification | The printed verb is “Send”; no retrieved ruling classifies the off-planet reserve transfer as a shipment for other arrival powers. The engine now **explicitly adopts** the supported shipment composition for the Heighliners token only — a positive Sneak Attack in Shipment and Movement accrues the token's once-per-turn income for its owner, with a zero-force resolution accruing nothing. Bene Gesserit spiritual-advisor accompaniment and Intrusion stay gated because their ordering inherits the unresolved arrival-timing boundary. This is an implementation interpretation, not a publisher ruling. | [Source boundary and adopted classification](MORITANI_ASSASSINATION_SNEAK.md#adopted-shipment-classification-7-october-2026) | | Orgiz collection | Basic retains the publisher-p13 per-blow ambiguity and provisional one-transfer-per-positive-deposit mapping. Authorized supplied Advanced rulebook physical24 instead says each time Collection occurs in a territory containing spice: Advanced applies one transfer per actual positive territory Collection, only to a unique rival payer. Multiple source keys do not add transfers. Contested ownership, inconsistent/different payers and Ecaz shared lots still withhold only uncertain transfers; ordinary Collection proceeds. No new publisher ruling on those pending cases is claimed. | [Advanced source cutover and Basic boundary](DISCOVERY_PROTOTYPE.md#authorized-advanced-collection-wording) | | Jacurutu physical dial | The user question asks whether the losing player should choose the physical normal/elite forces represented by its dial. When legal Advanced allocations produce different rewards, income remains explicitly unpaid; numeric wheel subtraction is not a ruling. | [Discovery interpretations](DISCOVERY_PROTOTYPE.md#interpretations-retained-for-refinement) | | Testing Station contest/timing | Only sole non-advisor occupation is prototyped. Two-occupant priority/stacking remains unresolved; opening after the ordinary storm-card window is an explicit source-timing inference. Weather Control is excluded by the printed text. | [Discovery interpretations](DISCOVERY_PROTOTYPE.md#interpretations-retained-for-refinement) | diff --git a/game/engine.ts b/game/engine.ts index 547e7023..cba2434a 100644 --- a/game/engine.ts +++ b/game/engine.ts @@ -16951,9 +16951,6 @@ function sneakAttackOptions( ) blocked = 'Sneak Attack combined with Bene Gesserit arrival reactions is still being implemented.'; - if (!blocked && g.phase === 5 && g.techTokens) - blocked = - 'Sneak Attack combined with Heighliners technology is still being implemented.'; return { maximum, blocked }; } function terrorAllianceBlocked( @@ -17191,6 +17188,11 @@ function decideTerror(g: Game, p: Player, action: Action) { p.reserves -= amount; place(p, entry.territory, entry.sector, amount); observeOccupation(g); + // A positive Sneak Attack transfers Moritani's expressly off-planet + // reserves onto the board, so it is classified as an off-planet + // shipment for the Heighliners token. The token itself enforces its + // working phase, its Guild exception and one accrual per turn. + techIncome(g, 'heighliners', p); log( g, `${p.name} sent ${amount} reserves into ${territory(entry.territory).name} through Sneak Attack.`, diff --git a/game/reference.ts b/game/reference.ts index 74550acf..374879f3 100644 --- a/game/reference.ts +++ b/game/reference.ts @@ -2892,7 +2892,7 @@ export const RULE_TOPICS: RuleTopic[] = [ 'Robbery: choose between taking half the entrant’s spice, rounded up, and drawing the top Treachery Card. If the draw exceeds your hand limit, choose a card to discard afterward; a full hand does not remove the draw option.', 'Sabotage: randomly draw and discard a Treachery Card from the entrant if possible. Then you may give that player a card of your choice from your own hand. Giving a card is optional.', 'Sneak Attack: send up to five of your reserve forces into the triggered territory at no cost, respecting storm and occupancy restrictions. This particular entry is allowed even when Atomics Aftermath is present.', - 'Before revealing Sneak Attack, inspect its private available maximum and any reason positive entry is blocked. You may keep it hidden. Revealing spends the token even if you then choose Send no forces. For a supported entry, select zero through the displayed maximum; forces enter the arrival’s territory and sector without consuming your ordinary shipment or movement turn. Zero remains available when positive entry is blocked. Aftermath’s printed exception allows this reserve entry even after Atomics resolves.', + 'Before revealing Sneak Attack, inspect its private available maximum and any reason positive entry is blocked. You may keep it hidden. Revealing spends the token even if you then choose Send no forces. For a supported entry, select zero through the displayed maximum; forces enter the arrival’s territory and sector without consuming your ordinary shipment or movement turn. Zero remains available when positive entry is blocked. Aftermath’s printed exception allows this reserve entry even after Atomics resolves. A positive entry during Shipment and Movement is treated as an off-planet shipment for the Heighliners token, which accrues its once-per-turn income for its owner.', 'The random Assassination token is separate from the advanced Assassinate Leaders advantage. Neither ability supplies the other’s selection or timing rules.', ], related: [ diff --git a/tests/moritani-force-leader.test.ts b/tests/moritani-force-leader.test.ts index c16d0640..6717a5ef 100644 --- a/tests/moritani-force-leader.test.ts +++ b/tests/moritani-force-leader.test.ts @@ -315,16 +315,25 @@ void test('advanced Harkonnen tables uniformly disable Assassination without exp rejected(held, { type: 'decision', reveal: true }); }); -void test('Sneak Attack rejects an altered client sector and unsupported technology classification while allowing zero', () => { +void test('Sneak Attack rejects an altered client sector and accrues Heighliners once for a positive entry while allowing zero', () => { const revealed = decide(offered('sneakAttack'), { reveal: true }); rejected(revealed, { type: 'decision', amount: 1, sector: 9 }); const valid = decide(revealed, { amount: 1, sector: 10 }); assert.equal(player(valid, 'm').forces['arrakeen:10'], 1); const technology = reload(revealed); technology.techTokens = createTechTokens(); - technology.techTokens.heighliners.owner = 'm'; + technology.techTokens.heighliners.owner = 'a'; technology.techTokens.heighliners.spice = 2; - rejected(technology, { type: 'decision', amount: 1 }); + const accrued = decide(technology, { amount: 1, sector: 10 }); + assert.equal(player(accrued, 'm').forces['arrakeen:10'], 1); + assert.equal(accrued.techTokens!.heighliners.triggeredTurn, accrued.turn); + assert.equal(accrued.techTokens!.heighliners.spice, 1); + assert.equal( + accrued.log.some((line) => + line.text.includes('Heighliners accrued 1 spice for Atreides'), + ), + true, + ); const passed = decide(technology, { amount: 0 }); assert.deepEqual(passed.techTokens, technology.techTokens); assert.deepEqual(passed.players, technology.players);