← all tickets

53 — Implement: Sericulture Expansion (35049)

Status: done

What to build: Implement 35049 — Sericulture Expansion (elev agenda, adv 3, 2 points, Expansion). Card text: "Dividends 1 *(When you score this agenda, place 1 agenda counter on it for each excess advancement counter.)* When your discard phase ends, you may remove 1 hosted agenda counter to place 2 advancement counters on 1 installed card. *(You cannot score that card this turn.)*"

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

Watch out for: (1) Dividends counts *excess* advancement counters at score time — score with exactly 3 → 0 counters. (2) The discard-phase-end trigger is optional and targets any installed card — Jinteki: Restoring Humanity (30043) already hooks on_discard_phase_end (sg.rs:256), so verify multiple triggers in that window coexist and order sanely. (3) The "cannot score that card this turn" restriction needs a turn-scoped flag on the target; if the engine has no such restriction mechanism yet, raise it in a comment before approximating.

Comments

Targeting widened to the full installed board (review follow-up to 4d49fce). The first pass offered only advanceable cards in remote roots, which is narrower than "1 installed card". Nothing in the state model justified the narrowing: InstalledCard::advancement_tokens exists in every zone, and placing advancement counters is not advancing — is_advanceable gates the click action and Seamless Launch's *install* clause, not what an ability may put counters on. So no target class is excluded now.

Action::SericultureTarget therefore carries a target_id: String (InstalledId) instead of a (server_index, slot_index) pair, which could not name central ice, remote ice or the Runner's rig at all. The _id suffix puts it on the existing wire contract — snapshot::normalize_action_ids swaps the live decimal id for a "#N" walk ordinal leaving the engine and denormalize_action_ids reverses it on the way in, matching the id field server/serialize.py already gives each installed card, so static/game.js resolves the target across the whole board. Per ADR-0003 this is a case-3 *public position* key (sericulture_target:{target_id}): board positions and installed ids are visible to both players, so both trees share the key and no observed_action_key arm is needed.

New coverage for the reachable classes: `card_35049_offers_every_installed_ card_on_either_side` (central ice, remote ice, remote root and rig, in walk_installed order, plus the snapshot's normalized candidate list), ..._places_counters_on_unadvanceable_central_ice, ..._places_counters_on_a_runner_program, ..._places_counters_on_a_non_advanceable_asset, and ..._refuses_targets_outside_the_frozen_list.

One consequence worth naming: the log names a Runner target but still writes "an unrezzed card" for an unrezzed Corp one, since only the Corp's own facedown board is hidden from the Runner.