BeamNG.Earth
BeamNG.drive용 1:1 비율 실시간 지구, 실제 고도·위성 이미지·부동 원점·보조 서버에서 LOD 스트리밍
브라우저에서 AI 상대와 매직: 더 개더링 커맨더를 플레이, 규칙 엔진과 실제 카드 제공, 다운로드 필요 없음
Play Magic: The Gathering Commander against AI opponents — right in your browser. Full rules engine, real cards, no downloads.
게임TypeScript · JavaScriptGitHub @skwirls-lab온라인운영자 미확인
써 보셨어요? 한 번만 눌러 주세요. 로그인은 필요 없어요.
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기Collection App (First use case: MTG)
저장소 README에서 가져옴
README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
Play Magic: The Gathering Commander against AI opponents in your browser.
The rules engine is Forge, running headless on a
small Java server (undercroft-forge-server). This repository is the web client: sign in,
import a decklist, pick opponents, play.
Open http://localhost:3000. The game server URL defaults to the
Railway deployment; override it with NEXT_PUBLIC_FORGE_SERVER_URL to point at a local
undercroft-forge-server (ws://localhost:7000/game).
Every signed-in screen, and the game board itself, can be rendered with no Firebase project
and no game server:README 전문 보기 ↗Play Magic: The Gathering Commander against AI opponents in your browser.
The rules engine is Forge, running headless on a
small Java server (undercroft-forge-server). This repository is the web client: sign in,
import a decklist, pick opponents, play.
npm install
cp env.template .env.local # fill in the Firebase web-app config
npm run dev
Open http://localhost:3000. The game server URL defaults to the
Railway deployment; override it with NEXT_PUBLIC_FORGE_SERVER_URL to point at a local
undercroft-forge-server (ws://localhost:7000/game).
Every signed-in screen, and the game board itself, can be rendered with no Firebase project and no game server:
NEXT_PUBLIC_DEV_MOCK_AUTH=1 npx next dev -p 3100
/dev/board renders the real game screen against a seeded mid-game 4-player state.
?open=me or ?open=ai-2 opens a player's board on load./?signedOut=1 shows the landing page.This is a development-only mode: it is gated on NODE_ENV, which is inlined at build time,
so a production bundle cannot enable it and /dev/board is a 404 there.
Screenshot every screen at phone and desktop size, with an audit that fails if the game board scrolls vertically (it is designed to fit the viewport exactly):
npm run screenshot # writes to ./screenshots
| Layer | Technology |
|---|---|
| Framework | Next.js 16 (App Router), React 19, TypeScript |
| UI | Tailwind v4, shadcn/ui, Framer Motion, Lucide icons |
| Auth | Firebase Authentication (Google OAuth) |
| Data | Cloud Firestore — users/{uid}/decks per player (shelves and plan on users/{uid}), a shared cards collection of Scryfall data |
| Rules engine | Forge, headless, on a Java WebSocket bridge (separate repo) |
| AI opponents | Forge's own AI |
| Hosting | Vercel (frontend), Railway (game server) |
The client keeps no rules logic. It renders state snapshots the server pushes and sends back
decisions the server asks for (src/lib/forgeClient.ts → src/store/forgeGameStore.ts →
src/lib/forgeStateAdapter.ts → the game components).
Decks live in the vault (/decks). Each deck has its own page (/decks/{id}) that shows
every card as itself — scans from the shared card collection, grouped by type, with a reader
for rules text — plus colour identity, a mana curve and the deck's verification status.
Editing is a mode on that page: quantities, removals, an add-card search, renaming, choosing
the commander, or replacing the whole list as text. Every edit saves as it happens and
re-verifies the changed cards against the card database and the Forge engine, the same
pipeline import uses (src/lib/deckCards.ts).
Building from nothing. "New Deck" in the vault asks for a commander (a search limited to
cards that can lead a deck), suggests a name, and opens the deck page in edit mode. The
Add cards search asks Scryfall first — word matching anywhere in a name, ranked by how
often EDHREC sees the card, filtered to the commander's colour identity on the server side —
and falls back to the shared card collection when Scryfall is unreachable
(src/lib/cardSearch.ts). A card outside the commander's colours is shown, marked, and cannot
be added. The deck check (src/lib/deckRules.ts) reports 100 cards, one commander, the
singleton rule with its exceptions (basics, "any number", "up to seven"), colour identity,
unknown names and cards the engine lacks; it is one badge in the header and a dialog on tap.
Ready means legal. The badge on a deck in the vault and on the setup screen is the stored verdict of the deck check, refreshed whenever the deck page recomputes it, so "Ready" means every name resolved and the deck passes the Commander rules. A deck that predates the check reads "Unchecked" until it is opened. Pressing Start re-judges your deck and any vault deck handed to an AI, live; a failing deck gets a warning naming its issues with a link to fix it, and a "Play anyway" — it is a simulator, so the game is never refused.
Shelves are the vault's folders: one level, a name and an accent colour, filed on the player's profile document. A deck sits on at most one shelf.
Opponent decks. On the game setup screen each AI seat can play a random house deck (the
default), a specific house deck, or any vault deck with a commander — including the one you
are about to play, if a mirror match is the test. src/lib/opponentDecks.ts resolves the
choices into start_game payloads; the server names each AI seat after its deck.
Paste a list from Arena, Moxfield, Archidekt, TappedOut or plain text. Per line: an
optional count, the name, then anything an export appends — (SET) 123, a bare collector
number of up to three digits, *F* / *E* markers, [tags], *CMDR*. A name may end in a
year ("Spider-Man 2099"). // Commander and Commander: headers name the commander;
Sideboard, Maybeboard and Considering sections are skipped. Names resolve against the
card database by exact name, then spelling variations, then the front face of a double-faced
card, then Scryfall itself, so a card newer than the last weekly sync still resolves.
Every card has one face in the app: its oldest ordinary paper printing, the art most
players know it by. The weekly card sync (scripts/sync-cards.mjs) stores paper printings
only (no Arena-only sets) and marks that printing preferred; the name resolver and the
deck builder's Scryfall search (game:paper prefer:oldest) both pick it. Promos, full-art
and gold-bordered printings lose to any regular set.
Every seat plaque and the board header carry an (i). It opens the seat inspector: life, poison against 10, library and hand counts, the mana pool, commander damage taken from every opposing commander against the 21 that ends a game, and the graveyard, exile and command zone laid out as cards with a reader. Tabs across the top switch seats without closing.
The reader answers its own tags. Tap a card to read it; under the text, every keyword and counter is a button that explains itself in one line (from the Apprentice's glossary), and an attachment — or the card this one is attached to — opens that card instead.
The log says why. The engine describes every event as it happens (GameEventForwarder
in the server): who cast or activated what and at whom, damage with its source and whether
it was combat, every life change with its cause, poison, counters, attackers and blocks,
tokens, sacrifices, what left the battlefield and what did it, eliminations and how. Tap the
ticker for the full log, grouped by turn; Since my last turn shows only what happened
after your last turn began, which is the question after an auto-pass; Details adds mana
taps and phases. Hidden information stays hidden: an opponent's draw, tuck or search is a
line without a card name. One function phrases every line (src/lib/gameLog.ts), and the
recap the Archivist reads uses the same words.
Peek at the table. Every prompt — targets, attackers, blockers, tutors, mana — has a Peek button that hides it so you can look at every board; a pill brings you back, Escape too, and nothing is sent in between.
Mana. A source that makes several mana "in any combination" (Vivi Ornitier) asks how to split it; "activate only once each turn" holds for mana abilities; a Phyrexian symbol such as {U/P} offers two life instead of the mana.
Apprentice mode is the rulebook read at the right moment, with no model involved. It is on for new players and lives in two places:
PROMPT_GUIDE, one entry per server prompt). Notes fire once per game the
first time something matters: the stack, the commander leaving its zone, life at 10 or
less, seven poison, fifteen commander damage. Every prompt panel carries the same guide
at its foot. "Learn more" opens the lesson in a drawer beside the table, so the match is
never left. The deck check's issues each get a "Why?" link to the rule they break.Content is typed data in src/content/lessons/. npm run test:lessons checks that every
phase, step and server prompt the overlay renders has a guide, every quiz answer is in
range, and every link lands on a lesson and section that exist — so a new prompt cannot
ship without an explanation. Toggle: Settings → Learning, the cap in the game header, or the
switch on /learn. Harness: /dev/board?apprentice=0, ?choice=attackers.
Four guided tours of Undercroft's own controls, one per screen: the vault, a deck, setting up a game, and the table (plaques, the (i), the phase tracker, Pass and auto-pass, the hand strip and how paying works, the stack, the log, the Archivist). A spotlight cuts a hole in a dark mask over one control at a time with a card beside it; arrows move, Escape leaves. On a phone the card docks to whichever half the control is not in. A step whose control is not on the page (a Patron feature, an empty stack) is skipped.
Each tour runs once, the first time its screen opens, while Settings → Learning → "Show
tutorials" is on; completion is remembered on the device and mirrored to the profile
(users/{uid}.toursDone) so another device does not replay it. Settings lists the tours to
replay; the table tour replays with the next game. Content is src/content/tours.ts; anchors
are data-tour attributes on the existing components. npm run test:tours (Playwright,
against the mock dev server) walks every tour at phone and desktop and checks that each
step's target is on the page, in the viewport and not covered by the card.
The Archivist is the resident helper: a keeper of records who has read every card. It is asked, never volunteers, and every request counts against a monthly allowance shown in Settings.
One route, POST /api/archivist (src/app/api/archivist/route.ts): verify the player,
check the app switch and the plan, reserve one request, build the prompt
(src/lib/archivist/prompts.ts), call OpenRouter, stream the answer back, record the tokens.
The client (src/lib/archivist/client.ts, useArchivist()) streams into the panels and maps
the server's codes to one typed error. When the admin switch is off every entry point says
"The Archivist is resting" and nothing is sent. ARCHIVIST_STUB=1 answers from a canned
script so the whole path can be exercised without a key; the dev harness does the same in
mock mode (/decks/mock-atraxa?archivist=improve|swaps|strategy,
/dev/board?archivist=advice|recap).
Pass sends priority on. With an empty stack in your own main phase it ends the phase, so
the button says End main phase then, and the priority bar says which main phase you are
in. If you press it having done nothing that step while you still have plays available, the
table asks "End your main phase?" once; Pass anyway goes through, and the question is not
repeated within the same step. It never asks with a spell on the stack, on an opponent's
turn, in combat, or with auto-pass on. scripts/test-pass-guard.ts pins the rule.
scripts/test-game-log.ts pins the log's phrasing and what it keeps hidden.
A match keeps running while you visit the rest of the app: the keystone in the game header asks before leaving, and a gold banner at the top of every page returns you to the table until the game ends. Reloading the page or closing the tab does end it (the engine session is a live socket; there is no reconnect yet).
Every game you finish — or walk away from after turn 1 — is written to
users/{uid}/matches/{matchId} and listed under History: the result and how it was
decided (the reason the last seat fell: life, poison, commander damage, a card), the turn
count and length, your deck and commander against every opponent's, and per seat the
finishing place, final life, spells cast, damage dealt and life lost, and how and when it was
eliminated. The Archivist's recap, if you asked for one at the end, is saved on the record.
The dashboard shows the last match. src/lib/matchHistory.ts builds the record from the
final state, the described log and the server's per-seat outcome (npm run test:match-history); an older server still yields a record with the winner and the turns.
Settings has an Administration section for the admin allowlist: the Archivist's master
switch, model and allowances (config/app), a notice banner, Patron grants by email, and
this month's usage. Server-side code lives in src/lib/server/ (Admin SDK, bearer-token
auth, config cache, usage metering) and is reached through route handlers under
src/app/api/. SECURITY_SETUP.md has the environment variables and the rules.
Two plans, and one line between them: everything that is play is free. Free: the engine, pods of any size, choosing what each opponent plays, two decks in the vault with shelves, the deck builder and deck check, ten Archivist requests a month, the Apprentice, the tutorials and the match history. Patron ($4.99/month) buys the two things that cost money to provide: room in the vault beyond two decks, and the Archivist's model calls — a larger monthly allowance plus the tasks that only make sense with one (the Archivist at the table, the post-game recap, commander ideas). Wizards' Fan Content Policy asks that fan content be free to play and allows Patreon-style support; the line is drawn to keep to that.
src/lib/entitlements.ts is the single price list (FEATURES, LIMITS); useEntitlements()
is what a screen asks before showing a gated control, and every locked control opens the
Patron sheet with the reason rather than sitting dead. The launch switch is
NEXT_PUBLIC_ENFORCE_ENTITLEMENTS=1: while it is unset the vault gate answers yes. The
Archivist's plan gate and allowance are enforced on the server regardless.
A free account with more than two decks keeps them all: the two most recently touched stay
live, the rest are read-only (badge, banner, not playable) until the player becomes a Patron.
Billing is Stripe Checkout and the Customer Portal (/api/billing/checkout, /portal) with a
webhook (/api/billing/webhook) that sets users/{uid}.plan; the policy is the pure function
in src/lib/billing.ts (npm run test:billing): active or trialing → Patron, anything else →
free, an admin grant is never touched, every event applied once. The client never writes the
plan; firestore.rules forbids it. SECURITY_SETUP.md §7 has the Stripe setup.
src/
├── app/ # Routes: landing/dashboard, decks, game setup, game board, learn, admin, dev
│ └── api/ # Route handlers: me, archivist, billing (checkout, portal, webhook)
├── components/
│ ├── brand/ # Keystone, Arch, Alcove — the Undercroft visual identity
│ ├── decks/ # Deck page, builder search, deck check, card tiles, reader, shelves
│ ├── game/ # Board, seats, cards, hand, prompts, the Apprentice strip and prompt guide
│ ├── archivist/ # The Archivist's sheet, in-game panel, commander ideas, notices
│ ├── history/ # The match ledger
│ ├── learn/ # The lessons index, a lesson with diagrams and quiz
│ ├── tour/ # The spotlight tour overlay
│ ├── patron/ # The Patron sheet and upsell provider
│ ├── admin/ # The Administration section of Settings
│ └── ui/ # shadcn primitives
├── content/ # Typed content: lessons, guides (phase, step, prompt, issue), tours
├── hooks/ # useEntitlements, useArchivist, useTour, useCardRecords, useFitToRow, Firestore sync
├── lib/ # Forge client + adapter, deck cards + verification, entitlements, plan, metering,
│ ├── archivist/ # prompts, contexts, client
│ └── server/ # Admin SDK, bearer auth, app config, metering, Stripe, billing store
├── store/ # Zustand: decks, settings, app config, game state
└── dev/ # Mock game, decks, cards and admin data for the development harness
scripts/ # Tests, protocol check, card sync, screenshot sweep, tour walk
src/app/icon.svg is the favicon: the Keystone, WUBRG left to right. node scripts/render-icons.mjs renders it to the PNGs the home-screen icon and the web app
manifest (src/app/manifest.ts) need. Re-run it after editing the SVG.
npx tsc --noEmit # types
npx eslint src # lint
npm run check:protocol # every server prompt has a renderer and matching response key
npm run test:parser # decklist parser
npm run test:events # game-log synthesiser
npm run test:sync # Scryfall → Firestore sync helpers
npm run test:deck # deck grouping/stats, opponent seat resolution
npm run test:entitlements # the price list under both launch-switch positions, plan resolution
npm run test:metering # Archivist allowance arithmetic
npm run test:archivist # prompt builders, context serialisation, swap/idea parsing
npm run test:lessons # Apprentice content: every phase, step and prompt explained; quiz and link integrity
npm run test:tours # every tour step resolves and is visible at phone and desktop (needs the mock dev server)
npm run test:billing # the Stripe webhook policy against recorded event shapes
npm run test:rules # firestore.rules against the emulator (needs Java)
npx next build
All of these run in CI on every push and pull request.
SECURITY_SETUP.md covers Firestore rules, the admin allowlist, and the two scheduled
jobs that keep card data current: Scryfall → Firestore here, and the Forge engine + card
scripts in the server repo.