# Netrunner Offline — offline deployment fork

This repository is a fork of [NEU-DrBo6/chiriboga](https://github.com/NEU-DrBo6/chiriboga) —
*"Netrunner: Solo Mode"*, a fan implementation of Android: Netrunner with AI opponents,
a rogue-lite gauntlet mode and a cyberpunk interface, built on the original
[Chiriboga engine](https://github.com/bobtheuberfish/chiriboga) by bobtheuberfish.
Player-facing surfaces are branded **Netrunner Offline** (menu page, page titles,
chrome); all upstream credit stays in place (see Credits and the menu footer).

Upstream serves the game through PHP. This fork makes the whole thing run as a **fully
static site** (no PHP anywhere) with a headless test harness, so it can be played offline,
deployed to static hosts such as Vercel, and exercised by automated AI-vs-AI games.
Rules and AI retain their existing implementations. The live table adds presentation hooks
and separates engine callbacks from human input; the optional AI tuning hooks remain as documented below.

![License](https://img.shields.io/badge/license-GPL--3.0-green)

**Upstream demo:** https://chiriboga.cronbach.com

## Live perspective table

Human vs AI now launches the accepted perspective table for either seat. See
[Using the live table](table/README.md) for launch, controls, the legacy route, asset sources
and the current local-verification limits. Run and access controls now include current ICE
strength, subroutine states, ability confirmation, access prices and game-over exits. The new view has no match save/resume promise.

For a local distribution using the assets already present, run `node tools/build-local.mjs ../table-public`, then `node harness/serve.js 8123 ../table-public` and open `http://127.0.0.1:8123/`. The build output must be new and outside this source tree. This command does not download or deploy. The table documentation includes the continuous both-seat, format and offline check commands. Offline evidence covers the warmed shell and required table assets; unvisited card artwork can use a neutral fallback.

## What this fork adds

| Path | Purpose |
|---|---|
| `menu.html` | Static launcher menu (Quick Game, Human vs AI, AI vs AI spectate, tutorials, Standard/Startup format filter, deck picker with search) in the cyberpunk console theme — the site root (`/`). Generated, do not hand-edit. |
| `tools/make-menu-page.mjs` | Regenerates `menu.html` from the `precons/` directory (all decks with side, faction, identity, flags and format legality; tutorial links extracted from `index.php`). |
| `offline.html` | Static equivalent of `engine.php` (same script includes, PHP blocks pre-rendered, analytics removed, rebranded `<title>` + bundled-font `@font-face`). Generated — do not hand-edit. |
| `tools/make-offline-page.mjs` | Regenerates `offline.html` from `engine.php`. |
| `table/` | Accepted perspective scene, live state/decision bridge, local catalog, assets and usage documentation. |
| `style.css` | Game chrome (run004 cyberpunk retheme: near-black blue base, electric cyan accents, corp steel blue / runner hot red side coding). Includes the table-era small-screen deck/settings dialog layout. |
| `fonts/` | Bundled OFL fonts (Share Tech Mono terminal face + Orbitron wordmark) with their license files. No CDN. |
| `harness/` | Headless Chrome harness: `serve.js` (static server), `run-game.js` (one AI-vs-AI game), `batch.js` (parallel batches), `tuner.js` (AI parameter tuning), `compare.js` (A/B evaluation with true Newcombe method-10 CIs, `--log-decisions` per-decision JSONL logging, process sharding), `validate-decisions.js` (decision-log schema validator), `verify-learned-config.js` (learned-config load check), `verify-menu.js` (menu end-to-end verification), `capture-views.js` (rendered menu/game captures for visual review), `configs.json` (named AI overrides), `decks/` (deck pairs + NetrunnerDB imports). Run011 additions: `paired-compare.mjs` (PAIRED same-seed A/B evaluation — both arms play the identical deal, paired delta/sign-test/bootstrap CIs next to the unpaired Newcombe view, `--skip-existing` resume), `corpus-run011.mjs` (500-game decision-log corpus generator), `recompute-run011.mjs` (independent recomputation of paired stats + model metrics from raw artifacts), `verify-evalmodel-run011.mjs` (evalModel integration smoke), and seeded-PRNG injection + opening-hand deal fingerprints in `lib/inproc.js`. |
| `tools/learn-weights.mjs` + `harness/learned-config.json` | Run007 learning pipeline (pure Node, no deps): a (1+lambda) evolution strategy learns the RunCortex run-search config (7 value weights + k + veto threshold) from self-play fitness games; the learned artifact plus the honest 400-game evaluation and the decision-data logger are documented in OFFLINE.md section 4.7 (outcome: honest null — the default config was kept, search ships OFF). |
| `tools/gen-opponent-corpus.mjs`, `tools/train-opponent-model.mjs` + `harness/corp-opponent-model.json` | Run008 opponent model (pure Node): generates a logged runner-decision corpus (stock / runsearch / random-weight-mutant runners) and trains a logistic regression on P(runner makes a run \| state) with an 80/20 by-game held-out split (accuracy + AUC). Consumed informationally by the corp deception module (OFFLINE.md section 4.8). |
| `tools/learn-deception.mjs` + `harness/learned-deception.json` | Run008 deception learner: a (1+lambda)-ES evolves three corp-deception PERSONA-RANGE dimensions (bluff-advance probability, rez-hold factor, ambush-install weight) against a randomized runner population (stock / runsearch / random mutants drawn per fitness game) with a composite corp win-rate + agenda-margin fitness; the corp plays each game with a persona sampled from the ranges (mixed policy). OFFLINE.md section 4.8. |
| `ai_corpdecept.js` | Run008 corp deception module ("CorpDecept"). Inert unless enabled with `AIOverrides.corp.deception` — parameterized bluff-advance / opponent-model-informed rez-hold / ambush-install actions, per-game persona sampling, decision telemetry, fail-safe to stock. See OFFLINE.md section 4.8 for the design, the held-out evaluation (incl. the generalization gap) and how to enable it. |
| `tools/import-nrdb-deck/` | Imports NetrunnerDB decklists as playable precon + harness deck files, with card-implementation coverage checks and Standard/Startup format classification (`--scan` crawls recent listings for implemented + format-legal decks). |
| `tools/format-data.json` | The two supported tournament formats as data: NSG Standard (post-Seventh Rotation) and Startup pools, both ban lists (names + NRDB codes + implemented flags), the Startup agenda rule, the Quick Game eligibility map, source URLs and dates. Classified by `harness/lib/formats.js`; per-deck report in `tools/format-report.md`. |
| `ai_corp.js`, `ai_runner.js` | Surgical additions only: a `window.AIOverrides` merge hook in each AI constructor plus three named tunables (20 lines total), one guarded 17-line call site activating the optional Runner run search (default OFF, run006), and two guarded hook sites (21 changed lines) routing advance/install/rez choices through the optional corp deception module (default OFF, run008). No algorithm changes. |
| `ai_runcortex.js` | Optional Monte Carlo run-decision search for the Runner AI ("RunCortex", run006 experiment). Inert unless enabled with `AIOverrides.runner.runSearch` — see OFFLINE.md section 4.6 for what it does, how to enable/evaluate it, and the honest A/B result. Run011 adds an optional learned position-evaluator blend (`config.evalModel`, default OFF): when enabled, each candidate's rollout meanEV is blended with the learned win-probability from `window.LEARNED_EVAL` (mean-centered, fail-safe to pure meanEV on any model error). See OFFLINE.md section 4.9. |
| `tools/train-eval-model.mjs` + `harness/learned-eval.json` | Run011 learned position evaluator (pure Node, no deps): logistic P(runner wins \| 22 state features) trained on the 500-game decision corpus with an 80/20 BY-GAME split; held-out accuracy 64.5% vs 52.1% majority baseline, balanced accuracy 64.2%, AUC 0.696. Blended into RunCortex via `config.evalModel` (default OFF, proven inert); honest paired V4 evaluation in OFFLINE.md section 4.9. |
| `ai_corpdecept.js` | Optional corp deception module ("CorpDecept", run008). Inert unless enabled with `AIOverrides.corp.deception` — see OFFLINE.md section 4.8. |
| `vercel.json` + `tools/vercel-build.sh` | Static Vercel deployment: fetches card images at build time and assembles the `public/` output directory. |
| `sw.js` | Run009 service worker (NEW): versioned precache of the app shell + all engine set files, cache-first runtime caching for card images; registered from both generated pages; bounded offline table verification uses `harness/verify-pwa.js` with page and worker networking disabled. OFFLINE.md section 3.5. |
| `carddata/vantagepoint.json` + `carddata/liberation.json` | Run009 catalogs (NEW files): Vantage Point (66 cards, pack `vp`) and the Liberation cycle (tai + rwr, 130 cards) from NetrunnerDB, with sources + fetch date; resolve the run005 pack-code caveats. OFFLINE.md section 3.5. |
| `sets/vantagepoint.js` | Run009 implementation tranche (NEW file): the 20 cards (19 Vantage Point + Elevation's Embedded Reporting) played by the six imported Worlds-2026-Startup tournament decks, under real NRDB codes with basic AI hints; per-card smokes via `harness/smoke-tranche-run009.js`. OFFLINE.md section 3.5. |
| `harness/verify-format-data-run009.mjs` + `harness/verify-pwa.js` + `harness/smoke-tranche-run009.js` | Run009 verification tools (NEW): merged-catalog format-data re-check, headless PWA offline verification, and per-card tranche smokes. OFFLINE.md section 3.5. |
| `sets/liberation.js` | Run010 implementation tranche (NEW file): the 11-card missing-card union of the four newly format-legal tournament decks found by the fresh post-VP NRDB scan (Nebula Talent Management flip identity, Next Big Thing, Bigger Picture, IP Enforcement, Beta Build, Méliès City Luxury Line, Ansel 2.0, Caveat Emptor, The Red Room, Grubber, Lethe), under real NRDB codes with basic AI hints; per-card smokes via `harness/smoke-tranche-run010.js`. OFFLINE.md section 3.6. |
| `harness/scan-run010.mjs` + `harness/proof-run010.mjs` + `harness/batch-run010.mjs` + `harness/smoke-tranche-run010.js` | Run010 verification tools (NEW): fresh NRDB tournament scan (recent + tournament listings, formats classification, missing-card union), per-deck in-proc AIA game proofs, new-deck batch smoke, and per-card tranche smokes. OFFLINE.md section 3.6. |
| `tools/meta-rank.mjs` | Run012 meta grounding (NEW): ranks current-format archetypes from the public AlwaysBeRunning JSON API (`/api/entries?id=` placements + self-reported NRDB decklist URLs) joined with the NRDB public decklist API and classified with `harness/lib/formats.js`. Placement-weighted only (ABR exposes no round-level win rates); 15 curated 2026 events straddling the 26.09 ban list, 140 decklists fetched, per-deck representability + missing-card counts. OFFLINE.md section 4.10. |
| `harness/gauntlet-run012.mjs` + `harness/mine-disagreements-run012.mjs` + `tools/audit-mining-run012.mjs` + `tools/recount-mining-run012.mjs` + `harness/eval-matrix-run012.mjs` + `tools/meta-eval-report-run012.mjs` | Run012 meta gauntlet + disagreement mining (NEW): meta-grounded 10-deck gauntlet with per-deck playability proofs; 50 fully-logged games (768 RunCortex decisions) with per-candidate learned-evaluator telemetry (`evalModel.pByCandidate` passthrough in `harness/lib/decisionlog.js`); the audit turns the raw JSONL into `mining-report.json` + `mining-AUDIT.md` (top divergent states with full board context, classified heuristic-/search-/model-suspect, per-archetype divergence tables); the recount is an independent re-derivation gate; the eval matrix + report give per-archetype AI-vs-AI win rates (stock vs search arm, honestly framed as scripted-seat ordering, not human-comparable). OFFLINE.md section 4.10. |
| `carddata/newerpacks.json` + `sets/newerpacks.js` + `sets/liberation013.js` + `harness/unions-run013.mjs` + `harness/smoke-tranche-run013.js` + `harness/proof-run013.mjs` + `harness/batch-run013.mjs` + `harness/verify-run013-implementation.mjs` + `harness/verify-run013-format.mjs` | Run013 top-meta identity tranche (NEW): implements the 61-card union (58 support + identities Sebastião 34066, Nuvem SA 34121, V.I.P.I. 36009) of the three smallest Standard-legal weight-8 meta archetypes from run 012's ranking (Méliès U excluded - Standard-banned identity 36036), under real NRDB codes in NEW set files (run010's `sets/liberation.js` untouched); three representative tournament lists imported, classified Standard-legal, menu-generated and playable (3 AIA proofs + 8-game batch, 0 fatal). OFFLINE.md section 4.11. |
| `sets/tranche014.js` + `harness/unions-run014.mjs` + `harness/smoke-tranche-run014.js` + `harness/proof-run014.mjs` + `harness/batch-run014.mjs` + `harness/verify-run014-implementation.mjs` + `harness/verify-run014-format.mjs` | Run014 weight-8 completion tranche (NEW): implements the 35-card union of the last two partially representable weight-8 archetypes - René "Loup" Arcemont (30001, identity already present; 13 support cards) and Ob Superheavy Logistics (33057, identity implemented here; 21 support + identity) - under real NRDB codes in the NEW set file `sets/tranche014.js` (set code `tp14`; `sets/liberation.js` untouched, no engine edits); eight tournament lists imported (4 René + 4 Ob), classified Standard-legal, menu-generated and playable (2 AIA proofs + 8-game batch, 0 fatal); msbp printings 32002/32007 deliberately kept outside the format pools. OFFLINE.md section 4.12. |

The PHP entry points (`index.php`, `engine.php`, `gauntlet.php`, `decklauncher.php`) are kept
for reference/regeneration but are inert on static hosts.

## Quick start (local)

Prerequisites: Node.js >= 18 and Chrome (see `OFFLINE.md` section 2 for details).

```
npm --prefix harness install
node harness/serve.js 8123
```

Then open **http://127.0.0.1:8123/** — the launcher menu. Pick Quick Game (drawn from a
format-legal pool), Human vs AI or AI vs AI spectate, and optionally narrow every deck
pool to **Standard** or **Startup**-legal decks with the format filter (decks illegal in
both formats — e.g. the classic Gateway starters with their banned cards — stay visible
in the ALL DECKS grid as CASUAL ONLY but are never launchable). The game page itself
lives at `http://127.0.0.1:8123/offline.html`.

### Choosing a deck in the menu

Each deck slot (Your deck and AI opponent deck under Human vs AI, Corp deck and Runner
deck under AI vs AI) shows the selected deck as a card: the identity's art, the deck
name, the identity, the faction (a coloured dot and its name), the format tags
(Standard, Startup, Casual only), the card count and the first lines of the deck's
description, when it has one. A deck without a description (the decks imported from
NetrunnerDB) shows its **deck at a glance** there instead: the card-type mix (for example
"15 ICE · 9 agendas · 17 assets · 1 upgrade · 7 operations", identity not counted) and its
three most-copied cards ("Most copies 3× Cacophony · 3× Carnivore · 3× Fermenter"). For an
AI opponent left on Auto it shows the starter deck the AI will play under the current
format filter.

Clicking the card, or pressing Enter or Space while it has focus, opens the deck list
for that seat. Its title names the seat and the side (Corp or Runner). It has:

- a search field that filters as you type. It matches part of the deck name, the
  identity or the faction, ignoring case;
- faction chips: All, plus one chip for each faction in the list. When the chips do not
  fit, the strip scrolls sideways and fades out at the side that holds more chips; a chip
  reached with Tab scrolls fully into view;
- one row per deck. A row shows the art, deck name, identity, faction, format tags and
  card count, grouped by faction in the same order as before. A tick marks the selected
  deck. On short landscape screens (height up to 500 px, e.g. a phone held sideways) a
  row takes two lines - the name, then the identity, format tags and card count - so at
  least four rows show at once; the faction stays as the row's colour bar and the group
  heading;
- a details panel with the description, or the deck at a glance for a deck without one.
  It shows the selected deck when the list opens and follows the row you move to with
  the arrow keys, or the row under the mouse once the mouse moves. It is beside the list
  on wide screens and under it on phones. When its content is taller than the panel, it
  is a Tab stop (so the keyboard can scroll it) and shows the menu's yellow focus ring.

On touch screens each row also has an info key (a circled "i") at its right end. Tapping
it shows that deck in the details panel without choosing it; tapping anywhere else on
the row chooses the deck.

Clicking a row chooses that deck and closes the list. With the keyboard, focus stays in
the search field: the Up and Down arrows (and Page Up / Page Down) move through the
visible rows, Enter chooses, and Escape or the Close button closes the list without a
change. Either way focus goes back to the slot card.

Under the **All** filter the list ends with the side's casual-only decks under a "Casual
only" heading. They are there for reference and cannot be chosen. Clicking one, or
pressing Enter on it, shows its details and explains in plain words why it is casual
only (for example "Offworld Office (3 copies) is banned in Standard"). Under Standard
and Startup the list holds only the decks legal in that format. The format filter, the
decks each seat offers, the Quick Game pools and the launch links are unchanged.

The native deck `<select>`s (`#hva-deck`, `#hva-opponent`, `#aia-corp`, `#aia-runner`)
are still the source of truth. They keep their ids, option values (the precon file),
`data-deck` attributes, faction groups and `change` events, which the launch code and
`harness/verify-menu.js` use. The picker reads its rows from them, and choosing a row
sets the select's value and fires one `change` event. The menu needs script: the page
script fills the selects and runs the launch keys, so without script the selects are
empty and nothing launches. The picker is set up after the menu itself; if it fails to
start, the page shows the filled selects again and the menu works with them.
Identity art comes from `images/<5-digit code>.jpg`, the same files the offline pack
already stores. A missing image is replaced by the faction's initials.

Where a deck's description comes from: the `notes` of its precon file, published only
when they are player-facing text. NetrunnerDB imports keep their import log in `notes`
(source URL, implemented card lines, format classification, importer tool and date);
the generator (`tools/make-menu-page.mjs`, `playerNotes`) splits a note into sentences
and treats a sentence as import log only when it starts with one of the log's own
openings (`NRDB import of "`, `Card lines implemented`, `All cards implemented`,
`Format-legal:`, `Imported by`, `Printings stored as`). A note with no such sentence is
published byte for byte; a note made only of them is published empty; a note that mixes
both makes the build fail ("partly dropped", naming the precon file), so a hand-written
sentence is never removed silently. The same function fills the menu data and the
`notes` carried in each launch link, so the game's own deck information (Table tools ->
Decks, and the legacy view's deck-info popup) shows the hand-written description or
nothing - never the import log. The Decks panel then shows the same deck at a glance,
computed from the deck list in the launch link and the engine's card data, with card
titles spelled as in `carddata/carddata.json` (four engine titles use straight quotes,
for example `"Knickknack" O'Brian`), so it reads and orders ties exactly as the menu does
for every deck; it prints deck names as plain text (NetrunnerDB names with `&#039;` show an apostrophe). The deck
at a glance is built from the deck list itself (card types from `carddata/carddata.json`,
or the engine card definition for the reprint codes it lacks), so it is always true to
the list. Launch links keep their identity, cards, name and URL; only `notes` changed
for the imports (it is now left out). The filter can be tried on its own:
`node tools/make-menu-page.mjs --notes-filter-test <cases.json>` (reads
`{"cases":[{"id","notes"}]}`, prints the published notes, writes no file).

Headless (no browser window) AI-vs-AI game from the command line:

```
node harness/run-game.js --pair gateway-starters --ap 6 --out harness/results/my-game.json
```

Menu flows can be verified end-to-end headless:

```
node harness/verify-menu.js
```

Rendered captures of the menu and game views (visual review) are produced by:

```
node harness/capture-views.js --out .gauntlet/evidence/run004/round1
```

Batch runs, AI tuning and NetrunnerDB deck imports are documented in
[OFFLINE.md](OFFLINE.md) (sections 4.2–4.4).

## Quick start (Vercel)

1. Import this repository into Vercel (framework preset **Other** — everything is
   preconfigured via `vercel.json`).
2. Deploy. The build step (`bash tools/vercel-build.sh`) downloads the card images and
   assembles the `public/` output directory automatically; no environment variables needed.
3. Every push to the production branch redeploys.

## Card images

Card art is **not included in the repository**. The game logic boots without it, but
rendering needs the images, so `tools/vercel-build.sh` fetches them at build time
(and locally you can fetch them once, see `OFFLINE.md` section 5). This keeps the repo
small and keeps third-party art out of the project's licenses. Card art belongs to Null
Signal Games (NSG), Fantasy Flight Games and their artists; it is not available under
an open license and is used for non-commercial fan play only. See [NOTICE.md](NOTICE.md).

## Documentation

**[OFFLINE.md](OFFLINE.md)** is the full operations manual: serving, headless harness
usage, deck imports, AI tuning hooks, image setup, menu regeneration and the Vercel
build.

## How do I report bugs?

Upstream bugs: https://github.com/drbo6/chiriboga/issues
(issues with this fork's offline/harness tooling should first make sure they reproduce
on the upstream demo or with the documented commands in OFFLINE.md).

If you do, two things help a lot:
- If there is a relevant game state, open the in-game menu and click "Download Debug
  Log", and attach the txt file.
- On desktop, open the developer console (F12 in most browsers) and copy-paste any errors.

## Credits

### Original Engine
**Chiriboga** — developed by [bobtheuberfish](https://github.com/bobtheuberfish)
Source: https://github.com/bobtheuberfish/chiriboga

### Solo Mode Extension
Developed by [DrBo6](https://github.com/drbo6)
Enhanced interface, game modes, and Gauntlet system

### Preconstructed Decks
- Girometics SG+SU21 and NSG Core precons
- Additional precons curated by DrBo6

### Offline fork
Static deployment, headless harness, NRDB importer and launcher menu added on top of
upstream; see [OFFLINE.md](OFFLINE.md).

### Special Thanks
Testers: BadEpsilon, bowlsley, D-Smith, eniteris, Kwaice, Mentlegen, olompumpa, R41B, saff, Saintis, Ysengrin

## License

This project is GPL-3.0 licensed, like upstream chiriboga — see [LICENSE](LICENSE).
The complete source of the deployed game is at https://github.com/ianicius/NROFF (branch `main`).

The offline fork's own code (table interface, AI modules, offline layer and tooling) is
Copyright (C) 2026 Rafał Janicki and carries additional GPL-3.0 section 7 terms on
attribution and naming: see [ADDITIONAL-TERMS.md](ADDITIONAL-TERMS.md). The fork's table
art is licensed separately under CC BY-NC-ND 4.0: see [ASSETS-LICENSE.md](ASSETS-LICENSE.md).
[NOTICE.md](NOTICE.md) maps who wrote what and lists third-party content.

This program comes with ABSOLUTELY NO WARRANTY, to the extent permitted by law; see
sections 15 and 16 of the license.

## Legal

*Netrunner* and *Android* are trademarks of Fantasy Flight Publishing, Inc. and/or
Wizards of the Coast LLC. This is a fan-made project and is not affiliated with or
endorsed by FFG, WotC, or Null Signal Games.

Card art, card text and game symbols are property of Null Signal Games and their artists.
Game symbols from NSG's public asset pack are used under
[CC BY-ND 4.0](https://creativecommons.org/licenses/by-nd/4.0/); card art is not covered by
that license and is shown for non-commercial fan use only.


The standard player-area layout uses the current service-worker shell family `nroff-complete-offline-v15`; `sw.js` carries the exact content revision. See `table/README.md` for the central ICE routes, Runner depth rows, scrolling and local checks.
