From c3576b2debffcba442afc30673180e4bdab9e2f3 Mon Sep 17 00:00:00 2001 From: Quintine <15965533+Quintine@users.noreply.github.com> Date: Tue, 27 Oct 2026 17:20:39 +1100 Subject: [PATCH] Resolve prior leaders in Advanced battle explosions --- docs/CURRENT_STATUS.md | 16 +++ docs/EFFECTIVE_BATTLE_WEAPONS.md | 27 +++++ docs/IMPLEMENTATION_STATUS.md | 20 ++++ docs/RULE_DECISIONS.md | 10 ++ game/engine.ts | 15 +++ game/reference.ts | 1 + ...battle-territory-explosion-custody.test.ts | 39 +++++++ tests/battle-territory-explosion-edge.test.ts | 73 +++++++++++++ tests/battle-territory-explosion.test.ts | 28 +++++ tests/engine.test.ts | 3 + tests/fixture-battle-territory-explosion.ts | 100 ++++++++++++++++++ 11 files changed, 332 insertions(+) create mode 100644 tests/battle-territory-explosion-custody.test.ts create mode 100644 tests/battle-territory-explosion-edge.test.ts create mode 100644 tests/battle-territory-explosion.test.ts create mode 100644 tests/fixture-battle-territory-explosion.ts diff --git a/docs/CURRENT_STATUS.md b/docs/CURRENT_STATUS.md index 8cd642df..22a99277 100644 --- a/docs/CURRENT_STATUS.md +++ b/docs/CURRENT_STATUS.md @@ -20,6 +20,22 @@ open mode gates. Keep identified Basic-function gaps ahead of Advanced and wider interactions. Prototype labels and final acceptance gates remain. +**Advanced explosion and prior surviving leaders — 27 October. Prototyped / Partial.** +On Arrakis, an actual Lasgun–Shield explosion now also kills still-present +living leader discs left by earlier battles in that territory, without repeating +current leader deaths or awarding bounty. Traitor precedence and captured / +concealed exclusions remain. Basic, Homeworld, shared Duke, Kwisatz and special +custody interpretations are separate. [Source and scope](EFFECTIVE_BATTLE_WEAPONS.md#earlier-leaders-in-an-advanced-explosion--27-october-2026). + +An original Advanced Emperor/Atreides/Guild setup reaches two real battles in +Imperial Basin: both surviving discs from the first battle die in the second +explosion, together with the two newly selected leaders. Separate controlled +leader-history cases cover other territories, prior deaths, Traitor precedence +and concealed captive custody. No browser, deployed or complete-mode claim. +Eight focused files pass 162/162 and type/lint checks pass. The older controlled +Ghola/Revival test now clears its phase-owned ordering frame when explicitly +skipping to Mentat; its elite-limit assertions are unchanged. + **Distrans before Richese special bidding — 27 October. Prototyped / Partial.** An untouched Once Around or Silent cache/Black Market lot or normal Black Market lot provisionally allows the existing transfer before the first bid, pass or sealed submission. Preserve diff --git a/docs/EFFECTIVE_BATTLE_WEAPONS.md b/docs/EFFECTIVE_BATTLE_WEAPONS.md index 1476bf65..8f1724a3 100644 --- a/docs/EFFECTIVE_BATTLE_WEAPONS.md +++ b/docs/EFFECTIVE_BATTLE_WEAPONS.md @@ -1,5 +1,32 @@ # Physical cards and effective battle weapons +## Earlier leaders in an Advanced explosion — 27 October 2026 + +**Prototyped / Partial.** The user-authorized revised rulebook physical p11 / +printed p9 leaves surviving leader discs in their battle territory and makes +them subject to later territory-wide effects, explicitly including a +Lasgun–Shield explosion. In an Advanced Arrakis battle that actually explodes, +living prior-used player leaders at that territory die once alongside the two +current plan leaders, including prior losing leaders. Prior-used native and +foreign-ghola roster discs follow the same location marker; captive or concealed +discs are excluded because their earlier stamp alone does not establish current +physical presence. No bounty is added. Actual Traitor outcomes retain precedence +and do not explode. Existing dead-leader Skill cleanup runs normally. + +The separate shared Duke and Kwisatz marker, Homeworld explosions, Basic source +cutover and exceptional custody interactions remain outside this slice; this +does not certify complete modes or change their existing policy. + +The focused native regression uses an original Advanced Emperor/Atreides/Guild +setup, paid shipments and movement to earn two successive Imperial Basin battles. +The prior winner's and loser's surviving discs are checked before the second +explosion. Additional controlled staged histories cover other territories, +already-dead leaders, current-leader deduplication, mutual Traitors and a +concealed captive. Neither fixture establishes full mode or deployed acceptance. +Eight related files pass 162/162, with type checking and lint. One older +controlled Ghola test clears the phase-owned Revival frame before its direct +Mentat jump; it retains its original elite-limit assertions. + The 7 September foundation now feeds a bounded, playable Mirror Weapon battle path (28 September 2026). The historical focused test counts below predate that integration. ## Contract and integration diff --git a/docs/IMPLEMENTATION_STATUS.md b/docs/IMPLEMENTATION_STATUS.md index c9675866..e87dedac 100644 --- a/docs/IMPLEMENTATION_STATUS.md +++ b/docs/IMPLEMENTATION_STATUS.md @@ -1,5 +1,25 @@ # Dune implementation status +## 27 October 2026 — Advanced explosion and prior leaders + +Use the user-authorized Advanced rulebook physical11/printed9 to include living +prior-used roster leaders still in an Arrakis territory in an actual +Lasgun–Shield explosion. Current plan discs are killed once, Traitors retain +precedence, no bounty is added, and captured/concealed prior discs remain outside +this location inference. Basic, Homeworld, shared Duke, Kwisatz and special +custody boundaries remain open. [Contract](EFFECTIVE_BATTLE_WEAPONS.md#earlier-leaders-in-an-advanced-explosion--27-october-2026). + +The native Advanced Emperor/Atreides/Guild regression reaches two actual +Imperial Basin battles, using paid shipments and ground movement. It selects +unopened starting cards, Spice positions and native scalar lotteries; it does +not install played leaders or armies. Separate controlled staged-history tests +cover noncombatants, other territory, dead leaders, Traitor precedence and a +concealed captive. Complete modes, browser and deployment remain unverified. +Eight selected files pass 162/162 and types/lint pass. A historical controlled +Ghola fixture clears the Revival order at its explicit phase jump; its original +elite-limit assertions remain. Private proof is +`20261027-advanced-explosion-leaders-proof-v2/`. + ## 27 October 2026 — Distrans before Richese special bidding Prototype the existing pre-bid timing interpretation for untouched Once Around diff --git a/docs/RULE_DECISIONS.md b/docs/RULE_DECISIONS.md index deeb1b3f..1b0db7e0 100644 --- a/docs/RULE_DECISIONS.md +++ b/docs/RULE_DECISIONS.md @@ -1,5 +1,15 @@ # Rule decision index +## Earlier leaders in an Advanced Arrakis explosion — 27 October 2026 + +Apply the supplied Advanced rulebook physical11/printed9: surviving leaders +remain in their battle territory and suffer a subsequent territory-wide +Lasgun–Shield explosion. An actual Arrakis explosion kills each still-present +living prior-used roster disc once; Traitors retain precedence and no bounty is +added. Earlier stamps on captured or concealed discs do not prove present +location. Basic, Homeworld, shared Duke, Kwisatz and exceptional custody remain +separate. [Source and scope](EFFECTIVE_BATTLE_WEAPONS.md#earlier-leaders-in-an-advanced-explosion--27-october-2026). + ## Distrans before Richese special bidding — 27 October 2026 Provisionally extend the existing pre-bid interpretation to an untouched Once diff --git a/game/engine.ts b/game/engine.ts index 74e7d1a5..cfa63157 100644 --- a/game/engine.ts +++ b/game/engine.ts @@ -31336,6 +31336,21 @@ function resolveBattle(g: Game, retreat?: DiplomatRetreatSelection) { observeOccupation(g); dead(al); dead(dl); + // Surviving discs from earlier battles remain in their Arrakis territory + // through the Battle phase. Captives have changed custody and do not use + // their old battle-location stamp as proof of physical presence. + if (g.advanced && !b.territory.startsWith('homeworld:')) { + let previousLeaders = 0; + for (const owner of g.players) + for (const leader of owner.leaders) + if (!leader.dead && !leader.capturedBy && !leader.concealed && + leader.usedAt === b.territory) { + dead(leader); + previousLeaders++; + } + if (previousLeaders) + log(g, `${previousLeaders} surviving leader disc${previousLeaders === 1 ? '' : 's'} from earlier battles in this territory ${previousLeaders === 1 ? 'was' : 'were'} also killed by the explosion.`); + } for (const [p, plan] of [ [a, ap], [d, dp], diff --git a/game/reference.ts b/game/reference.ts index 7f375f58..8cea4dfc 100644 --- a/game/reference.ts +++ b/game/reference.ts @@ -2815,6 +2815,7 @@ export const RULE_TOPICS: RuleTopic[] = [ 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. 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.', + 'In Advanced on Arrakis, a surviving leader remains in its battle territory for the rest of the Battle phase. A later Lasgun–Shield explosion there also kills previously surviving leader discs still present, even if a different leader fights that later battle. A successful Traitor call prevents the explosion.', '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-territory-explosion-custody.test.ts b/tests/battle-territory-explosion-custody.test.ts new file mode 100644 index 00000000..231c6cc8 --- /dev/null +++ b/tests/battle-territory-explosion-custody.test.ts @@ -0,0 +1,39 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { viewGame } from '../game/engine'; +import { actSalusaGhola, advanceSalusaGhola, salusaGholaClean, + type SalusaGholaStep } from './fixture-salusa-ghola'; +import { createAdvisorBattleFlipFixture } from './fixture-advisor-battle-flip'; + +void test('Advanced explosion does not kill or reveal a captured disc with a stale territory stamp', () => { + const f = createAdvisorBattleFlipFixture({ explosion: true }); + let game = structuredClone(f.game); + // Controlled custody supplement: a real BG leader in its native roster has + // already been captured by the actually seated Harkonnen. Preserve the + // capture snapshot that hides its physical custody from the other player; + // the inherited battle, plans, cards, armies and all costs are native. + const owner = game.players.find(p => p.id === 'beneGesserit')!; + const captive = owner.leaders[0]; assert.ok(captive && !captive.dead && !captive.capturedBy); + captive.usedAt = 'arrakeen'; + captive.concealed = { captor: 'harkonnen', controller: owner.id, dead: captive.dead, + deaths: captive.deaths, usedAt: captive.usedAt }; + captive.capturedBy = 'harkonnen'; + const id = captive.id, physicalBefore = structuredClone(captive); + const viewBefore = viewGame(game,'atreides').players.find(p => p.id === owner.id)!.leaders.find(l => l.id === id)!; + assert.equal(viewBefore.capturedBy,undefined); + const steps: SalusaGholaStep[] = []; + game = advanceSalusaGhola(game,g => salusaGholaClean(g) && !!g.battle && !g.battle.revealed && + !g.battle.preparation && (!g.battle.preLeader || g.battle.preLeader.closed),steps); + for (const plan of f.plans) game = actSalusaGhola(game,plan.actor,plan.action,steps); + game = advanceSalusaGhola(game,g => !g.battle && salusaGholaClean(g),steps); + const after = game.players.find(p => p.id === owner.id)!.leaders.find(l => l.id === id)!; + assert.equal(after.dead,physicalBefore.dead); + assert.equal(after.deaths,physicalBefore.deaths); + assert.equal(after.capturedBy,'harkonnen'); + assert.ok(after.concealed); + const viewAfter = viewGame(game,'atreides').players.find(p => p.id === owner.id)!.leaders.find(l => l.id === id)!; + assert.equal(viewAfter.capturedBy,undefined); + assert.equal(viewAfter.dead,viewBefore.dead); + assert.equal(viewAfter.deaths,viewBefore.deaths); + assert.ok(game.log.some(entry => /Lasgun and shield exploded/.test(entry.text))); +}); diff --git a/tests/battle-territory-explosion-edge.test.ts b/tests/battle-territory-explosion-edge.test.ts new file mode 100644 index 00000000..fc84ca83 --- /dev/null +++ b/tests/battle-territory-explosion-edge.test.ts @@ -0,0 +1,73 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { + createAdvisorBattleFlipFixture as fixture, + commitAdvisorBattleFlip as commit, + advisorBattleFlipPlayer as player, +} from './fixture-advisor-battle-flip'; + +// The original Advanced game, forces, hands and Lasgun/Shield battle are real +// actions from the shared fixture. Prior leaders' usedAt/dead history and, +// for the mutual-Traitor case, exact physical Traitor custody are staged below: +// this supplement does not claim an earned first battle or Traitor deal. +function staged(mutualTraitors = false) { + const f = fixture({ explosion: true }); + const game = structuredClone(f.game); + if (mutualTraitors) { + // Swap original identities across actual held/reserve slots; no Traitor is + // invented or duplicated. The battle driver now calls the two real votes. + const pools = [game.traitorReserve ?? [], ...game.players.flatMap(p => [p.traitors, p.traitorChoices])]; + for (const [actor, identity] of [[f.atreides, 'harkonnen-3'], [f.harkonnen, 'atreides-0']] as const) { + const hand = player(game, actor).traitors; + if (hand.includes(identity)) continue; + const source = pools.find(pool => pool.includes(identity)); + assert.ok(source && hand.length > 0); + const index = source.indexOf(identity); + [hand[0], source[index]] = [source[index], hand[0]]; + } + f.mutualTraitors = true; + } + const bg = player(game, f.bg); + const [prior, elsewhere, unused, alreadyDead] = bg.leaders; + assert.ok(prior && elsewhere && unused && alreadyDead); + assert.ok([prior, elsewhere, unused, alreadyDead].every(leader => !leader.dead && !leader.usedAt)); + prior.usedAt = f.territory; + elsewhere.usedAt = 'carthag'; + alreadyDead.dead = true; + alreadyDead.deaths = 1; + const current = player(game, f.atreides).leaders.find(leader => leader.id === f.atreidesPlan.leader); + assert.ok(current && !current.dead && !current.usedAt); + current.usedAt = f.territory; + return { f, game, prior: prior.id, elsewhere: elsewhere.id, unused: unused.id, + alreadyDead: alreadyDead.id, current: current.id }; +} + +void test('Advanced territory-wide explosion kills a prior noncombatant leader exactly once', () => { + const s = staged(), done = commit(s.game, s.f).game; + assert.equal(done.lastBattleContext?.result, 'explosion'); + const bg = player(done, s.f.bg); + const byId = (id: string) => { const leader = bg.leaders.find(item => item.id === id); assert.ok(leader); return leader; }; + const prior = byId(s.prior); + assert.equal(prior.dead, true); + assert.equal(prior.deaths, 1); + assert.equal(prior.usedAt, undefined); + const current = player(done, s.f.atreides).leaders.find(leader => leader.id === s.current)!; + assert.equal(current.dead, true); + assert.equal(current.deaths, 1, 'The current plan leader is not killed twice'); + assert.equal(current.usedAt, undefined); + assert.equal(byId(s.elsewhere).dead, false); + assert.equal(byId(s.elsewhere).usedAt, 'carthag'); + assert.equal(byId(s.unused).dead, false); + assert.equal(byId(s.unused).usedAt, undefined); + assert.equal(byId(s.alreadyDead).dead, true); + assert.equal(byId(s.alreadyDead).deaths, 1, 'A dead leader stays in the Tanks without another death'); +}); + +void test('mutual Traitors take precedence over a revealed Lasgun and Shield', () => { + const s = staged(true), done = commit(s.game, s.f).game; + assert.equal(done.lastBattleContext?.result, 'mutualTraitors'); + const prior = player(done, s.f.bg).leaders.find(leader => leader.id === s.prior)!; + assert.equal(prior.dead, false); + assert.equal(prior.deaths, 0); + assert.equal(prior.usedAt, s.f.territory); +}); diff --git a/tests/battle-territory-explosion.test.ts b/tests/battle-territory-explosion.test.ts new file mode 100644 index 00000000..596f9b09 --- /dev/null +++ b/tests/battle-territory-explosion.test.ts @@ -0,0 +1,28 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { actSalusaGhola, advanceSalusaGhola, salusaGholaClean, + type SalusaGholaStep } from './fixture-salusa-ghola'; +import { createBattleTerritoryExplosionFixture } from './fixture-battle-territory-explosion'; + +void test('Advanced explosion kills leaders left from a completed earlier battle in the same territory', () => { + const f = createBattleTerritoryExplosionFixture(); + const previous = f.afterFirst.players.find(p => p.id === 'atreides')!.leaders.find(l => l.id === f.priorLoser)!; + const winner = f.afterFirst.players.find(p => p.id === f.priorWinnerOwner)!.leaders.find(l => l.id === f.priorWinner)!; + assert.equal(previous.dead,false); assert.equal(previous.usedAt,f.territory); + assert.equal(winner.dead,false); assert.equal(winner.usedAt,f.territory); + const actions: SalusaGholaStep[] = []; + let game = structuredClone(f.game); + for (const plan of f.plans) game = actSalusaGhola(game,plan.actor,plan.action,actions); + game = advanceSalusaGhola(game,g => !g.battle && salusaGholaClean(g),actions); + assert.ok(actions.filter(step => step.action.type === 'traitorCall').every(step => step.action.call === false)); + for (const [owner,id] of [['atreides',f.priorLoser],[f.priorWinnerOwner,f.priorWinner], + ['emperor',f.emperorLeader],['guild',f.guildLeader]] as const) { + const leader = game.players.find(p => p.id === owner)!.leaders.find(l => l.id === id)!; + assert.equal(leader.dead,true,`${owner}'s ${id} is killed by the territory-wide explosion`); + assert.equal(leader.deaths,1); + } + assert.ok(game.log.some(entry => /Lasgun and shield exploded/.test(entry.text))); + assert.equal(game.discard.filter(card => card.id === f.shield || card.id === f.lasgun).length,2); + assert.equal(game.players.flatMap(p => Object.entries(p.forces).filter(([key]) => key.startsWith(`${f.territory}:`))) + .reduce((sum,[,amount]) => sum + amount,0),0); +}); diff --git a/tests/engine.test.ts b/tests/engine.test.ts index 2dbb755e..d3446275 100644 --- a/tests/engine.test.ts +++ b/tests/engine.test.ts @@ -2221,6 +2221,9 @@ void test('Ghola and normal revival share the elite limit for the entire turn, n ); g = applyAction(g, 'p0', { type: 'revive', amount: 2, elite: 0 }); conservation(g); + // This controlled fixture jumps directly past Revival to Mentat. Retire the + // phase-owned ordering frame that ordinary phase completion would clear. + delete g.revivalOrder; g.phase = 8; g.ready = []; g = readyAll(g); diff --git a/tests/fixture-battle-territory-explosion.ts b/tests/fixture-battle-territory-explosion.ts new file mode 100644 index 00000000..b96d37c3 --- /dev/null +++ b/tests/fixture-battle-territory-explosion.ts @@ -0,0 +1,100 @@ +import assert from 'node:assert/strict'; +import { createGame, joinGame, newPlayer, viewGame, type Action, type Game } from '../game/engine'; +import { actSalusaGhola, advanceSalusaGhola, salusaGholaClean, + type SalusaGholaStep } from './fixture-salusa-ghola'; + +const seat = (g: Game, id: string) => { const p = g.players.find(p => p.id === id); assert.ok(p); return p; }; +const readyForPlan = (g: Game) => salusaGholaClean(g) && !!g.battle && !g.battle.revealed && + !g.battle.preparation && (!g.battle.preLeader || g.battle.preLeader.closed); +function livingLeader(g: Game, id: string, strongest: boolean, excluded?: string) { + const leaders = seat(g, id).leaders.filter(l => !l.dead && !l.capturedBy && l.id !== excluded && + (!l.usedAt || l.usedAt === 'imperial_basin')).sort((a,b) => strongest ? b.strength-a.strength : a.strength-b.strength); + assert.ok(leaders[0]); return leaders[0].id; +} +/** Real public Advanced E/A/G source: three actual forces meet in Imperial + * Basin; the first battle's surviving leader discs then remain in that same + * territory while the two remaining armies fight. Only the unopened three-card + * starting deal, two unopened land Spice positions and scalar lotteries are + * selected; no played forces, leaders, wallet, battle or clock is assigned. */ +export function createBattleTerritoryExplosionFixture() { + let game = createGame('TERRITORYEXPLOSION', newPlayer('emperor','Emperor','emperor'), true); + joinGame(game,newPlayer('atreides','Atreides','atreides')); + joinGame(game,newPlayer('guild','Guild','guild')); + const actions: SalusaGholaStep[] = []; + const act = (actor: string, action: Action) => { game = actSalusaGhola(game,actor,action,actions); }; + const reach = (until: (g: Game) => boolean) => { game = advanceSalusaGhola(game,until,actions); }; + for (const p of game.players) act(p.id,{type:'ready'}); + const lobby = structuredClone(game); + act(game.host,{type:'start',advancedPreview:true}); + const setup = structuredClone(game); + assert.equal(game.status,'setup'); assert.ok(game.players.every(p => p.hand.length === 0)); + const shield = game.deck.find(c => c.kind === 'shield'); + const ordinary = game.deck.find(c => c.kind === 'projectile'); + const lasgun = game.deck.find(c => c.kind === 'lasgun'); + assert.ok(shield && ordinary && lasgun); + const selected = [shield,ordinary,lasgun]; + const startingSelection = selected.map((card,position) => { + const from = game.deck.findIndex(c => c.id === card.id); assert.ok(from >= position); + [game.deck[position],game.deck[from]] = [game.deck[from],game.deck[position]]; + return {position,from,card:card.id}; + }); + reach(g => g.status === 'playing'); + const afterSetup = structuredClone(game); + assert.deepEqual(game.players.flatMap(p => p.hand.map(c => c.id)),selected.map(c => c.id)); + const spiceSelection = [0,1].map(position => { + const from = game.spiceDeck.findIndex((c,index) => index >= position && + 'territory' in c && !c.discovery && c.territory !== 'imperial_basin'); + assert.ok(from >= position); const [c] = game.spiceDeck.splice(from,1); + game.spiceDeck.splice(position,0,c); return {position,from,card:structuredClone(c)}; + }); + reach(g => g.phase === 5 && salusaGholaClean(g)); + const shipments: SalusaGholaStep[] = []; + for (let n = 0; game.phase === 5 && n < 100; n++) { + if (!salusaGholaClean(game)) { reach(salusaGholaClean); continue; } + const owner = game.active!; + if (owner === 'atreides') { + act(owner,{type:'move',from:'arrakeen:10',territory:'imperial_basin',sector:9,amount:1}); + } else { + const action: Action = {type:'ship',territory:'imperial_basin',sector:9, + amount:owner === 'emperor' ? 2 : 1,elite:0,allyPayment:0}; + shipments.push({actor:owner,action}); act(owner,action); + } + reach(salusaGholaClean); + act(owner,{type:'endMovement'}); + } + assert.equal(game.phase,6); + reach(g => salusaGholaClean(g) && !g.battle && !!g.active); + const beforeFirst = structuredClone(game), chooser = game.active!; + const choice = viewGame(game,chooser).battleChoices.find(c => c.territory === 'imperial_basin' && + (c.attacker === 'atreides' || c.defender === 'atreides')); + assert.ok(choice, 'Choose the actual first contest against Atreides'); + const other = choice.attacker === 'atreides' ? choice.defender : choice.attacker; + act(chooser,{type:'chooseBattle',territory:'imperial_basin',target:chooser === 'atreides' ? other : 'atreides'}); + reach(readyForPlan); + const priorLoser = livingLeader(game,'atreides',false), priorWinner = livingLeader(game,other,true); + act('atreides',{type:'battlePlan',leader:priorLoser,dial:0,support:0}); + act(other,{type:'battlePlan',leader:priorWinner,dial:0,support:0}); + reach(g => !g.battle && salusaGholaClean(g) && g.phase === 6 && !!g.active); + const afterFirst = structuredClone(game); + assert.equal(game.lastBattleContext?.winner,other); + assert.ok(seat(game,'atreides').leaders.some(l => l.id === priorLoser && !l.dead && l.usedAt === 'imperial_basin')); + assert.ok(seat(game,other).leaders.some(l => l.id === priorWinner && !l.dead && l.usedAt === 'imperial_basin')); + const secondChooser = game.active!; + const next = viewGame(game,secondChooser).battleChoices.find(c => c.territory === 'imperial_basin' && + [c.attacker,c.defender].includes('emperor') && [c.attacker,c.defender].includes('guild')); + assert.ok(next, 'The actual two remaining armies must battle'); + act(secondChooser,{type:'chooseBattle',territory:'imperial_basin', + target:secondChooser === 'emperor' ? 'guild' : 'emperor'}); + reach(readyForPlan); + const beforeExplosion = structuredClone(game); + const emperorLeader = livingLeader(game,'emperor',false,other === 'emperor' ? priorWinner : undefined); + const guildLeader = livingLeader(game,'guild',false,other === 'guild' ? priorWinner : undefined); + const plans = [ + {actor:'emperor',action:{type:'battlePlan',leader:emperorLeader,dial:0,support:0,defense:shield.id} as Action}, + {actor:'guild',action:{type:'battlePlan',leader:guildLeader,dial:0,support:0,weapon:lasgun.id} as Action}, + ]; + return {game,beforeExplosion,beforeFirst,afterFirst,lobby,setup,afterSetup,startingSelection,spiceSelection, + priorLoser,priorWinner,priorWinnerOwner:other,emperorLeader,guildLeader,plans,actions,shipments, + shield:shield.id,lasgun:lasgun.id,territory:'imperial_basin', + staging:'Only unopened starting cards and Spice positions and scalar lotteries are selected. The board entry, first battle and survivor discs use public native actions.'}; +}