Enable Reinforcements in clean Ecaz-Moritani battles

This commit is contained in:
Quintine committed 2026-09-29 20:59:52 +10:00
1 parent 1083c6ccbf
commit 6ac5dabdee
11 files changed
+180 -28

No files matched your search

+25 -9
View File
@@ -9477,14 +9477,30 @@ function recruitsModeSupported(g: Game) {
!g.discoveryEnabled
);
}
function ecazAllyAtBattle(g: Game, side: Player, territoryId: string) {
const ally = g.players.find(player => player.id === side.ally && player.ally === side.id);
return !!ally && (side.faction === 'ecaz' || ally.faction === 'ecaz') &&
Object.entries(ally.forces).some(([key, amount]) => amount > 0 && splitLocation(key).territory === territoryId);
}
function reinforcementsModeSupported(g: Game) {
const pairedEcaz = g.expansions.length === 1 && g.expansions[0] === 'ecaz';
return !!g.ecazTreachery && recruitsModeSupported(g) && !g.sandtrout &&
g.expansions.length === 0 &&
g.players.every(player => faction(player.faction).expansion === 'base');
(g.expansions.length === 0 || pairedEcaz) &&
g.players.every(player => {
const expansion = faction(player.faction).expansion;
return expansion === 'base' || (pairedEcaz && expansion === 'ecaz');
});
}
function currentReinforcementsCost(g: Game, p: Player) {
requireRule(reinforcementsModeSupported(g),
'Reinforcements currently requires the standalone Ecaz card variant with classic factions.');
'Reinforcements requires the standalone Ecaz card variant with classic or Ecaz/Moritani factions.');
if (g.battle) {
const other = getPlayer(g, g.battle.attacker === p.id ? g.battle.defender : g.battle.attacker);
requireRule(!ecazAllyAtBattle(g, p, g.battle.territory) &&
!ecazAllyAtBattle(g, other, g.battle.territory),
'Reinforcements with co-present Ecaz allies awaits combined-army battle integration.');
}
try {
return quoteReinforcements(p.reserves - (p.elites?.reserves ?? 0), p.elites?.reserves ?? 0);
} catch (error) {
@@ -9500,9 +9516,12 @@ function reinforcementsPreview(g: Game, p: Player) {
const elite = Math.min(3 - normal, Math.max(0, p.elites?.reserves ?? 0));
let blocked: string | null = null;
if (!reinforcementsModeSupported(g))
blocked = 'Reinforcements currently requires the standalone Ecaz card variant with classic factions.';
blocked = 'Reinforcements requires the standalone Ecaz card variant with classic or Ecaz/Moritani factions.';
else if (g.status !== 'playing' || g.phase !== 6 || battle.revealed || battle.plans[p.id])
blocked = 'Choose Reinforcements before sealing your Battle Plan.';
else if (ecazAllyAtBattle(g, p, battle.territory) ||
ecazAllyAtBattle(g, getPlayer(g, battle.attacker === p.id ? battle.defender : battle.attacker), battle.territory))
blocked = 'Reinforcements with co-present Ecaz allies awaits combined-army battle integration.';
else if (normal + elite < 3)
blocked = 'Reinforcements needs three physical forces in your reserves.';
return { blocked, normal, elite };
@@ -15186,11 +15205,8 @@ function harassWithdrawContext(g: Game, p: Player): HarassWithdrawContext {
blocked = 'Harass & Withdraw with other optional modules, including Homeworlds, is still being implemented.';
else if (g.players.some(player => player.faction === 'richese') || g.richeseCache !== undefined || g.richeseRemoved !== undefined)
blocked = 'Harass & Withdraw with the Richese card family awaits the Stone Burner timing ruling.';
else if ([p, other].some(side => {
const ally = g.players.find(player => player.id === side.ally && player.ally === side.id);
return ally && (side.faction === 'ecaz' || ally.faction === 'ecaz') &&
Object.entries(ally.forces).some(([key, amount]) => amount > 0 && splitLocation(key).territory === b.territory);
})) blocked = 'Harass & Withdraw with co-present Ecaz allies awaits combined-army battle integration.';
else if (ecazAllyAtBattle(g, p, b.territory) || ecazAllyAtBattle(g, other, b.territory))
blocked = 'Harass & Withdraw with co-present Ecaz allies awaits combined-army battle integration.';
else if (p.noField?.deployed?.location.territory === b.territory)
blocked = 'Reveal your No-Field before using Harass & Withdraw.';
const locations = Object.fromEntries(Object.entries(p.forces)
+5 -5
View File
@@ -4225,17 +4225,17 @@ export const RULE_TOPICS: RuleTopic[] = [
summary: 'Add two to battle strength while paying three own reserve counters into the Tanks when the physical card is revealed.',
steps: [
'In the independent Ecaz Treachery Cards variant, place your held Reinforcements card in either battle-card slot; it is neither a weapon nor a defense. A category Prescience answer is None, not the name of this card. One physical card cannot fill both slots.',
'Have three own forces in reserves when sealing. The current Basic/Advanced classic-faction prototype spends ordinary reserves first and then elite reserves, moving exactly three to their corresponding Tanks subpools when the battle resolves. No extra forces arrive in the battle territory.',
'Have three own forces in reserves when sealing. The current Basic/Advanced classic-faction and paired Ecaz/Moritani prototype spends ordinary reserves first and then elite reserves, moving exactly three to their corresponding Tanks subpools when the battle resolves. No extra forces arrive in the battle territory.',
'The normal-outcome score gains 2 without adding to the physical dial, spice support or on-board casualties. The card and its reserve cost are consumed even on a Traitor victory, a mutual Traitor result or a Lasgun/Shield explosion; successful Traitor calls still decide the outcome directly.',
'Payment on every outcome, deterministic normal-first selection and counting the reserve transfer as battle losses are documented provisional interpretations, not publisher rulings. Harass/Stone Burner combinations, other optional modules and expansion factions remain unavailable in this bounded prototype.',
'Payment on every outcome, deterministic normal-first selection and counting the reserve transfer as battle losses are documented provisional interpretations, not publisher rulings. Harass/Stone Burner, co-present Ecaz allied armies, other optional modules and other expansion pairings remain unavailable in this bounded prototype.',
],
related: ['battle-cards', 'battle', 'card-recruits', 'card-harass-withdraw', 'implementation-checklist'],
checklist: [
{ area: 'Implementation', status: 'Partial', detail: 'Physical card custody, either battle slot, three typed reserve counters, normal score modifier and mandatory discard connect in classic-faction Basic/Advanced games only.', evidence: ['game/reinforcements.ts', 'game/battle-resolution-quote.ts', 'game/engine.ts'] },
{ area: 'Implementation', status: 'Partial', detail: 'Physical card custody, either battle slot, three typed reserve counters, normal score modifier and mandatory discard connect in classic or paired Ecaz/Moritani Basic/Advanced games.', evidence: ['game/reinforcements.ts', 'game/battle-resolution-quote.ts', 'game/engine.ts'] },
{ area: 'Player controls', status: 'Partial', detail: 'Owner-only availability, exact cost guidance and both battle selectors support the card; private category answers remain None.', evidence: ['components/battle-preparation.tsx', 'components/game-table.tsx'] },
{ area: 'AI', status: 'Partial', detail: 'Four profiles consider legal physical slot candidates and the normal score bonus; strategy and calibration remain unfinished.', evidence: ['game/bots.ts'] },
{ area: 'Documentation', status: 'Partial', detail: 'The printed effect and unresolved Traitor, explosion, type-selection and accounting interpretations are distinguished from this temporary prototype.', evidence: ['docs/ECAZ_TREACHERY_RULES.md', 'docs/REINFORCEMENTS_RUNTIME.md'] },
{ area: 'Verification', status: 'Partial', detail: 'Rule, engine, controls and saved concurrent recovery cases cover this bounded behavior; full variant and combined modes are not certified.', evidence: ['tests/reinforcements.test.ts', 'tests/reinforcements-engine.test.ts', 'tests/reinforcements-controls.test.tsx', 'tests/reinforcements-recovery.test.ts'] },
{ area: 'Verification', status: 'Partial', detail: 'Rule, paired-faction engine/controls and saved concurrent recovery cases cover bounded behavior; co-present Ecaz allied armies, the full card variant and combined optional modules remain uncertified.', evidence: ['tests/reinforcements.test.ts', 'tests/reinforcements-engine.test.ts', 'tests/reinforcements-controls.test.tsx', 'tests/reinforcements-recovery.test.ts'] },
],
},
{
@@ -4273,7 +4273,7 @@ export const RULE_TOPICS: RuleTopic[] = [
'Development setup supports two through six distinct seats from the base factions and selected expansion sets. Expansion selection and optional modules are separate; public expansion starts remain disabled.',
'Ecaz places six starting forces in Imperial Basin, allocated among sectors 9, 10 and 11, and keeps fourteen in reserve. Finish Fremen placement first; Bene Gesserit then places its Advanced starting advisor. Moritani places its six forces in an unoccupied territory after all other setup is complete.',
'Ixians keep their private starting-card selection and physical cyborg/suboid forces. Tleilaxu receives three private Face Dancers. Advanced CHOAM receives its Auditor. Richese has a separate ten-card cache and three private No-Fields. Ecaz and Moritani receive their Ambassador and Terror inventories and share one Duke Vidal disc.',
'Without Ix, CHOAM adds Poison Tooth and Artillery Strike to the 33-card ordinary deck. With Ix, its 47-card deck contains those physical cards once. Ecaz faction selection adds no Treachery Cards: the separate three-card variant has Recruits, bounded Harass & Withdraw and bounded Reinforcements development controls for classic-faction games.',
'Without Ix, CHOAM adds Poison Tooth and Artillery Strike to the 33-card ordinary deck. With Ix, its 47-card deck contains those physical cards once. Ecaz faction selection adds no Treachery Cards: the separate three-card variant has Recruits, bounded Harass & Withdraw and bounded Reinforcements development controls for classic or Ecaz/Moritani games.',
'Advanced Ixians may explicitly decline Technology for one Richese special lot. Their once-per-round use remains available later. The special-lot exchange itself is unfinished pending replacement-card custody interpretation.',
'Fresh unsupported movement arrivals still reject before opening prevention choices or reserving a new movement card. A saved CHOAM allow choice can return an uncommitted unsupported move for another choice, preserving its existing reserved Ornithopter. The bounded Ecaz/Moritani same-entry Ambassador/Terror overlap instead snapshots both eligible owners in storm order without replaying the original move.',
'Ordinary shipment checks unfinished arrival combinations before saving the Guild interception choice. An already saved ordinary shipment may return unspent when its still-gated arrival cannot commit; older No-Field, Smuggler, Nexus and Ambassador shipments retain their separate recovery boundaries. Classic plus Ecaz/Moritani Ambassador/Terror overlap has a provisional saved storm-order sequence, while other simultaneous priorities remain unfinished.',