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

@@ -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');