diff --git a/docs/CURRENT_STATUS.md b/docs/CURRENT_STATUS.md index 9a805b76..84a08b9f 100644 --- a/docs/CURRENT_STATUS.md +++ b/docs/CURRENT_STATUS.md @@ -25,6 +25,19 @@ authorization and deployment/publication safeguards remain intact. Use the [bounded context index](DEVELOPMENT.md#context-index) and [feedback/proof schedule](DEVELOPMENT.md#feedback-and-proof-schedule). +**Authorized Advanced Moritani forfeiture:** the supplied PDF physical35 resolves +the old duration question: a normal own-Traitor-Card reveal forfeits Assassinate +Leaders for the rest of the game, across turns and opponents. The persisted +normal-call flag already enforced that scope; guidance, public history and the +log now name the sourced forfeiture. Basic, exceptional leader custody and the +private-step UX preview gates are unchanged. A permanent engine regression +exercises a genuine turn advance plus a real later loss with no opportunity and +the owner-facing forfeiture reason. See +[the cutover](MORITANI_ASSASSINATE_LEADERS.md#authority-and-advanced-duration-cutover) +and [authorized source](RULE_DECISIONS.md#authorized-source-amendment--1-october-2026). +Final types/lint, coherent assassination/guide38/38, build and390px Prototyped +guide pass; four changed-region local links/anchors are valid. + **Independent cards + Advanced Occupy — bounded Development Verified:** fresh explicit CLI `--ecaz-treachery` adds all three physical cards36/50/38/50. Own Ecaz Harass returns only its floor count while original mandatory ceil diff --git a/docs/IMPLEMENTATION_STATUS.md b/docs/IMPLEMENTATION_STATUS.md index ca738e7b..7c9f2809 100644 --- a/docs/IMPLEMENTATION_STATUS.md +++ b/docs/IMPLEMENTATION_STATUS.md @@ -1,5 +1,32 @@ # Dune implementation status +## 2 October 2026 — authorized Advanced Moritani normal-reveal forfeiture + +The user-authorized supplied revision, physical p35 Assassinate Leaders D, +resolves the previously open duration: Moritani must not have revealed its +Traitor Card normally **this game**. A normal own-card reveal therefore +forfeits the ability for the rest of the game, across later turns and all +opposing factions, regardless of that battle's result. + +The persisted `normalTraitorCall` flag already enforced exactly that scope, so +no state-machine change was needed. Owner-facing blocked text, the public +reveal log and internal guidance now name the sourced forfeiture instead of an +unresolved-duration guard. The change is source alignment, not a newly located +GF9 erratum or a Basic ruling; exceptional leader custody and the independent +private-step UX/public-preview gates are unchanged. + +An exact normal-reveal case passes. A new permanent engine regression advances a +genuine turn, swaps to a still-living opposing faction and stages a real later +loss: the flag persists, no assassination opportunity or decision appears and +the owner sees the game-long forfeiture reason. Throwaway native smoke confirmed +the same real-reveal and turn-2 persistence. No database reset, new assurance +campaign or preview-gate change. + +Final `check:quick` types/lint, coherent assassination/guide38/38, build and +390px Partial / Development: Prototyped guide pass; four changed-region local +links/anchors are valid. The throwaway driver is removed and the verifier +worker is closed. + ## 2 October 2026 — independent Treachery cards with Advanced Occupy Existing `ecaz-occupy` now accepts explicit fresh CLI `--ecaz-treachery`, diff --git a/docs/MORITANI_ASSASSINATE_LEADERS.md b/docs/MORITANI_ASSASSINATE_LEADERS.md index 4f475d30..db7bde24 100644 --- a/docs/MORITANI_ASSASSINATE_LEADERS.md +++ b/docs/MORITANI_ASSASSINATE_LEADERS.md @@ -4,12 +4,13 @@ preview. Normal game-start and publication gates remain closed. This is the battle-loss ability, separate from the random Assassination Terror token. -## Authority and unresolved interpretation +## Authority and Advanced duration cutover The [GF9 Ecaz & Moritani rulebook](https://www.gf9games.com/dune/wp-content/uploads/EcazMoritani-Rulebook-LOWRES.pdf), pp. 6, 14 and 16, supplies the ability, captured-own-leader answer and Karama table. -Independent source review found no official clarification of the disputed duration -below. The runtime uses the following undisputed sequence: +The historical publisher review found no official duration clarification. +The user-authorized revised Advanced PDF now supplies an explicit game-long +condition, recorded below. The runtime retains the following sequence: 1. Native Advanced Moritani loses a normal battle against a surviving opposing leader disc, with no traitor called. @@ -23,12 +24,19 @@ below. The runtime uses the following undisputed sequence: private physical replacement. Use the advantage once per opposing faction. Karama has no effect on this ability. -The printed statement that a normal traitor reveal loses this advantage does not -specify a battle, faction or game duration. The prototype records the normal call -and explicitly guards further use; it does not label that guard as a settled -permanent forfeiture. The user has been asked whether to keep that interaction -gated or choose a duration. Previously normally revealed cards and exceptional -leader custody also remain outside this first runtime contract. +**Authorized Advanced cutover — 2 October 2026:** the supplied +`UNOFFICIAL_Revised_Dune_Rulebook_v.2.3_web.pdf`, physical p35 / logical p33, +Assassinate Leaders D condition(d), says Moritani must **“not yet [have] +revealed your Traitor Card normally this game.”** A normal own-card reveal +therefore forfeits the advantage for the rest of this game, across later turns +and all opposing factions. The existing persisted normal-call flag already +enforced this scope; owner guidance and public reveal history now describe +the sourced forfeiture rather than an unresolved-duration guard. + +This resolves the old duration question under the authorized Advanced source, +not as a newly located GF9 erratum. Basic, exceptional leader interactions and +the independent private-step UX/public-preview gates remain unchanged. See +[source identity and precedence](RULE_DECISIONS.md#authorized-source-amendment--1-october-2026). The captured-own-leader answer preserves original faction identity: a Moritani leader used by Harkonnen does not become a Harkonnen-faction assassination card. diff --git a/docs/RULE_DECISIONS.md b/docs/RULE_DECISIONS.md index 13979e47..b5a99750 100644 --- a/docs/RULE_DECISIONS.md +++ b/docs/RULE_DECISIONS.md @@ -121,6 +121,21 @@ See [combined-card boundaries](ECAZ_OCCUPY_RULES.md#independent-cards-with-combi Comprehensive recovery/privacy/custody review follows all rules rather than becoming another prototype assurance wave. +## Advanced Moritani normal-reveal duration cutover — 2 October 2026 + +The authorized supplied revision resolves the previously open duration: +**Assassinate Leaders D, physical PDF page 35**, requires that Moritani has +**not yet revealed its Traitor Card normally this game**. A normal own-card +reveal therefore forfeits Assassinate Leaders for the rest of the game, across +later turns and every opposing faction, whether that battle was won or lost. + +This supersedes the historical “normal call, unresolved duration” guard as +source resolution, not a newly located GF9 erratum or a Basic ruling. The +persisted normal-call flag already enforced game scope; owner guidance, public +reveal history and the log now name the sourced forfeiture. Exceptional leader +custody and the independent private-step UX/public-preview gates are unchanged. +See [the cutover and Advanced sequence](MORITANI_ASSASSINATE_LEADERS.md#authority-and-advanced-duration-cutover). + ## Recorded contracts and implementation boundaries | Topic | Decision to preserve | Detailed authority and evidence | diff --git a/game/engine.ts b/game/engine.ts index 716968ce..0cd8f20b 100644 --- a/game/engine.ts +++ b/game/engine.ts @@ -7581,7 +7581,7 @@ function projectedMoritaniAssassinate(g: Game, viewer: string) { const own = viewer === state.owner; const choices = pending && own ? nexusRule(() => moritaniAssassinateChoices(moritaniAssassinateContext(g,pending))) : null; return {owner:state.owner,blocked:own && state.normalTraitorCall - ? 'Assassinate Leaders after a normal traitor call awaits clarification of the printed loss-of-advantage rule.' : null, + ? 'Moritani forfeited Assassinate Leaders for the rest of this game after revealing its Traitor Card normally.' : null, pending:pending ? {event:pending.event,opponent:pending.opponent,territory:pending.territory, cards:choices?.cards ?? [],blocked:choices?.blocked ?? null} : null, history:state.opportunities.filter(r => r.stage === 'revealed' || r.stage === 'replaced').map(r => ({ @@ -20400,7 +20400,7 @@ function resolveBattle(g: Game, retreat?: DiplomatRetreatSelection) { ?.name ?? identity); log(g, `${holder.name} revealed ${name} as a traitor.`); if (holder.id === g.moritaniAssassinate?.owner) - log(g, `${holder.name} called a traitor normally. Further Assassinate Leaders use awaits clarification of the printed loss-of-advantage rule.`, {faction:'moritani',name:'Assassinate Leaders boundary'}); + log(g, `${holder.name} revealed a Traitor Card normally and forfeited Assassinate Leaders for the rest of this game.`, {faction:'moritani',name:'Assassinate Leaders forfeited'}); } const dead = (l: Leader | undefined) => { if (l) { diff --git a/game/moritani-assassinate.ts b/game/moritani-assassinate.ts index a4a312c7..5c46421e 100644 --- a/game/moritani-assassinate.ts +++ b/game/moritani-assassinate.ts @@ -18,6 +18,7 @@ export type MoritaniAssassinateReceipt = { export type MoritaniAssassinateState = { version: 1; owner: string; + /** Authorized Advanced source: normal reveal forfeits this ability for the game. */ normalTraitorCall: boolean; opportunities: MoritaniAssassinateReceipt[]; }; @@ -291,7 +292,7 @@ export function moritaniAssassinateChoices( return stopped('This Assassinate opportunity has already ended.'); if (state.normalTraitorCall) return stopped( - 'Assassinate after an ordinary Moritani traitor call awaits its duration ruling.', + 'Moritani forfeited Assassinate Leaders for the rest of this game after revealing its Traitor Card normally.', ); if ( state.opportunities.some( diff --git a/game/reference.ts b/game/reference.ts index 6fceb01b..6db1e13a 100644 --- a/game/reference.ts +++ b/game/reference.ts @@ -2997,7 +2997,7 @@ export const RULE_TOPICS: RuleTopic[] = [ 'After you lose a battle, this advanced advantage is available only if the opposing leader disc survived and no Traitor was called.', 'You may reveal a Traitor Card for that opposing faction, but it must name a different leader from the one you just fought. If that named leader is not in the Tanks, kill it and collect spice for its value.', 'An already-dead named leader remains a legal reveal, awarding no death or spice. During Mentat Pause, set the revealed card aside face up as a marker and draw a new Traitor Card. You can use this advantage only once against each faction in the game.', - 'The printed rule says a normal traitor reveal loses this advantage, but its duration remains unresolved. The preview guards further assassination after a normal call. Karama has no effect against Assassinate Leaders.', + 'The authorized revised Advanced PDF, physical page35, requires that you have not revealed your Traitor Card normally this game. A normal own-card reveal therefore forfeits Assassinate Leaders for the rest of this game, across turns and opponents. Existing state preserves that forfeiture; Karama has no effect against the ability. This is the authorized source, not a new GF9 erratum.', 'If Harkonnen has captured your own leader and you hold its Traitor Card, you may call that traitor normally; you may not use Assassinate Leaders on that basis.', 'Terror Assassination chooses a random leader when its token is triggered. It does not use this battle-loss condition, different-leader Traitor Card or once-per-faction allowance. Moritani starts remain unavailable in Basic and Advanced.', ], @@ -3005,7 +3005,7 @@ export const RULE_TOPICS: RuleTopic[] = [ {area:'Implementation',status:'Partial',detail:'Explicit Advanced Moritani/base-opponent preview before any normal traitor call; native disc deaths, printed bounty, dead-target zero bounty and exact Mentat replacement.',evidence:['game/moritani-assassinate.ts','game/engine.ts']}, {area:'Player controls',status:'Partial',detail:'Uniform private choice/decline, named target and bounty, public face-up markers and readable inspection; normal activation awaits private-step UX decision.',evidence:['components/moritani-assassinate.tsx','components/game-table.tsx']}, {area:'AI',status:'Partial',detail:'All four profiles choose from the owner-only quote or decline; no hidden rival data or strength-calibration claim.',evidence:['game/bots.ts','tests/moritani-assassinate-engine.test.ts']}, - {area:'Documentation',status:'Partial',detail:'Official authority, dead-target conditional, normal-call ambiguity, exceptional custody and optional-module boundaries remain explicit.',evidence:['docs/MORITANI_ASSASSINATE_LEADERS.md']}, + {area:'Documentation',status:'Partial',detail:'Printed effect and authorized Advanced game-long normal-reveal forfeiture are explicit; exceptional leader custody, private-step UX and optional-module boundaries remain open.',evidence:['docs/MORITANI_ASSASSINATE_LEADERS.md','docs/RULE_DECISIONS.md']}, {area:'Verification',status:'Partial',detail:'Rules, controls, genuine-setup staged battles, private views, authenticated recovery, concurrent writes and physical replacement chains; full faction acceptance remains open.',evidence:['tests/moritani-assassinate-quote.test.ts','tests/moritani-assassinate-engine.test.ts','tests/moritani-assassinate-controls.test.tsx','tests/moritani-assassinate-recovery.test.ts']}, ], related: [ diff --git a/tests/moritani-assassinate-engine.test.ts b/tests/moritani-assassinate-engine.test.ts index 4456f2f8..64bf1dd1 100644 --- a/tests/moritani-assassinate-engine.test.ts +++ b/tests/moritani-assassinate-engine.test.ts @@ -85,17 +85,30 @@ void test('stale, other-seat, same-leader, dishonest mixed and duplicate assassi assassinationRejects(mentat,'m',{type:'decision',event,card:'guild-1'}); }); -void test('a normal Moritani traitor call records an explicit unresolved guard and cannot create an assassination opportunity',()=>{ +void test('a normal Moritani traitor reveal forfeits assassination and cannot create an opportunity',()=>{ const staged=stageAssassinationBattle(assassinationGame(),'guild-0'); const done=resolveAssassinationBattle(staged,{normalCall:true}); assert.equal(done.moritaniAssassinate!.normalTraitorCall,true); assert.equal(done.moritaniAssassinateCallEvents!.length,1); assert.equal(done.moritaniAssassinate!.opportunities.length,0); - assert.match(viewGame(done,'m').moritaniAssassinate!.blocked!,/clarification/); const corrupt=structuredClone(done);corrupt.moritaniAssassinate!.normalTraitorCall=false; assert.throws(()=>viewGame(corrupt,'m'),/original event/); }); +void test('a sourced normal-reveal forfeiture persists across turns and denies a later genuine loss',()=>{ + const first=assassinationToMentat(resolveAssassinationBattle(stageAssassinationBattle(assassinationGame(['m','g','e']),'guild-0'),{normalCall:true})); + assert.equal(first.moritaniAssassinate!.normalTraitorCall,true);assert.equal(first.moritaniAssassinate!.opportunities.length,0); + assert.match(viewGame(first,'m').moritaniAssassinate!.blocked!,/rest of this game/); + first.turn++; + first.players=[first.players[0],first.players[2],first.players[1]]; + const second=resolveAssassinationBattle(stageAssassinationBattle(first)); + assert.equal(second.lastBattleContext!.result,'normal');assert.notEqual(second.lastBattleContext!.winner,'m'); + assert.equal(second.moritaniAssassinate!.normalTraitorCall,true); + assert.equal(second.moritaniAssassinate!.opportunities.length,0); + assert.notEqual(second.decision?.kind,'moritaniAssassinate'); + assert.match(viewGame(second,'m').moritaniAssassinate!.blocked!,/rest of this game/); +}); + void test('saved assassination receipts and cleanup obligations fail closed after corruption',()=>{ const pending=resolveAssassinationBattle(stageAssassinationBattle(assassinationGame())); for(const breakSave of [ diff --git a/tests/moritani-assassinate-quote.test.ts b/tests/moritani-assassinate-quote.test.ts index b084b0bd..77b4590a 100644 --- a/tests/moritani-assassinate-quote.test.ts +++ b/tests/moritani-assassinate-quote.test.ts @@ -187,16 +187,16 @@ void test('already-dead targets remain revealable for no death or spice, while p assert.deepEqual(context, before); }); -void test('ordinary traitor-call ambiguity and already normally revealed cards stay explicit private guards', () => { +void test('the sourced normal-reveal forfeiture and already normally revealed cards stay explicit private guards', () => { const context = fixture(); context.state.normalTraitorCall = true; assert.match( moritaniAssassinateChoices(context).blocked!, - /ordinary.*duration ruling/, + /forfeited Assassinate Leaders for the rest of this game/, ); assert.throws( () => quoteMoritaniAssassinate(context, 'atreides-1'), - /duration ruling/, + /forfeited Assassinate Leaders for the rest of this game/, ); context.state.normalTraitorCall = false; context.normallyRevealed = ['atreides-1'];