// 04 RESTRICTED
realtime-mtg
A real-time strategy game played with Magic: The Gathering cards on a hex map. Lands are buildings that make mana and creatures are units. The world runs continuously, but every 16 seconds both players hit the War Beat: privately queued attacks reveal at once, blocks are proposed and edited, tricks resolve on a stack, and all damage lands together. Because the beat stands in for the end of a turn, 14 of the 15 supported keywords work exactly as printed. Card data comes from Scryfall's bulk export, kept in a local Postgres.
The beat
A beat is 320 ticks at 20 a second. Ten seconds of open play, where units move, lands go down, spells channel and attacks are queued privately; then the reveal; a block window of four seconds, half a second more per attack beyond four and six at most; then tricks; then damage and cleanup in the last half second. Damage takes half a second rather than an instant so that first strike can land a quarter second ahead of normal damage. The first draft ran at 12 seconds and played faster than it could be understood. Everything is counted in beats, so lengthening it slowed the whole game evenly.
"Until end of turn" means until cleanup, and marked damage wipes there, so about 17,600 creatures need no per-card tuning. The map is 127 hexes with the two Nexuses ten apart. A creature crosses three hexes a beat at power 1–3, two at 4–6 and one at 7 or more, and fliers get one more; movement stops at the reveal, so combat resolves against positions both players saw. There are no ranged attacks and blocking is by standing in the way, because a creature that could hit from across the board would never enter the geometry where blocking happens. Decks are 100-card singleton, led by a commander who walks the board as a hero unit, and 21 commander damage wins.
Deterministic by construction
The simulation imports no DOM, React or PixiJS. It uses integer math only and a seeded random generator of its own, and takes commands as its sole input, silently dropping illegal ones. Wall-clock time decides only how many ticks to run. A test asserts that two runs of the same match produce identical per-tick state hashes, which is what will let lockstep multiplayer arrive without a rewrite. The scripted opponent reads the same state the HUD shows and submits commands through the same queue with the same input delay, so the match loop cannot tell it from a human.
Casting taps your mana sources for you only when there is nothing to decide. The design doc's rule: if it can't know what to tap, you tap it yourself.
The card data
A fetch script downloads the roughly 530 MB bulk export with conditional GETs and a check of the file's own updated_at, so a rerun with nothing new costs almost nothing, and an importer upserts every row on its Scryfall ID. Rules text lives once per card in oracle_cards (38,233 rows) and cards holds each printing (115,874), both with the raw JSON beside the extracted columns. A FastAPI service serves the catalog, decks and art to a React, Vite and PixiJS client. Creatures are derived from their stat block and keywords; a card whose text the game implements by hand gets its own module, and a coverage test prints the curated cards still waiting for one.
Stated honestly
- The design doc names the beat as the whole bet: if blind simultaneous reveals produce coin-flip all-out attacks instead of reads, the model is wrong at its root. It has been lengthened once, from 12 seconds to 16.
- Keywords outside the supported fifteen are dropped from the derived card. Thirty-one spells are on the hand-implemented list, and two have modules so far.
- One player against the scripted opponent, on one machine. Lockstep multiplayer is planned, not built, and nothing is deployed.