15 KiB
Ecaz Occupy runtime readiness
Read-only architecture review, 7 September 2026, after Guild Ambassador integration. No production, test, package or database state was changed. References below identify the reviewed functions; line numbers are snapshot anchors rather than stable API locations. The independent source reviewer records the formula and interaction rulings in ECAZ_OCCUPY_RULES.md, including the remaining rounding discrepancy. Root verification found that the numerical correction comment was written by a community member, not the designer; it is not authoritative errata. Territorial co-occupation remains independently supported by p.8 and the verified October designer clarification.
Recommended next slice: shared territorial coalition identity, ordinary entry/path legality and end-of-movement coexistence. Dedicated Fremen/Guild Ambassador effects already permit Ecaz co-occupation. Ordinary movement still contradicts that supported resulting board state. Combat, shared desert collection and the special victory threshold require separate consumers and must not be advertised as complete merely because entry is fixed.
Current surfaces
| Surface | Current behavior and relevant code | Required boundary |
|---|---|---|
| Special entry | fremen-ambassador.ts:133–150 and guild-ambassador.ts:134–167 independently identify a reciprocal Ecaz pair and count it as one stronghold side. Both permit co-occupation. Fremen relocation also allows a source-approved BG fighter request beside Ecaz while preserving locks. |
Replace duplicated public coalition identity with one helper. Preserve each producer's distinct storm, range, HMS, typed custody, stance and source-event rules. |
| Ordinary destination/path | engine.ts:5766 allowedEntry rejects allied presence outside Polar Sink; its stronghold limit counts separate players. pathBlocked near 8269 also counts players. bot-mobility.ts:32–158 mirrors both assumptions. |
Territorial Ecaz pairs count as one side and are exempt from the ordinary allied-entry prohibition. Paths and final destination must use the same identity. This does not grant range, city ornithopters or HMS shipment permission through an ally. |
| Ordinary turn completion | finishMovementTurn near engine.ts:6368 destroys the ending player's co-occupying forces once its ally is absent from the remaining queue, except for the current-turn alliance grace period. |
Ecaz territorial coexistence is permanent under Occupy, not another deadline. Keep ordinary non-Ecaz grace/departure behavior intact, including Guild order and Hajr turns. Do not change reserves, casualties or queues merely to implement identity. |
| BG | Normal move has an additional allied-presence restriction for optional fighter requests near engine.ts:16360; advisors.ts supplies stance and locks. settledBoard automatically releases lone advisors before battle/collection/victory. |
Apply the source-reviewed Ecaz exception consistently; retain opposite-stance matching and accompaniment locks. Advisors remain distinct from fighting presence, even when their faction belongs to the coalition. |
| Battle enumeration | board-resolution-quote.ts:104 quoteBattleBoard enumerates player pairs in storm order, skipping an ally but independently pairing each ally against a third party. engine.ts:16511 chooseBattle installs that pair directly. |
A territorial coalition requires one battle side with a separate choice of acting faction. Preserve the ordinary battle chooser and attack/defend orientation; do not let both allies fight the same opposing group as two unrelated battles. |
| Plans, support, powers | Battle near engine.ts:427 has only attacker/defender IDs. combatForces near 3314 reads the selected player's troops; validatePlan near 9271 uses its leader/cards, force pool and funds. combat-response-quote.ts:160–186 derives powers from those two IDs. |
Separate acting faction, dialed force owner and Ecaz contribution. Do not copy an ally's troops onto a fictitious player or substitute its hand. Native powers, support source, Voice/Prescience, KH, stronghold support and traitor ownership need explicit source-based attribution. |
| Resolution and aftermath | battle-resolution-quote.ts accepts two ResolutionCombatants. resolveBattle near engine.ts:10509 pays those players, kills the losing selected player's army and loses the winner's selected dial. Explosion is already territorial. takeBattleLosses near 3352 allocates one owner's typed losses. Aftermath helpers then handle capture, Face Dance, Auditor, cards and tech. |
Add a typed coalition commitment/result before enabling combat. Loss allocation must conserve both physical owners and distinguish win, loss, traitor, explosion and special weapon outcomes. Do not infer the whole coalition's effects from a single winner ID. |
| Collection | quoteSpiceCollection near board-resolution-quote.ts:157 drains each sector in storm order, crediting the first collector before the second. It already pays each fighter owner's Advanced city income independently. collect near engine.ts:8944 immediately applies balances. |
Existing city income can coincide with Ecaz's printed double collection, but desert sharing/agreement and Ecaz Collection cancellation are absent. A future collection quote needs an explicit allocation/continuation; ordinary bribes defer to Mentat and are not a substitute. |
| Victory and control | victory-quote.ts:153–168 deduplicates an allied union's normal strongholds but requires four for every alliance. It has no three jointly occupied strongholds condition. stronghold-cards.ts:199–206 already awards control to Ecaz when only the reciprocal pair occupies a stronghold. |
Keep actual Stronghold Card control separate from victory thresholds and held-card custody. Add the source-confirmed joint condition to the shared victory quote while preserving ordinary four-hold victory, prediction and turn-ten fallbacks. Do not silently treat a tech-token bonus as a jointly occupied map stronghold. |
| Human/AI projection | game-table.tsx:267 uses battle.ownForces, then displays one player's force/card plan. chooseBattle controls near 3730 target a single player. bots.ts battle plans and bot-mobility.ts have the same player-centric assumptions. |
Movement can consume shared public identity now. Future combat needs authoritative participant/pool descriptors and an Ecaz acting-faction choice before sealed plans. Only the entitled actor receives its private hand, leader availability or plan constraints. |
Ordinary shipments, Guild transport, worm entry and other existing producers call some of these common checks. Audit their actual callers when integrating the helper; replacing only the visible ship action would leave inconsistent routes. Conversely, the special Ambassador destination grant must retain its own independent action allowance.
Reproduced current behavior
An in-memory Node/tsx probe used createGame, newPlayer, actual applyAction for ordinary shipment/end movement, and the exported pure board/victory quotes. Each case used three seated factions (Ecaz, Emperor ally, Harkonnen opponent), reciprocal Ecaz/Emperor alliance formed on turn one, current turn two, storm18, empty hands and conserved 20-force inventories. These were deliberately constructed development positions, not a claim of a publicly startable Ecaz game. The probe never called room APIs or wrote a save.
- With Emperor's one force in Carthag11, Ecaz's ordinary one-force shipment there rejects:
You cannot enter a territory occupied by your ally. - With one force each in Carthag11 and only Ecaz remaining in the movement queue, actual Ecaz
endMovementleaves Emperor's force, removes Ecaz's force and increases Ecaz's Tanks by one. - With Ecaz5, Emperor4 and Harkonnen1 in Carthag11,
quoteBattleBoardreturns both Ecaz→Harkonnen and Emperor→Harkonnen battles in that territory. - With two forces each in Wind Pass14 and four spice there,
quoteSpiceCollectionreturns Ecaz4 and Emperor0. It has no agreement or equal-split choice. - With one force from each ally in Arrakeen10, Carthag11 and Sietch Tabr14, phase-eight
quoteVictoryreturns no winner.
Cases 1–2 demonstrate the immediate coexistence inconsistency. Cases 3–5 demonstrate missing later Occupy behavior; the pair-list observation itself is not proof of a resolved combat formula. These are existing gaps in gated expansion support, not regressions introduced into enabled classic Basic starts. The recently implemented Ambassador grants make their consequences reachable in development journeys. No runtime was modified by this audit.
The minimal setup can be reproduced without a file by piping JavaScript to node --import tsx: construct the three players with createGame('OCCUPYREAD', newPlayer('ec','Ecaz','ecaz'), false, []), append Emperor al and Harkonnen en, set the controlled status/phase/order above, assign reciprocal ally and allySinceTurn = 1, and set each player's reserves to 20 - sum(forces). Import quoteBattleBoard/quoteSpiceCollection from game/board-resolution-quote.ts and quoteVictory from game/victory-quote.ts. Use actual {type:'ship',amount:1,territory:'carthag',sector:11} and {type:'endMovement'} for the first two observations.
Smallest shared contract and implementation order
- Public territorial identity and coexistence. A pure helper consumes only seated IDs, factions, reciprocal alliances and an explicit location kind. Its stable identity/members distinguish the same player, an Ecaz alliance and other factions. Homeworlds remain explicitly unmerged. Root's proposed
ecazOccupancyIdentity/ecazOccupancyRelationcontract matches this need. Stronghold capacity uses fighting sides; no hidden No-Field denomination or private resources enter identity. Replace the duplicate Ambassador implementations, ordinary engine/path checks and bot filters, then exempt this pair from compulsory allied separation. Test actual shipment/movement/end-turn and canceled/replacement movement paths, not only identity functions. - Keep unsupported combat explicit. Do not enable full Ecaz starts in the coexistence slice. Before later combat activation, settle a public
BattleSidecontract containing the acting faction, dialed physical owner, contributor and participant set. Ecaz's representative decision belongs before plan/power commitment. Source-confirmed formula/cancellation must enter the same quote used by plan validation, projection, resolution and saved response preflight; an effectful trial on a cloned game is not that contract. - Implement bounded collection and victory separately. The joint stronghold win is a small shared-quote extension once the source reviewer settles token and advisor details. Desert collection requires a real agreement/fallback allocation rather than two independent storm-order drains. Keep its live balances and proof in a pure quote, with only the unresolved choice persisted. Add the separate Advanced Collection Karama window without reducing the ally's normal income.
- Complete combat before broad activation. Cover selected faction versus force owner for native powers, support, casualty types, bounty, traitors, stronghold effects, Face Dance, capture, Auditor, tech/card aftermath and all already supported weapons. Reuse existing pure casualty/aftermath components with explicit inputs; do not reinterpret an old two-player receipt as a coalition receipt. Match UI/AI and SQL recovery to the new committed participant structure, preserving historical saves without fabricating contributions.
The coordinator has chosen step one as the next bounded production task. This document is its pre-change audit, not evidence that the new helper or adapters have landed. It intentionally leaves battle, collection and victory activation to subsequent source-backed slices.