When Configuration Becomes a Programming Language
I love configuration-driven systems. I thought an open D&D database could drive character creation all the way through. The Monk taught me where configuration ends, interpretation begins, and writing the rule can be simpler than building a language for describing it.
Why this existsWhere Everything Meets →Good enough to begin
dnd5eapi.co is an open-source D&D 5e database with a hosted API. The Mastodon adventure bot already used it. The hosted service had aggressive rate limits, but the project could also run locally. It was good enough for that bot, which made it good enough for the Discord bot I wanted to build.
When configuration becomes a programming language
The Monk made the hidden cost visible. One tool-proficiency prompt was a choice whose options were themselves choices, each opening another options array. One weapon prompt was either a counted reference or a nested choice backed by an equipment-category lookup. The data was valid. A generic character creator still had to execute its control flow.
Where did I think the pieces belonged?
The toolkit repository began as one README and an ambitious map: shared core/, generic systems/, and game-specific games/. It called the project TypeScript-first in prose, but the commit contained no implementation. The broad dream existed. The boundaries did not.
Where does any of this get saved?
The next day, eight storage and persistence documents landed across five commits, with two early versions already replaced. The strongest idea was repository injection. A module declared the access pattern it needed; Redis, Mongo, memory, or files could implement it. The backing store should not matter. Nobody yet knew where those repositories ultimately belonged.
What if I just code what Fighter gets?
I still do not know whether the source model was wrong for what I wanted or whether I fell short of finding the right interpreter. I honestly thought I could conquer it. It got too messy. The breakthrough was not a better recursive parser. It was giving the D&D rulebook permission to state what a Fighter grants and which decisions creating one requires.
A character, finally
Character creation stopped executing a generic choices language. It asked a class for requirements, collected named selections, validated them where the rules lived, and compiled automatic grants into a live character. Fighter worked. Barbarian worked. The project could finally create the person at the center of the game.
ToData changed everything
The API already had the live character because it was using it. The rulebook did not need a repository. It only needed a clean way to turn the character into plain data and reconstruct it later. That separation made loading and saving almost boring.
Load: get CharacterData from the repository and call LoadFromData. Now the API has a fully functioning character. Save: call ToData on that character and hand the result back to the repository.
LoadFromData. Boom, you have a fully functioning character. Saving it? Call ToData and send the result to the repository.Well, Rage is part of that data, right?
ToData solved the character round trip. But a Barbarian carries Rage. If LoadFromData returns a fully functioning character, Rage has to come back fully functioning too. It cannot return as a name in an array. It has behavior spread across resources, attacks, damage, time, and its own ending.
ToJSON, and the first event bus.We had solved how to create and save a Barbarian. Rage asked what “fully functioning” actually meant.
Next field note The Nervous System →