Files
Dune/docs/HOMEWORLD_COMBAT_INTEGRATION_AUDIT.md
Quintine 28ba1618ba Implement Homeworld mechanics through Caladan reinforcement checkpoint
Connect typed Homeworld custody, transport, battle, income, revival and Caladan reinforcement flows with private multiplayer recovery and controls. Preserve the optimized test workflow and keep unfinished rules modes gated. Verified with 3682 offline tests, typecheck, lint, production build and 40 HTTP tests; existing saved games remain unchanged.
2026-09-10 08:12:26 +10:00

16 KiB
Raw Permalink Blame History

Homeworld combat integration audit

Read-only architecture audit, 9 September 2026. The contract is Homeworld rules, with transport/occupation boundaries in invasion rules. This document identifies adapters required for actual combat; it does not enable Homeworlds or certify complete games. Line numbers below identify the inspected worktree and may shift during concurrent implementation; function names are the durable anchors.

Current boundary

Native and visitor inventory already has an independent representation: homeworldForceGroups and quoteHomeworldCustody in homeworld-custody.ts return exact {normal, elite} groups and detached transactions. Native primary counts derive from player reserve totals; Salusa is an allocation inside Emperor's total. Visitor groups are separate and never belong in Player.forces. homeworldPopulations supplies the printed card, native owner, current side and bonus, but deliberately does not resolve occupation entitlements.

The concurrently added homeworld-battle-rules.ts and Homeworld branch of battle-resolution-quote.ts already accept an explicit native/card/side/army contract. At inspection, the pure result can add native strength, restrict traitor callers and return typed native explosion losses. The live engine does not yet supply this contract or consume the explosion result. A calculator result is not evidence of live battle support.

Keep gameTerritories, validLocation, splitLocation, storm geometry, territory occupancy and stronghold control restricted to their existing Arrakis/HMS domain. Do not add a fabricated homeworld:…:0 force key or append Homeworld cards to TERRITORIES to make existing validators pass.

Minimum adapter contracts

Introduce one explicit combat-location resolver used by discovery, plans, resolution, aftermath, bots and controls. A discriminated location {kind:'territory', id} or {kind:'homeworld', id} can coexist with legacy saved Battle.territory strings through a strict decoder. The resolver must reject unseated worlds and Basic Salusa. It should supply:

  • A stable canonical identity and display name; territory geometry only for the territory variant.
  • Current physical armies by seated player, with native versus visitor provenance on Homeworlds. Public presence must not inspect concealed No-Field values.
  • For a Homeworld, its native owner, card, applicable side and printed native bonus; use this only for that native faction's score. Preserve occupation as a separate input once its lifecycle is settled.
  • Legal combatant pairs, current attacker order, entitled traitor callers, and eligible Face Dancer owner.
  • A detached casualty transaction and receipt that binds exact source pool, normal/elite losses, Tanks deltas and resulting custody. This must also serve replacement/return effects without conflating them with deaths.

Do not require the native to be one of the two combatants: multiple visitors can fight each other. There is no Homeworld stronghold-capacity rule. Use the ordinary opposing-faction and turn-order rules for discovery without inventing native-first ordering. Allied noncombatants remain relevant to an explosion even when they do not fight each other.

Live function map

Boundary Inspected code Required adaptation
Battle frontier quoteBattleBoard, board-resolution-quote.ts:93; battles, engine.ts:8650 Retain the planet quote, then compose a separate Homeworld conflict quote from custody and current order. No gameDistance, storm sector, advisor merge, Polar Sink exclusion or stronghold capacity applies to a world. Preserve the original advisor-release operations for planet battles.
Phase progress quoteBattleBoardContinuation:364; currentBattleAftermathQuote, finishBattle; phase-6 initialization and automatic response normalization Every test for “no remaining battles” must use the same combined frontier. Updating only chooseBattle would allow automatic advance to Collection while a Homeworld conflict remains. Preserve aid refunds and the existing CHOAM/Ix opening boundaries.
Choice and opening chooseBattle action, engine.ts:17398; Battle type:480; beginStrongholdBattle, beginBattlePowers Validate the selected canonical location/pair against that frontier; persist its identity in the battle event. Use normal sealed preparation. Stronghold copy/effects remain unavailable on Homeworlds; ordinary Voice, Prescience and other faction powers retain their independent scope. Do not add Homeworld bonus cancellation windows.
Force commitment combatForces:3453; validatePlan:9972; feasiblePrescience:10912; prescienceAnswer; plan completion/truth helpers Replace the board-force reduction with the exact location army. Continue to reuse CombatForces, casualtyOptions, leader/card legality and aid pricing. Basic dial parsing in prescienceAnswer currently caps against at(p,b.territory) and also needs this adapter. Audit plan-completion simulations, not just final plan submission.
Final pure input currentBattleResolutionQuote:11160 Supply homeworld from authoritative custody and current applicable face. Keep stronghold benefits null. Bind the actual native army, including when it is a noncombatant; do not infer it from a visitor's counters. Keep private hands and traitor identities server-only.
Removal preflight force-loss-preflight.ts validateBattleForceLoss; currentBattleResolutionQuote The existing validator checks canonical sector keys only. Add Homeworld pool preflight through the custody quote, including aggregate reserve and Tanks consistency. Do not relax board-key checks.
Removal commit kill:3402; killTerritory:3431; takeBattleLosses:3490; resolveBattle:11228; settleWinnerCasualties:11460 Dispatch by location. Native deaths withdraw from the exact primary/Salusa pool and increment Tanks; visitor deaths withdraw from visitors and increment that faction's Tanks. Update elite Tanks and battle-loss counts exactly once. Ordinary deaths must not become reserve returns.
Delayed choices continueResolvedBattle, battleLosses, settleWinnerCasualties, pendingIxSubstitution, ixSubstitution decision/response Reuse owned casualty choices and automatic singleton handling, but bind them to a typed Homeworld pool. Ix substitution currently stores sector-key loss/source/recovery maps and directly reads board forces: it needs location-aware records and a real survivor/cyborg exchange.
Aftermath currentBattleAftermathQuote:11500; battle-aftermath-quote.ts; battleCleanupInput:11762; karama-battle-preflight.ts Replace the territoryIds = gameTerritories(...) validation boundary with explicit valid combat locations. Carry identical location identity through capture, Auditor, card retention, tech transfer and Face Dance. Keep their established sequencing and ownership; accepting a Homeworld identity must not grant territory-specific effects.

offerMoritaniDuke is another consumer of battles(g): it currently calls territory(battle.territory).type. Once the combined frontier is returned, filter the territory variant before the stronghold predicate. Homeworld combat does not qualify as a stronghold battle for this power.

Strength and casualty details that need explicit handling

CombatForces.freeSupport is currently one boolean for the entire army. The Salusa high face gives Sardaukar free support, including when normal forces have been transferred there. Setting the existing boolean would accidentally make normal forces free too. Extend the shared arithmetic to represent per-type free support, then use it in casualtyOptions, plan validation, pure support payment, saved casualty validation, bots and the plan wheel. Preserve Basic strength-one starred counters, Ix fixed-half Suboids and the independent Emperor-versus-Fremen exception. Salusa occupation's loss of Sardaukar advantage is a separate printed modifier; it cannot be implemented as ordinary Karama cancellation.

The native printed bonus is a score addition, not additional physical counters, a larger dial, a spice-support charge or ordinary winner casualties. The source's “current” bonus should be supplied by the authoritative resolution adapter. Do not silently lock a high face for the whole battle merely because its opening was saved; conversely, once a resolution is committed, its receipt must retain the accepted effective facts while delayed casualties change population.

For Lasgun/Shield, resolveBattle currently loops through all players and calls killTerritory(...,Infinity,true); it ignores the new pure homeworldExplosion result. The Homeworld branch must destroy actual foreign armies and commit only the native printed casualty count, capped by available natives. When multiple normal/elite allocations exist, create a native-owned casualty choice with a saved event; when only one exists, apply it automatically. Both combatants still lose and leaders die according to the explosion result. Preserve surviving natives and recalculate population after actual losses. Do not carry this named exception into Stone Burner, mutual Traitors, ordinary losses or arbitrary board destruction.

The exploding native may be a noncombatant. Test this explicitly: using the winner's ordinary casualty continuation alone cannot represent the choice because an explosion has no winner. An empty native army must not create a meaningless confirmation. Resolve the native choice before any successor consumes the final custody/occupation state.

Traitors and Face Dancers

traitorVoters currently returns both combatants and sometimes a remote Harkonnen ally. On a Homeworld return only the native if it is a combatant. A native who is not fighting cannot become a remote traitor beneficiary; two visitors fighting therefore have no traitor voters. The same helper feeds traitorCall, automatic decline/resolution, cancellation preflight and projected traitorVoters; change them together to avoid an invisible waiting decision. Preserve revealed-card reactions, including Stone Burner and Poison Tooth, before automatic zero-voter resolution.

resolveBattle currently creates pendingFaceDance for any Tleilaxu seat when somebody else wins. On a Homeworld, only native Tleilaxu may receive that opportunity. A remote Tleilaxu cannot call on another faction's world, even for an ally. The occupied Tleilax immunity for an occupier/ally is a separate condition to compose when the occupation contract is available.

The faceDance decision (engine.ts:15526) currently requires a sector and uses at(winner, territory), board source keys, direct reserve decrements, direct winner reserve returns and place. The Homeworld branch must instead quote the remaining winning foreign army, selected permitted Tleilaxu replacement sources, and a destination deposit into native Tleilax custody. Return the actual winner's normal/elite pieces through the native-return adapter, preserving Emperor's distinct pools and any unresolved return-routing interpretation. Preserve leader death, shared Cheap Hero identity, revealed-dancer custody and deck refresh. Source scope of replacement sources must be audited explicitly; the old reserves/sector parser does not authorize arbitrary foreign garrisons.

Recovery boundaries

The mandatory battle-discard validator (engine.ts:3080–3252) both requires gameTerritories(...).some(...) and rechecks winner casualties by reducing winner.forces. It will reject an otherwise valid Homeworld battle even after the live resolver is adapted. Carry explicit combat identity and exact pool snapshots into battleResolved, battleCleanup, lastBattleContext, casualty choices and any new explosion continuation. Revalidate nested saved parents before read/action/normalization effects, including Nullentropy or other interruptions of those decisions.

Receipts must distinguish “resolution committed, typed losses pending” from “losses already committed, cleanup pending.” Do not rederive a completed native bonus after casualties flip its card or rerun support payment, bounty, leader death, card discard, tech transfer or capture on refresh. Same-total Salusa/native/visitor reallocations must invalidate a receipt bound to a different pool. Existing territory-only saves should continue through their current decoder and checks.

Atreides high reinforcement belongs after the appropriate completed winner/casualty sequence and requires a surviving own force at that combat location. It needs an owned optional reserve-transfer action, not a new ordinary shipment. Do not fold occupation rewards into generic battle victory: presence changes and qualification events need their own event-bound settlement, and the unresolved occupation cases remain documented in the source contract.

Bots and controls

viewGame already projects owner-only battle.ownForces, sealed own plans, revealed plans and entitled prescience separately. Reuse that privacy boundary and add public combat location/name, native bonus and public opponent army counts. Do not expose a server-side ResolutionCombatant object; it contains private cards.

bot-battle-choices.ts independently reconstructs only planetary battles. Replace that duplication with shared legal combat choices. In bots.ts, plans already prefers b.ownForces, but its opponent estimate uses presenceAt(other,b.territory) and would see zero defenders on a Homeworld. Update both legal candidates and strength estimates with the public adapter; add native bonus to evaluation, not dial. Face Dance strategy currently selects board sectors; Ix substitution and explosion choices also need exact legal descriptors for all four difficulty levels.

game-table.tsx prefers ownForces but still uses territory(g.battle.territory).name in the battle heading and multiple cleanup/replacement labels. Use the combat display adapter; no sector selector should appear for a Homeworld-only target. Show the native bonus separately from the dial, per-type support costs, exact casualty pools and clear decision ownership. Mandatory single-option effects remain automatic with existing animation notices; optional replacement/casualty choices stay actionable after reload.

Evidence required before enabling

Use actual dispatcher paths from a conserved invasion fixture through battle choice, sealed plans, reveal, owned casualties, card disposal and aftermath. Cover native/visitor and visitor/visitor pairs, ordinary turn order, allied third-party explosion losses, zero-voter automatic progress, remote Harkonnen/Tleilaxu rejection, Basic starred identity, mixed Salusa forces, population crossings, ordinary traitor losses and the explosion exception. Check typed Ix substitution and native Tleilax Face Dance without creating counters.

Test JSON and in-memory D1 recovery before/after each committed stage, concurrent final plans, duplicate casualty submissions and corrupted exact-pool receipts. Demonstrate all four bots can finish these paths without secret information. Browser-check Homeworld battle selection, sealed plan controls, native bonus, typed support/casualty choices, Face Dance, labels and refresh on desktop/mobile. Broader setup-to-endgame and every-faction/module evidence remains necessary for the full release gate.

This audit changed only this document and ran no game, test suite, server or saved-room operation. Current pure-calculator edits were observed, not independently certified. Occupation lifecycle and other existing source ambiguities remain unresolved here.