Resolve Duke uncertainty on public events
Verify and publish checkpoint container / container (push) Canceled after 0s
Verify and publish checkpoint container / container (push) Canceled after 0s
This commit is contained in:
1 parent
57e7611468
commit
59cb336c80
14 files changed
+360
-25
No files matched your search
@@ -11,7 +11,7 @@ export type DukeCapturePublicSource = {
|
||||
|
||||
/** Called for every completed Harkonnen capture, regardless of its concealed
|
||||
* selection. Once the public cannot rule out Duke, keep the uncertainty until
|
||||
* a separate public resolution. An unstamped legacy game remains unstamped:
|
||||
* a separate public resolution explicitly clears it. An unstamped legacy game remains unstamped:
|
||||
* its past capture history cannot be reconstructed from current state. */
|
||||
export function nextDukeCaptureUncertainty(
|
||||
marker: boolean | undefined,
|
||||
|
||||
@@ -13,7 +13,7 @@ export type EcazDukeContext = Omit<Partial<FactionModuleProfile>, 'players'> & {
|
||||
dukeVidal?: DukeState;
|
||||
dukeCaptureUncertain?: boolean;
|
||||
};
|
||||
type Failure = 'context' | 'custody' | 'table' | 'captureUncertain' | 'alreadyControlled' | 'sameHolderTable';
|
||||
type Failure = 'context' | 'custody' | 'table' | 'captureUncertain' | 'alreadyControlled';
|
||||
export class EcazDukeAcquisitionError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
@@ -34,8 +34,6 @@ const whole = (n: unknown): n is number =>
|
||||
Number.isSafeInteger(n) && (n as number) >= 0;
|
||||
const tableBlock =
|
||||
'Duke Vidal acquisition with Advanced Harkonnen is still being implemented.';
|
||||
const sameHolderTableBlock =
|
||||
'Same-holder Duke acquisition with Advanced Harkonnen is not available in this preview.';
|
||||
const alreadyBlock =
|
||||
'Ecaz already controls Duke Vidal through its Ambassador.';
|
||||
const allyAlreadyBlock = 'The chosen beneficiary already controls Duke Vidal through the Ambassador.';
|
||||
@@ -97,12 +95,6 @@ export function quoteEcazDukeAcquisition(
|
||||
whole(duke.leader.deaths),
|
||||
'The shared Duke Vidal disc has invalid physical identity or history.',
|
||||
);
|
||||
// Harkonnen may privately capture Duke without changing his public prior
|
||||
// controller. Keep same-holder availability independent of that hidden
|
||||
// custody until a public-safe capture continuation is implemented.
|
||||
requireDuke(duke.controller !== recipientId || !g.advanced ||
|
||||
!g.players.some(player => player.faction === 'harkonnen'),
|
||||
sameHolderTableBlock, 'sameHolderTable');
|
||||
// A direct Ambassador grant is already in force for this recipient. Other
|
||||
// sources may be converted to this until-battle tenure without moving or
|
||||
// recreating the disc. Check public tenure before private custody details.
|
||||
@@ -144,7 +136,6 @@ export function ecazDukeAcquisitionBlock(
|
||||
if (error instanceof EcazDukeAcquisitionError) {
|
||||
if (error.reason === 'captureUncertain') return dukeCaptureUncertaintyBlock(g);
|
||||
if (error.reason === 'table') return tableBlock;
|
||||
if (error.reason === 'sameHolderTable') return sameHolderTableBlock;
|
||||
if (error.reason === 'alreadyControlled')
|
||||
return recipientId === ownerId ? alreadyBlock : allyAlreadyBlock;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { selectedPhaseModuleRoster, type FactionModuleProfile } from './faction-module-profile';
|
||||
|
||||
/** Public, monotonic uncertainty about a possible concealed Duke capture.
|
||||
* Fresh games explicitly begin false. True, absent, or malformed values never
|
||||
* establish safety; in particular, legacy saves are not silently converted. */
|
||||
/** 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. */
|
||||
export type DukeCapturePolicyContext = Omit<Partial<FactionModuleProfile>, 'players'> & {
|
||||
advanced: boolean;
|
||||
players: readonly { faction: string }[];
|
||||
|
||||
+14
-1
@@ -3887,6 +3887,7 @@ function playNexusEcazDuke(g: Game, p: Player, action: Action) {
|
||||
(g.nexusEcazDukeEvents ??= []).push(receipt.event);
|
||||
log(g, `${p.name} spent Ecaz Nexus Cunning to take Duke Prad Vidal for this turn. The shared leader remains physically unique and returns after use or at turn end.`,
|
||||
{ faction: p.faction, name: 'Ecaz Nexus Cunning' });
|
||||
g.dukeCaptureUncertain = false;
|
||||
}
|
||||
|
||||
function playMoritaniBetrayal(g: Game, p: Player, action: Action) {
|
||||
@@ -27441,6 +27442,7 @@ function decideAmbassador(g: Game, p: Player, action: Action) {
|
||||
}
|
||||
log(g, `${p.name} revived ${name} for free through the Tleilaxu Ambassador.${selected === DUKE_VIDAL_ID ? ' The living shared disc is set aside, unclaimed.' : ''} The ordinary leader allowance is unchanged.`,
|
||||
{ faction: 'tleilaxu', name: 'Tleilaxu Ambassador' });
|
||||
if (selected === DUKE_VIDAL_ID) g.dukeCaptureUncertain = false;
|
||||
finishAmbassadorRevival(g, p, returned);
|
||||
return;
|
||||
}
|
||||
@@ -28058,6 +28060,7 @@ function decideTerror(g: Game, p: Player, action: Action) {
|
||||
finishLeaderSkillCustody(g);
|
||||
p.spice += bounty;
|
||||
log(g, `${victim.name} was assassinated; ${p.name} received ${bounty} spice.`);
|
||||
if (victim.id === DUKE_VIDAL_ID) g.dukeCaptureUncertain = false;
|
||||
} else {
|
||||
log(g, `${p.name} revealed Assassination, but the entrant controlled no living leader. No leader died and no spice was awarded.`);
|
||||
}
|
||||
@@ -31308,6 +31311,7 @@ function claimHomeworldEcazDuke(g: Game, actor: string, event: unknown, recipien
|
||||
(g.homeworldEcazDukeGrants ??= []).push(receipt);
|
||||
log(g, `${getPlayer(g, actor).name} granted the living Duke Vidal to ${getPlayer(g, recipient).name} through occupied Ecaz. This provisional grant permits one battle and ends unused at turn end; only Ecaz may revive him.`,
|
||||
{ faction: getPlayer(g, actor).faction, name: 'Occupied Ecaz Duke grant' });
|
||||
g.dukeCaptureUncertain = false;
|
||||
}
|
||||
function reconcileHomeworldEcazDuke(g: Game) {
|
||||
if (g.status !== 'playing' || !g.homeworldOccupationPreview || !byFaction(g, 'ecaz') || homeworldEcazDukeBoundaryBlocked(g)) return;
|
||||
@@ -32216,6 +32220,7 @@ function playResidualPoison(
|
||||
`${p.name} played Residual Poison against ${target.name}. ${victim.name} was randomly selected from the available leaders and sent to the Tanks. No spice is awarded, no forces are lost, and battle preparation continues.`,
|
||||
{ faction: p.faction, name: 'Residual Poison' },
|
||||
);
|
||||
if (victim.id === DUKE_VIDAL_ID) g.dukeCaptureUncertain = false;
|
||||
if (semutaAdvancedNativeProducerEnabled(g)) {
|
||||
const continuation: ResidualPoisonDiscardContinuation = {
|
||||
kind: 'residualPoisonDiscard', player: p.id, target: target.id,
|
||||
@@ -33118,6 +33123,7 @@ function applyGholaEffect(g: Game, p: Player, action: Action, cardId?: string) {
|
||||
g,
|
||||
`${p.name} revived ${l.name} with Ghola.${l.id === DUKE_VIDAL_ID ? ' The living shared disc is set aside, unclaimed.' : ' The leader may fight again this turn;'} No spice or normal leader-revival allowance was spent.`,
|
||||
);
|
||||
if (l.id === DUKE_VIDAL_ID) g.dukeCaptureUncertain = false;
|
||||
} else {
|
||||
const n = integer(
|
||||
action.amount ?? Math.min(5, p.tanks),
|
||||
@@ -35627,8 +35633,12 @@ function resolveBattle(g: Game, retreat?: DiplomatRetreatSelection) {
|
||||
if (choices.length)
|
||||
g.pendingTech = { player: winner.id, loser: loser.id, choices };
|
||||
}
|
||||
if (g.dukeVidal && [ap.leader, dp.leader].includes(DUKE_VIDAL_ID))
|
||||
if (g.dukeVidal && [ap.leader, dp.leader].includes(DUKE_VIDAL_ID)) {
|
||||
g.dukeVidal = consumeDuke(g.dukeVidal);
|
||||
// Revealed battle plans publicly establish the unique disc's location.
|
||||
// A subsequent Harkonnen capture may make the result uncertain again.
|
||||
if (b.revealed) g.dukeCaptureUncertain = false;
|
||||
}
|
||||
g.lastBattle = [a.id, d.id];
|
||||
const battleEvent = b.event ?? g.pendingAuditor?.event ?? crypto.randomUUID();
|
||||
const inspection: NexusChoamInspectionReceipt | null =
|
||||
@@ -37281,6 +37291,7 @@ function finishRevival(
|
||||
`${p.name} revived ${leader!.name}${revival.kind === 'foreignGhola' ? ' as a ghola' : ''}.${sharedDuke ? ' The living shared disc is set aside, unclaimed.' : ''}`,
|
||||
{ faction: p.faction, name: 'Revival' },
|
||||
);
|
||||
if (sharedDuke) g.dukeCaptureUncertain = false;
|
||||
}
|
||||
}
|
||||
g.pendingRevival = null;
|
||||
@@ -38500,6 +38511,7 @@ function finishResponse(g: Game, canceled: boolean, settlement?: BureaucratSettl
|
||||
} else if (!canceled && !g.dukeVidal.leader.dead) {
|
||||
g.dukeVidal = acquireDuke(g.dukeVidal, response.owner, g.turn, 'moritani');
|
||||
log(g, `${getPlayer(g, response.owner).name} acquired Duke Vidal for one battle this turn.`);
|
||||
g.dukeCaptureUncertain = false;
|
||||
} else if (!canceled) {
|
||||
// A qualified public acquisition resolves the concealed disc's availability;
|
||||
// never revive a privately executed Duke or leak it through a private-only gate.
|
||||
@@ -38507,6 +38519,7 @@ function finishResponse(g: Game, canceled: boolean, settlement?: BureaucratSettl
|
||||
'The unavailable acquisition lost the original executed Duke return.');
|
||||
g.dukeVidal = consumeDuke(g.dukeVidal);
|
||||
log(g, `${getPlayer(g, response.owner).name}'s Duke acquisition resolved with the shared disc in the Tanks. No acquisition occurred.`);
|
||||
g.dukeCaptureUncertain = false;
|
||||
} else
|
||||
log(g, 'Karama prevented Moritani from acquiring Duke Vidal this turn.');
|
||||
nextPhase(g);
|
||||
|
||||
+2
-2
@@ -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 for the rest of the current prototype, regardless of which leader was secretly captured. Older games without this capture record use the same conservative guard. Before such uncertainty, supported plain faction games can use both grants. 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 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.',
|
||||
'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.',
|
||||
@@ -4157,7 +4157,7 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
'Duke Vidal is separate from both factions’ five ordinary leaders. His Ecaz identity does not mean Ecaz currently controls him. Inspect the shared disc to see its current controller, whether it is set aside, or whether it is in the Tanks.',
|
||||
'At the end of Shipping and Movement, Moritani gains Vidal if it faces battles in at least two strongholds, excluding battles involving Ecaz, and Vidal is not in the Tanks. The printed rule takes him from the current controller. Moritani has him for one battle; it may acquire him again on a later turn by meeting the condition again. At turn end, set him aside unless he is in the Tanks or captured.',
|
||||
'Karama can prevent Moritani from acquiring Vidal. That cancellation does not remove him once Moritani already controls him.',
|
||||
'Ecaz may acquire Vidal through a triggered Ambassador for itself or its current reciprocal ally outside Tanks/captured/ghola custody. Ordinary acquisition lasts until battle use, including unused control across turns; this shared tenure is provisional. A current recipient under a different temporary tenure may provisionally renew control through the Ambassador; Advanced Harkonnen same-holder cases remain guarded. The alternative forms a consensual alliance while both factions are unallied. Basic can then lend Duke optionally; the supplied Advanced physical p30 adds an available Duke to the new ally automatically and sets an unused disc aside at turn end.',
|
||||
'Ecaz may acquire Vidal through a triggered Ambassador for itself or its current reciprocal ally outside Tanks/captured/ghola custody. Ordinary acquisition lasts until battle use, including unused control across turns; this shared tenure is provisional. A current recipient under a different temporary tenure may provisionally renew control through the Ambassador, including with Advanced Harkonnen when public capture history is clear. The alternative forms a consensual alliance while both factions are unallied. Basic can then lend Duke optionally; the supplied Advanced physical p30 adds an available Duke to the new ally automatically and sets an unused disc aside at turn end.',
|
||||
'Only Ecaz may revive Vidal, including with Ghola Treachery. Ecaz may revive him for five spice regardless of how many of its leaders are in the Tanks. Five spice is his special revival cost; his printed battle strength is six. Ecaz may begin ordinary leader revival when five leaders are in the Tanks, counting Vidal, even if it still holds another leader.',
|
||||
'Ordinary Ecaz returns now use an explicit provisional native cohort. Five actual Tanked discs including Duke, or the existing unavailable-native-roster exception, opens it; completed native IDs preserve the remainder and later openings. A late first death can join a later cohort. Independent Duke deaths do not set or hold its ordinary number, and a real Duke resurrection preserves an already satisfied opening. Original native prices and one ordinary leader slot remain.',
|
||||
'An unallied native Ecaz holder may spend its actual Ecaz Nexus at a quiet boundary in any phase to take Duke for the current turn, including original captured custody or the set-aside disc in Tanks. Finish committed interactions first. One battle or unused turn expiry sets him aside. Nexus liberation permanently clears captivity; it does not return him to a former captor. Actual Ecaz Ghola returns him alive and set aside before a separate Cunning play.',
|
||||
|
||||
Reference in new issue
Block a user