Handle prior leaders in Basic battle explosions
This commit is contained in:
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);
|
||||
});
|
||||
@@ -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');
|
||||
|
||||
Reference in new issue
Block a user