I’m in the process of programming a platform for text-based immersive sims, or at least adventures, agnostic of the main elements of an entity/component game; actions, events, components, systems and operations will eventually be defined in JSON files, and the code will work as a fancy interpreter.
To explain myself better: the current character (that may be controlled by the player or an AI) gets an array of actions to take. Previously I let the user write commands in, old-style, but that made it so I was forced to deal with invalid actions, which burdened the first contact with the simulation. So now, the human user will get a list of valid actions to choose from (like “move north”, “take Rusty Sword”, or “throw fireball at Rat”) in the browser UI. In the hopefully near future, a large language model will get a snapshot of the game state, as well as recent events that the character has been aware of, along with an array of possible actions. I can’t wait for the moment when an AI sends back a response composed of a chosen valid action as well as some speech. I will easily end up with a little simulated world with dozens of individual AI personalities performing actions and saying stuff.
Anyway, the loop goes like this:
Action: a character chooses a previously validated action. Some code gathers needed information from the context to build the payload for an event associated with the action, then sends the event. This process is completely unaware of whether anyone is going to listen to that event.
Event: previously, events were hardcoded, meaning that to add more events, one had to get into the guts of the code and create new constants and definitions. I’ve managed to make events data-driven. Now an event is a simple JSON file in the “data/events” folder. Events look like this:
{
"$schema": "http://example.com/schemas/event-definition.schema.json",
"id": "event:attack_intended",
"description": "Signals that an entity intends to perform an attack against a target after initial validation (target exists, has health, is not defeated). Does not guarantee the attack hits or deals damage yet.",
"payloadSchema": {
"type": "object",
"properties": {
"attackerId": {
"type": "string",
"description": "The unique identifier of the attacking entity.",
"$ref": "./common.schema.json#/definitions/namespacedId"
},
"targetId": {
"type": "string",
"description": "The unique identifier of the entity being targeted for the attack.",
"$ref": "./common.schema.json#/definitions/namespacedId"
}
},
"required": [
"attackerId",
"targetId"
],
"additionalProperties": false
}
}
System: a system is whatever part of the app listens to events and modifies the game state (usually data in components). Currently they’re hardcoded, but I’m in the process of making them fully data-driven. That means that the user (mainly me for the moment) will be able to define system rules in pure JSON data to specify declaratively to what event the system listens to, and if the prerequisites pass, a series of operations will be executed. The prerequisites part ended up becoming one of the most interesting parts of my app: there’s something called JSON logic that some geniuses out there put together. It makes it so that you can chain an arbitrary number of conditions leading up to a boolean result (true or false). It looks like this:
Combines conditions with `AND` - Actor has key, target is specific door, door is locked.
{
"and": [
{
"!!": {
"var": "actor.components.game:quest_item_key"
}
},
{
"==": [
{
"var": "target.id"
},
"blocker:main_gate_door"
]
},
{ // Check component exists before accessing state for robustness
"!!": { "var": "target.components.game:lockable" }
},
{
"==": [
{
"var": "target.components.game:lockable.state"
},
"locked"
]
}
]
}
The example above could easily block a series of operations meant to unlock a door from triggering, and all defined in pure JSON.
Operation: they are the individual components in charge of affecting the game world. Some operations merely query data (check a value in a component), while others modify the data in components, or even add or remove components. There are IF operations that offer branching paths.
Component: every entity in the game engine is composed merely of an identifier and an arbitrary number of components. Some of those components are mere tags. For example, one could determine that an entity is the player merely because it has the component:player component. Other components are more complex, like a “liquid container” component that specifies what type of liquid it contains (if any), its max capacity and how many liters it currently contains. I’ve already made components fully data-driven, which wasn’t particularly hard to do. Example:
{
"id": "component:container",
"description": "Defines the state for an entity that can hold other item entities.",
"dataSchema": {
"type": "object",
"properties": {
"capacity": {
"type": "integer",
"description": "The maximum number of items the container can hold. Use -1 for infinite capacity.",
"minimum": -1,
"default": -1
},
"contains": {
"type": "array",
"description": "A list of the namespaced IDs of the item entities currently inside this container.",
"items": {
"$ref": "http://example.com/schemas/common.schema.json#/definitions/namespacedId"
},
"default": []
},
"allowedTags": {
"type": "array",
"description": "Optional. If present, only items possessing ANY of these tags can be placed inside.",
"items": {
"type": "string",
"pattern": "^[a-zA-Z0-9_\\-]+$"
},
"uniqueItems": true,
"default": []
}
},
"required": [
"capacity",
"contains"
],
"additionalProperties": false
}
}
In entity/component systems, the systems that operate on components are generally programmed to filter for the presence of components in entities, as well as for specific values in the components’ data, which leads to emergent behavior. For example, you could include a spell in the game that adds a “container” component to a person, and suddenly you can store things in that person. Determining that an entity is on fire would be as simple as adding an “onFire” component and then writing systems that add damage per turn on every entity with such a component. The possibilities are endless.
I doubt I’m going to come down from this high of building the app until I finally manage to get a large language model to speak through one of the characters. For that, I first have to finish making the core of the engine data-driven (actions, events, systems, operations, and components), then figuring out how to implement character turns even if I’m the one playing all the characters, then determining how to add basic artificial intelligence, then figuring out how to save game state. Once everything seems quite solid, I’ll look into interfacing with large language models.
Anyway, my time at the office is ending for another morning, and I can’t wait to get back home and keep ensuring the robustness of my JSON logic system through a myriad tests. Nearly 1,400 tests implemented so far.

You must be logged in to post a comment.