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
Standard·Pioneer·Modern·Legacy·Commander용 합법 MTG 덱을 카드 이유·마나 곡선·가격과 함께 구성
Build legal Magic: The Gathering decks for Standard, Pioneer, Modern, Legacy and Commander, with a reason for every card, a mana curve and a price.
게임MDX · PythonGitHub @JaredECohen온라인운영자 미확인
써 보셨어요? 한 번만 눌러 주세요. 로그인은 필요 없어요.
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기메이커가 쓴 소개는 위의 한 줄이 전부입니다. 더 자세한 내용은 저장소 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) |