Hide concealed Duke execution from Ghola guidance
Verify and publish checkpoint container / container (push) Successful in 11m22s

This commit is contained in:
Quintine committed 2026-10-29 19:11:55 +11:00
1 parent 4ec4f843a7
commit 5f4eeee4f1
6 files changed
+89 -7

No files matched your search

+42
View File
@@ -0,0 +1,42 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { createGame, newPlayer, viewGame } from '../game/engine';
import { acquireDuke, captureDuke, createDukeVidal, DUKE_VIDAL_ID, executeCapturedDuke } from '../game/duke-vidal';
import { newRevivalRules } from '../game/revival';
import { createNexusCards } from '../game/nexus-cards';
import { baseDeck } from '../game/cards';
import { ecazDukeRevivalBlock } from '../game/ecaz-duke-revival';
/** Staged public-equivalent custody in a supported Advanced Nexus roster. */
function fixture() {
const game = createGame('DUKEGHOLAPRIVACY', newPlayer('ec', 'Ecaz', 'ecaz'), true, ['ecaz']);
game.players.push(newPlayer('h', 'Harkonnen', 'harkonnen'), newPlayer('g', 'Guild', 'guild'));
Object.assign(game, { status:'playing', phase:0, turn:1, order:['ec','h','g'],
revivalRules:newRevivalRules(), nexusCards:{cards:createNexusCards(game.players, () => 0.5),phase:null} });
game.deck = baseDeck();
const ghola = game.deck.findIndex(card => card.effect === 'ghola');
assert.ok(ghola >= 0);
game.players[0].hand.push(game.deck.splice(ghola, 1)[0]);
const prior = acquireDuke(createDukeVidal(), 'g', 1, 'ecaz');
game.dukeVidal = captureDuke(prior, 'h', 'g', 1, 'arrakeen');
return game;
}
void test('Ecaz Ghola options do not disclose a secretly executed foreign Duke captive', () => {
const alive = fixture(), executed = structuredClone(alive);
executed.dukeVidal = executeCapturedDuke(executed.dukeVidal!, 'h', executed.turn);
assert.ok(ecazDukeRevivalBlock(alive, 'ec'), 'Actual living captive is unavailable');
assert.equal(ecazDukeRevivalBlock(executed, 'ec'), null, 'Secret execution changes authoritative eligibility');
const before = viewGame(alive, 'ec'), after = viewGame(executed, 'ec');
assert.deepEqual(before.dukeVidal, after.dukeVidal, 'Duke custody remains publicly equivalent');
assert.deepEqual(before.ghola, after.ghola, 'Ghola options must not reveal the hidden execution');
assert.equal(after.ghola.leaders.some(leader => leader.id === DUKE_VIDAL_ID), false);
});
void test('Ecaz is offered its publicly dead Duke for Ghola', () => {
const game = fixture();
game.dukeVidal = createDukeVidal();
game.dukeVidal.leader.dead = true;
game.dukeVidal.leader.deaths = 1;
assert.equal(viewGame(game, 'ec').ghola.leaders.some(leader => leader.id === DUKE_VIDAL_ID), true);
});
+7 -4
View File
@@ -9,6 +9,9 @@ import {
} from './fixture-nexus-duke-capture';
type Transition = { before: Game; after: Game; leader: string };
/** Do not stop at another faction's ordered Advanced Revival opportunity. */
const ownerRevival = (game: Game, owner: string) => game.phase === 4 && nexusDukeClean(game) &&
(!game.revivalOrder || game.revivalOrder.remaining[0] === owner);
export type NativeEcazCohortFixture = {
game: Game; owner: string; captor: string; plan: NexusDukePlan; actions: NexusDukeStep[];
deaths: Transition[]; funding: { before: Game; after: Game; amount: number };
@@ -73,7 +76,7 @@ export function createNativeEcazCohortFixture(): NativeEcazCohortFixture {
game = stepNexusDuke(game, plan, { actor: captor,
action: { type: 'bribe', target: owner, amount } }, actions);
const funding = { before: fundingBefore, after: structuredClone(game), amount };
game = advanceNexusDuke(game, g => g.phase === 4 && nexusDukeClean(g), plan, actions);
game = advanceNexusDuke(game, g => ownerRevival(g, owner), plan, actions);
const firstBefore = structuredClone(game);
const firstLeader = native[0];
assert.ok(viewGame(game, owner).revival.leaders.some(l => l.id === firstLeader));
@@ -81,7 +84,7 @@ export function createNativeEcazCohortFixture(): NativeEcazCohortFixture {
action: { type: 'reviveLeader', leader: firstLeader } }, actions);
const first = { before: firstBefore, after: structuredClone(game), leader: firstLeader };
const repeat = kill(firstLeader);
game = advanceNexusDuke(game, g => g.phase === 4 && nexusDukeClean(g), plan, actions);
game = advanceNexusDuke(game, g => ownerRevival(g, owner), plan, actions);
plan.ship = {};
assert.equal(nexusDukePlayer(game, owner).leaders.find(l => l.id === firstLeader)!.deaths, 2);
assertNexusDukeCustody(game);
@@ -119,7 +122,7 @@ export function createNativeEcazSecondCohortFixture(): NativeEcazSecondCohortFix
const paid: Transition[] = [], repeats: Transition[] = [];
let ghola!: Transition;
for (let i = 0; i < 3; i++) {
game = advanceNexusDuke(game, g => g.phase === 4 && nexusDukeClean(g), plan, actions);
game = advanceNexusDuke(game, g => ownerRevival(g, owner), plan, actions);
const before = structuredClone(game);
game = stepNexusDuke(game, plan, { actor: owner,
action: { type: 'reviveLeader', leader: native[i] } }, actions);
@@ -138,7 +141,7 @@ export function createNativeEcazSecondCohortFixture(): NativeEcazSecondCohortFix
}
const last = poisonDeath(game, death, native[3]);
repeats.push(last);
game = advanceNexusDuke(last.after, g => g.phase === 4 && nexusDukeClean(g), plan, actions);
game = advanceNexusDuke(last.after, g => ownerRevival(g, owner), plan, actions);
plan.ship = {};
assert.equal(nexusDukePlayer(game, owner).leaders.find(l => l.id === native[4])!.dead, false);
assert.equal(game.dukeVidal!.leader.dead, true);