Underground 2 Revived
Play Need for Speed: Underground 2 (2004) in Chrome and Firefox with WebGPU graphics, Web Audio, up to 120 fps widescreen, controller support, free roam and Online races with friends. Fan project, not
翻譯準備中
尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證正在載入完整 README。 以下為已儲存的摘錄。
Before the oil. Before the corruption. There was only perfection.
An unofficial Magic: The Gathering rules engine and online play platform. Not affiliated with, endorsed, sponsored, or specifically approved by Wizards of the Coast LLC.
Play now at magic.wingedsheep.com · Join our Discord
Argentum Engine is a modular MTG implementation composed of:檢視完整 README ↗Before the oil. Before the corruption. There was only perfection.
An unofficial Magic: The Gathering rules engine and online play platform. Not affiliated with, endorsed, sponsored, or specifically approved by Wizards of the Coast LLC.
Play now at magic.wingedsheep.com · Join our Discord
Want to help? Missing a card from your deck? Run just contribute and let Claude Code or Codex build it — see Contributing.
Argentum Engine is a modular MTG implementation composed of:
Live set completion tracker → magic.wingedsheep.com/set-completion — per-set coverage, and every card in a set marked implemented, missing, or not planned. Missing cards that Argentum Assay already reads end to end are flagged ⚡ Assay-ready: they need no new engine vocabulary, so they're the cheapest ones to pick up. The same page carries the Assay explorer as a second tab.
Distinct implemented cards, day by day since the project began. Regenerated daily by the
Refresh card progress workflow, which runs scripts/card-progress-graph (an interactive version lives at card-implementation-progress.html).
# Initialize environment
just init
# Start Redis (optional, for session persistence)
just docker-up
# Start the game server
just server
# In another terminal, start the web client
just client
The client runs at http://localhost:5173 and connects to the server at http://localhost:8080.
Build & Test
| Command | Description |
|---|---|
just build | Build the entire project |
just test | Run all tests |
just test-rules | Run rules-engine tests only |
just test-server | Run game-server tests only |
just test-gym | Run gym tests only |
just test-gym-server | Run gym-server HTTP tests only |
just test-gym-trainer | Run gym-trainer (MCTS + self-play) tests only |
just clean | Clean build artifacts |
Development
| Command | Description |
|---|---|
just server | Start the game server (port 8080) |
just gym-server | Start the gym HTTP server (port 8081) — for RL/MCTS training |
just client | Start the web client dev server |
just client-install | Install web client dependencies |
Environment
| Command | Description |
|---|---|
just init | Create .env from .env.example |
just docker-up | Start local Docker services (Redis) |
just docker-down | Stop local Docker services |
just docker-logs | View Docker logs |
Copy .env.example to .env to configure:
| Variable | Default | Description |
|---|---|---|
CACHE_REDIS_ENABLED | false | Enable Redis for session persistence |
REDIS_HOST | localhost | Redis host |
REDIS_PORT | 6379 | Redis port |
GAME_AI_ENABLED | true | Enable AI opponent |
GAME_AI_MODE | engine | AI mode: engine (built-in), llm (requires API key), jev (local profile), or the mode of a registered AiControllerProvider. An unrecognised value fails startup. |
OPENROUTER_API_KEY | Shared OpenRouter API key for llm and jev modes | |
GAME_AI_MODEL | google/gemini-3.1-flash-lite-preview | LLM model (only for llm mode) |
Host booster drafts with up to 8 players. Create a draft lobby, invite friends, and build your deck from freshly opened packs.
Play Magic against friends with fully implemented MTG rules. The engine automatically handles the stack, priority, combat, triggers, and state-based actions—so you can focus on the game.
Play multiplayer Free-for-All games for 2–6 players (CR 806). From a lobby, switch the mode toggle from Tournament to Free-for-All to seat every player in a single shared game instead of a 2-player bracket — any pool-building format (sealed, draft, custom decks) composes with it.
Eliminated players are removed from the table while the remaining seats play on: conceding (or losing) takes you out, your opponents' boards keep going, and the opponent rail shows a live tombstone for anyone knocked out. Standings follow elimination order, and a "Play Again" ready loop lets the pod rematch without rebuilding the lobby.
Play against an AI opponent using the built-in engine, a chat model, or Jev (local development).
The built-in rules-engine AI runs locally with no external dependencies. It uses multi-ply game tree search with alpha-beta pruning, a composite board evaluator, and a specialized combat advisor.
Works out of the box — no API key or configuration needed:
# AI is enabled by default in engine mode
GAME_AI_ENABLED=true
GAME_AI_MODE=engine
Start the server and client, then click "Play vs AI" on the main menu.
Alternatively, you can use an LLM-powered AI that sends game state to an OpenAI-compatible API for decisions.
Setup:
.env file:GAME_AI_ENABLED=true
GAME_AI_MODE=llm
OPENROUTER_API_KEY=sk-or-v1-your-key-here
# GAME_AI_MODEL=google/gemini-3.1-flash-lite-preview # optional, change model
The LLM AI receives the same masked game state as a human player and responds through the standard game protocol. When the LLM fails to respond or returns an unparseable answer, it falls back to heuristic play.
Jev makes structured choices through OpenRouter's Decisions API.
Add these to your .env, then run just dev (which activates the local Spring profile):
GAME_AI_ENABLED=true
GAME_AI_MODE=jev
OPENROUTER_API_KEY=your-openrouter-key
Choose Just me, pick a deck source, and start playing (or use Add AI in a lobby); the opponent's name ends in (Jev).
No Ollama or TypeSafe account is needed. The default model is typesafe/jev-1.13.
Jev shares OPENROUTER_API_KEY with OpenRouter chat models and has its own endpoint, so local Ollama defaults cannot redirect its requests.
Do not put Jev in GAME_AI_MODEL: that setting uses the chat-completions protocol.
Jev chooses plays, targets, X, modes, attackers, blockers, damage assignments, card selections, ordering, piles, additional/alternative payments, mulligans, bottom cards, and draft picks. Ordinary mana payment can use the engine solver when Jev selects auto-pay. Sealed deck construction uses the existing heuristic builder. All gameplay choices use typed options from server metadata; the engine previews the completed action and supplies validation feedback for one correction. The engine AI is used only after a failed request, exhausted budget, or invalid corrected move; server logs explicitly report fallback. Jev receives the player's masked view, permitted decision metadata and known decklist. The provider rebuilds a masked view even when local debug mode reveals both hands on screen; debug event logs are excluded from requests.
| Variable | Default | Description |
|---|---|---|
GAME_AI_JEV_MODEL | typesafe/jev-1.13 | Decisions model |
GAME_AI_JEV_ENDPOINT | https://openrouter.ai/api/alpha/decisions | Decisions endpoint (also useful for a local mock) |
GAME_AI_JEV_TIMEOUT_MS | 30000 | Total budget per decision, including follow-up choices and correction; max 120000 |
The adapter caps each decision at 64 API calls and each request at 28 KB to stay conservatively
within the model context window. Choices with more than 255 options use hierarchical selection;
they are not silently truncated. Very large positions can hit the budget and fall back. This is an
experimental opponent, not a claim of competitive playing strength. Calls are billed by OpenRouter.
The jev provider is registered only under the local profile; other profiles reject that mode.
| Variable | Default | Description |
|---|---|---|
GAME_AI_ENABLED | true | Enable the AI opponent feature |
GAME_AI_MODE | engine | engine — built-in AI (no API key needed); llm — LLM-powered AI; jev — local Jev opponent; or the mode of an AiControllerProvider bean supplied by another build. An unrecognised value fails startup. |
GAME_AI_BASE_URL | https://openrouter.ai/api/v1 | LLM API endpoint (LLM mode only) |
GAME_AI_API_KEY | API key for LLM provider (LLM mode only) | |
GAME_AI_MODEL | google/gemini-3.1-flash-lite-preview | LLM model name (LLM mode only) |
GAME_AI_DECKBUILDING_MODEL | Separate model for AI deckbuilding; falls back to GAME_AI_MODEL if not set (LLM mode only) |
argentum-engine/
├── mtg-sdk/ # Shared contract — DSLs, data models, primitives
├── mtg-sets/ # Aggregator — re-exports the whole card corpus, plus the set catalog
│ ├── core/ # CardDiscovery, token art, cards belonging to no set
│ └── <era>/ # Card definitions, one module per release-year range (`just where <SET>`)
│ └── tests/ # That era's card scenario tests
├── rules-engine/ # Core MTG rules engine (no server dependencies)
├── gym/ # RL/MCTS environment wrapper (GameEnvironment, MultiEnvService)
├── gym-server/ # Spring Boot HTTP transport for gym (Python trainers)
├── gym-trainer/ # JVM-side MCTS + self-play SPI for AlphaZero-style projects
├── game-server/ # Spring Boot game server & matchmaking
├── web-client/ # React/TypeScript browser UI
└── e2e-scenarios/ # Playwright end-to-end tests
The rules engine is a standalone library with no server dependencies. It models the complete game state immutably and exposes a pure functional API:
Cards are defined as pure data using a Kotlin DSL — no card-specific logic in the engine.
For agent research and reinforcement-learning training, the engine also ships as a Gymnasium-style environment. A trainer drives many games in parallel against a stable JSON contract, without touching the game server or the browser UI.
gymA transport-agnostic Kotlin library that wraps the rules engine in a stateful reset / step / observe / legalActions API with MCTS-friendly affordances:
GameEnvironment.fork() returns a sibling env pointing at the same GameState. Because state is never mutated in place, tree expansion is free.MultiEnvService.snapshot() returns an opaque handle; restore() swaps the env back to that state in O(1). Designed to grow a byte-blob variant for cross-process MCTS workers.MultiEnvService.stepBatch() fans out per-env steps across a work-stealing pool, so vectorised rollouts run in parallel.PendingDecisions (scry, targets, search, distribute…); simple decisions fold into the numeric action-ID space, complex ones expose a structured response channel.TrainingObservation has a schemaHash so Python clients fail fast on contract drift; every observation carries a stateDigest usable as an MCTS transposition-table key.revealAll is available for debug scripts.gym-serverA thin Spring Boot shell that exposes MultiEnvService over HTTP so a Python agent can drive the engine without a JVM embedding:
| Method & path | Maps to |
|---|---|
POST /envs | MultiEnvService.create |
GET /envs | listEnvs |
DELETE /envs | dispose |
GET /envs/{id} | observe |
POST /envs/{id}/reset | reset |
POST /envs/{id}/step | step |
POST /envs/step-batch | stepBatch |
POST /envs/{id}/decision | submitDecision (structured DecisionResponse) |
POST /envs/{id}/fork?count=N | fork |
POST /envs/{id}/snapshot | snapshot |
POST /envs/{id}/restore | restore |
GET /schema-hash | observation-schema version (fail-fast on drift) |
GET /health | liveness probe |
JSON is handled end-to-end by kotlinx.serialization — sealed hierarchies (DeckSpec, DecisionResponse) round-trip via @SerialName discriminators without extra adapter code.
Start the server with just gym-server (port 8081, so it doesn't collide with the game server on 8080). Running it does not require the web client or Redis.
Deliberately out of scope for the current scaffold: authentication, env-lifetime TTLs, byte-based snapshots, metrics. Bind to localhost until you add auth.
gym-trainerFor AlphaZero-shaped projects that want tree search in the JVM and only use Python as a stateless NN inference server (MageZero-style): a small SPI + a PUCT MCTS + a self-play loop.
StateFeaturizer<T>, ActionFeaturizer (multi-head first-class), Evaluator<T>, SelfPlaySink<T>.AlphaZeroSearch with PUCT, optional Dirichlet root noise, using GameEnvironment.fork() for O(1) tree expansion.SelfPlayLoop with temperature schedule that labels training rows with the final outcome before flushing.BoardEvaluator, a structural featurizer, a hash-bucket action featurizer, a JSONL sink, and a random structured-decision resolver.RemoteHttpEvaluator POSTs features + legal slots and parses {priors, value}. Swap codec by subclassing.See gym-trainer/README.md for the full design write-up and a 30-line hello-world.
Contributions are very welcome, and I'm genuinely grateful for every PR. To keep the project healthy, there's one rule above all others:
No slop PRs. A reviewer's time is the scarcest resource here. Please open a PR only when it's been tested, it's faithful to the actual Magic rules (no shortcuts), and every change in it is there on purpose. Built with a frontier model through
just contribute? Great — the gate and review it runs are exactly that check. A large unverified PR is still worth less than a small careful one.
The fastest way to help is to make the cards you want to play work. One command walks you through it:
just contribute
It checks your setup, then asks three questions:
just loop-gate),
reviewed by fresh-context subagents, and committed on one local branch. Nothing is pushed. When
it's done, skim the commits, push the branch to your fork, and open one PR.Cards that need new engine features aren't faked: the agent records them as gaps and builds each
missing capability properly (with the add-feature skill) before the cards that need it.
You'll need: JDK 21+, just, git, jq, Python 3, and
Claude Code or Codex. The GitHub CLI (gh) is optional — it lets the agent
skip cards someone else's open PR already adds.
Before you start: main is the source of truth for what's implemented; open PRs are what's in
flight. Check how much of a deck already works with:
just card-list-status my-deck.txt # implemented vs missing, from your checkout
The wizard is a front end for three loops you can also run directly (each --helps):
| Command | What it does |
|---|---|
just card-loop <list-file> [model] [--local] | Implements every missing card of a list or decklist |
just set-loop <set> [model] | Implements a whole set from its backlog, one PR at a time |
just commander-loop [model] [top] | Makes EDHREC's top Commander decks playable, deck by deck |
Models: opus, sonnet, astra (Codex gpt-6-astra), any claude-* ID, or codex:<model-id>.
just loops shows what every running loop is doing; just loop-watch <name> opens a live dashboard.
Using AI to implement cards is encouraged — most of the card catalog is data, and the project ships
agent skills that automate the workflow correctly (Scryfall
lookup, oracle errata, set registration, scenario tests, reprint handling). They live in
.claude/skills/ (Claude Code) and .agents/skills/ (Codex):
add-card <CARD_NAME> <SET_CODE> — implement a specific card.add-random-card <SET_CODE> — pick a random unimplemented card from a set and implement it.If a card adds a new UI / UX element, test it manually before opening the PR — AI can build the
flow, but a human needs to confirm it actually feels right in the client. Run the app (just server
just client), set up the situation (the generate-scenario skill can inject a board state), and
click through the decision yourself.:oracle-assay is a first-party Oracle-text grammar: Scryfall JSON in,
mtg-sdk models out. Its trick is that every rule is written in both directions — a rule that can
parse a phrase must also print it — so the whole corpus checks the grammar without a human reading
the output. If print(parse(text)) == text, the reading was right.
That makes it an auditor before it is a generator, and the thing it audits is us. A card Assay
cannot read is a card whose meaning the SDK has no way to say, so the ranked list of declines is a
continuously-updated report of what the card vocabulary is missing, ordered by how many cards each gap
blocks. Coverage is the lagging indicator: 100% would mean mtg-sdk can express every card in Magic.
just assay parse "Serra Angel" # the normalized lines, and the SDK model each one parses to
just assay explain "Wall of Omens" # the same, with a caret on the token a decline died on
just assay-gate # the touchstone over all 34,882 cards; exits 1 on a bug
just assay-report --rank tail # the SDK gap report, ranked by what each family would unlock
just assay-differential # Assay's readings vs. the hand-written cards — the sharper gate
just assay-explore # all of it in a browser, against the grammar on your classpath
just assay-bake # re-bless the per-card verdict ledger the coverage page reads
Two gates keep it honest. The touchstone proves a reading round-trips; the differential compares
Assay's reading of a card against the hand-written cardDef we already ship, which is the only thing
that catches a parse that round-trips byte-perfectly while meaning something else. It has found real
bugs in hand-written cards — the outcome worth the most.
It is deliberately not a card loader, and won't become one: a human-authored cardDef with a
passing scenario test stays the only ground truth. The one carve-out is the Scenario Builder's
dev-gated sandbox, where you can paste a custom card and play it if — and only if — Assay reads every
line of it.
The explorer (just assay-explore, or the Assay Explorer tab on the set completion page) is the
readable version of all of the above: the ranked declines with the cards behind each one, every card's
reading beside its printed text, the wired grammar, the differential, and a box for text that was
never printed. It runs against the grammar on the classpath rather than a snapshot, so a rule you just
edited is one restart from being re-measured.
See oracle-assay/README.md for the design, the verdict table, and what the
gates have found so far.
Assay replaced the earlier :mtgish-tooling pipeline, which mapped the
mtgish oracle-IR corpus by i5jb
onto our SDK. That project's clean IR got the first auto-drafts off the ground, and its explorer is
what Assay's is modelled on — thank you. The module is deprecated and kept only for reference.
When bringing up an entire set, this flow has proven much faster than implementing cards one at a time (it's how Invasion was done):
add-feature skill, which encodes these principles end to end.add-card skill to implement a handful
of cards, and have each open a PR when its batch is done.review-changes <PR_URL> on it — this checks for elegance and
correctness and keeps the engine/SDK clean. (Card-only PRs with no engine changes don't need it.)just assay-differential --set <SET>: any DIVERGENT row
means Assay's reading of a card and the hand-written cardDef disagree — classify it before calling
the set done. New SDK vocabulary you added isn't taught to Assay in the same PR; name it in the PR
body, and the assay-loop widens the grammar to read it as its
own measured band.A set can also finish with a card or two deliberately left out — Legends' Falling Star needs physical
dexterity the Chaos Orb table-flip doesn't cover. Add those to
coverage/card-exclusions.json with a reason (required) and re-run
scripts/gen-set-totals: they drop out of the denominator and show as "not planned" on the
set completion tracker, so completion means
everything we intend to build is built. It's a policy list, not a backlog — a card that's merely
hard, or waiting on a feature we do plan to build, stays a normal gap.
AGENTS.md and docs/architecture-principles.md
first — they describe the load-bearing rules (immutability, projected state, events-not-mutations,
server-authoritative client) that PRs are reviewed against.docs/card-sdk-language-reference.md in the same
change whenever you add or change anything in the SDK..txt is too large to fetch
into context — download it and grep locally.just loop-gate (it runs the tests your diff can break; CI runs the full suite) and confirm
green before opening the PR. Card changes: just loop-gate --rebless and check only your cards moved.Questions or ideas? Join the Discord.
Argentum was a plane of mathematical perfection, created by the planeswalker Karn. Every angle intentional, every law absolute. It was governed by rules so elegant they seemed inevitable.
That's what a rules engine should be.