Apply Low Ecaz opponent-owned battle lead choice

This commit is contained in:
Quintine committed 2026-10-25 21:04:38 +11:00
1 parent 96de962622
commit 33523d1926
15 files changed
+597 -17

No files matched your search

+22
View File
@@ -0,0 +1,22 @@
import type { Game } from './engine';
import { HomeworldCustodyError } from './homeworld-custody';
import { homeworldContext } from './homeworld-game';
import { homeworldPopulations } from './homeworld-population';
export type EcazBattleLeadQuote = { player: string; lowEcaz?: true };
/** Quote the printed Low Ecaz chooser at battle opening. The engine keeps this
* original owner on the decision; later population changes do not reopen it. */
export function quoteEcazBattleLead(
context: Pick<Game, 'advanced' | 'players' | 'homeworlds'>,
ecazId: string,
opponentId: string,
): EcazBattleLeadQuote {
if (!context.players.some(player => player.id === ecazId && player.faction === 'ecaz') ||
ecazId === opponentId || !context.players.some(player => player.id === opponentId))
throw new HomeworldCustodyError('The combined battle needs its actual native Ecaz and a distinct seated opponent.');
if (!context.homeworlds?.custody) return { player: ecazId };
const low = homeworldPopulations(homeworldContext(context), context.homeworlds.custody)
.some(home => home.native === ecazId && home.card === 'ecaz' && home.side === 'low');
return low ? { player: opponentId, lowEcaz: true } : { player: ecazId };
}
+2 -1
View File
@@ -19,9 +19,10 @@ export function ecazOccupyLeadOptions(game: GameView) {
const battle = game.battle;
if (decision?.kind !== 'ecazBattleLead' || !battle?.ecazOccupy ||
decision.event !== battle.event || decision.event !== battle.ecazOccupy.event) return [];
const chooser = game.players.find(player => player.id === decision.player)?.name ?? decision.player;
const blocked = game.status !== 'playing' || game.phase !== 6
? 'This battle is not awaiting a lead choice.'
: decision.player !== game.me ? 'Only Ecaz chooses the lead.' : null;
: decision.player !== game.me ? `Only ${chooser} chooses the lead.` : null;
return decision.choices.map((lead) => ({
lead,
label: game.players.find((player) => player.id === lead)?.name ?? lead,
+9 -3
View File
@@ -59,6 +59,7 @@ import { quoteNexusMoritaniRetention, nexusMoritaniRetentionSignature, type Nexu
import { quoteNexusChoamBetrayal, validateNexusChoamBetrayal, nexusChoamBetrayalSignature, type NexusChoamBetrayal } from './nexus-choam-betrayal';
import { quoteEcazInquiry, ecazInquiryAnswer, ecazInquiryHistory, ecazInquirySignature, validateEcazInquiry, type EcazInquiryReceipt } from './nexus-ecaz-inquiry';
import { nexusCleanPlayBlocked } from './nexus-play-boundary';
import { quoteEcazBattleLead } from './ecaz-homeworld-lead';
import { sourceForRicheseCunning, richeseCunningRateMatches, validRicheseCunningRateSource, RicheseCunningRateError, type RicheseCunningRateSource } from './richese-cunning-rate';
import { EMPEROR_NEXUS_REVIVALS, emperorNexusLegacyRevivalModeSupported, quoteEmperorNexusRevival, createEmperorNexusRevival, settleEmperorNexusRevival, validateEmperorNexusRevival, type NexusEmperorRevival } from './nexus-emperor-secret-ally';
import { emperorNexusPurchaseEvent, quoteEmperorNexusPurchase, signEmperorNexusPurchase, validateEmperorNexusPurchase, emperorNexusBankPurchaseModeSupported, emperorNexusSellerPurchaseModeSupported, quoteEmperorNexusSellerPurchase, createEmperorNexusSellerPurchase, emperorNexusAmbassadorPurchaseModeSupported, quoteEmperorNexusAmbassadorPurchase, createEmperorNexusAmbassadorPurchase, quoteEmperorNexusSpecialPurchase, createEmperorNexusSpecialPurchase, type EmperorNexusPurchase } from './nexus-emperor-purchase';
@@ -880,7 +881,7 @@ export type Auction = {
allyPayment?: number;
};
export type Decision =
| { kind: 'ecazBattleLead'; player: string; event: string; choices: string[] }
| { kind: 'ecazBattleLead'; player: string; event: string; choices: string[]; lowEcaz?: true }
| { kind: 'harassWithdraw'; player: string; event: string }
| ({ kind: 'smugglerCollection' } & SmugglerBattleCollectionOffer)
| ({ kind: 'sandmasterVictorySpice' } & SandmasterVictorySpiceOffer)
@@ -34114,7 +34115,7 @@ function applyActionInner(
ecazBattleProfile(g);
g.decision = null;
g.response = { kind: 'ecazOccupy', owner: occupy.ecaz, passed: [] };
log(g, `${getPlayer(g, action.lead).name} leads the combined Ecaz battle, using their own leaders, cards and spice.`, { faction: 'ecaz', name: 'Occupy' });
log(g, `${p.name} chose ${getPlayer(g, action.lead).name} to lead the combined Ecaz battle, using the lead’s own leaders, cards and spice.`, { faction: 'ecaz', name: 'Occupy' });
return g;
}
if (
@@ -37176,7 +37177,12 @@ function applyActionInner(
};
if (combined) {
g.battle.ecazOccupy = { event: battleEvent, ecaz: ecaz!.id, ally: ally!.id, lead: null, canceled: false };
g.decision = { kind: 'ecazBattleLead', player: ecaz!.id, event: battleEvent, choices: [ecaz!.id, ally!.id] };
const opponent = [choice.attacker, choice.defender].find(id => id !== ecaz!.id && id !== ally!.id)!;
const leadChoice = homeworldRule(() => quoteEcazBattleLead(g, ecaz!.id, opponent));
g.decision = { kind: 'ecazBattleLead', ...leadChoice, event: battleEvent, choices: [ecaz!.id, ally!.id] };
if (leadChoice.lowEcaz)
log(g, `${getPlayer(g, opponent).name} chooses the allied battle lead because Ecaz is at Low population.`,
{ faction: 'ecaz', name: 'Low Ecaz' });
} else {
createMentatQuestion(g);
beginStrongholdBattle(g);
+6 -6
View File
@@ -1469,9 +1469,9 @@ export const RULE_TOPICS: RuleTopic[] = [
category: 'Advanced & expansions',
coverage: 'Partial',
developmentStage: 'Prototyped',
summary: 'Ecaz chooses who leads its combined army. Basic provisionally follows the main printed paragraph for odd as well as even counts: ceiling contribution/loss and floor survivors. Advanced retains its user-authorized revision. Usable controls and legal participation are connected; interaction refinement remains.',
summary: 'Ecaz chooses who leads its combined army; its opponent chooses when Ecaz is Low. Basic provisionally follows the main printed paragraph for odd as well as even counts: ceiling contribution/loss and floor survivors. Advanced retains its user-authorized revision. Usable controls and legal participation are connected; interaction refinement remains.',
steps: [
'When reciprocal Ecaz allies coexist with fighting forces, Ecaz chooses Ecaz or its ally to lead. Every participating whole-territory army must be clear of storm and connected; no component-only battle or casualties are created. The selected lead uses its own leaders, Treachery cards and spice; the original battle-order chooser is not replaced. Advisors are not fighters but retain their native voluntary fighter-conversion opportunity.',
'When reciprocal Ecaz allies coexist with fighting forces, Ecaz chooses Ecaz or its ally to lead. With Homeworlds, Low Ecaz at zero through six native reserves gives that choice to the opponent; High at seven or more keeps it with Ecaz. The panel names the current chooser, and already-opened decisions retain their saved owner. Every participating whole-territory army must be clear of storm and connected; no component-only battle or casualties are created. The selected lead uses its own leaders, Treachery cards and spice; the original battle-order chooser is not replaced. Advisors are not fighters but retain their native voluntary fighter-conversion opportunity.',
'Bare Basic Ecaz tables support native Ixians, Tleilaxu, CHOAM and Richese with their selected families, classic seats and optional Moritani. Real setup and combined-battle guards remain; optional configurations retain their existing boundaries. Ecaz contributes and loses the ceiling of half its forces on an ordinary win; survivors round down. This follows the main paragraph provisionally because its odd-count FAQ conflicts. Basic support, funding and battle-support income are zero; CHOAM keeps five native leaders and Emperor/Fremen have no Advanced elite inventory.',
'With Advanced Occupy active, the total dial includes the ceiling of half the Ecaz fighters as mandatory full-strength contribution without spice cost, plus the chosen allied army strength. The variable army retains its native Fremen/Fedaykin or Emperor/Sardaukar properties even when Ecaz forms the plan.',
'An ordinary victory leaves the floor of half the Ecaz fighters alive and separately applies the variable army’s dial losses. A sole successful Traitor-call winner preserves both armies; an ordinary defeat, mutual Traitor result or resolved explosion removes the actual combined side. Ecaz and its ally share the printed same-faction Traitor/Face Dancer relation.',
@@ -1485,10 +1485,10 @@ export const RULE_TOPICS: RuleTopic[] = [
'Advanced numbers come from the user-authorized revised PDF, physical pages 32 and 19, not a new publisher erratum. Basic follows the main printed paragraph provisionally. Original faction/deck prerequisites, separate ten-card Richese cache and independent three-card variant selection stay. Ordinary-plus-marker Basic/Advanced Occupy extends the provisional additive interpretation, not the unresolved FAQ cap. Fresh plain/card Ecaz and standalone Ecaz/classic Skills retain ordinary Arrakis Occupy, including independent cards without other modules. Bare Basic native mixed markers are connected; unsupported overlays and full-mode acceptance remain separate.',
],
checklist: [
{ area: 'Implementation', status: 'Partial', detail: 'Bare Basic selected native-family starts now retain their original setup and owner-labelled coalition outcomes, including allied zero/mixed Richese presence and actual Ix typed losses/substitution. Basic support stays zero; current physical pools drive half-step controls. Main-paragraph odd Basic and additive-marker policies remain provisional; optional/exotic refinement remains.', evidence: ['game/ecaz-occupy-battle.ts', 'game/board-resolution-quote.ts', 'game/battle-resolution-quote.ts', 'game/engine.ts'] },
{ area: 'Player controls', status: 'Partial', detail: 'Actual Basic wheels expose half-step Ixian totals under either lead, whole steps after Ecaz-only cancellation, zero-support native plans and private Richese mixed bounds. Nine current rendered-table programmes retain original seal/reveal, typed/substitution and Collection controls. Earlier mobile journeys stay separate; no new visual/browser certification.', evidence: ['components/ecaz-occupy.tsx', 'components/face-dance-decision.tsx', 'components/game-table.tsx', 'game/ecaz-occupy-options.ts'] },
{ area: 'AI', status: 'Partial', detail: 'Four existing Basic profiles make twelve legal lead/plan/native-Ixian-substitution proposals through original Collection. Existing Advanced legal participation remains separate. Strategy and strength calibration remain deferred.', evidence: ['game/ecaz-occupy-options.ts', 'game/bots.ts', 'tests/ecaz-occupy-options.test.ts'] },
{ area: 'Documentation', status: 'Implemented', detail: 'Authorized Advanced arithmetic and provisional main-paragraph Basic arithmetic are distinguished from the conflicting FAQ, specific Duke ruling and deferred interaction refinement.', evidence: ['docs/ECAZ_OCCUPY_RULES.md', 'docs/RULE_DECISIONS.md', 'game/reference.ts'] },
{ area: 'Implementation', status: 'Partial', detail: 'New original Homeworld coalitions assign the public lead choice to the opponent at Low Ecaz, preserving High/no-World and existing in-flight owners. Bare Basic selected native-family starts now retain their original setup and owner-labelled coalition outcomes, including allied zero/mixed Richese presence and actual Ix typed losses/substitution. Basic support stays zero; current physical pools drive half-step controls. Main-paragraph odd Basic and additive-marker policies remain provisional; optional/exotic refinement remains.', evidence: ['game/ecaz-homeworld-lead.ts', 'game/ecaz-occupy-battle.ts', 'game/board-resolution-quote.ts', 'game/battle-resolution-quote.ts', 'game/engine.ts'] },
{ area: 'Player controls', status: 'Partial', detail: 'Lead controls and blocked reasons name the actual chooser and printed Low penalty. Actual Basic wheels expose half-step Ixian totals under either lead, whole steps after Ecaz-only cancellation, zero-support native plans and private Richese mixed bounds. Nine current rendered-table programmes retain original seal/reveal, typed/substitution and Collection controls. Earlier mobile journeys stay separate; no new visual/browser certification.', evidence: ['components/ecaz-occupy.tsx', 'components/face-dance-decision.tsx', 'components/game-table.tsx', 'game/ecaz-occupy-options.ts'] },
{ area: 'AI', status: 'Partial', detail: 'The opponent uses the original shared legal lead adapter at Low Ecaz; strategic refinement remains deferred. Four existing Basic profiles make twelve legal lead/plan/native-Ixian-substitution proposals through original Collection. Existing Advanced legal participation remains separate. Strategy and strength calibration remain deferred.', evidence: ['game/ecaz-occupy-options.ts', 'game/bots.ts', 'tests/ecaz-occupy-options.test.ts'] },
{ area: 'Documentation', status: 'Implemented', detail: 'The printed Low Ecaz opponent-choice penalty is distinct from authorized Advanced arithmetic and provisional main-paragraph Basic arithmetic are distinguished from the conflicting FAQ, specific Duke ruling and deferred interaction refinement.', evidence: ['docs/ECAZ_OCCUPY_RULES.md', 'docs/RULE_DECISIONS.md', 'game/reference.ts'] },
{ area: 'Verification', status: 'Partial', detail: 'Current Basic native12-file244/244, types/lint/build and nine controlled gameplay/rendering programmes pass. Public setup and actual Nexus/alliance/marker shipping precede labelled board/card/typed/budget staging; both leads, total3.5, native substitution, cancellation and zero/capped/mixed markers reach Collection. Four existing profiles provide12 legal proposals. Earlier Advanced/mobile/full-game evidence is historical; full modes, optional configurations, comprehensive assurance and deployed acceptance remain open.', evidence: ['tests/basic-ecaz-native-entry.test.ts', 'tests/basic-ecaz-native-occupy.test.ts', 'tests/fixture-basic-ecaz-native.ts', 'tests/fixture-ecaz-occupy-richese.ts', 'tests/ecaz-occupy-mixed-no-field.test.ts', 'tests/fixture-ecaz-occupy-mixed-no-field.ts', 'tests/ecaz-occupy-mixed-no-field-opponent.test.ts', 'tests/ecaz-occupy-battle.test.ts', 'tests/basic-ecaz-occupy-runtime.test.ts', 'tests/basic-ecaz-tleilaxu-occupy.test.ts', 'tests/ecaz-occupy-richese.test.ts', 'tests/ecaz-occupy-stone.test.ts', 'tests/ecaz-occupy-cards-runtime.test.ts', 'tools/faction-games.ts', 'docs/ECAZ_OCCUPY_RULES.md'] },
],
related: ['faction-ecaz', 'advanced-ecaz', 'ecaz-ambassadors', 'battle', 'victory', 'implementation-checklist'],