Part of nhoople/netrunner. These repositories stay separate and are consumed by release tag.
Hand-authored TypeScript rules engine library for Android: Netrunner. It is not a Comprehensive Rules → AST compiler and not a networked game client.
Repo: github.com/nhoople/netrunner-engine
Depends on:
- netrunner-comprehensive-rules-data pinned to tag
v26.03 - netrunner-cards-data pinned to tag
v1.147.6
Match cards-data and this engine by the same semver tag. Pin a release tag, not master.
| Pairing | cards-data | engine |
|---|---|---|
| Current | v1.147.6 |
v1.147.9 (cards pin v1.147.6; ice, asset, and upgrade install cost follows the Comprehensive Rules) |
Prior set-complete / maintenance tags: cards Releases · engine Releases.
Declared pin: data/cards-pin.json (npm run fetch-cards). GitHub Releases are cut at set-complete (wave pool status → supported, advertised host floor) or by explicit manual release. Pack IR/wiring work lands as PR merges; pin consumers to a release tag, not master. Intermediate progress tags may still exist on GitHub history — prefer the Current floor tag above. Maintenance / quality tags (e.g. v1.34.0) still publish when consumers should pin past a prior milestone.
- Node.js 24+
Pin story for a clean checkout:
- Declare pins in
data/cr-pin.json(CR tag) anddata/cards-pin.json(cards-data tag) — always a release tag, nevermaster. - Fetch into gitignored
vendor/(CI and local tests both need this). - Run tests or development hosts that call the library API.
npm install
npm run prepare-data # npm run fetch-cr && npm run fetch-cards → vendor/
# or separately:
npm run fetch-cr # pinned CR JSON → vendor/cr-data/
npm run fetch-cards # pinned card JSON → vendor/cards-data/
npm run lint # eslint (recommended ruleset)
npm run typecheck # tsc --noEmit
npm test # vitest; needs both vendor treesCI runs lint, typecheck, and test on every PR. Fix only what those surface — no separate Prettier pass.
If the cards-data GitHub tag is not yet published, fetch-cards falls back to a local checkout at ../netrunner-cards-data, /home/ubuntu/repos/netrunner-cards-data, or $CARDS_DATA_ROOT.
Hosts are thin scripts over createGame / queryLegality / applyIntent / getPublicView. They are not a game client (no UI/network).
npm run cli:help # usage + pin/fetch reminder
npm run demo:library # host loop: createGame → legality → applyIntent → getPublicView
npm run demo # decline-rez empty remote (stopAfterFirstCycle)
npm run demo:ice-break # rez Ice Wall + Marjanah break → success
npm run demo:ice-etr # rez + unbroken ETR → unsuccessful
npm run demo:pump-break # Palisade (remote) + pump Marjanah → break
npm run demo:multi-sub-etr # Hortum unbroken: gain ¢ then ETR
npm run demo:fortify-pump # Palisade remote strength + pump past it
npm run demo:tithe # Tithe: net damage + Corp gains ¢
npm run demo:rototurret # Rototurret: trash program + ETR
npm run cli # interactive stepper (number / state / view / quit)Pure functions only — no sockets, HTTP, or UI in this package. This is the stable surface a future online Project should consume:
| Function | Role |
|---|---|
createGame(options?) |
New game (stopAfterFirstCycle: false by default) |
queryLegality(state) |
Legal intents + window / priority / cites |
applyIntent(state, intent) |
Apply one intent; returns new state or cited error |
getPublicView(state, side) |
Side-filtered public snapshot for hosts |
import {
createGame,
queryLegality,
applyIntent,
getPublicView,
} from "netrunner-engine";
let state = createGame({ agendaPointsToWin: 7 });
const legal = queryLegality(state);
// Prefer picking from legality.legal (each entry has actor + CR cites).
const result = applyIntent(state, legal.legal[0]!.action);
if (result.ok) state = result.state;
const view = getPublicView(state, "runner");createInitialState / applyAction / legalActions remain exported for older demos and tests; new hosts should prefer the four functions above. Run npm run demo:library for a printed example of that loop.
See CONTRIBUTING.md for the CR adherence gate (PR template) and CI hard-fails: cite-map (tests/cr-cite-map.test.ts), timing stepIds (tests/engine.test.ts), and supported-wave empty-unsupported (tests/pool-supported-invariant.test.ts). All run under the default npm test job.
Corpus status: Floor v1.147.6 — Magnum Opus complete (8/8; mor absorbed). Cards pin v1.147.6 (printed link, deck size, influence limit, and influence cost; ice, assets, and upgrades omit a copied install cost). Gateway → Vantage Point, Uprising, Downfall, System Core 2019, Reign and Reverie, FFG Core-forward through Kitara, and Magnum Opus are fully supported. Eternal + RAM unique-title Magnum Opus gaps closed. Confidence extras live under tests/confidence-*.test.ts / tests/fixtures/goldens/ (#193). Skip napd/draft/championship; defer tdc. GitHub Releases only at set-complete or explicit manual — not per-slice.
| Mechanism | Location |
|---|---|
| Declared pin | data/cr-pin.json — tag, repo, raw GitHub base URL, file list |
| Fetch script | scripts/fetch-cr-data.mjs — npm run fetch-cr |
| Vendored files | vendor/cr-data/ (index.json, timing-structures.json, nodes.json, PIN.json) |
nodes.json is listed in the pin and fetched with the rest; it is gitignored (large) so a clean checkout needs npm run fetch-cr before tests. Engine host code cites CR numbers and stable ids. Tests resolve numbers through the pinned index.json, require every pin-listed vendor file (including nodes.json), and check graph stepIds against timing-structures.json.
CR data is authority for citations and timing IDs, not executable card behavior. The engine does not compile nodes.json into effects. When Null Signal ships a new CR, follow docs/cr-pin-bump-checklist.md.
Cards remain pure data. Definitions live in the sibling consumer repo netrunner-cards-data; this engine keeps loader / Effect IR / eval.
| Mechanism | Location |
|---|---|
| Declared pin | data/cards-pin.json — tag, repo, archive URL, required paths |
| Fetch script | scripts/fetch-cards-data.mjs — npm run fetch-cards |
| Vendored files | vendor/cards-data/ (schema.json, pool.json, release dirs, PIN.json) |
vendor/cards-data/ is gitignored; a clean checkout needs npm run fetch-cards (or npm run prepare-data) before tests. The loader validates Effect IR and fails closed on unknown nodes. pool.json declares the supported corpus and corpus order through Magnum Opus (floor v1.147.6; cards pin v1.147.6; mor absorbed) then SC19 → Downfall → Uprising → Gateway → SU21 → Midnight Sun → Parhelion → TAI → RWR → Elevation → Vantage Point. Partial cards mark unimplemented clauses in an unsupported array.
Authoritative corpus: pinned cards pool.json — 68/68 waves supported (see corpusOrder + wave status / card counts). Do not treat any shorter README wave list as complete.
Highlights at floor v1.147.6: printed link, deck size, influence limit, and influence cost. Ice, assets, and upgrades keep rezCost and omit a copied installCost. Magnum Opus 8/8 (mor absorbed); fixtures wave removed (Plascrete under WLA); Gateway → VP, Uprising, Downfall, SC19, RaR, and FFG Core-forward through Kitara all supported.
Synthetic stubs/ / wave1/ / wave2/ dirs were removed in cards-data v0.2.0; demos use real Gateway/SU21 cards (Ice Wall, Marjanah, Palisade, Hortum, Tithe, Rototurret, …).
Hand-authored AST evaluated by evalEffect — not compiling nodes.json.
Effect ::= seq [Effect…]
| do Primitive
| if Cond then Effect [else Effect]
| prevent jack_out
| choose {chooser, options[]}
Primitive ::= end_the_run
| gain_credits | lose_credits {side, amount, tally?, then?}
| lose_clicks | gain_clicks {side, amount, tally?}
| pump_strength {amount, duration?: encounter|run}
| fortify_ice | weaken_ice {amount}
| net_damage | meat_damage {amount, tally?} | core_damage | brain_damage {amount}
| give_tags {amount, tally?}
| trash_program | trash_resource {pick: first|choose}
| take_hosted_credits | place_hosted_credits {amount}
| add_virus_counter | gain_credits_per_virus
| increase_hand_size {side, amount}
| remove_tags {amount, tally?} | gain_credits_per_advancement | bypass_current_ice
| remove_power_counter
| sabotage {amount, interactive?}
| identify_mark
| charge {pick: self|choose|card, cardId?}
| bonus_access {amount}
| breach_server_when_run_ends {server: hq|rd|archives}
| install_from_grip_discount {types[], discount, mayCharge?}
| install_grip_card {cardId, discount}
| may_charge_card {cardId}
| may_derez_installed {excludeSelf?, then?}
| may_trash_installed {excludeSelf?, then?}
| trash_corp_card {cardId}
| spark_of_inspiration_resolve {discount?}
| may_trash_other_installed_search_stack_same_type_install {discount}
| search_stack_type_install {cardType, discount}
| draw {side, amount, tally?}
| take_hosted_bad_publicity {amount}
| may_flip_archives_ice_resolve_subroutine
| flip_archives_ice_resolve_subroutine {iceId, subIndex}
| trash_encounter_ice_resolve_subroutine {subIndex}
| may_choose_server
| set_named_server {serverId}
| search_stack_host_virus_or_weapon {max}
| host_stack_card_on_source {cardId}
| may_add_hosted_card_to_grip
| add_hosted_card_to_grip {cardId}
| turn_hosted_cards_faceup
| host_copy_from_grip {title}
| matryoshka_break
| matryoshka_break_resolve {amount, hostedId}
| trace {strength, onSuccess, onFailure?}
| add_agenda_counter {amount}
A kind is snake_case and names the procedure: a verb, then the object (gain_credits, trash_program). may_ means the player may decline. Amounts, sides, and zones are fields. Do not prefix a kind with a pack code or a card title. The allowed verbs are PROCEDURE_VERBS in src/effects/primitiveNames.ts.
Card hooks that carry Effect trees: subroutines[].effect, paidAbilities[].effect, onRez, onPlay, onScore, onSteal, onEncounter, onTurnBegin, onInstall, onFirstTagThisTurn, onFirstCoreDamageThisTurn, onFirstRdRunBeginThisTurn, onFirstArchivesRunBeginThisTurn, onFirstVirusInstallThisTurn, onFirstSuccessfulMarkRunThisTurn, onFirstSuccessfulHqRunThisTurn, onProgramOrHardwareInstall. Threshold field: onPowerCountersGte (Environmental Testing). Paid-ability gates include requireEncounterSubtype and requireAttackingMark (Backstitching mark-run trash-to-bypass).
- Nested priority / paid-ability windows (CR 9.2.4 / 9.2.4d)
- Timing step graph (11.2–11.5) with appendix labels
- Basic actions, operations/events, identity click abilities
- Advance / score / steal agendas; win by agenda points or flatline
- Central breach access (HQ / R&D / Archives candidates)
- Run ice: rez, break (multi-break / AI
*), multi-sub resolve, jack-out - Damage types + prevention hooks; traces; recurring / hosted / virus costs
- Effect choices (
choose/choose_option) queryLegality/explainAction, checkpoints, cannot effects
- UI / multiplayer / networking (out of scope for this Project)
- Full NetrunnerDB / every NSG expansion beyond the declared
pool.jsoncorpus (Gateway → Vantage Point today) - Compiling
nodes.jsonor Comprehensive Rules prose into executable behavior. Effect IR is hand-authored. Example boards intests/cr-examples-chN.test.tsprove the outcomes those examples name. A catalog sweep proves an ability ran on its step, a missing window, cost, encounter, or target was refused, a score or steal moved the agenda (CR 1.17.6, CR 1.17.7), and a one-instruction subroutine changed the board by its printed number (tests/printed-board.test.ts). A play ability such as Hedge Fund resolves from its instruction and does not need that sentence stored. - Deckbuilding at deck submit.
deckbuildingReportchecks extra copies and influence reductions. Minimum deck size, the identity's influence limit, and agenda points are outside the report. CR 3.1.2: those limits do not apply once the game has started. - Perfect fidelity for every printed clause outside the supported mapping (partial cards use explicit
unsupportednotes; supported waves must keep those empty unless allowlisted) - Full 9.12 continuous-effect compiler. Blanking order (CR 9.12.1d / 9.12.1e) covers Standard. The chain where a gained subtype feeds a further grant needs Mother Goddess, Egret, and Warden Fatuma, which are Eternal and outside Standard.
data/
cr-pin.json # CR tag pin
cards-pin.json # cards-data tag pin
vendor/
cr-data/ # npm run fetch-cr
cards-data/ # npm run fetch-cards (gitignored)
src/
api/library.ts # createGame / applyIntent / getPublicView
effects/eval.ts # card-specific effect sequences
effects/*Primitives.ts # shared rules (credits, damage, tags, runs, counters, …)
cards/ # load + short-game setup + runtime helpers
timing/
legality/candidates.ts # collectCandidateActions coordinator
legality/candidate*.ts # pending interrupts, run windows, click actions
actions/apply.ts # applyAction and choice dispatch
actions/ # install, run, rez, break, play, score, access, phase
demo/verticalSlice.ts
tests/
confidence-*.test.ts # interaction smoke / cross-pack / cannot / goldens / fail-closed
fixtures/goldens/ # regression transcripts (see README there)
cr-*.test.ts # CR adherence lock-ins
short-game.test.ts
system-gateway.test.ts
…