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。 儲存庫 README ↗
正在載入完整 README。
檢視完整 README ↗Full-stack deck-construction app with a first-principles simulator + optimizer
that produces tournament-grade decks for all five major formats. FastAPI
backend, Next.js frontend, structured DeckRationale output narrated by an
optional LLM coach.
Current state: 486 backend tests passing, frontend TypeScript clean
(next build green), end-to-end optimizer flow live behind a feature
flag, multi-format meta + deck-evaluation engine + embedding retrieval
wired.
api/: FastAPI backend, simulator kernel, optimizer, critic loopweb/: Next.js frontendpackages/shared/: shared product/domain documentationinfra/: local infrastructure notes.claude/skills/: versioned LLM system prompts (card-evaluator,
deck-critic, builder-responder, goldfish-coach)cd api
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
python -m app.scripts.seed_sample_cards
python -m app.scripts.ingest_tournament_decks
python -m app.scripts.build_archetypes
python -m app.scripts.build_card_profiles # every card; --modern-only to opt out
uvicorn app.main:app --reload
cd web
npm install
API_BASE=http://127.0.0.1:8000 MTG_API_KEY=<same key as the API, if set> npm run dev
The browser never talks to the API directly: web/src/app/api/backend/[...path]
is a server-side proxy that forwards /v1/* and /api/* calls to API_BASE
and adds X-API-Key from the web server's MTG_API_KEY, so the key never
ships in the JS bundle. Run the API with MTG_TRUST_FORWARDED=1 behind it so
rate limits stay per client.
cd api
python -m pytest -q # 486 tests, about 30 s, no skips
Tests always run against api/tests/fixtures/cards.db.gz: a deterministic
cut of about 2k cards from the full Scryfall ingest, plus the full archetype
and tournament corpus. conftest.py decompresses it into a temp dir per run,
so local runs and CI see the same data and never write to var/.
python -m app.scripts.build_test_fixture.MTG_TEST_DATABASE_URL=sqlite:///../var/mtg_deck_builder.db python -m pytest..github/workflows/ci.yml runs on every push
to main and every PR: the backend pytest suite and the frontend
unit tests, tsc --noEmit, lint, and next build.
The web app deploys to Vercel and the API to one Fly.io machine, with SQLite on a volume (about $6/month). docs/DEPLOY.md has the setup steps, secrets, the weekly card-refresh workflow and spend caps.
Traffic and the deck funnel are recorded with Vercel Web Analytics
(@vercel/analytics). The script is in the root layout. Nothing is stored until
Web Analytics is enabled for the Vercel project, and no environment variable is
required.
mtg-deck-builder project in the Vercel dashboard.Page views, including guide pages, are collected on the Hobby plan (50,000 events per month, then collection pauses). Custom events require Pro. On Hobby, Vercel does not record them, so the funnel stays empty until the team is on Pro. On Pro, each custom event can carry two properties. Web Analytics Plus ($10/month) raises that to eight, which this site does not need.
| Event | When it fires | Properties |
|---|---|---|
deck_generated | A deck build succeeds | format, source (new or alternative) |
deck_refined | A refine succeeds | format |
deck_exported | An Arena, Moxfield, or Archidekt export is built | target, format |
deck_shared | A share link is created | format |
tcgplayer_cart_clicked | "Buy entire deck on TCGplayer" is clicked | format |
guide_viewed | A guide article is opened | slug, category |
format is one of standard, modern, pioneer, legacy, commander.
target is arena, moxfield, or archidekt. Plain-text and CSV exports are
not part of the funnel. Prompts, deck titles, card lists, emails, and share
tokens are rejected before send. Page-view URLs have the brief and deck
query parameters removed, so a guide's "try this" link and a share link do not
land in analytics. /privacy describes the same limits.
/v1/decks/generate — rules-based deck assembly using archetype templates/v1/decks/refine, /v1/decks/analyze, /v1/decks/validate —
archetype-similarity analysis and rule checkingPOST /v1/jobs/optimize — submit a constraint envelope (format,
colors, budget, includes, excludes, archetype recipes)GET /v1/jobs/{id} — poll for completionPOST /v1/jobs/{id}/prose — lazily produce LLM-narrated coach prose
(202 while job is still running, 200 when ready, 404 if unknown)To make the optimizer the default for /v1/decks/generate:
export MTG_USE_OPTIMIZER_DEFAULT=true
The wrapper returns the same DeckResponse shape and falls back to the
legacy generator on any error so prod traffic isn't affected by a bad deploy.
A fixture-database comparison is in qa/OPTIMIZER_COMPARISON.md.
The default stays off until that comparison is re-run on the full card
database and the optimizer clearly wins.
POST /v1/commander/precon-upgrade turns an official precon (or a pasted
list) and a dollar budget into ranked one-for-one swaps, a Commander
legality check, a bracket estimate, and a TCGplayer cart for the adds.
GET /v1/commander/precons searches the precon catalog. The workshop's
"Upgrade a precon" tab calls both.
Eleven independently-testable layers, all running headlessly without FastAPI/DB dependencies:
| Layer | Module | Purpose |
|---|---|---|
| 1 | api/app/oracle/ | Parse oracle text into typed CardProfile: cost vector, effect vector, role weights, combo primitives, alt-cost detection (Evoke/Suspend/Cascade/Delve/Channel/Flashback/Cycling) |
| 2 | api/app/sim/mana.py | Mana-base solver: closed-form hypergeometric (Karsten thresholds) + Monte Carlo for fetch/shock/MDFC/painland interactions |
| 3 | api/app/sim/goldfish.py | Solitaire turn-by-turn simulator with 4 pluggable policies (aggro/combo/control/midrange) → kill-turn distribution. Tron-assembly rule, ETB-tapped tracking |
| 4 | api/app/synergy/ | Synergy graph: produces→requires edges, tutor→closer edges, 12 known combo cliques (Splinter Twin, Storm, Living End, Yawgmoth, Ad Nauseam, Hardened Scales, Devoted Druid, Goryo's, Thoracle, Scapeshift, Amulet Titan). Criticality scored via vanilla-replacement ∆kill-turn. Cached by deck identity + profile_version |
| 5 | api/app/optimizer/ | Simulated annealing with targeted swap proposals (driven by worst fitness axis), constraint-repair branch, monotonic violation reduction, format-aware playset cap (4-of constructed, 1-of singleton) |
| 6 | api/app/workers/ | Backend-agnostic async job queue. ThreadJobQueue (default, in-process) and CeleryJobQueue (Redis-backed, selectable via MTG_WORKER_BACKEND=celery). Result caching by hash |
| 7 | api/app/critic/ |
The deck workshop (web/src/components/deck-workshop.tsx)
shows the legacy generator's output by default. When a user clicks
"Run optimizer," the workshop submits to /v1/jobs/optimize via the
useDeckRationale hook, polls for
completion, and renders DeckRationaleView
with full sections: headline, why-this-wins, key turns, mulligan guide,
matchup table, weakness callouts, critic transcript, and sideboard plan.
Under .claude/skills/. Used out-of-loop with results cached so generation stays fast and deterministic.
card-evaluator — fallback parser for cards whose oracle text the
deterministic parser can't fully cover (writes back to card_profiles)deck-critic — GPT-5.5 system prompt for the critic loopbuilder-responder — Claude system prompt for the builder sidegoldfish-coach — narrates structured DeckRationale into prose| Variable | Purpose | Default |
|---|---|---|
DATABASE_URL | Postgres connection | sqlite:///var/mtg_deck_builder.db |
CORS_ORIGINS | Allowed CORS origins | http://localhost:3000,http://127.0.0.1:3000 |
MTG_USE_OPTIMIZER_DEFAULT | Route /v1/decks/generate through optimizer | false |
MTG_WORKER_BACKEND | thread or celery | thread |
MTG_WORKER_THREADS | ThreadPoolExecutor size | 4 |
REDIS_URL | Celery broker + result backend | redis://localhost:6379/0 |
ANTHROPIC_API_KEY | Builder client + coach prose | (unset → mocks) |
OPENAI_API_KEY | Critic client | (unset → mocks) |
MTG_BUILDER_MODEL | Override Claude model | claude-opus-4-7 |
MTG_CRITIC_MODEL | Override GPT model | gpt-5.5 |
MTG_COACH_MODEL | Override Haiku model for prose | claude-haiku-4-5-20251001 |
MTG_CRITIC_MAX_TOKENS_PER_CALL | Per-call output cap | 4096 |
MTG_CRITIC_MAX_USD_PER_JOB | Per-job cost cap (shared across rounds) | 1.50 |
OPENAI_EVAL_MODEL | Judge model for qa/run_eval.py | gpt-6-luna |
MTG_COACH_DISABLE | Force-skip coach prose even with key set | (unset) |
MTG_API_KEY | Comma-separated API keys; enables X-API-Key auth on optimize/prose/evaluate/save/precon-upgrade | (unset → auth off) |
MTG_USE_PGVECTOR |
Monetized via TCGplayer affiliate links (the marketplace where players actually buy MTG singles). Surfaces:
POST /v1/decks/chat,
API-key gated like the other LLM endpoints). Questions get deck-specific answers;
a message that asks for a change comes back with a proposed refinement and an
Apply to deck button that runs it through the normal Refine flow (visible in
the Refine box, undoable). The transcript is held client-side and resets when a
different deck is generated. Needs ANTHROPIC_API_KEY on the API; without it the
panel reports that chat is unavailable.TCGplayer's program runs through Impact; once approved you
get a deep-link prefix — set both variables above to it. Links are wrapped at the
API read boundary (app/services/affiliate.py) and at build time on the
frontend, so changing the prefix never requires re-ingesting card data. The
required affiliate disclosure renders alongside every link surface.
cd api
createdb mtg_deck_builder
python -m app.scripts.seed_sample_cards
python -m app.scripts.ingest_scryfall # full Scryfall ingest (optional)
python -m app.scripts.ingest_tournament_decks
python -m app.scripts.ingest_official_decks --download # MTGJSON precons; see docs/DATA_SOURCES.md
python -m app.scripts.build_archetypes
python -m app.scripts.build_card_profiles # every card; --modern-only to opt out
python -m app.scripts.build_card_embeddings # dense embeddings → card_embeddings
Embeddings power the retriever's vector mode (works on SQLite too) and
the pgvector <=> path on Postgres. The default embedder is a
deterministic feature-hash projection (app/services/embeddings.py)
so it runs offline; swap embed_features for a learned model without
changing the table schema, ingest script, or retriever.
The optimizer reads from card_profiles when populated and falls back
to on-the-fly profiling when it isn't (logs a warning).
SynergyGraph instance (avoids 9000 graph rebuilds per optimization)The simulator now ships a multi-signal evaluation layer (api/app/sim/evaluation.py) that grades a concrete decklist beyond a single kill-turn number:
disruption_rate goldfish primitive).Exposed at POST /v1/decks/evaluate and folded into the optimizer's
fitness as the resilience + inevitability axes (computed on
deep_eval=True, off in the hot anneal loop).
POST /v1/decks/evaluate — run the evaluation battery on a decklistPOST /v1/decks/diff — card-by-card before/after diffPOST /v1/decks/save, GET /v1/decks/history,
GET /v1/decks/saved/{id}, GET /v1/decks/shared/{token},
DELETE /v1/decks/saved/{id} — persistent history + sharingGET /v1/cards/{name}/similar — semantic-ish neighbours (pgvector
fast-path, deterministic lexical fallback)Optional API-key auth (MTG_API_KEY, X-API-Key header) gates every
endpoint that spends LLM tokens or touches saved decks — generate, refine,
analyze, chat, evaluate, optimize, prose, save/history — while validate,
parse, export, diff, card/commander search, meta and shared-deck reads stay
public behind a per-client rate limit. Saved decks store a hash of the key,
and no response contains it.
constraints — the budget, includes/excludes, owned cards, mode, match
type and bracket it was built under; a refine keeps them.game_plan — win conditions, exact opening-hand land odds, mulligan
rules, a sideboard guide (each card's job, when it comes in, what goes
out) and matchup notes; Commander decks also get ramp/draw/removal/wipes/
tutors packages. Deterministic, from the list itself.bracket (Commander) — Game Changers, mass land denial, extra turns,
tutors and two-card combos with an estimated bracket floor
(game_changers.json, versioned).alternatives — 1-2 other shells that fit; archetype_name builds one.color_sources, unpriced_cards, cost_to_complete_usd, deck_id,
and on refine changes (the card diff).source_type (template = curated/synthesized shape, not
observed results), plus llm_added_cards, budget_substitutions and
dropped_includes with reasons.Request options beyond the basics: mode (fast-competitive /
constraint-aware / creative-but-viable), match_type (bo1 → no sideboard),
bracket (1-5), owned_cards, archetype_name. Briefs are parsed
deterministically when no LLM key is set (colors, guilds, budgets,
playstyles, "no X", commander names). Export targets: plain, Arena,
Moxfield, Archidekt, CSV.
qa/benchmark.py runs 108 deterministic scenarios (all formats, colors,
budgets, modes, Bo1/Bo3, brackets, prompts, scripted refines) and reports
legality, color fidelity, budget adherence, sideboard completeness, bracket
compliance, refine success, prompt fidelity and latency — see
qa/README.md. A 40-scenario subset gates CI
(api/tests/test_benchmark_gate.py).
META_BY_FORMAT now ships Standard,
Pioneer, Legacy, and Commander opponent sets plus Modern Yawgmoth &
Amulet Titan; build_matchup_matrix grades each candidate against its
own format's meta.suggest_clique_candidates() mining tournament lists for new
candidates.vector/pgvector
retrieval modes — is wired and tested today)owner field is a hash of the API key)ingest_deck_source (CSV/JSON
adapters) and build_archetypes.unpriced_cards) rather than counting them as free.| Builder (Claude) ⇄ Critic (GPT-5.5) loop with strict JSON envelope, 6-item evidence-bound rubric, deterministic short-circuit, auto-downgrade if critic over-approves, monotonic-improvement rollback, 4-round cap. Live SDK clients with retries + shared per-job cost budget; falls back to deterministic mocks when API keys are absent |
| 8 | api/app/services/deck_rationale.py | Structured DeckRationale: headline, why-this-wins, key turns, mulligan guide, soft matchups, weakness callouts, critic transcript. Optional LLM-narrated coach prose, fetched lazily via POST /v1/jobs/{id}/prose |
| 9 | api/app/sim/match.py + meta_archetypes.py | Two-player matchup simulator: parallel goldfish + simplified combat with blocking + opponent instant-removal probe (taps mana — no infinite removal). Per-side format mulligan profiles for cross-format matchups. Pre-baked Modern meta opponents (Burn, Murktide, Tron, Living End) |
| 10 | api/app/optimizer/format_config.py | Format-specific config: deck size, singleton rule, starting life, ideal land range, mulligan profile floor. Drives Modern, Standard, Pioneer, Legacy, and Commander (99-card singleton, 40 life, 35-42 lands) |
| 11 | api/app/optimizer/sideboard.py | Sideboard generator: targets losing matchups first, classifies opponent archetype, ranks pool cards by answer category. Respects deck color identity; returns empty plan for Commander |
Use the pgvector NN path for /similar when the card_embeddings table exists |
false |
TCGPLAYER_AFFILIATE_URL | TCGplayer affiliate deep-link prefix from Impact (e.g. https://tcgplayer.pxf.io/c/123456/789012/21018); every TCGplayer link served by the API is wrapped for attribution | (unset → untracked links) |
NEXT_PUBLIC_TCGPLAYER_AFFILIATE_URL | Same prefix, inlined into the frontend for client-built "Shop This Deck" and mass-entry links | (unset) |