Restore safe Duke grants in early legacy games
Verify and publish checkpoint container / container (push) Successful in 10m31s

This commit is contained in:
Quintine committed 2026-10-29 19:35:55 +11:00
1 parent 59cb336c80
commit 22b463a395
10 files changed
+263 -11

No files matched your search

+9
View File
@@ -17,6 +17,15 @@ open mode gates.
## Current checkpoint and work
**Early legacy Duke proof and occupied Ecaz displacement — 29 October.
Prototyped / Partial.** An unstamped game can establish a clear capture history
from public first-turn, pre-battle phase and continuation state; an explicit
uncertain marker and later or ambiguous states remain guarded. An actual public
occupied-Ecaz displacement of an ordinary living Duke grant also clears the
marker. Eleven related files pass 73 cases, including native setup and occupation
with controlled staging. Wider interactions, capture assurance and deployed verification remain
open. [Scope](ECAZ_DUKE_ACQUISITION.md#public-first-turn-proof-and-occupied-ecaz-displacement--29-october-2026).
**Resolving public Duke capture uncertainty — 29 October. Prototyped /
Partial.** Publicly established ordinary Duke battle, revival, acquisition or named
death outcomes clear the conservative capture marker, including in older games
+28 -4
View File
@@ -1,5 +1,28 @@
# Ecaz Ambassador: direct Duke acquisition
## Public first-turn proof and occupied Ecaz displacement — 29 October 2026
**Prototyped / Partial.** An unstamped legacy game can establish clear Duke
capture history from public state while it is playing in turn one, phases 0–5,
with no battle, previous battle context or pending capture, and no capture
response or decision. The check does not inspect Duke custody or the selected
captive. Explicit uncertainty and later or ambiguous states remain guarded.
Viewing the game does not mutate the receipt; automatic normalization or an
accepted live action records the proven clear state. Later qualifying capture
events can make the history uncertain again.
Actual occupied-Ecaz displacement of a living ordinary Duke grant also clears
the marker: the displacement is publicly logged before the occupier's eligible
recipient choice. Merely having an occupation or an unrelated continuation
does not establish that displacement. Existing eligibility and custody checks
remain in force.
Focused tests cover a real Advanced opening and Ambassador arrival with an
unstamped receipt, public-state boundary and private-custody comparisons, and
the real occupied-Ecaz battle with a staged prior ordinary grant and uncertainty
receipt. Broader combinations, capture assurance and deployed verification
remain open; these prototypes do not certify a complete rules mode.
## Public resolution of capture uncertainty — 29 October 2026
**Prototyped / Partial.** A public event that establishes the actual unique
@@ -14,7 +37,8 @@ new uncertainty. The existing eligibility and custody checks still apply.
An Advanced Harkonnen game with clear public history can now also use the
provisional same-recipient conversion from another temporary tenure. A current
ordinary Ecaz Ambassador tenure cannot be redundantly renewed. The public
history safeguard remains uniform when the marker is uncertain or absent.
history safeguard remains uniform when the marker is uncertain, or absent
without the public first-turn proof above.
Twelve related files pass 87 cases. The real battle fixture verifies a later
Ambassador grant and renewed uncertainty after capture. Other action fixtures
@@ -31,9 +55,9 @@ Once that possibility arises it stays recorded until a supported public
resolution above. Direct Ambassador acquisition
and the automatic Advanced alliance loan are uniformly unavailable while the
record is uncertain, regardless of actual hidden custody. A game saved before
this receipt existed is also treated conservatively: its truncated history
cannot prove that a concealed capture never occurred. No saved game is reset
or retroactively assigned a fabricated clean history.
this receipt existed remains conservative unless its public first-turn state
proves capture could not yet have occurred, as described above. No saved game
is reset or assigned a fabricated clean history.
When the record is clear, a plain Advanced Ecaz–Harkonnen game can use the
ordinary direct grant and automatic new-ally loan; the original disc, reusable
+13 -2
View File
@@ -1,10 +1,20 @@
# Rule decision index
## First-turn Duke proof and occupied Ecaz displacement — 29 October 2026
**Prototyped / Partial.** Public first-turn pre-battle state can prove that an
unstamped legacy game has no earlier capture; explicit uncertainty and ambiguous
or later history remain guarded. The public occupied-Ecaz displacement of a
living ordinary Duke grant also resolves uncertainty. This is a provisional
online policy, with broader interactions and deployed verification still open.
[Scope](ECAZ_DUKE_ACQUISITION.md#public-first-turn-proof-and-occupied-ecaz-displacement--29-october-2026).
## Public resolution of Duke capture uncertainty — 29 October 2026
A revealed ordinary Duke battle or named public revival, acquisition or death resolves the prior
capture uncertainty; a later possible Harkonnen capture can restore it. This
is a provisional online policy and does not infer legacy capture history.
is a provisional online policy; the bounded first-turn public proof above also
applies to eligible unstamped legacy games.
[Scope](ECAZ_DUKE_ACQUISITION.md#public-resolution-of-capture-uncertainty--29-october-2026).
## Duke Ambassador capture uncertainty — 29 October 2026
@@ -12,7 +22,8 @@ is a provisional online policy and does not infer legacy capture history.
Fresh games record whether a public capture could have involved Duke, without
using the secret selection. Direct acquisition and automatic alliance loan
are uniformly guarded while that history is uncertain; legacy games without
the receipt remain conservative. This is a provisional online policy.
the receipt remain conservative except where the first-turn public proof above
applies. This is a provisional online policy.
[Scope](ECAZ_DUKE_ACQUISITION.md#public-capture-uncertainty--29-october-2026).
## Concealed Duke and Ghola guidance — 29 October 2026
+21 -3
View File
@@ -1,22 +1,40 @@
import { selectedPhaseModuleRoster, type FactionModuleProfile } from './faction-module-profile';
/** Public uncertainty about a possible concealed Duke capture. Fresh games
* explicitly begin false; only a public resolution may restore false. True,
* absent, or malformed values never establish safety. */
* explicitly begin false; some early legacy states can prove safety publicly. */
export type DukeCapturePolicyContext = Omit<Partial<FactionModuleProfile>, 'players'> & {
advanced: boolean;
players: readonly { faction: string }[];
dukeCaptureUncertain?: boolean;
status?: string;
turn?: number;
phase?: number;
battle?: unknown;
lastBattleContext?: unknown;
pendingCapture?: unknown;
response?: { kind: string } | null;
decision?: { kind: string } | null;
};
export const DUKE_CAPTURE_UNCERTAIN_BLOCK =
'Duke Vidal acquisition is unavailable while Harkonnen capture history may be unresolved.';
/** Before the first battle of the first turn, a legacy game has no capture
* history. Use public phase and continuation state only, never Duke custody. */
export function legacyDukeCaptureHistoryClear(g: DukeCapturePolicyContext): boolean {
return g.dukeCaptureUncertain === undefined && g.status === 'playing' && g.turn === 1 &&
typeof g.phase === 'number' && Number.isInteger(g.phase) && g.phase >= 0 && g.phase <= 5 &&
g.battle == null && g.lastBattleContext == null && g.pendingCapture == null &&
g.response?.kind !== 'capture' && g.decision?.kind !== 'captureOffer' &&
g.decision?.kind !== 'capturedLeader';
}
/** This result depends only on public table configuration and the public
* uncertainty receipt, never on actual Duke custody or capture identity. */
export function dukeCaptureUncertaintyBlock(g: DukeCapturePolicyContext): string | null {
return g.advanced && g.players.some(player => player.faction === 'harkonnen') &&
g.dukeCaptureUncertain !== false ? DUKE_CAPTURE_UNCERTAIN_BLOCK : null;
g.dukeCaptureUncertain !== false && !legacyDukeCaptureHistoryClear(g)
? DUKE_CAPTURE_UNCERTAIN_BLOCK : null;
}
/** Original native faction tables with no selected optional module. Native
+4 -1
View File
@@ -147,7 +147,7 @@ import { battleCardSlotEligible, battleCategoryInspectionValue, fixedBattleInspe
import { HARASS_WITHDRAW_CARD, HarassWithdrawError, isHarassWithdraw, quoteHarassWithdraw, defaultHarassWithdrawAllocation, harassWithdrawNeedsAllocation, type HarassWithdrawSelection, type HarassWithdrawContext, type HarassWithdrawPreview } from './harass-withdraw';
import { originalAuctionInspectionLot } from './auction-inspection-source';
import { nextDukeCaptureUncertainty } from './duke-capture-uncertainty';
import { dukeCaptureUncertaintyBlock, plainAdvancedEcazDukeProfile } from './ecaz-duke-capture-policy';
import { dukeCaptureUncertaintyBlock, legacyDukeCaptureHistoryClear, plainAdvancedEcazDukeProfile } from './ecaz-duke-capture-policy';
import { discoveryChoices, discoveryStashSignature, type DiscoveryStash } from './discovery-actions';
import { createStormSource, validateStormSource, discoveryStormOffer, createDiscoveryStorm, chooseDiscoveryStorm, validateDiscoveryStorm, finishDiscoveryStorm, discoveryStormDistance, discoveryStormSaphoModes, reorderDiscoveryStorm, type StormMovementSource, type DiscoveryStorm } from './discovery-storm';
import { completeDiscoveryFlight, discoveryFlightOffer, quoteDiscoveryFlight, validateDiscoveryFlight, type DiscoveryFlightReceipt } from './discovery-flight';
@@ -31327,6 +31327,7 @@ function reconcileHomeworldEcazDuke(g: Game) {
!duke.leader.dead && !duke.leader.capturedBy && !duke.leader.gholaBy && !duke.leader.concealed) {
g.dukeVidal = consumeDuke(duke);
log(g, 'Occupied Ecaz displaced Duke Vidal’s ordinary faction grant; the occupier chooses its eligible recipient.');
g.dukeCaptureUncertain = false;
}
const available = homeworldRule(() => quoteHomeworldEcazDuke(g));
if (!available.blocked && available.event && available.owner && available.recipients.length === 1)
@@ -44038,6 +44039,7 @@ export function normalizeAutomaticGame(state: Game): Game {
ecazCollectionIntegrity(state);
ecazAllianceIntegrity(state);
const g = structuredClone(state);
if (legacyDukeCaptureHistoryClear(g)) g.dukeCaptureUncertain = false;
reconcileHomeworldEcazDuke(g);
syncTupileSlots(g);
prepareTupileCleanup(g);
@@ -44155,6 +44157,7 @@ function applyActionInner(
'Finish the original occupied Tupile hand cleanup before another action.');
ecazTreacheryIntegrity(state);
const g = structuredClone(state);
if (execution === 'live' && legacyDukeCaptureHistoryClear(g)) g.dukeCaptureUncertain = false;
if ((g.pendingNexusEmperorPayment || g.pendingNexusEmperorSellerPurchase || g.pendingAmbassador?.nexusPurchase) &&
!g.truthtrance && !pendingNexusTraitors(g)) {
const p = getPlayer(g, id);
+1 -1
View File
@@ -1727,7 +1727,7 @@ export const RULE_TOPICS: RuleTopic[] = [
'Destroyed tokens return to supply; triggered regular tokens are set aside, Bene Gesserit is permanently removed, and replenishment waits for all five random tokens. Storm/explosion destruction remains separate. The reusable Ecaz token returns to supply after acquisition or alliance proposal. Basic has an optional new-ally Duke loan; the supplied Advanced p30 automatically grants an available Duke after accepted consent.',
'When an eligible entrant reaches the reusable Ecaz token, Ecaz may acquire the available shared Duke for itself or its current reciprocal ally. The token returns to supply and its companions are unchanged. Direct acquisition persists unused until battle use or Moritani acquisition; that shared tenure is provisional. It is distinct from the optional Basic or automatic Advanced new-alliance turn loan. A fresh token entry is required, with no standing transfer action.',
'Unallied Ecaz may propose an alliance to an unallied entrant. Acceptance activates alliance abilities immediately. Basic then offers an optional Duke loan; Advanced automatically adds an available Duke under supplied PDF p30, without another acknowledgement. The first version releases a surviving loan after one battle, provisionally; an unused living loan is set aside at turn end. Unavailable Duke never prevents consent.',
'In Advanced games with Harkonnen, a public capture that could have involved Duke makes the direct grant and automatic loan unavailable, regardless of which leader was secretly captured. A public event establishing the actual Duke, such as a revealed ordinary Duke battle or named revival, clears this uncertainty; a later possible capture can restore it. Older games without the capture record remain guarded until a supported public resolution. This provisional restriction protects the hidden captive identity and may prevent an otherwise legal grant.',
'In Advanced games with Harkonnen, a public capture that could have involved Duke makes the direct grant and automatic loan unavailable, regardless of which leader was secretly captured. A public event establishing the actual Duke, such as a revealed ordinary Duke battle, named revival or logged occupied-Ecaz displacement of an ordinary grant, clears this uncertainty; a later possible capture can restore it. An older game without the record can also establish clear history through public first-turn pre-battle state; later or ambiguous history remains guarded until a supported resolution. This provisional restriction protects the hidden captive identity and may prevent an otherwise legal grant.',
'When another eligible faction enters a marked stronghold, Ecaz may trigger its Ambassador for itself or its current ally, or leave it in place. Ecaz itself, its ally, advisors and the faction matching the physical Ambassador do not trigger it. Ordinary shipment and native Guild transport settle their own income, BG Intrusion and accompaniment controls before queued entry, without replaying payment or forces. Actual busy controls and unsupported paired or module combinations remain guarded.',
'In supported Basic games with Ecaz and Moritani, a single entry that earns both an Ambassador and Terror opportunity retains both in their existing provisional storm order, including selected native factions and the independent Ecaz cards. A Fremen relocation or Guild shipment from the Ambassador resolves its own child arrival before the original queue continues. If that child reveals the same Terror token, it cannot trigger again for the original entrant; if it is left hidden, the original opportunity remains. Unsupported module interactions remain guarded.',
'Supported Advanced games with both Ecaz and Moritani and other native faction decks also offer both entry reactions in provisional storm order. CHOAM may prevent an eligible move first; otherwise the move happens once before the token decisions. Fremen and Guild Ambassador effects that create another arrival remain unavailable in this Advanced overlap.',
@@ -0,0 +1,43 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { acquireDuke } from '../game/duke-vidal';
import { normalizeAutomaticGame } from '../game/engine';
import { actHomeworldEcazDuke, createHomeworldEcazDukeFixture } from './fixture-homeworld-ecaz-duke';
const displacement = 'Occupied Ecaz displaced Duke Vidal’s ordinary faction grant';
for (const legacy of [false, true]) void test(
`actual occupied Ecaz displacement clears ${legacy ? 'legacy' : 'uncertain'} Duke capture history`, () => {
const f = createHomeworldEcazDukeFixture({ advanced: true, allied: true });
// The public occupation and its recipient choice are earned by the native
// fixture. Stage only the prior ordinary grant and uncertainty receipt at
// the real battle boundary, then replay the fixture's actual battle plans.
let game = structuredClone(f.worldResolution.before);
game.dukeVidal = acquireDuke(game.dukeVidal!, f.ecaz, game.turn, 'ecaz');
if (legacy) delete game.dukeCaptureUncertain;
else game.dukeCaptureUncertain = true;
const leader = structuredClone(game.dukeVidal.leader);
for (const step of f.worldResolution.steps) game = actHomeworldEcazDuke(game, step.actor, step.action);
assert.ok(game.log.some(entry => entry.text.includes(displacement)));
assert.equal(game.dukeCaptureUncertain, false);
assert.equal(game.dukeVidal?.controller, null);
assert.equal(game.dukeVidal?.source, null);
assert.deepEqual(game.dukeVidal?.leader, leader);
assert.ok(f.ally);
assert.equal(game.homeworldEcazDukeGrants?.length ?? 0, 0, 'the occupier still has a recipient choice');
});
for (const legacy of [false, true]) void test(
`without an ordinary Duke displacement ${legacy ? 'legacy' : 'uncertain'} capture history remains`, () => {
const f = createHomeworldEcazDukeFixture({ advanced: true, allied: true });
// This earned occupied position has no ordinary holder to displace and has
// two recipients, so merely checking the opportunity must not clear it.
const source = structuredClone(f.game);
if (legacy) delete source.dukeCaptureUncertain;
else source.dukeCaptureUncertain = true;
const beforeLogs = source.log.filter(entry => entry.text.includes(displacement)).length;
const done = normalizeAutomaticGame(source);
assert.equal(done.dukeCaptureUncertain, legacy ? undefined : true);
assert.equal(done.log.filter(entry => entry.text.includes(displacement)).length, beforeLogs);
assert.deepEqual(done.dukeVidal, source.dukeVidal);
});
@@ -116,6 +116,11 @@ void test('a real Duke-possible capture marks uncertainty for either concealed s
void test('legacy games without the capture marker uniformly guard equivalent Duke custody', () => {
const {source, free, captured} = paired();
// The paired fixture stages an alternate prior capture. That history cannot
// occur before the first Battle, so place both public-equivalent outcomes
// at a later turn before removing the legacy marker.
free.turn = 2;
captured.turn = 2;
delete free.dukeCaptureUncertain;
delete captured.dukeCaptureUncertain;
const freeEntry = viewGame(free, source.owner).ambassadorEntry;
@@ -0,0 +1,83 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { applyAction, createGame, joinGame, newPlayer, normalizeAutomaticGame, viewGame, type Action, type Game } from '../game/engine';
import { acquireDuke, captureDuke } from '../game/duke-vidal';
import { actDukeAmbassador, advanceDukeAmbassador, dukeAmbassadorClean } from './fixture-duke-ambassador';
/** Actual Advanced setup and Harkonnen arrival, with only the unopened Spice
* cards and the existing fixture's scalar lotteries controlled. */
function initialArrival(): Game {
let game = createGame('LEGACYINITIALDUKE', newPlayer('ecaz', 'Ecaz', 'ecaz'), true, ['ecaz']);
joinGame(game, newPlayer('harkonnen', 'Harkonnen', 'harkonnen'));
joinGame(game, newPlayer('guild', 'Guild', 'guild'));
const act = (actor: string, action: Action) => { game = actDukeAmbassador(game, actor, action); };
const reach = (until: (state: Game) => boolean) => { game = advanceDukeAmbassador(game, until); };
for (const player of game.players) act(player.id, { type: 'ready' });
act(game.host, { type: 'start', advancedPreview: true });
reach(state => state.status === 'playing');
const token = game.ecazAmbassadors!.tokens.find(item => item.effect === 'ecaz');
assert.ok(token && token.zone === 'supply');
for (const position of [0, 1]) {
const index = game.spiceDeck.findIndex((card, i) => i >= position && 'territory' in card && !card.discovery);
assert.ok(index >= position);
[game.spiceDeck[position], game.spiceDeck[index]] = [game.spiceDeck[index], game.spiceDeck[position]];
}
reach(state => state.phase === 4 && !state.response && state.decision?.kind === 'ecazPlacement');
act('ecaz', { type: 'decision', token: token.id, territory: 'arrakeen' });
reach(state => state.phase === 4 && !state.response && state.decision?.kind === 'ecazPlacement');
act('ecaz', { type: 'decision', decline: true });
reach(state => state.phase === 5 && state.active === 'harkonnen' && dukeAmbassadorClean(state));
act('harkonnen', { type: 'ship', amount: 1, territory: 'arrakeen', sector: 10 });
reach(state => !state.response && state.pendingAmbassador?.stage === 'offer');
assert.equal(game.turn, 1);
assert.equal(game.pendingAmbassador!.token, token.id);
assert.equal(game.dukeCaptureUncertain, false);
delete game.dukeCaptureUncertain; // The only change simulates an older saved game.
return game;
}
void test('an unstamped first-turn Advanced arrival allows the Duke and stamps only an accepted action', () => {
const game = initialArrival();
const original = structuredClone(game);
assert.equal(viewGame(game, 'ecaz').ambassadorEntry?.dukeAcquisition?.blocked, null);
assert.deepEqual(game, original, 'Viewing a legacy save does not mutate it');
assert.throws(() => applyAction(game, 'guild', { type: 'decision', event: game.pendingAmbassador!.event,
trigger: true, beneficiary: 'ecaz', choice: 'duke' }));
assert.deepEqual(game, original, 'A rejected action leaves both state and marker unchanged');
const after = applyAction(game, 'ecaz', { type: 'decision', event: game.pendingAmbassador!.event,
trigger: true, beneficiary: 'ecaz', choice: 'duke' });
assert.equal(after.dukeCaptureUncertain, false);
assert.deepEqual([after.dukeVidal?.controller, after.dukeVidal?.source], ['ecaz', 'ecaz']);
assert.deepEqual(game, original, 'Applying an action leaves the input snapshot intact');
});
void test('automatic continuation records first-turn public proof without mutating the source', () => {
const game = initialArrival();
const original = structuredClone(game);
const after = normalizeAutomaticGame(game);
assert.equal(after.dukeCaptureUncertain, false);
assert.deepEqual(game, original);
});
void test('unstamped later games uniformly guard publicly equivalent Duke custody', () => {
const first = initialArrival();
for (const boundary of [{ turn: 1, phase: 6 }, { turn: 2, phase: 5 }]) {
const free = structuredClone(first);
Object.assign(free, boundary);
const prior = acquireDuke(free.dukeVidal!, 'guild', free.turn, 'ecaz');
free.dukeVidal = prior;
const captured = structuredClone(free);
captured.dukeVidal = captureDuke(prior, 'harkonnen', 'guild', free.turn, 'carthag');
const freeEntry = viewGame(free, 'ecaz').ambassadorEntry;
const capturedEntry = viewGame(captured, 'ecaz').ambassadorEntry;
assert.ok(freeEntry && capturedEntry);
assert.deepEqual(capturedEntry, freeEntry);
assert.ok(freeEntry.dukeAcquisition?.blocked, 'Unknown past capture stays uniformly guarded');
for (const game of [free, captured]) {
const original = structuredClone(game);
assert.throws(() => applyAction(game, 'ecaz', { type: 'decision', event: game.pendingAmbassador!.event,
trigger: true, beneficiary: 'ecaz', choice: 'duke' }));
assert.deepEqual(game, original);
}
}
});
+56
View File
@@ -0,0 +1,56 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import {
DUKE_CAPTURE_UNCERTAIN_BLOCK,
dukeCaptureUncertaintyBlock,
legacyDukeCaptureHistoryClear,
type DukeCapturePolicyContext,
} from '../game/ecaz-duke-capture-policy';
function earlyLegacy(): DukeCapturePolicyContext {
return {
advanced: true,
players: [{ faction: 'ecaz' }, { faction: 'harkonnen' }],
status: 'playing', turn: 1, phase: 0,
battle: null, lastBattleContext: null, pendingCapture: null,
response: null, decision: null,
};
}
void test('public first-turn phases before battle prove an unstamped legacy game safe', () => {
for (let phase = 0; phase <= 5; phase++) {
const game = { ...earlyLegacy(), phase };
Object.defineProperty(game, 'dukeVidal', { get() { throw new Error('private custody read'); } });
Object.defineProperty(game.players[1], 'capturedLeaders', { get() { throw new Error('private capture read'); } });
assert.equal(legacyDukeCaptureHistoryClear(game), true);
assert.equal(dukeCaptureUncertaintyBlock(game), null);
}
const unrelatedContinuation = { ...earlyLegacy(), response: { kind: 'nexus' }, decision: { kind: 'advisor' } };
assert.equal(legacyDukeCaptureHistoryClear(unrelatedContinuation), true);
});
void test('late, incomplete, or capture-continuation states cannot prove legacy safety', () => {
const variants: Partial<DukeCapturePolicyContext>[] = [
{ status: 'lobby' }, { status: undefined }, { turn: 2 }, { turn: undefined },
{ phase: -1 }, { phase: 6 }, { phase: 0.5 }, { phase: undefined }, { phase: NaN },
{ battle: {} }, { lastBattleContext: {} }, { pendingCapture: {} },
{ response: { kind: 'capture' } }, { decision: { kind: 'captureOffer' } },
{ decision: { kind: 'capturedLeader' } },
];
for (const variant of variants) {
const game = { ...earlyLegacy(), ...variant };
assert.equal(legacyDukeCaptureHistoryClear(game), false, JSON.stringify(variant));
assert.equal(dukeCaptureUncertaintyBlock(game), DUKE_CAPTURE_UNCERTAIN_BLOCK, JSON.stringify(variant));
}
});
void test('explicit uncertainty is preserved and existing policy profiles are unchanged', () => {
const uncertain = { ...earlyLegacy(), dukeCaptureUncertain: true };
assert.equal(legacyDukeCaptureHistoryClear(uncertain), false);
assert.equal(dukeCaptureUncertaintyBlock(uncertain), DUKE_CAPTURE_UNCERTAIN_BLOCK);
const knownSafe = { ...earlyLegacy(), dukeCaptureUncertain: false, turn: 2 };
assert.equal(legacyDukeCaptureHistoryClear(knownSafe), false);
assert.equal(dukeCaptureUncertaintyBlock(knownSafe), null);
assert.equal(dukeCaptureUncertaintyBlock({ ...uncertain, advanced: false }), null);
assert.equal(dukeCaptureUncertaintyBlock({ ...uncertain, players: [{ faction: 'ecaz' }] }), null);
});