For the last two weeks, I've been building the Codex Engine, a procedural VTT. The architecture was standard: a database with tables for maps, markers, npcs, and settings, with a Pygame frontend to render it all. It worked, but it was brittle. Adding a new feature meant changing schemas, writing new DB functions, and wiring up new UI.
Today, I threw that all away. I replaced the entire relational backend with a single, recursive table: a Unified Node Registry.
This wasn't a bug fix; it was a fundamental pivot. And the effect was transformative.
The Old Way: A Collection of Silos
Previously, the app was a collection of specialized parts. The CampaignMenu knew how to query the campaigns table. The GeoController knew how to query the vectors and markers tables. The SettingsEditor had hardcoded logic for managing a list of AI providers.
If I wanted to add a "Weather" system, I would have to:
Add a weather table to the database.
Add get_weather() and update_weather() to my DBManager.
Write a new UI panel with hardcoded labels for "Wind Speed" and "Rain Chance".
This is the standard, brittle way we build software. The logic is spread everywhere, and the code is tightly coupled to the data.
The New Way: The World as a Graph
The new architecture is built on a single, powerful premise: everything in the world is a node in a tree.
My database now has one important table: registry.
CREATE TABLE registry (
id INTEGER PRIMARY KEY,
parent_id INTEGER,
type TEXT,
name TEXT,
properties TEXT
);
A Campaign is a node. A World Map is a child of a Campaign. A Marker is a child of a Map. An NPC is a child of a Marker. Critically, an AI Provider Setting is just another node, a child of the AI Registry, which itself is a child of the global System Settings node.
The Payoff: UI That Builds Itself
This is where the magic happened. I refactored my UnifiedSettingsEditor. It no longer has hardcoded tabs for "Server" or "AI". Instead, its logic is simple:
When opened, it's given a "root" node ID (e.g., the "System Settings" node).
It calls db.get_children() on that root.
For every child node it finds, it creates a new tab using the child's name.
When a tab is clicked, it inspects that node's properties JSON blob and dynamically generates an editable input field for every key-value pair it finds.
The result is pure automation. I define the structure of my settings once in a config.json seed file. The bootstrap process translates that JSON into a tree of nodes in the database. From that moment on, the UI discovers the hierarchy and builds itself entirely from the data it finds.
Effortless Extensibility
This is the real victory. To add a new "Weather Engine" settings panel to my app, I no longer write a single line of Python.
I just add this to my config.json:
{
"type": "weather_config",
"name": "Weather Engine",
"properties": {
"simulation_speed": 1.5,
"enable_rain": true
}
}
I run the app. The bootstrap creates the node. I open the settings menu, and a new "Weather Engine" tab appears, complete with editable fields for speed and rain. The rest of the program can now query for this node (db.find_node('weather_config')) and use its properties to drive simulations.
A Decoupled Platform
By making the database a self-describing graph, I've decoupled the World State from the Renderer.
Frontends (Web/Mobile): A web-based character sheet doesn't need to know about Pygame. It just needs to ask the database for the children of a character node and a way to update their properties.
Backends (AI/Importers): An AI that processes session transcripts can add new lore nodes to the graph without the main application even being aware of it. The new lore will simply appear the next time the relevant map is rendered.
I stopped building features and started building a system. The database is no longer just for storage; it is the operating system of the world. And the applications—whether Pygame, web, or AI—are just clients that read from and write to it.
No comments:
Post a Comment