Notes from building a state-driven game engine — where the rules are data, twelve very different games share one interpreter, and some actions drop you into a whole other game to get resolved.
The source code is here: https://github.com/BuckRogers1965/GDL
We set out to test a stubborn idea: that a game's rules can be data, not code. Not "data" as in a config file that a hand-written chess program reads — data as in the entire rulebook, interpreted by an engine that knows nothing about chess, or cards, or dice, or dragons. You write a JSON spec; the engine plays it.
The bet was that if you find the right handful of primitives, you don't build games anymore. You describe them. A few days in, we have twelve games running on one engine, and the thing that surprised us most isn't any single game — it's that the primitives stopped growing.
Here's the whole cast of characters the engine now understands. Not games — primitives, the atoms the games are assembled from:
Things. A positioned piece with an owner and a type. A hidden token in a container. An item whose location might be a room or your pocket. A resource counter. An ownable deed. A generated token. And the player themselves — an entity with attributes like money or score.
Places. Four topologies cover an astonishing range: a grid (chess, Go, Othello, tic-tac-toe), zones (a deck, a hand, a discard pile), a graph (rooms joined by exits), and a track (a loop you roll around). That's very nearly the entire structural vocabulary of turn-based board and card games.
Verbs. Move, place, capture, chain, draw, play, roll, bid, trade, take, navigate, type a word, buy, pass.
The invisible machinery. Hidden information and per-player views. Seeded randomness, so every game replays exactly. Position history (which is all "no repeating the board" — Go's ko rule — turned out to need). Turn order with direction and skips. Multi-phase turns. Choices. Declarative win and draw conditions.
The single most telling metric: each new game costs less engine code than the last. Chess and checkers essentially were the engine. Adding Uno brought the whole idea of hidden hands and shuffling. By the time we added Monopoly — dice, money, property, a full economy — the new engine code was a topology branch and a menu prompt. The auction and trade features that came after it added zero lines to the engine core.
That falling cost is the real result. When new games stop demanding new primitives, it means the atom set is essentially complete. You feel the vocabulary close, and after that a "new game" is mostly a new JSON file and, occasionally, one small function.
Not everything fits in a rules-as-data condition. "Is this checkmate?" isn't a fact about the current board — it's a search over every reply the opponent could make. Forcing that into a declarative rule language turns the language into a bad programming language.
So we drew a line and held it: mechanics are data; a small, explicit set of terminal checks may be code. A spec can declare a trusted native function — checkmate detection, Othello's line-flipping, Go's liberty counting, a Boggle dictionary lookup — and call it from a rule exactly like a built-in. The remarkable part is how few shapes these take. Nearly every one is the same: hand it a position, get back a boolean or a score. Flanking, liberties, word-validity, poker hands — one pattern, different bodies. The engine stays game-agnostic; the hard 5% lives in named, sandboxable functions instead of leaking into the core.
Here is the idea we're most excited about, and it's the one that changes how you think about "special actions."
In most engines, a rule that behaves unusually is a special case — an if branch, a carve-out, a mode flag threaded through the turn loop. We found something better. A special action can temporarily hand control to an entirely different game, with entirely different rules, and then hand you back the result.
Three examples, all running today:
The dragon's duel. In our little text adventure, a dragon guards a key. Walk into its lair and it challenges you to tic-tac-toe. For the length of that fight, you are no longer playing an adventure — you're playing a board game, with a board, an AI opponent, and its own win condition. Beat it and you walk out with the key; the adventure resumes where it paused. A complete game, nested inside another, its outcome changing the world around it.
The property auction. Land on an unowned property in Monopoly and decline to buy, and the property goes to auction. For those few moments the rules change completely: it's not "roll and move" anymore, it's a bidding game — every player takes turns raising or passing until one remains. The winner pays their bid and takes the deed. Then normal Monopoly resumes.
The trade negotiation. On your turn you can open a negotiation with another player: offer a deed and some cash for one of theirs, and they accept or decline. Another temporary rule-set — propose, counter, agree — spliced into the middle of an ordinary turn.
Think about what that gives a game designer. A special action isn't a special case — it's a doorway. You don't bend the main rules to accommodate the weird thing; you step through into a pocket where different rules apply, resolve it, and step back. Auctions, duels, negotiations, minigames, "resolve this conflict by playing a hand of cards" — they all become the same move: temporarily play a different game.
And it isn't a hack bolted onto the side. It falls straight out of one decision made early: a game is a first-class, runnable object. Once that's true, nothing stops one game from running inside another. We didn't build a "nesting feature." We discovered nesting was already possible the moment the engine became a plain library — and then we just wired it up. The whole dragon-duel-inside-the-adventure was a few dozen lines and zero changes to the engine's core.
There's even a natural taxonomy that emerged once we had it working:
- A separate sub-game — the dragon's tic-tac-toe, with its own fresh players and board.
- A mini-game over the live state — the auction and the trade, which don't spin up a copy of the world; they bid with the money you actually have and trade the deeds you actually own, writing the result straight back.
- And its opposite, worth naming: a constraint like jail, which is not a mini-game at all — it has no turns and no result, it just restricts what you can do. The rule of thumb writes itself: does it take turns and return a result? → nest it. Does it just limit you? → constrain it.
The engine can now express a startling range of games from a small, stable set of parts, and it can compose those games into each other. What's left isn't more primitives — it's two dimensions we haven't pushed yet. One is the players: today a seat can be a human or a hand-written opponent, but the engine should be able to enumerate legal moves so any seat can be a solver or a bot. We do this in the adventure game where you play tic tac toe against a dragon. The other is presentation: getting these games out of the terminal and in front of two people connected through a server, each in their own browser.
Neither adds an atom. Both just cash in the atoms we already have.
Rules as data was the hypothesis. Twelve games later, the surprise is how little was left to invent — and that the most powerful feature, games nested inside games, was something the architecture was quietly capable of all along. Turtles, as they say, all the way down.
No comments:
Post a Comment