Allow Ecaz Ambassadors on the placed mobile stronghold
Share dynamic stronghold legality across placement, cancellation, bots and controls. Keep the HMS sheltered, preserve original fees and entry effects, and display its public Ambassador attached to the stronghold interior. Cover genuine Basic/Advanced placement, paid movement entry, card continuations, prevention and native relocation. No Terror or direct-shipment permission is added.
This commit is contained in:
1 parent
659e2a4a9a
commit
52832dc0fd
16 files changed
+1129
-55
No files matched your search
+1
-9
@@ -1814,15 +1814,7 @@ function policyActions(g: GameView): Action[] {
|
||||
ambassadors.nextCost <= (me.spice ?? 0)
|
||||
) {
|
||||
const token = ambassadors.tokens.find((t) => t.zone === 'supply');
|
||||
const target = TERRITORIES.find(
|
||||
(t) =>
|
||||
t.type === 'stronghold' &&
|
||||
t.id !== MOBILE_STRONGHOLD &&
|
||||
!t.sectors.includes(g.storm) &&
|
||||
!ambassadors.tokens.some(
|
||||
(a) => a.zone === 'placed' && a.location === t.id,
|
||||
),
|
||||
);
|
||||
const target = ambassadors.destinations[0];
|
||||
if (token && target)
|
||||
return [{ type: 'decision', token: token.id, territory: target.id }];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
import type { Game, Player } from './engine';
|
||||
import { MOBILE_STRONGHOLD, TERRITORIES, territory } from './board';
|
||||
import { placeAmbassador } from './ecaz-ambassadors';
|
||||
|
||||
export type AmbassadorPlacementBoard = Pick<
|
||||
Game,
|
||||
'turn' | 'storm' | 'mobileStronghold' | 'ecazAmbassadors'
|
||||
>;
|
||||
|
||||
function mobileStrongholdPlaced(g: AmbassadorPlacementBoard): boolean {
|
||||
const pointer = g.mobileStronghold?.location;
|
||||
return (
|
||||
typeof pointer === 'string' &&
|
||||
TERRITORIES.some(
|
||||
(destination) =>
|
||||
destination.type !== 'stronghold' &&
|
||||
destination.sectors.some(
|
||||
(sector) => pointer === `${destination.id}:${sector}`,
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/** Compose E3 p.7's any-stronghold permission with E1 p.9's sheltered HMS.
|
||||
* This is a general-rule composition, not a separate HMS/Ambassador ruling.
|
||||
*/
|
||||
export function ambassadorPlacementDestination(
|
||||
g: AmbassadorPlacementBoard,
|
||||
to: string,
|
||||
): { id: string; stronghold: boolean; inStorm: boolean; allowed: boolean } {
|
||||
const destination = TERRITORIES.find((candidate) => candidate.id === to);
|
||||
const mobile = to === MOBILE_STRONGHOLD && mobileStrongholdPlaced(g);
|
||||
return {
|
||||
id: to,
|
||||
stronghold: mobile || destination?.type === 'stronghold',
|
||||
inStorm: mobile ? false : !destination || destination.sectors.includes(g.storm),
|
||||
allowed: mobile || !!destination,
|
||||
};
|
||||
}
|
||||
|
||||
/** Location choices only; token supply, funds and the placement turn are quoted
|
||||
* separately. Other dynamic locations do not inherit the HMS permission.
|
||||
*/
|
||||
export function ambassadorPlacementDestinations(
|
||||
g: AmbassadorPlacementBoard,
|
||||
): { id: string; name: string; sheltered: boolean }[] {
|
||||
const destinations = TERRITORIES.filter(
|
||||
(destination) => destination.type === 'stronghold',
|
||||
).map(({ id, name }) => ({ id, name, sheltered: false }));
|
||||
if (mobileStrongholdPlaced(g))
|
||||
destinations.push({
|
||||
id: MOBILE_STRONGHOLD,
|
||||
name: territory(MOBILE_STRONGHOLD).name,
|
||||
sheltered: true,
|
||||
});
|
||||
return destinations.filter(({ id }) => {
|
||||
const destination = ambassadorPlacementDestination(g, id);
|
||||
return (
|
||||
destination.stronghold &&
|
||||
destination.allowed &&
|
||||
!destination.inStorm &&
|
||||
!g.ecazAmbassadors?.tokens.some(
|
||||
(token) => token.zone === 'placed' && token.location === id,
|
||||
)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export function quoteEcazAmbassadorPlacement(
|
||||
g: AmbassadorPlacementBoard,
|
||||
owner: Pick<Player, 'spice'>,
|
||||
token: string,
|
||||
to: string,
|
||||
): ReturnType<typeof placeAmbassador> {
|
||||
if (!g.ecazAmbassadors) throw new Error('No Ambassador supply is available.');
|
||||
return placeAmbassador(g.ecazAmbassadors, token, {
|
||||
turn: g.turn,
|
||||
availableSpice: owner.spice,
|
||||
destination: ambassadorPlacementDestination(g, to),
|
||||
});
|
||||
}
|
||||
+6
-18
@@ -143,6 +143,7 @@ import {
|
||||
import { homeworldRevivalDestinations, quoteHomeworldRevivalDestination } from './homeworld-revival-destinations';
|
||||
import { terrorEntrySignature, terrorSelectionSignature, validateTerrorEntrySignature } from './terror-entry-receipt';
|
||||
import { arrivalOverlapSignature, overlapOwnsAmbassadorDiscard, overlapOwnsTerrorDiscard, overlapOwnsWormTerror } from './arrival-overlap';
|
||||
import { ambassadorPlacementDestination, ambassadorPlacementDestinations, quoteEcazAmbassadorPlacement } from './ecaz-ambassador-placement';
|
||||
import { GuildAmbassadorMarkerError, markerDestinations, markerOwner, quoteGuildAmbassadorMarker,
|
||||
type GuildAmbassadorMarker, type GuildAmbassadorMarkerQuote } from './guild-ambassador-no-field';
|
||||
import {
|
||||
@@ -468,7 +469,6 @@ import {
|
||||
import {
|
||||
validateAmbassadors,
|
||||
createAmbassadors,
|
||||
placeAmbassador,
|
||||
triggerAmbassador,
|
||||
destroyAmbassador,
|
||||
replenishAmbassadors,
|
||||
@@ -7119,12 +7119,9 @@ function treacheryDiscardIntegrity(g: Game) {
|
||||
['choam', 'ixians'].includes(entry.effect!) &&
|
||||
['none', 'wormRide'].includes(entry.resume) &&
|
||||
validAmbassadorResume(g, entry) &&
|
||||
TERRITORIES.some(
|
||||
(t) =>
|
||||
t.id === entry.territory &&
|
||||
t.type === 'stronghold' &&
|
||||
t.sectors.includes(entry.sector),
|
||||
) &&
|
||||
ambassadorPlacementDestination(g, entry.territory).allowed &&
|
||||
ambassadorPlacementDestination(g, entry.territory).stronghold &&
|
||||
validGameLocation(g, entry.territory, entry.sector) &&
|
||||
owner?.faction === 'ecaz' &&
|
||||
beneficiary &&
|
||||
(beneficiary.id === owner.id ||
|
||||
@@ -21167,19 +21164,9 @@ function ambassadorPlacementQuote(
|
||||
token: string,
|
||||
to: string,
|
||||
) {
|
||||
const destination = TERRITORIES.find((t) => t.id === to);
|
||||
requireRule(g.ecazAmbassadors, 'No Ambassador supply is available.');
|
||||
try {
|
||||
return placeAmbassador(g.ecazAmbassadors, token, {
|
||||
turn: g.turn,
|
||||
availableSpice: owner.spice,
|
||||
destination: {
|
||||
id: to,
|
||||
stronghold: destination?.type === 'stronghold',
|
||||
allowed: !!destination,
|
||||
inStorm: !destination || destination.sectors.includes(g.storm),
|
||||
},
|
||||
});
|
||||
return quoteEcazAmbassadorPlacement(g, owner, token, to);
|
||||
} catch (error) {
|
||||
throw new RuleError(
|
||||
error instanceof Error ? error.message : 'Invalid Ambassador placement.',
|
||||
@@ -36277,6 +36264,7 @@ export function viewGame(state: Game, id: string) {
|
||||
ambassadors: g.ecazAmbassadors
|
||||
? {
|
||||
tokens: g.ecazAmbassadors.tokens.map((token) => ({ ...token })),
|
||||
destinations: ambassadorPlacementDestinations(g),
|
||||
nextCost:
|
||||
(g.ecazAmbassadors.placement?.turn === g.turn
|
||||
? g.ecazAmbassadors.placement.count
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { terrorLocationAllowed, type TerrorLocationContext } from './terror-location';
|
||||
import type { Game, ResponseWindow } from './engine';
|
||||
import { TERRITORIES } from './board';
|
||||
import { ambassadorPlacementDestination } from './ecaz-ambassador-placement';
|
||||
import {
|
||||
blockAmbassadorPlacement,
|
||||
type AmbassadorState,
|
||||
@@ -31,6 +31,8 @@ export type PlacementCancellationContext = Pick<
|
||||
| 'status'
|
||||
| 'phase'
|
||||
| 'turn'
|
||||
| 'storm'
|
||||
| 'mobileStronghold'
|
||||
| 'players'
|
||||
| 'pendingEcazPlacement'
|
||||
| 'ecazPlacementTurn'
|
||||
@@ -89,9 +91,8 @@ export function quotePlacementCancellation(
|
||||
Array.isArray(state.tokens) &&
|
||||
Array.isArray(state.cohort) &&
|
||||
state.tokens.filter((t) => t.id === pending.token).length === 1 &&
|
||||
TERRITORIES.some(
|
||||
(t) => t.id === pending.territory && t.type === 'stronghold',
|
||||
) &&
|
||||
ambassadorPlacementDestination(g, pending.territory).allowed &&
|
||||
ambassadorPlacementDestination(g, pending.territory).stronghold &&
|
||||
whole(pending.cost) &&
|
||||
pending.cost > 0,
|
||||
'No current physical Ambassador placement is awaiting cancellation.',
|
||||
|
||||
+12
-8
@@ -1543,43 +1543,47 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
'During Shipment and Movement, Guild can use its special Karama to stop another eligible off-planet Ambassador shipment before forces arrive; it does not stop this grant during a phase-one worm sequence. A successful off-planet shipment may invite Bene Gesserit accompaniment and accrues applicable phase-five Heighliner income once. BG can accompany Ixians into the mobile stronghold. Projected accompaniment choices show blocked sectors and keep Polar Sink and decline available when legal. Intrusion, separate Terror and accompanying fighter-triggered Ambassadors finish before the original entrant or worm rider resumes.',
|
||||
'In bare Basic Richese/Ecaz tables, No-Field may replace the whole Guild grant under an explicit provisional policy: concealed native0/3/5 counts as one and ships free; allied use reveals immediately within four available counters and keeps the one/two-spice transport payment to Guild or Bank. Richese chooses its token/payer and the beneficiary must accept before transfer. Prior native revelation, shared token history and physical fallback after scoped prevention remain; ordinary allowances are unchanged. Advanced/optional profiles remain separate. In bounded classic plus Ecaz/Moritani games, a shared Ambassador/Terror entry gives both owners saved optional prompts in current storm order without repeating the committed entry; this is a provisional table order, not a printed publisher priority. A Terror entry now waits for the arrival’s own interaction — Guild income, a Bene Gesserit fighter Intrusion or advisor choice, or Fremen storm protection — and then opens without replaying payment or force transfer. Ambassador-created child arrivals retain their own contracts. The native-module extension now composes ordinary paid arrival order with Atomics, Homeworlds and Nexus; other combinations and complete rules remain unfinished.',
|
||||
'The Tleilaxu Ambassador returns one eligible own leader or up to four physical forces free. First-death leaders and the first-killed Atreides KH need not wait for the ordinary cohort. Under the provisional repeat interpretation, later native leaders must satisfy their ordinary death condition, and later KH returns must meet their saved next-cohort marker. Preserve the actual opening before resurrection; KH advances its marker beyond both owner and old KH cycles without resetting activation losses or adding a native disc. Ecaz retains native identity bookkeeping. Ecaz may instead return the shared Duke alive and unclaimed under his independent permission; its ally cannot inherit that permission. Native and BG copied grants share eligibility and exclusive alternatives. A positive return provisionally earns seated Tleilaxu one bank spice per beneficiary/turn, sharing ordinary free revival; Ghola stays separate. Canceling income keeps the return and spent entitlement. Ordinary allowance independence and wider custody/module combinations remain provisional.',
|
||||
'All eleven effect guides are readable. Emperor, Atreides, Harkonnen, CHOAM, Ixian, Richese, Fremen and Guild entry effects are integrated, together with Bene Gesserit copies of those eight effects and direct Duke acquisition or consensual alliance through the reusable Ecaz token. Advanced combined Occupy has a separate bounded Development Verified guide, and Basic even-force Occupy now composes the same shared controls. Remaining effects, exceptional Duke custody and complete Ecaz rules remain unfinished. Mobile stronghold placement is gated. Normal public Ecaz starts remain development previews; complete rules and unsupported combinations stay gated.',
|
||||
'All eleven effect guides are readable. Emperor, Atreides, Harkonnen, CHOAM, Ixian, Richese, Fremen and Guild entry effects are integrated, together with Bene Gesserit copies of those eight effects and direct Duke acquisition or consensual alliance through the reusable Ecaz token. Advanced combined Occupy has a separate bounded Development Verified guide, and Basic even-force Occupy now composes the same shared controls. Remaining effects, exceptional Duke custody and complete Ecaz rules remain unfinished. Ambassadors can be placed on the genuinely placed Hidden Mobile Stronghold, including when its pointer is in storm: the interior is sheltered. Its face-up marker stays attached through Ixian movement. Placement still pays the increasing fee and can be prevented by Karama. This supplies no Terror or direct-shipment permission. Normal public Ecaz starts remain development previews; complete rules and unsupported combinations stay gated.',
|
||||
],
|
||||
checklist: [
|
||||
{
|
||||
area: 'Implementation',
|
||||
status: 'Partial',
|
||||
detail:
|
||||
'Basic Guild No-Field now connects native concealment, allied request/supply/consent, immediate materialization, provisional transport payment, source-scoped prevention and original arrival. A shared ordinary death-cohort quote admits eligible native/KH repeats without importing normal phase/slot/price or native early-service gates into the independent grant. Preserve actual openings before native resurrection, retain Ecaz identity tracking and advance KH above its actual cycle while keeping five native discs and loss activation. Existing Duke, source-bound income and original entrant continuation remain; wider custody/modules stay partial.',
|
||||
evidence: ['game/engine.ts', 'game/guild-ambassador-no-field.ts', 'game/guild-ambassador-continuation.ts', 'game/revival.ts', 'game/tleilaxu-ambassador-leaders.ts', 'game/tleilaxu-ambassador-income.ts', 'game/karama-context.ts', 'game/ecaz-duke-revival.ts', 'game/bot-arrival.ts'],
|
||||
'Placed HMS now shares one eligible-destination quote across paid placement, projection, cancellation and minimal bots; its CHOAM/Ix card parents retain the actual entrant. Basic Guild No-Field now connects native concealment, allied request/supply/consent, immediate materialization, provisional transport payment, source-scoped prevention and original arrival. A shared ordinary death-cohort quote admits eligible native/KH repeats without importing normal phase/slot/price or native early-service gates into the independent grant. Preserve actual openings before native resurrection, retain Ecaz identity tracking and advance KH above its actual cycle while keeping five native discs and loss activation. Existing Duke, source-bound income and original entrant continuation remain; wider custody/modules stay partial.',
|
||||
evidence: ['game/ecaz-ambassador-placement.ts', 'game/engine.ts', 'game/guild-ambassador-no-field.ts', 'game/guild-ambassador-continuation.ts', 'game/revival.ts', 'game/tleilaxu-ambassador-leaders.ts', 'game/tleilaxu-ambassador-income.ts', 'game/karama-context.ts', 'game/ecaz-duke-revival.ts', 'game/bot-arrival.ts'],
|
||||
},
|
||||
{
|
||||
area: 'Player controls',
|
||||
status: 'Partial',
|
||||
detail:
|
||||
'Persistent No-Field controls show exact marker/destination/payment choices, provisional guidance, busy reasons and both refusal routes, including a native zero-reserve grant. Persistent free-return guidance explains first-return and later-cohort eligibility, KH’s separate marker, ordinary-slot independence and the provisional shared income. Original exact native/KH/Duke buttons, busy controls, force alternatives and committed-return/income continuation remain. Earlier mobile arrival evidence stays separate.',
|
||||
evidence: ['components/ecaz-entry.tsx', 'components/ambassador-shipment.tsx', 'components/ambassador-no-field.tsx', 'components/game-table.tsx'],
|
||||
'Placement controls show the available sheltered HMS; public face-up board markers stay on its interior through native relocation. Persistent No-Field controls show exact marker/destination/payment choices, provisional guidance, busy reasons and both refusal routes, including a native zero-reserve grant. Persistent free-return guidance explains first-return and later-cohort eligibility, KH’s separate marker, ordinary-slot independence and the provisional shared income. Original exact native/KH/Duke buttons, busy controls, force alternatives and committed-return/income continuation remain. Earlier mobile arrival evidence stays separate.',
|
||||
evidence: ['components/ecaz-ambassadors.tsx', 'components/ambassador-board-markers.tsx', 'components/ecaz-entry.tsx', 'components/ambassador-shipment.tsx', 'components/ambassador-no-field.tsx', 'components/game-table.tsx'],
|
||||
},
|
||||
{
|
||||
area: 'AI',
|
||||
status: 'Partial',
|
||||
detail:
|
||||
'Eight new existing-profile continuations accept12 legal proposals from native-empty or allied-request states, without strategy/calibration. Twelve existing legal profiles select repeated native/copy/KH choices and finish the original entrant, using server-private eligibility. Earlier original Duke/income and placement/consent evidence remains separate. No strategy refinement or strength calibration.',
|
||||
'Eight HMS-entry existing-profile continuations accept eight legal proposals across Basic and Advanced. Eight earlier No-Field existing-profile continuations accept12 legal proposals from native-empty or allied-request states, without strategy/calibration. Twelve existing legal profiles select repeated native/copy/KH choices and finish the original entrant, using server-private eligibility. Earlier original Duke/income and placement/consent evidence remains separate. No strategy refinement or strength calibration.',
|
||||
evidence: ['game/bots.ts', 'tests/ambassador-revival-income-engine.test.ts', 'tests/duke-ambassador-engine.test.ts', 'docs/TLEILAXU_AMBASSADOR_RULES.md'],
|
||||
},
|
||||
{
|
||||
area: 'Documentation',
|
||||
status: 'Partial',
|
||||
detail:
|
||||
'Guild No-Field now distinguishes the selected Basic cap/fee policy from printed authority, independent allowances and remaining Advanced/module gates. Source-bound effects, copies and Duke guides remain. Native own-arrival child-first/live-token order stays provisional. Separate native transport permission, independent purchased typed prices and own/allied marker billing preserve original sources and approvals. Immune Junction now keeps its offered tariff/payees with an explicit unused-purchase choice. Cunning opportunity scope, special-stop card return and unused-price expiry are selected interpretations; full combinations remain open.',
|
||||
'The placed-HMS permission records general stronghold/storm-shelter source composition without implying a designer-specific ruling or new transport/Terror permission. Guild No-Field now distinguishes the selected Basic cap/fee policy from printed authority, independent allowances and remaining Advanced/module gates. Source-bound effects, copies and Duke guides remain. Native own-arrival child-first/live-token order stays provisional. Separate native transport permission, independent purchased typed prices and own/allied marker billing preserve original sources and approvals. Immune Junction now keeps its offered tariff/payees with an explicit unused-purchase choice. Cunning opportunity scope, special-stop card return and unused-price expiry are selected interpretations; full combinations remain open.',
|
||||
evidence: ['docs/ECAZ_AMBASSADORS_RULES.md', 'docs/GUILD_AMBASSADOR_IMPLEMENTATION.md', 'docs/GUILD_AMBASSADOR_RULES.md', 'docs/TLEILAXU_AMBASSADOR_RULES.md', 'docs/DEFERRED_SHIPMENT_ARRIVAL.md', 'docs/BOT_ARRIVAL_PREFLIGHT.md'],
|
||||
},
|
||||
{
|
||||
area: 'Verification',
|
||||
status: 'Partial',
|
||||
detail:
|
||||
'Current Basic Guild-marker proof passes13 files129/129, types/lint/build, fifteen gameplay/control/GameTable SSR journeys and eight minimal-policy continuations. Genuine53/71/74-action sources plus two explicitly conserved shortage arrangements cover native0/3/5, prior reveal, consent, Guild/Bank fee, prevention/refusal and physical fallback. Earlier repeat13-file114/114, types/lint/build and three controlled gameplay/rendering programmes pass: original Basic paid-entry sources plus assigned native histories reach the next actual ordinary Revival; separate Advanced actual entry returns KH with old2/cohort3 → marker4. Twelve legal policies finish original entry. First-opening, income allow/cancel and ordinary controls pass. Controlled histories are not earned repeat-battle/full-game proof. Earlier original Duke/income evidence stays separate; full modes, wider combinations, assurance and deployed acceptance remain open.',
|
||||
'Placed-HMS proof passes12 files135/135, types/lint/build, twelve actual placement-control/gameplay/GameTable SSR journeys and eight minimal policies. Actual46/50-action Basic/Advanced sources cover paid placement, entry/Emperor5/decline, held Karama, storm shelter and native relocation; Basic CHOAM/Ix retain original card continuations. Browser/deployed and complete-mode acceptance remain open. Earlier Basic Guild-marker proof passes13 files129/129, types/lint/build, fifteen gameplay/control/GameTable SSR journeys and eight minimal-policy continuations. Genuine53/71/74-action sources plus two explicitly conserved shortage arrangements cover native0/3/5, prior reveal, consent, Guild/Bank fee, prevention/refusal and physical fallback. Earlier repeat13-file114/114, types/lint/build and three controlled gameplay/rendering programmes pass: original Basic paid-entry sources plus assigned native histories reach the next actual ordinary Revival; separate Advanced actual entry returns KH with old2/cohort3 → marker4. Twelve legal policies finish original entry. First-opening, income allow/cancel and ordinary controls pass. Controlled histories are not earned repeat-battle/full-game proof. Earlier original Duke/income evidence stays separate; full modes, wider combinations, assurance and deployed acceptance remain open.',
|
||||
evidence: [
|
||||
'tests/fixture-ecaz-hms-ambassador.ts',
|
||||
'tests/ecaz-hms-ambassador-engine.test.ts',
|
||||
'tests/ecaz-hms-ambassador-controls.test.tsx',
|
||||
'tests/ecaz-hms-placement-quote.test.ts',
|
||||
'tests/ambassador-repeat-leader-engine.test.ts',
|
||||
'tests/fixture-ambassador-repeat-leaders.ts',
|
||||
'tests/tleilaxu-ambassador-leaders.test.ts',
|
||||
|
||||
Reference in new issue
Block a user