← all tickets

59 — Implement: Mycoweb (35053)

Status: done

What to build: Implement 35053 — Mycoweb (elev ice, Code Gate, rez 8, str 5). Subroutines: 1. "You may install 1 piece of ice from Archives, ignoring all costs." 2. "You may rez 1 installed piece of ice, paying 2[credit] less." 3. "Resolve 1 subroutine on a rezzed sentry." 4. "Resolve 1 subroutine on another rezzed code gate."

Follow docs/adding-a-card.md: hook block in rust/netrunner-core/src/cards/elev.rs, ≥1 card_35053_* test, flags in game/cards_impl/elev.json.

Watch out for: hardest card in the batch — deliberately sequenced last. Sub 1: install-from-Archives needs a server/position choice and cost-ignore path; check what install plumbing the engine exposes outside the Corp's install action. Sub 2: rez discount interacts with the unimplemented-ice rule ("can be installed but never rezzed") — rezzing an unimplemented ice via Mycoweb must still be blocked. Subs 3–4: cross-ice subroutine resolution (pick a rezzed sentry / *another* rezzed code gate, then pick one of its subs) — two-level target selection with per-target MCTS keys (ADR-0003), and the resolved sub executes in the context of the current encounter. If cross-ice sub resolution needs engine machinery that doesn't exist, split that machinery into its own ticket and note it here rather than approximating silently.

Comments

Cross-ice resolution needed no new machinery — it needed one signature. engine::fire_subroutine already takes the firing ice by InstalledId, so resolving a subroutine on another piece of ice is that call with a different id, and ticket 57's RunState::sub_decision_pause already holds the encounter for whatever window the borrowed subroutine opens (covered by card_35053_waits_for_a_window_the_borrowed_subroutine_opens, which borrows Empiricist's draw-then-ask subroutine and watches Mycoweb's last subroutine wait for the answer). What was missing was smaller: a SUBROUTINE_OVERRIDE hook took only &mut GameState, so a scripted subroutine could not tell which copy it was printed on — and "another rezzed code gate" is a question about self. The hook type now carries the ice's InstalledId; deriving it from run.position instead would have been wrong exactly when it matters, since a borrowed subroutine fires while the run sits on the Mycoweb that borrowed it. No ticket split: the change is four call sites and one type.

Two windows per targeted subroutine, not one flat cross product. Sub 1 asks which ice, then which server (the ice stays in Archives until the second answer, so no step holds a card outside every zone); subs 3–4 ask which ice, then which of its subroutines. Each window's list stays short and each half gets the key it deserves: the facedown Archives pick is the only private one (ADR-0003 case 2, redacted in mcts2::observed_action_key and in serialize.py::_redact_log), while the server, the rezzed ice and the subroutine index are public positions both trees key alike.

The rez rule had no machinery at all, so it became ticket 61. The "unimplemented ICE can be installed but never rezzed" rule in docs/adding-a-card.md is not enforced anywhere in the engine: no implemented/vanilla bits reach netrunner-core (Game::new takes deck codes and a seed), get_ice_subs still fabricates the retired default "End the run.", and none of the three existing rez paths checks anything. Rather than approximate silently in either direction, Mycoweb's own rez filters its targets through a new hooks::card_is_implemented (does this code register any hook), so the discount is not a way around the rule, and .scratch/unimplemented-card-rules/issues/61-... carries the pipeline and the other three paths. The helper documents what it cannot see — a deliberately vanilla card — which is why it is not spread further in this ticket.

Two engine corners the card walks into. Installing an ice on the server being run inserts at index 0, which shifts every position the run has already walked, so run.position moves with it — without that the Runner is sent back to re-approach the ice they just passed (card_35053_an_ice_installed_behind_the_runner_is_not_re_approached). And Tread Lightly's "+3 to rez each piece of ice" is a run-long rule, not an approach-window one, so the discounted rez pays it too.

Subtypes are read through one helper now. "A rezzed sentry" has to mean what a killer's break check means, so legal::breakable_subs's inline "printed keywords plus hosted Trojans" moved to hooks::effective_ice_keywords and both read it. That makes a Mycoweb wearing a Chromatophores a legal target for its own subroutine 3 — which is also where the broken-subroutine filter earns its place: a subroutine the Runner broke this encounter cannot be resolved, and the encountered ice is the only ice that can carry that mark.

Rust: 241 passed. Python: 196 passed.