From 34dccdfd99758730e4dddd49423d29621812722b Mon Sep 17 00:00:00 2001 From: Quintine <15965533+Quintine@users.noreply.github.com> Date: Mon, 26 Oct 2026 08:31:55 +1100 Subject: [PATCH] Allow Ecaz Ambassador Duke acquisition for an existing ally --- components/ecaz-duke-acquisition-choices.tsx | 78 ++++++++ components/ecaz-entry.tsx | 32 +--- docs/CURRENT_STATUS.md | 40 +++- docs/ECAZ_AMBASSADOR_EFFECTS_AUDIT.md | 2 +- docs/ECAZ_DUKE_ACQUISITION.md | 74 ++++++-- docs/IMPLEMENTATION_STATUS.md | 30 +++ docs/RULE_DECISIONS.md | 13 ++ game/bots.ts | 9 +- game/duke-vidal.ts | 6 +- game/ecaz-duke-acquisition.ts | 35 +++- game/engine.ts | 13 +- game/nexus-ecaz-duke.ts | 4 +- game/reference.ts | 9 +- tests/ecaz-duke-acquisition.test.ts | 9 +- tests/ecaz-duke-ally-controls.test.ts | 189 +++++++++++++++++++ tests/ecaz-duke-ally-engine.test.ts | 149 +++++++++++++++ tests/ecaz-duke-ally-quote.test.ts | 131 +++++++++++++ tests/ecaz-duke-bots.test.ts | 26 ++- tests/fixture-ecaz-duke-ally.ts | 171 +++++++++++++++++ tests/nexus-ecaz-duke-captured.test.ts | 12 ++ tests/nexus-ecaz-duke-quote.test.ts | 19 +- 21 files changed, 965 insertions(+), 86 deletions(-) create mode 100644 components/ecaz-duke-acquisition-choices.tsx create mode 100644 tests/ecaz-duke-ally-controls.test.ts create mode 100644 tests/ecaz-duke-ally-engine.test.ts create mode 100644 tests/ecaz-duke-ally-quote.test.ts create mode 100644 tests/fixture-ecaz-duke-ally.ts diff --git a/components/ecaz-duke-acquisition-choices.tsx b/components/ecaz-duke-acquisition-choices.tsx new file mode 100644 index 00000000..1d85e18b --- /dev/null +++ b/components/ecaz-duke-acquisition-choices.tsx @@ -0,0 +1,78 @@ +'use client'; + +import type { Action, GameView } from '@/game/engine'; +import { Button } from './ui/button'; + +export function EcazDukeAcquisitionChoices({ + game, + act, + busy, +}: { + game: GameView; + act: (action: Action) => void; + busy: boolean; +}) { + const entry = game.ambassadorEntry; + if ( + !entry || + entry.stage !== 'offer' || + entry.effect !== 'ecaz' || + entry.owner !== game.me || + game.decision?.kind !== 'ecazAmbassador' || + game.decision.player !== game.me + ) + return null; + + const recipients = entry.beneficiaries.length + ? entry.beneficiaries + : [{ + player: entry.owner, + blocked: entry.dukeAcquisition + ? entry.dukeAcquisition.blocked + : 'Duke acquisition is unavailable for this opportunity.', + }]; + + return ( +
+

+ Acquire Duke Vidal for a listed recipient. The Ambassador returns to + your supply. +

+

+ Provisional ordinary acquisition: the chosen recipient keeps Duke Vidal + until battle use, including when unused beyond this turn, unless another + rule takes him away. The separate Duke loan after a new alliance lasts + for the current turn. Only Ecaz may revive him. +

+ {recipients.map((recipient) => { + const blocked = recipient.blocked; + const disabled = busy || !!blocked; + const reasonId = `ecaz-duke-${entry.event}-${recipient.player}-blocked`; + const name = game.players.find((player) => player.id === recipient.player)?.name ?? recipient.player; + return ( +
+ + {blocked &&

{blocked}

} +
+ ); + })} +
+ ); +} diff --git a/components/ecaz-entry.tsx b/components/ecaz-entry.tsx index e8081f2f..787f6270 100644 --- a/components/ecaz-entry.tsx +++ b/components/ecaz-entry.tsx @@ -13,6 +13,7 @@ import { LeaderInspector } from './leader-inspector'; import { AmbassadorInspector, AMBASSADOR_COVERAGE } from './ecaz-ambassadors'; import { AmbassadorMovement } from './ambassador-movement'; import { AmbassadorShipment } from './ambassador-shipment'; +import { EcazDukeAcquisitionChoices } from './ecaz-duke-acquisition-choices'; const buttonClass = 'min-h-11 whitespace-normal motion-reduce:transition-none'; @@ -51,11 +52,6 @@ export function EcazEntry({ (choice) => !choice.blocked && selected.includes(choice.card.id), ); const legalSelection = entry.effect === 'choam' || chosen.length === 1; - const dukeBlock = - entry.dukeAcquisition?.blocked ?? - (entry.dukeAcquisition - ? null - : 'Duke acquisition is unavailable for this opportunity.'); const allianceBlock = entry.allianceOffer?.blocked ?? (entry.allianceOffer ? null : 'This alliance opportunity is unavailable.'); @@ -84,11 +80,6 @@ export function EcazEntry({ <> {entry.effect === 'ecaz' ? ( <> -

- Acquire Duke Vidal for Ecaz. The Ambassador returns to your - supply. Ecaz keeps Duke until battle use or until Moritani takes - him. -

{duke && ( )} - - {dukeBlock && ( -

- {dukeBlock} -

- )} +