← all tickets

52 — Implement: Proprionegation (35048)

Status: done

What to build: Implement 35048 — Proprionegation (elev agenda, adv 4, 2 points, Security). Card text: "When you score this agenda, place 1 agenda counter on it. Hosted agenda counter: The Runner moves to the outermost position of Archives. *(They approach any ice in that position.)* Use this ability only during a run."

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

Watch out for: the counter ability is a mid-run redirect — the Runner's position and attacked server change to Archives' outermost piece of ice. Check what the engine's run state supports before promising full fidelity; if a faithful redirect needs new run-state plumbing, that design question goes in a comment here before approximating. On-score counter placement plus the paid-ability window ("only during a run") are the core to test.

Comments

Design notes (2026-08-03)

The redirect is faithful — no approximation. RunState already carries the two fields the redirect needs, target and position, and both are plain mutable state. So use_proprionegation re-points the run at Archives and hands straight back to advance_run_position, the engine's own "the Runner is now approaching X" path: it does the breaker-strength reset, the approach log line and the priority handoff, so a redirected approach is indistinguishable from a normal one. Rewinding to the pre-run -1 and letting that function advance one step lands on position 0. Position 0 *is* the outermost ice: install does server_ice_mut(..).insert(0, ic), so the newest and therefore outermost piece sits at index 0, and a run walks that vector front to back. An unprotected Archives falls through the same branch a fully-passed run takes, into the approach-server window — which is what "the outermost position" means for a server with no ice. Per-approach state (broken_subs, fired_subs, encounter_breaks, jack_out_window, ice_str_debuff) is cleared, since the old approach is over.

Two things the engine *did* need:

1. Counters on scored agendas. corp.scored_agendas was Vec<Arc<Card>>, with nowhere to hold "place 1 agenda counter on it". It is now Vec<ScoredAgenda> (card + counters). The zone is push-only, so a score-area index names the same copy for the rest of the game — which is what lets the action name a copy by index and what makes the MCTS key stable. Ticket 53 (Sericulture Expansion) hosts agenda counters too and should reuse this. The counters are state, so they are carried by the canonical snapshot (corp.scored is the one corp zone that is not a bare code list — its entries are {code, counters}) and by the client projection, where the score-area pile renders the usual [n] counter badge. Scored agendas and their counters are public to both sides, so nothing is redacted. 2. A paid-ability window for score-area cards. Every existing offer registry is keyed to an *installed* card (InstalledId + rig/root slot); a scored agenda has neither. New registry scored_agenda_offers, dispatched from the corp's two windows inside a run in legal::run_actions (approach-ice and approach-server). Those two windows are the whole of the corp's priority during a run in this engine, so they are exactly what "use this ability only during a run" comes to here.

Deliberately not done: turn_flags.mark_run is *not* re-marked for Archives. The run was initiated on the original server and the run-start triggers already fired for it; a mid-run redirect changes the attacked server, not the fact of "you ran server X this turn".

AI: the rule-based corp AI never spends the counter (the offers are appended after each window's own actions, so its actions[0] fallback still picks the plain pass). Per docs/adding-a-card.md that is a follow-up ticket, not part of the bar — MCTS sees the action and can learn it.

Verified (2026-08-03)

cargo test --workspace green (164 core unit tests, incl. 7 card_35048_*, + 5 AI invariants); Python suite green (186 passed) after make rust-py.