Connect the Basic Ecaz new-ally Duke loan

This commit is contained in:
Quintine committed 2026-10-08 08:30:38 +11:00
1 parent c28e24b5fa
commit 2d0f20b69c
11 files changed
+106 -25

No files matched your search

+2
View File
@@ -1911,6 +1911,8 @@ function policyActions(g: GameView): Action[] {
me.noField.deployed.location.sector !== g.storm)
);
if (entry.stage === 'allianceReply') return [{ ...action, accept: true }];
if (entry.stage === 'loan')
return [{ ...action, loan: !!entry.dukeLoan && !entry.dukeLoan.blocked }];
if (entry.stage === 'revival') {
const revival = entry.revival;
if (!revival || revival.blocked) return [{ ...action, decline: true }];
+5 -4
View File
@@ -76,13 +76,13 @@ export function consumeDuke(state: DukeState): DukeState {
/**
* End-turn custody only. Ecaz's ordinary Ambassador acquisition lasts until
* battle use; the distinct Ecaz Nexus Cunning acquisition lasts this turn.
* Allied loans and captured/ghola release destinations need a verified return contract.
* battle use; Ecaz Nexus Cunning and the Basic Ambassador ally loan last this
* turn. The prototype sets an unused loan aside, pending return-policy refinement.
*/
export function expireDuke(state: DukeState, turn: number): DukeState {
validTurn(turn);
if (
(state.source === 'moritani' || state.source === 'ecazNexus') &&
(state.source === 'moritani' || state.source === 'ecazNexus' || state.source === 'ally') &&
state.acquiredTurn !== null &&
state.acquiredTurn <= turn &&
!state.leader.dead &&
@@ -116,8 +116,9 @@ export const DUKE_VIDAL_RULES = Object.freeze({
revival: 'ecaz-only-set-aside',
ambassadorAcquisition: 'ecaz-self-implemented',
ambassadorAlliance: 'consent-implemented',
ambassadorNewAllyLoan: 'basic-prototype',
ambassadorAllyAcquisition: 'unresolved',
allyLoanReturn: 'unresolved',
allyLoanReturn: 'basic-set-aside-provisional',
sourceAudit: 'docs/DUKE_VIDAL_RULES.md',
}),
});
+42 -4
View File
@@ -1598,6 +1598,7 @@ export type Game = {
stage:
| 'offer'
| 'allianceReply'
| 'loan'
| 'copy'
| 'cards'
| 'income'
@@ -16395,6 +16396,18 @@ function ecazAllianceQuote(g: Game, owner: string, entrant: string) {
throw error;
}
}
function ecazAmbassadorLoanBlock(g: Game, owner: Player, entrant: Player) {
if (g.advanced) return 'This first-version Duke loan is available in Basic.';
if (owner.ally !== entrant.id || entrant.ally !== owner.id)
return 'The new Ecaz alliance is no longer current.';
const duke = g.dukeVidal;
if (!duke || duke.leader.id !== DUKE_VIDAL_ID ||
duke.leader.faction !== 'ecaz' || duke.leader.strength !== 6 ||
duke.leader.dead || duke.leader.concealed ||
duke.leader.capturedBy || duke.leader.gholaBy)
return 'Duke Vidal is unavailable for this loan.';
return duke.controller === owner.id ? null : ecazDukeAcquisitionBlock(g, owner.id);
}
function ecazAllianceIntegrity(g: Game) {
const entry = g.pendingAmbassador;
const continuation = g.pendingTreacheryDiscard?.continuation;
@@ -16414,7 +16427,7 @@ function ecazAllianceIntegrity(g: Game) {
);
return;
}
if (entry.stage !== 'allianceReply') return;
if (entry.stage !== 'allianceReply' && entry.stage !== 'loan') return;
const token = g.ecazAmbassadors?.tokens.find((t) => t.id === entry.token);
requireRule(
g.status === 'playing' &&
@@ -16424,18 +16437,21 @@ function ecazAllianceIntegrity(g: Game) {
typeof entry.event === 'string' &&
entry.event.length > 0 &&
entry.effect === 'ecaz' &&
entry.beneficiary === entry.entrant &&
entry.beneficiary === (entry.stage === 'loan' ? entry.owner : entry.entrant) &&
token?.effect === 'ecaz' &&
token.zone === 'supply' &&
token.location === null &&
controls.some(
(control) =>
control?.decision?.kind === 'ecazAmbassador' &&
control.decision.player === entry.entrant,
control.decision.player === (entry.stage === 'loan' ? entry.owner : entry.entrant),
),
'The Ecaz alliance reply has lost its original Ambassador or decision owner.',
);
ecazAllianceQuote(g, entry.owner, entry.entrant);
if (entry.stage === 'allianceReply') ecazAllianceQuote(g, entry.owner, entry.entrant);
else requireRule(getPlayer(g, entry.owner).ally === entry.entrant &&
getPlayer(g, entry.entrant).ally === entry.owner,
'The Duke loan lost its accepted Ecaz alliance.');
}
function decideAmbassador(g: Game, p: Player, action: Action) {
const entry = g.pendingAmbassador;
@@ -16452,6 +16468,19 @@ function decideAmbassador(g: Game, p: Player, action: Action) {
p.id === (entry.stage === 'offer' ? owner.id : entry.beneficiary),
'The Ambassador choice belongs to another player.',
);
if (entry.stage === 'loan') {
requireRule(typeof action.loan === 'boolean',
'Choose whether to lend Duke Vidal to the new ally.');
if (action.loan) {
const blocked = ecazAmbassadorLoanBlock(g, owner, entrant);
requireRule(!blocked, blocked ?? 'This Duke loan is unavailable.');
g.dukeVidal = acquireDuke(g.dukeVidal!, entrant.id, g.turn, 'ally');
log(g, `${owner.name} lent Duke Vidal to ${entrant.name} for this turn. The Basic prototype allows one battle; an unused living Duke is set aside at turn end.`,
{ faction: 'ecaz', name: 'Ambassador Duke loan' });
}
finishAmbassador(g);
return;
}
if (entry.stage === 'allianceReply') {
requireRule(
typeof action.accept === 'boolean',
@@ -16473,6 +16502,12 @@ function decideAmbassador(g: Game, p: Player, action: Action) {
`${owner.name} and ${entrant.name} formed an alliance through the Ecaz Ambassador. Both were unallied and ${entrant.name} accepted. Their alliance abilities apply immediately; the entrant’s remaining actions resume.`,
{ faction: 'ecaz', name: 'Ambassador alliance' },
);
if (!ecazAmbassadorLoanBlock(g, owner, entrant)) {
entry.stage = 'loan';
entry.beneficiary = owner.id;
g.decision = { kind: 'ecazAmbassador', player: owner.id };
return;
}
} else
log(
g,
@@ -32060,6 +32095,9 @@ export function viewGame(state: Game, id: string) {
id === owner.id
? { blocked: ambassadorEffectBlock(g, 'ecaz', owner, entrant) }
: null,
dukeLoan:
entry.stage === 'loan' && id === owner.id
? { blocked: ecazAmbassadorLoanBlock(g, owner, entrant) } : null,
beneficiaries:
entry.stage === 'offer' && id === owner.id
? g.players
+5 -5
View File
@@ -1316,9 +1316,9 @@ export const RULE_TOPICS: RuleTopic[] = [
'The current closing order is CHOAM market, deferred technology income, then Ecaz placement. Income received at that point can fund placement. This combined order is an implementation choice, not a priority established by a combined official FAQ.',
'The first placement costs one spice, the second two, and so on that turn. A declaration opens a Karama response before spice is paid or the token moves. Canceling stops the remaining placement opportunity for that turn; previous placements and payments remain.',
'Finish Ambassador placement advances the phase. On a later turn the price begins at one again and the earlier placement block expires. Pending declarations survive reload without charging twice.',
'The custody model returns destroyed tokens to supply and returns Ecaz after its trigger. Supported entry effects set regular tokens aside, permanently remove Bene Gesserit after its trigger, and replenish only after all five random tokens have triggered. Storm crossings return tokens from exposed Arrakeen and Carthag after the Shield Wall is destroyed. A Lasgun–shield explosion returns a token only if the explosion actually resolves; an overriding traitor victory leaves it placed. Destruction does not count toward the five triggers. The reusable Ecaz Ambassador can acquire Duke directly for Ecaz; it also offers a consensual alliance, while Duke loans remain unfinished.',
'When an eligible entrant reaches the reusable Ecaz token, Ecaz may choose Acquire Duke Vidal for Ecaz if the shared disc is available. The token immediately returns to supply, its five random companions are unchanged, and the entrant resumes. Ecaz keeps an unused Duke across turns until battle use or Moritani acquisition. Already controlling him, assigning this effect to an ally, loans and Advanced Harkonnen custody remain visibly unavailable while their interactions are unfinished.',
'Instead of acquiring Duke, unallied Ecaz can offer an alliance to an unallied entrant. The entrant accepts or refuses, then resumes its remaining actions. The triggered token returns to supply in either case. Acceptance activates both factions’ alliance abilities immediately and leaves Duke unchanged. An unavailable Duke does not prevent this alliance choice. Duke loans remain unfinished pending their custody ruling.',
'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 crossings and resolved explosions use destruction rather than trigger accounting. The reusable Ecaz token returns to supply after Duke acquisition or an alliance proposal. Basic now also offers a provisional new-ally Duke loan after consent.',
'When an eligible entrant reaches the reusable Ecaz token, Ecaz may acquire the available shared Duke. The token returns to supply and its five companions are unchanged. Unused Ecaz control persists until battle use or Moritani acquisition. Basic also connects the separate optional new-ally loan after an accepted Ambassador alliance; standing ally assignment and exceptional capture remain unfinished.',
'Unallied Ecaz can instead propose an alliance to an unallied entrant. The entrant accepts or refuses; the token returns to supply either way. Acceptance activates alliance abilities immediately. In Basic, Ecaz may then lend an available Duke to this newly allied faction or continue without a loan. The first-version loan allows one battle and sets an unused living Duke aside at turn end; those return/tenure details are provisional, not a resolved publisher ruling. An unavailable Duke never prevents alliance consent.',
'When another eligible faction enters a marked stronghold, Ecaz may trigger its Ambassador for itself or its current ally, or leave the token in place. Ecaz itself, its ally, advisors and the faction matching the physical Ambassador do not trigger it. Entries with competing arrival reactions remain blocked until their ordering is implemented; a rejected entry does not spend or move its pieces.',
'Emperor grants five bank spice. Atreides records the entrant’s current Treachery hand privately for the beneficiary; Harkonnen records one randomly selected held Traitor identity, or Face Dancer identity for a Tleilaxu entrant. These snapshots remain in the recipient’s private inspection history, identify when they were taken, and do not become a live view of later changes. Opening, enlarging or closing them needs no game acknowledgement.',
'CHOAM lets the beneficiary choose any available hand cards, including none, for three spice each. Ixians require one available card to discard before drawing its replacement. Card controls respect projected reservations. After committing an Ixian exchange, a voluntary interruption must leave a card available for its required discard; a queued Truthtrance counts as already committed. A Bene Gesserit trigger permanently removes its token, then the beneficiary chooses an available effect outside the original supply for this group; unsupported copies remain visibly blocked.',
@@ -1354,7 +1354,7 @@ export const RULE_TOPICS: RuleTopic[] = [
area: 'Documentation',
status: 'Partial',
detail:
'Placement, supported entry decisions, private historical inspections, inventory lifecycle and all eleven effect descriptions are available. The Ecaz guide distinguishes consent, token commitment and Duke loans awaiting a custody ruling. Complete timing and interaction coverage remains open.',
'Placement, entry choices, private inspections, token inventory and all eleven effect guides are available. The Basic new-ally Duke loan and its provisional tenure policy are distinguished from Advanced and exceptional-custody work. Complete timing and interaction refinement remains open.',
},
{
area: 'Verification',
@@ -2840,7 +2840,7 @@ export const RULE_TOPICS: RuleTopic[] = [
'Karama can prevent Moritani from acquiring Vidal. That cancellation does not remove him once Moritani already controls him.',
'Ecaz gains Vidal through its Ecaz Ambassador if he is neither in the Tanks, captured nor a ghola. Ecaz keeps him until battle use or Moritani takes him. The Ambassador can instead form a consensual alliance when both factions are unallied; Ecaz may then lend Vidal to that new ally for the turn.',
'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.',
'The current implementation supports Moritani acquisition, direct Ecaz Ambassador self-acquisition, consensual Ambassador alliances, one-battle use and Ecaz-only paid or physical Ghola revival to living set-aside custody. Native ordinary-disc skills can coexist with a separately controlled Duke, without assigning him a skill. Duke loans, reacquisition while already controlled, shared skill assignment and captured-leader interactions remain unfinished. This shared component does not enable complete Ecaz or Moritani games. Inspection does not commit a battle plan.',
'Current development support includes Moritani acquisition, Ecaz Ambassador self-acquisition, consensual alliances, a Basic optional new-ally loan, one-battle use, and Ecaz-only revival to living set-aside custody. The new loan uses provisional one-battle and unused-living turn-end set-aside policies. Native ordinary-disc skills coexist with Duke without assigning him a skill. Advanced loans, shared skill assignment and captured-leader interactions remain for later work; this does not certify complete modes.',
],
related: [
'faction-ecaz',