diff --git a/components/game-table.tsx b/components/game-table.tsx index 757d0fd7..4d381882 100644 --- a/components/game-table.tsx +++ b/components/game-table.tsx @@ -4695,6 +4695,13 @@ export function GameTable({ {' '}Tied battle: {g.players.find(p => p.id === g.battle!.tieWinner)?.name} {g.battle.tieWinner !== g.battle.aggressor ? ' (Habbanya Stronghold advantage)' : ''}.

+ {!g.battle.revealed && g.battle.leaderless.length > 0 && ( +

+ {g.battle.leaderless.map(id => g.players.find(p => p.id === id)?.name).join(', ')} + {' '}announced a Battle Plan without a leader or Cheap Hero. + No battle cards can be played; other plan choices remain sealed. +

+ )} {!!g.battle.native && (

Only the native faction may reveal Traitors or Face diff --git a/docs/CURRENT_STATUS.md b/docs/CURRENT_STATUS.md index d591d645..29bfd494 100644 --- a/docs/CURRENT_STATUS.md +++ b/docs/CURRENT_STATUS.md @@ -16,6 +16,25 @@ open mode gates. ## Current checkpoint and work +The authorized Advanced **leaderless Battle Plan announcement** is now +connected. Physical PDF page 11 requires disclosure before reveal when no +leader or Cheap Hero can be used; existing leader/card legality was already +enforced. Sealing that legal plan now records one public announcement and +shows a shared table notice while dial, support and other choices remain sealed. +No extra decision, cost, confirmation or serialized duplicate state is added; +Basic behavior is intentionally unchanged by this Advanced cutover. +Failing-before/passing-after privacy/refresh/duplicate regression passes. +Types/lint, **6,316 offline tests**, the **1,025-case selected union**, final +guide **10/10** and build pass. Actual native Advanced preview setup followed +by a conserved staged exhausted-leader battle proves public-before-reveal, +private other choices, JSON continuation and unchanged physical stock. +Backed-up real room `3Y5U53RW` v9 exercised human phone sealing, opponent +refresh, original reveal and native Collection continuation once. Saved proof +retains all thirty-three cards, typed forces, original identities and one +native stronghold income, with no announcement cost. The actual 390px notice +is 354px wide; all **57 original seats in nineteen QA rooms** reauthenticate. +This source-bound behavior is not a complete Advanced-game or deployed claim. + Ixian Nexus Betrayal now connects both actual native attempts: Bidding's extra-card inspection and Advanced Technology. Fresh local `ixian-betrayal` uses native Ix/allowed classic or Tleilaxu, the physical Ix deck and Nexus only. diff --git a/docs/IMPLEMENTATION_STATUS.md b/docs/IMPLEMENTATION_STATUS.md index 75fb00d6..847b4599 100644 --- a/docs/IMPLEMENTATION_STATUS.md +++ b/docs/IMPLEMENTATION_STATUS.md @@ -1,5 +1,46 @@ # Dune implementation status +## 1 October 2026 — Advanced leaderless declaration before reveal + +The supplied authorized Advanced PDF, physical page 11 **Leaders**, requires +announcement before reveal when a player cannot use a leader and is not playing +Cheap Hero; that plan may include no Treachery Cards. Current validation already +enforced leader availability and empty card slots. Its accepted plan lacked the +public announcement. Parent rejected a historical scout claim that live +Homeworld combat was disconnected: the later runtime checkpoint already connects +that path. No duplicate Homeworld implementation was introduced. + +Advanced sealing now appends the public announcement before ordinary reveal or +the original special full-plan read continuation. The public view derives only +leaderless submitted player IDs from the existing sealed plans; no second saved +flag or authority is created. A shared pre-reveal table notice names the declared +combatant while keeping dial, support and other choices private. The original +plan guard prevents duplicate submissions and announcements. Basic behavior +remains unchanged by this Advanced-source cutover. Existing legal bots, Voice, +Prescience, full-plan inspections, combat and casualty continuations are retained. + +The meaningful privacy regression fails before the implementation with missing +public announcement after an actual legal seal; it passes afterward, retaining +opponent/observer masking, JSON refresh, read immutability, duplicate rejection +and original reveal. Parent types/lint and **6,316/6,316** offline cases pass; +the selected ninety-file union is **1,025/1,025**. Final reference **10/10** +and build pass. + +Actual native normal Advanced-preview start/setup plus a conserved staged battle +with previously used living leaders exercises legal rejection, one pre-reveal +declaration, JSON normalization, duplicate rejection and canonical thirty-three +card/typed-force conservation. The stage is not a claimed natural full game. +Fresh exact-version private-backed-up room `3Y5U53RW` starts through the real +HTTP preview action, keeps its actual setup/dealt cards/identities, then exercises +390px human Emperor leaderless sealing and the opponent's refresh. The other +plan stays private until human Atreides sealing; original Traitor declines +advance to Collection v9 once. Read-only saved proof confirms one chronicle +announcement before reveal, all cards/typed forces, unchanged identities and +the original single native Arrakeen income, not a fictitious unchanged setup +wallet. The ready phone notice is 354px wide within a 390px page. The actual +guide explains this timing; all **57 original seats in nineteen QA rooms** +reauthenticate. No reset, public-mode change or deployment is performed. + ## 1 October 2026 — Ixian Nexus Betrayal at original native attempts Fresh local `ixian-betrayal` integrates native Ixian Bidding inspection/draw in diff --git a/docs/RULE_DECISIONS.md b/docs/RULE_DECISIONS.md index 14b14464..41f53030 100644 --- a/docs/RULE_DECISIONS.md +++ b/docs/RULE_DECISIONS.md @@ -50,6 +50,17 @@ opt-in profile or a publisher ruling: coexistence remain exceptions. Homeworlds are not Dune territories, and their native allied-entry prohibitions are unchanged. +The supplied revision's **Leaders, physical PDF page 11** also requires a +player unable to use a leader and not playing Cheap Hero to announce that fact +before revealing plans; no Treachery Cards can be part of that plan. Existing +leader/card legality remains unchanged. Advanced now publishes the legal +leaderless declaration when the original plan is sealed, before any reveal +or special full-plan read continuation, without exposing dial/support or +other private choices. The current battle status and chronicle preserve it +across refresh; no extra reaction, confirmation or plan submission is added. +Basic announcement behavior remains intentionally unchanged by this Advanced +source cutover. + The page 15 footnote explicitly identifies the changed timing and advisor coexistence as departures from the original rules and November 2020 FAQ. Page 40 explains that choice; it is the supplied revision's unofficial diff --git a/game/engine.ts b/game/engine.ts index 23069dbd..007d6b19 100644 --- a/game/engine.ts +++ b/game/engine.ts @@ -29521,6 +29521,8 @@ function applyActionInner( 'Wait for the plan requested by special prescience.', ); b.plans[id] = validatePlan(g, p, action); + if (g.advanced && b.plans[id].leader === null) + log(g, `${p.name} announced a Battle Plan without a leader or Cheap Hero; no battle cards can be played.`); if (b.fullPlan?.target === id) { recordFullPlanInspection(g, b.fullPlan.owner); return g; @@ -30880,6 +30882,9 @@ export function viewGame(state: Game, id: string) { }) : [], submitted: Object.keys(b.plans), + leaderless: g.advanced + ? Object.keys(b.plans).filter(playerId => b.plans[playerId].leader === null) + : [], plans: b.revealed ? b.plans : b.plans[id] diff --git a/game/reference.ts b/game/reference.ts index f75dad57..eedad958 100644 --- a/game/reference.ts +++ b/game/reference.ts @@ -2299,7 +2299,7 @@ export const RULE_TOPICS: RuleTopic[] = [ summary: 'Build a legal combination and decide what to keep after winning.', steps: [ 'Choose one weapon, one defense, both, or neither. A worthless card can occupy a weapon or defense slot.', - 'Choose an available leader or Cheap Hero when required. Without either, battle cards cannot be played.', + 'Choose an available leader or Cheap Hero when required. Without either, battle cards cannot be played. In Advanced, sealing a legal leaderless plan announces that fact to the whole table before reveal; its dial, spice and other choices remain sealed. Refresh retains the same announcement without submitting another plan.', 'A loser normally discards all played cards. If allied with Moritani and the battle has a winner, the defeated ally may retain one played card that a winner could keep. A winner may discard selected played cards; Cheap Heroes are discarded after use.', ], related: ['battle', 'card-karama', 'moritani-ally-retention'], diff --git a/tests/battle-preparation-paths.test.ts b/tests/battle-preparation-paths.test.ts index 3bd56a63..a13e626f 100644 --- a/tests/battle-preparation-paths.test.ts +++ b/tests/battle-preparation-paths.test.ts @@ -99,6 +99,30 @@ function allow(state: Game) { function seal(g: Game, plan: Plan, id = 'e') { return applyAction(g, id, { type: 'battlePlan', ...plan }); } + +void test('Advanced leaderless announcement is public before reveal without exposing sealed choices', () => { + let game = fixture(true); + for (const leader of game.players[0].leaders) leader.usedAt = 'carthag'; + game = seal(game, { dial: 1, support: 0, leader: null, weapon: null, defense: null }, 'a'); + assert.equal(game.battle!.revealed, false); + for (const observer of ['e', 'b']) { + const view = viewGame(game, observer); + assert.deepEqual(view.battle!.leaderless, ['a']); + assert.deepEqual(view.battle!.plans, {}); + assert.deepEqual(view.battle!.cards, []); + } + assert.equal(viewGame(game, 'a').battle!.plans.a.dial, 1); + const saved = JSON.stringify(game); + game = JSON.parse(saved); + assert.deepEqual(viewGame(game, 'b').battle!.leaderless, ['a']); + assert.equal(JSON.stringify(game), saved); + assert.throws(() => seal(game, { dial: 1, support: 0, leader: null, weapon: null, defense: null }, 'a'), RuleError); + assert.equal(JSON.stringify(game), saved); + game = seal(game, { dial: 1, support: 0, leader: game.players[1].leaders[1].id, weapon: null, defense: null }); + assert.equal(game.battle!.revealed, true); + assert.deepEqual(viewGame(game, 'b').battle!.leaderless, ['a']); + assert.equal(viewGame(game, 'b').battle!.plans.a.dial, 1); +}); const leaderClaim: PlanClaim = { kind: 'leader', leader: 'emperor-0' }; const supportClaim: PlanClaim = { kind: 'support', compare: 'gte', value: 1 }; const liveCards = (g: Game) =>