Handle prior leaders in Basic battle explosions

This commit is contained in:
Quintine committed 2026-10-27 17:24:21 +11:00
1 parent c3576b2deb
commit 474674f7fe
9 files changed
+194 -13

No files matched your search

+17 -2
View File
@@ -20,12 +20,27 @@ open mode gates.
Keep identified Basic-function gaps ahead of Advanced and wider interactions.
Prototype labels and final acceptance gates remain.
**Basic explosion and prior surviving leaders — 27 October. Prototyped / Partial.**
The Advanced Arrakis prior-leader handling now also applies provisionally in
Basic: a later actual explosion kills surviving discs from earlier battles in
that territory. The Basic source composition and its uncertainty are recorded
in the [contract](EFFECTIVE_BATTLE_WEAPONS.md#basic-explosion-and-prior-leaders--27-october-2026).
A public Basic Emperor/Atreides/Guild game uses real entry and two successive
battles to check prior and current leaders. An Advanced E3 supplement begins
with earned Duke acquisition but explicitly stages the later battle position
and cards: a set-aside Duke survives, while a Duke in the current plan dies.
Kwisatz's prior location remains unresolved; browser and deployed play are not
certified.
Eleven selected files pass 173/173 and type/lint checks pass. No build, browser,
mobile or deployed verification was performed for this focused rules change.
**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).
concealed exclusions remain. The later Basic interpretation is recorded above;
Homeworld, shared Duke, Kwisatz and special custody remain 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
+32 -3
View File
@@ -1,5 +1,34 @@
# Physical cards and effective battle weapons
## Basic explosion and prior leaders — 27 October 2026
**Prototyped / Partial.** Under the standing selected-interpretation authority,
apply the same prior-leader Arrakis explosion handling to Basic. The [GF9 base
rulebook, p11](https://www.gf9games.com/dunegame/wp-content/uploads/Dune-Rulebook.pdf)
places surviving leaders in their territory until battles finish. A
[separately indexed transcription](https://boardgamegeek.com/thread/2542307/lasgun-shield-explosion-do-leaders-who-previously)
attributes destruction of all leaders in the battle territory to the 2019
physical Lasgun card, but its actual face was not independently inspected.
This Basic composition is provisional and is not an official clarification.
Current and earlier leaders die once; a successful Traitor prevents the explosion
and no leader bounty is awarded. Existing custody exclusions remain.
The supplied Advanced source physical30/printed28 returns a surviving
Ambassador Duke next to the board after his battle, so his `usedAt` stamp alone
does not locate that shared disc in a later explosion. The Advanced Kwisatz
`usedAt` marker establishes a per-turn territory limit, but no retrieved primary
source establishes where the separate marker stays after its battle. Its
current-plan explosion behavior remains; a later explosion is not inferred from
the stamp. Homeworld and exceptional custody boundaries remain open.
A native public Basic game uses actual entry, two battles and both previous
surviving discs; its unopened card/Spice order and scalar lotteries are selected.
The Duke supplement earns his original acquisition but explicitly stages later
battle state and conserved cards. These checks do not establish broad custody,
full-mode or deployed acceptance.
Eleven related files pass 173/173 with type checking and lint; the first
verification attempt caught an unused import before the full passing rerun.
## Earlier leaders in an Advanced explosion — 27 October 2026
**Prototyped / Partial.** The user-authorized revised rulebook physical p11 /
@@ -13,9 +42,9 @@ 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 separate shared Duke and Kwisatz marker, Homeworld explosions, and
exceptional custody interactions remain outside that slice; the later provisional
Basic policy is recorded above. This does not certify complete modes.
The focused native regression uses an original Advanced Emperor/Atreides/Guild
setup, paid shipments and movement to earn two successive Imperial Basin battles.
+19
View File
@@ -1,5 +1,24 @@
# Dune implementation status
## 27 October 2026 — Basic explosion and prior leaders
Extend the Advanced Arrakis prior-leader explosion to Basic under the standing
selected-interpretation authority. Base GF9 rules leave surviving discs in their
territory, and a separately indexed card transcription supports territory-wide
leader deaths. The card face has not been independently confirmed; the Basic
composition is provisional. Shared Duke, Kwisatz, Homeworld and exceptional
custody remain separate. [Contract](EFFECTIVE_BATTLE_WEAPONS.md#basic-explosion-and-prior-leaders--27-october-2026).
An original public Basic Emperor/Atreides/Guild sequence reaches two real
battles after actual shipments and movement; only unopened initial cards and
Spice positions plus scalar lotteries are selected. A separate Advanced E3
supplement uses an earned Duke acquisition, then explicitly stages conserved
cards and later battle state to check the set-aside and current-plan cases. No
browser or deployment result is claimed.
Eleven focused files pass 173/173 and types/lint pass. The private proof is
`20261027-basic-explosion-leaders-proof-v2/`; the first attempt stopped at an
unused test import, which was removed before the complete second run.
## 27 October 2026 — Advanced explosion and prior leaders
Use the user-authorized Advanced rulebook physical11/printed9 to include living
+13 -2
View File
@@ -1,5 +1,16 @@
# Rule decision index
## Basic explosion and prior leaders — 27 October 2026
Provisionally extend the existing Advanced Arrakis casualty handling to Basic.
The [publisher base rulebook, p11](https://www.gf9games.com/dunegame/wp-content/uploads/Dune-Rulebook.pdf)
keeps surviving leaders in their battle territory; a separately indexed
transcription attributes territory-wide leader destruction to the physical
Lasgun card. The latter has not been independently confirmed from the card face,
so this composition is an explicit interpretation, not a publisher ruling.
Captured and concealed leaders, shared Duke, Homeworlds and Kwisatz retain their
separate boundaries. [Source and scope](EFFECTIVE_BATTLE_WEAPONS.md#basic-explosion-and-prior-leaders--27-october-2026).
## Earlier leaders in an Advanced Arrakis explosion — 27 October 2026
Apply the supplied Advanced rulebook physical11/printed9: surviving leaders
@@ -7,8 +18,8 @@ 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).
location. Homeworld, shared Duke, Kwisatz and exceptional custody remain
separate; the later Basic interpretation is recorded above. [Source and scope](EFFECTIVE_BATTLE_WEAPONS.md#earlier-leaders-in-an-advanced-explosion--27-october-2026).
## Distrans before Richese special bidding — 27 October 2026
+1 -1
View File
@@ -31339,7 +31339,7 @@ function resolveBattle(g: Game, retreat?: DiplomatRetreatSelection) {
// 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:')) {
if (!b.territory.startsWith('homeworld:')) {
let previousLeaders = 0;
for (const owner of g.players)
for (const leader of owner.leaders)
+1 -1
View File
@@ -2815,7 +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.',
'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. The Basic application is provisional; 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'],
@@ -0,0 +1,29 @@
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('Basic provisional territory-wide explosion kills earlier surviving leaders in a later native battle', () => {
const f = createBattleTerritoryExplosionFixture({ advanced: false });
assert.equal(f.setup.advanced,false);
const firstLoser = f.afterFirst.players.find(p => p.id === 'atreides')!.leaders.find(l => l.id === f.priorLoser)!;
const firstWinner = f.afterFirst.players.find(p => p.id === f.priorWinnerOwner)!.leaders.find(l => l.id === f.priorWinner)!;
assert.equal(firstLoser.dead,false); assert.equal(firstLoser.usedAt,f.territory);
assert.equal(firstWinner.dead,false); assert.equal(firstWinner.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} dies in 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);
});
@@ -0,0 +1,77 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { handLimit } from '../game/engine';
import { DUKE_VIDAL_ID } from '../game/duke-vidal';
import { createMoritaniEcazDukeWinner, settleMoritaniEcazDukeWinner } from './fixture-moritani-ecaz-duke-winner';
import { pairedE3NexusInventory, pairedE3NexusPlayer, revealPairedE3Nexus, stepPairedE3Nexus } from './fixture-paired-e3-nexus-modules';
/** Controlled supplement on a genuine Advanced Ecaz/Moritani/Guild setup and
* earned Duke acquisition. Stage only the prior set-aside Duke history and
* physical Lasgun/Shield custody; battle choice, plans and votes are real. */
function source(setAside: boolean) {
const f = createMoritaniEcazDukeWinner();
const game = structuredClone(f.beforeBattle);
const duke = game.dukeVidal!;
assert.equal(duke.leader.id, DUKE_VIDAL_ID);
assert.equal(duke.leader.dead, false);
assert.equal(duke.controller, f.ecaz);
if (setAside) {
duke.controller = null;
duke.acquiredTurn = null;
duke.source = null;
duke.leader.usedAt = f.location;
}
const take = (kind: 'lasgun' | 'shield', actor: string) => {
const recipient = pairedE3NexusPlayer(game, actor);
const already = recipient.hand.find(card => card.kind === kind);
if (already) return already;
const pools = [game.deck, game.discard, ...game.players.map(player => player.hand)];
const pool = pools.find(cards => cards.some(card => card.kind === kind));
assert.ok(pool, `The original ${kind} remains in physical custody`);
const index = pool.findIndex(card => card.kind === kind);
const [card] = pool.splice(index, 1);
if (recipient.hand.length >= handLimit(recipient)) {
const replacement = recipient.hand.pop(); assert.ok(replacement);
pool.push(replacement);
}
recipient.hand.push(card);
return card;
};
const lasgun = take('lasgun', f.ecaz), shield = take('shield', f.moritani);
pairedE3NexusInventory(game);
const native = pairedE3NexusPlayer(game, f.ecaz).leaders.find(leader => !leader.dead && !leader.usedAt);
assert.ok(native);
const chosen = stepPairedE3Nexus(game, f.choice);
const plans = f.plans.map(step => step.actor === f.ecaz
? { ...step, action: { ...step.action, leader: setAside ? native.id : DUKE_VIDAL_ID, weapon: lasgun.id } }
: { ...step, action: { ...step.action, defense: shield.id } });
const ready = revealPairedE3Nexus(chosen, plans);
return { f, game, ready, priorDuke: structuredClone(duke), lasgun: lasgun.id, shield: shield.id };
}
void test('Advanced later explosion does not kill a surviving Duke already set beside the board', () => {
const s = source(true);
assert.equal(s.priorDuke.controller, null);
assert.equal(s.priorDuke.leader.usedAt, s.f.location);
const done = settleMoritaniEcazDukeWinner(s.ready);
assert.equal(done.lastBattleContext?.result, 'explosion');
assert.equal(done.dukeVidal!.leader.id, DUKE_VIDAL_ID);
assert.equal(done.dukeVidal!.leader.dead, false);
assert.equal(done.dukeVidal!.leader.deaths, s.priorDuke.leader.deaths);
assert.equal(done.dukeVidal!.controller, null);
assert.ok(done.players.every(player => player.leaders.every(leader => leader.id !== DUKE_VIDAL_ID)));
pairedE3NexusInventory(done);
});
void test('Advanced current-plan Duke still dies in a resolved Lasgun–Shield explosion', () => {
const s = source(false);
assert.equal(s.priorDuke.controller, s.f.ecaz);
const done = settleMoritaniEcazDukeWinner(s.ready);
assert.equal(done.lastBattleContext?.result, 'explosion');
assert.equal(done.dukeVidal!.leader.id, DUKE_VIDAL_ID);
assert.equal(done.dukeVidal!.leader.dead, true);
assert.equal(done.dukeVidal!.leader.deaths, s.priorDuke.leader.deaths + 1);
assert.equal(done.dukeVidal!.controller, null);
assert.ok(done.players.every(player => player.leaders.every(leader => leader.id !== DUKE_VIDAL_ID)));
pairedE3NexusInventory(done);
});
+5 -4
View File
@@ -11,13 +11,14 @@ function livingLeader(g: Game, id: string, strongest: boolean, excluded?: string
(!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
/** Real public 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);
export function createBattleTerritoryExplosionFixture(options: { advanced?: boolean } = {}) {
const advanced = options.advanced ?? true;
let game = createGame('TERRITORYEXPLOSION', newPlayer('emperor','Emperor','emperor'), advanced);
joinGame(game,newPlayer('atreides','Atreides','atreides'));
joinGame(game,newPlayer('guild','Guild','guild'));
const actions: SalusaGholaStep[] = [];
@@ -25,7 +26,7 @@ export function createBattleTerritoryExplosionFixture() {
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});
act(game.host,advanced ? {type:'start',advancedPreview:true} : {type:'start'});
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');