# Master of Vega — build plan and findings **Read this before touching `src/games/mastervega/`.** It records the traps found while building it; several were only discovered by instrumenting the soak and would be very easy to reintroduce. Master of Orion clone. Registered as `mastervega`, category `arcade-console-pc` ("Video Games"), iconFrame **92**. MOO1 rules with three MOO2 conveniences (colony building queue, colony detail view, hireable leaders). Preset ship hulls with an auto-refitting **Mark**, no ship designer. Tactical grid combat. Empires are the ten **species** themselves — this game deliberately does *not* use `data/opponents.json` or `ui/Portrait.js`. ## Architecture Split enforced by import discipline, modelled on `totalannihilation/` rather than `civilization/` (whose scene grew to 1792 lines). **Headless — zero Phaser imports, importable by Node:** | File | Role | |---|---| | `VegaRules.js` | `compileRules(json)` — validate + index the rules file | | `VegaGalaxyGen.js` | Deterministic galaxy: shapes, stars, planets, Gabriel-graph starlanes, homeworlds | | `VegaLogic.js` | The engine: colonies, sliders, buildings, research, fleets, combat dispatch, council, victory | | `VegaShips.js` | Preset hulls + Mark auto-refit; the knapsack loadout | | `VegaCombat.js` | Tactical battle **stepper** | | `VegaAI.js` | AI empire controller | | `VegaDiplomacy.js` | Treaties, attitudes, council politics | | `VegaLeaders.js` | Leader pool, offers, postings | **Render tier:** `MasterOfVegaGame.js` (scene), `VegaStarMap.js`, `VegaNebula.js`, `VegaSystemView.js`, `VegaCombatView.js`, `VegaScreens.js`, `VegaArt.js`, `VegaFx.js`. State is plain JSON with the RNG cursor inside it (`state.rngState`, explicit-step mulberry32), so replaying a seed reproduces the galaxy, the battles and the winner exactly. `serialize()` strips `rules` and every underscore-prefixed memo cache. ## Traps found during the build Each of these was a real bug that produced a plausible-looking but broken game. 1. **An immobile hull froze the entire fleet.** `fleetSpeed()` returned 0 if *any* ship in a fleet was immobile. A completed Star Base joins the fleet over its own colony — which is the empire's main battle fleet — and from that moment `canSendFleet` refused every order. Instrumented over one game: **338 of 338** valid attacks (in range, strong enough, at war) were refused for this reason. No colony could ever be attacked, so no war could be won. Fix: `fleetSpeed` skips immobile hulls; `sendFleet` splits them into a garrison that stays behind; `consolidateFleets` keys on mobility so the garrison is never re-merged. 2. **Conquest was unreachable without bombardment.** Even at a 2500-turn cap, *no empire was ever eliminated*: a cornered empire's beaten fleet always retreats to its last fortified homeworld and denies the attacker the clean orbit an invasion needs. `bombardPopKill` and the Stellar Converter's `planetCracker` were declared in the rules but never implemented. Implementing orbital bombardment (MOO1's actual answer) made conquest work immediately. 3. **The invasion window never opened.** Two versions of this failed the same way. Requiring `defenseHp === 0` never worked because combat resolves on the attacker's turn and the *defender's* turn rebuilds the batteries first. Requiring an empty sky failed too — a besieged colony finishes a ship every few turns and that one hull blocked the landing indefinitely. Measured: 1268 colony-turns under hostile orbit, **2** invasion attempts. Now the rule is orbital *superiority*, and surviving defences fight as ground support. 4. **Transports never left home.** `manageFleets` had an early `if (power <= 0) continue`, and transports carry no guns — so transport fleets skipped every movement branch. 3495 idle observations against 12 in transit. They now escort the main battle fleet. 5. **The AI threw transports away.** Without a forecast it launched every landing regardless of odds: 1971 failures against 130 successes. `invasionForecast()` now gates it — success rate went to ~91%. 6. **Nobody ever met anybody.** Contact required flying a ship into an occupied system, so most galaxies had no diplomacy and no wars at all. Empires now also make contact by proximity (`economy.contactRange`). 7. **The Council could never elect anyone.** Every empire stood as a candidate and therefore voted for itself — 30 sessions in an 800-turn game, all null. MOO1's rule: the **two largest** empires stand and everyone else votes between them. 8. **…and then it elected someone every time.** A dominant empire won its own council vote a hundred turns before it could finish a war, so conquest never completed. MOO1's **refusal to submit** (the defeated candidate walks out and the election is void) restored the balance. 9. **Combat had a decisive first-strike bias.** Firing was sequential, so whichever side acted first in the round contact was made won. Mirror matches swung from 100% to 0% attacker depending on tier, because the parity of the contact round changes with fleet speed. Damage is now **banked and applied together** at the end of the round. 10. **…and then ties were resolved in a fixed direction.** Symmetric fleets reach the disengage round *exactly* tied astonishingly often; giving the attacker every drawn battle produced a 44pp bias. The coin is now flipped. 11. **Every battle stalemated on the round cap.** Two causes: in-combat repair (6% of hull/round) outpaced damage, and the attrition tail of an even fight is enormously long. Repair was cut to 3%/6%, and a **disengage rule** now makes the weaker side withdraw at round 25 rather than grinding to the cap. 12. **An all-missile ship ran dry and stalemated.** At some tiers missiles score better damage-per-space than beams, so a pure knapsack built ships that emptied their racks in five rounds and then sat unarmed. `MISSILE_SHARE` caps missiles at 40% of tonnage. This is a design rule, not an optimisation. 13. **Researching a weapon could make ships worse.** A greedy "best damage-per-space" fill mounts one oversized gun and strands the leftover tonnage — the cruiser's beam damage *dropped* from 42 to 28 on learning Death Ray. The loadout is now an unbounded **knapsack**, which is provably monotonic in tech because researching only ever adds candidates. The one sanctioned exception is the tier where missiles first appear (see the verifier's section 4). 14. **Waste ran away and killed every colony.** Population fell 50 → 20 with 868 accumulated waste. Cleanup is now **mandatory** and taken off the top, pro-rata from the other channels — MOO1's eco slider snapping to the minimum. Related: factories beyond what the population can staff are **mothballed** (`effectiveFactories`), or a shrinking colony keeps polluting from factories nobody is left to run and can never recover. 15. **Espionage leaked the whole tech tree.** Crediting raw espionage score made every empire a spy agency; all five reached 60/60 techs, making the per-species availability roll meaningless. Only the surplus over a baseline now counts. 16. **The Council never convened.** `colonizedFraction` measured against *every* star, but a third of systems hold only gas giants and belts, so "half the galaxy colonised" was unreachable. It now measures against settleable stars. 17. **Fleets proliferated to 281.** Ships completing while the local fleet is in transit each spawn a new fleet. `consolidateFleets` merges idle fleets each turn. 18. **Dead empires left ghost fleets** on the map forever. Caught by the soak's invariant checker, not by playing. 19. **Worldgen fairness could not always be satisfied.** On large sparse galaxies a homeworld can have *no* star inside its opening fuel range, so there was nothing to upgrade and that empire simply could not expand. `guaranteeNearbyWorlds` now seeds new worlds when it must. ## Balance reference (27-game AI soak) ``` outcomes: { conquest: 17, council: 7, timeout: 3 } turns: min 110, median 282, max 800 AI turn time: 0.45 ms average (budget 50 ms) mirror-match bias: < 5.2pp at every tier, zero stalemates wins spread across 8 of 10 species ``` ## Files touched to register the game `src/data/gamesRegistry.js`, `src/main.js`, `src/scenes/GameRoomScene.js` (`slugDispatch`), `src/data/assetManifest.js`, `src/scenes/PreloadScene.js` (eager-loads `mastervega-artwork.json`), `src/services/soundtrack.js` (`hacker`). ## Verification ```bash node tools/verifyMasterOfVega.js # 809 checks, ~60s node tools/verifyMasterOfVega.js --quick # 808 checks, ~15s node tools/verifyMasterOfVega.js --games=50 # a deeper soak ``` Ten sections; section 2 runs the real procedural painters against a Proxy fake canvas, section 10 is the self-play soak with invariants and a turn-time budget. **Never browser-tested.** Everything above is engine- and Node-verified only. ## Art All sheets are optional and start `path: null`; `VegaArt.js` paints stand-ins at the identical frame geometry. See `src/games/mastervega/sprites.md` for the frame maps. Frame indexes are append-only.