← all tickets

18 — Migrate: Elevation + vp cards

What to build: Every implemented card from the two smaller packs (~15) plays through the registry system, same shape as the System Gateway migration: one contiguous block per card in its pack's module, match arms deleted as each card moves.

Blocked by: 16 — Expand: registry infrastructure + pilot card. (Can run in parallel with 17.)

Status: fixed

Comments

Implementation notes

All 15 implemented cards — 8 Elevation, 7 Voices of the Past (every implemented entry in game/cards_impl/elev.json / vp.json, none of them vanilla) — now live in cards/elev.rs and cards/vp.rs as one contiguous reg.card("…") block each, in NRDB code order, followed by their hook implementations in the same order. 26 registrations (14 elev + 12 vp), which takes the live registry to 72 cards / 133 registrations.

Hook bodies moved verbatim; the only edits were signature adjustments where a dispatcher passed arguments the port-era function didn't take (methuselah_run_end gains _successful, sipa_outermost_broken gains _prog_id), and inline match-arm bodies getting names (azimat_install, devadatta_install, side_hustle_install). hiram_peek is registered on both on_hardware_install and on_hardware_trash, which is what the two arms did.

Two cards needed more than a move:

engine::play_event because beta_build_pre_run needs start_run_tagged. That helper is now pub(crate) (same precedent as Diviner in 02), so the hook moved to cards/vp.rs unchanged and play_event consults only the registry.

credits to pay trash costs" is *not* a hook registry on either side of the port (none of the 45 mirrored registries covers it), so it stays inline in engine::trash_card and legal.rs's Access affordability check. Both sites now carry a comment saying so, and cards/elev.rs's module doc names it, so it doesn't read as a stray un-migrated arm.

With this, no dispatcher has a match-arm fallback left. Seventeen dispatchers in hooks.rs plus the RUN_EVENT_PRE site in engine::play_event lost their arms; sixteen of those collapsed to a registry lookup plus their documented default (run_event_specNone = "not a run event", pre_access_prompt / outermost_ice_broken_promptfalse, trojan_codescontains, and so on), and runner_install_cost is now purely the INSTALL_COST_MODS loop. hooks.rs is 894 → 631 lines and holds dispatchers only.

Ordering: INSTALL_COST_MODS is applied in registration order, so registry::build calls sg → elev → vp, which keeps Carmen, DZMZ, Principia in the order the port had them. That constraint is documented at build.

registry_manifest.rs is unchanged apart from its doc comment — a migration moves *where* a code is registered, not whether — so the frozen-fixture parity test passes untouched.

Testing: 02's every_system_gateway_registration_is_in_the_registry was a pack-specific check living in cards/sg.rs; with every pack migrated it is a registry-wide invariant, so it moved to cards/registry.rs as every_manifest_registration_is_in_the_registry, with the 30-prefix filter dropped and the guard counts raised to 72 cards / 133 registrations. Since no fallback arms remain, that test is now the machine-checked form of "every implemented card runs from its pack module" — a card left out of a block would be missing outright, not silently handled elsewhere. Verified non-vacuous by commenting out Knickknack's registration (test fails). No new per-card tests: these 15 cards are has_tests: false in their manifests, i.e. the same grandfathered set issue 05 addresses.

Verified (2026-07-30)

-- -D warnings`: clean.

count as before; the parity test moved rather than being added).

test_registry_parity.py against the frozen golden manifest.

afterwards) played 40 deterministic epsilon-heuristic games — 20 as Tāo Salonga, 20 as Hiram, whose identity hooks only fire in that slot — with a runner deck containing all 15 migrated cards against the System Gateway upgraded starter corp, dumping each game's full log. Every one of the 15 cards appears in the output (27–162 log lines each), and the 13,356-line dump is byte-identical before and after the migration. selfplay 12 200 (24 games of the two SG starter decks) is also byte-identical, confirming the shared dispatchers and the INSTALL_COST_MODS order are unchanged for the cards migrated in 02.