Magic: The Gathering has over 28,000 unique cards. Each one can interact with every other in ways the designers never anticipated. The comprehensive rules document is over 200 pages long. And I decided to build an engine that implements all of it.
The Problem with the Object Oriented Approach
Many MTG projects take the obvious approach: model cards as class hierarchies. A Creature extends Permanent, which extends Card. Simple, intuitive, wrong.
The problem is that in Magic, a card's identity is fluid. That creature you control? An opponent can steal it. That instant in your hand? It might be a creature on the battlefield (thanks, Zoetic Cavern). That 2/2 bear? With the right enchantments, it's now a 7/7 flying artifact creature with lifelink.
Object-oriented inheritance can't handle this. You can't re-parent an object at runtime. So most engines end up with sprawling conditional logic, special cases, and the inevitable bugs when two obscure cards interact in ways nobody tested.
The Architecture: Pure ECS
Argentum takes a different approach. Everything is an entity—just a unique ID. Cards, players, spells on the stack, even continuous effects like "Giant Growth gave this +3/+3 until end of turn." All entities.
Each entity is defined by its components: pure data bags with no behavior. A creature on the battlefield might have components marking it as tapped, tracking its controller, counting its +1/+1 counters. The entity doesn't "know" it's tapped. It simply has—or doesn't have—that data attached.
All logic lives in systems. The action processor takes a game state and an action, then returns a new state plus events. The state is immutable. Every game action creates a fresh snapshot, which makes replay, undo, and network sync trivial.
The Full Stack
The project is split into cleanly separated modules, each with a single responsibility:
The SDK defines the vocabulary—what a card is, what effects exist, what costs mean. It's pure data structures and a DSL for defining cards. No game logic, no dependencies. Think of it as the shared language everyone speaks.
The Card Sets use that DSL to define actual cards. Portal, Alpha, custom sets—they're all just data. Adding a new set means writing card definitions, nothing more. The sets don't know how the game works; they just describe what cards do.
The Rules Engine is the brain. It's a pure Kotlin library with zero server dependencies. Give it a game state and an action, it returns a new state. Completely deterministic. You could run it in a test, in a CLI, embedded in another application—it doesn't care. This is where the comprehensive rules live: the stack, combat, triggers, state-based actions, the layer system.
The Game Server wraps the engine for online play. It handles WebSocket connections, manages game sessions, authenticates players, and—critically—masks hidden information. You shouldn't see your opponent's hand, so the server filters the state before sending it. It also handles the lobby system, deck building, and draft coordination.
The Web Client is deliberately simple. It renders the game state it receives. It captures clicks and sends them to the server. That's it. No game logic, no validation, no state computation. When you click a creature to attack, the client doesn't check if it's your turn—it just tells the server what you tried to do. The server decides if it's legal.
This separation keeps each layer focused. The engine doesn't know about networking. The server doesn't know about React. The client doesn't know the rules. Each piece is testable in isolation.
Rule 613: The Layer System
Here's where it gets interesting.
Magic has this brilliant/terrifying concept called the layer system. When multiple effects modify a permanent, they apply in a specific order across seven layers: copy effects first, then control changes, then text changes, types, colors, abilities, and finally power/toughness modifications.
Power/toughness alone has five sublayers. A card that "sets power to 0" applies in layer 7b, while "+1/+1 counters" apply in layer 7d. Get the order wrong and your engine produces incorrect game states.
Argentum separates "base state" from "projected state." The base state is what's stored—the permanent as it exists without any modifications. The state projector then calculates what players actually see by applying all active continuous effects in the correct layer order.
The tricky part is dependencies. Sometimes effect A changes whether effect B applies, which might change whether effect A applies. The comprehensive rules handle this with timestamps and dependency checks. Argentum implements this with a trial application system—apply effects tentatively, detect circular dependencies, fall back to timestamp ordering.
Cards as Data, Not Code
Traditional engines hardcode card logic. Want to add a new card? Write a new class, handle all its edge cases, hope you didn't break something else.
Argentum treats cards as pure data definitions:
val GrizzlyBears = card("Grizzly Bears") {
manaCost = "{1}{G}"
typeLine = "Creature — Bear"
power = 2
toughness = 2
metadata {
rarity = Rarity.COMMON
collectorNumber = "169"
artist = "Jeff A. Menges"
flavorText = "Don't try to outrun one of Dominaria's grizzlies; it'll catch you, knock you down, and eat you."
imageUri = "https://cards.scryfall.io/normal/front/4/8/48e1b99c-97d0-48f2-bfdf-faa65bc0b608.jpg"
}
}
The engine doesn't know about Grizzly Bears specifically. It knows how to create continuous effects in layer 7c. It knows how to handle targets. The card definition just connects these primitives.
This means adding new cards rarely requires engine changes.
The Dumb Terminal Client
The web client follows what I call the "dumb terminal" pattern. It renders game state. It captures user intent. That's it.
No game logic runs client-side. When you click a creature to attack, the client doesn't validate that you're in the declare attackers step—it just sends "player wants to attack with this creature" to the server. The server checks legality, updates state, and broadcasts the result.
This eliminates an entire class of bugs (client/server disagreement) and makes the client trivial to reason about. It's just a visualization layer.
Why Build This?
Honestly? Because the rules of Magic are beautiful. They're a 30-year accretion of edge cases that somehow still form a coherent system. Building an engine that handles them correctly is a puzzle I couldn't resist.
But the real catalyst was a friend saying it would be fun to run draft tournaments online. MTG Arena doesn't support creating private tournaments with friends—you can play against each other, but organizing an actual draft pod and bracket? Not happening. So I figured: how hard could it be?
I started with Portal, the simplified beginner set from 1997. No instants, no activated abilities, just creatures and sorceries. A manageable scope to get the core engine working before tackling the full comprehensive rules.

How I Actually Built It This Fast
When I first scoped this project, it felt enormous. Implementing MTG's comprehensive rules correctly? That's easily six months of evenings and weekends. Maybe more. The kind of project that lives forever on a "someday" list.
But I'd been curious about Claude Code, so I decided to try something different: vibe coding.
I barely wrote any code myself. Instead, I guided the process—describing what I wanted, reviewing what came back, course-correcting when the implementation drifted. Think of it as pair programming where your partner types 50x faster than you but needs you to keep the big picture in focus.
What I loved most was plan mode. Before Claude starts building anything significant, it asks clarifying questions: "How should this interact with existing triggers? Should the effect be optional? What happens if there are no valid targets?" Then it forms a concrete plan and waits for approval. This back-and-forth is invaluable. Half the bugs in software come from misunderstood requirements—plan mode surfaces those misunderstandings before any code gets written.
For architecture reviews, I'd occasionally pull in Gemini. With its million-token context window, I could dump the entire codebase into a single request and ask "does this structure make sense?" or "what am I missing?" It's surprisingly useful to have a second opinion that can actually hold the whole project in its head at once. The combination works well: Claude Code for the actual building, Gemini for the birds-eye sanity checks.
The result is a codebase I understand deeply—because I co-designed it—but didn't have to type out line by line. It's a strange feeling, honestly. The code feels mine in every way that matters, but my fingers barely touched the keyboard.
Would I have finished this project without AI assistance? Probably not. It would have joined the graveyard of ambitious side projects that never quite made it. Instead, it's live, it's playable, and the layer system handles Humility correctly. I'm honestly impressed by how fast this came together, and sometimes am even suprised if something actually works.
Try It Yourself
The engine is open source at github.com/wingedsheep/argentum-engine.
Play live at magic.wingedsheep.com—host a draft with friends, or just watch the layer system do its thing.
— Vincent