Living Narrative Engine #5

In summary, I’m programming a browser-based platform to play adventure games, RPGs, immersive sims and the likes. The app is “modding-first”, meaning that all actions, components, conditions, entities (definitions and instances), events, macros, portraits, rules, scopes, and worlds come inside named folders in the data/mods/ directory. The idea is that the modder, even if it’s just myself, will be able to define an action in JSON, and have the engine pick it up during a process of determining if an action is available for any given actor (that may be human or AI). Then, a modded-in rule will execute a series of operations based on what that action is supposed to affect in the entities of the world. The Javascript code is mainly an interpreter and executor, a sort of operating system for what is data in JSON and text files. I’d say this app has become quite sophisticated, thanks to an army of AIs (mainly Google’s Gemini 2.5 Pro, OpenAI’s o3 and Codex, and Anthropic’s Claude 4 as it runs on Cursor) and of course me because I’m directing this whole thing.

I’ll leave Gemini 2.5 to explain in detail how the action discovery process works in the app.

The Complete Action Discovery Process

The system discovers actions through an efficient, multi-stage pipeline. Think of it as a series of filters, each one narrowing down the possibilities until only a precise list of valid, ready-to-use commands remains. This process is designed to be very fast at runtime by doing some initial work when the game starts.

Setup Step: Building the Action Index (Once at Startup)

Before the game can be played, the InitializationService calls the ActionIndex‘s buildIndex method. This method runs once and does the following:

  1. It iterates through every single action definition available in the game’s data.
  2. It creates a reverse index based on actor component requirements.
    • If an action has no required_components.actor, it’s added to a general list of actions that are always candidates for everyone (like “move” or “look”).
    • If an action does require actor components (e.g., ["core:leading"]), it’s mapped against those components. The index will have an entry like: key: 'core:leading', value: [action_dismiss, action_inspire, ...].

This one-time setup is crucial for runtime performance. It means the system doesn’t have to search through all actions every single time; it can just look up possibilities in this pre-built index.

Step 1: Finding Candidate Actions (The Actor Component Filter)

This is the first filter that runs whenever the game needs to know what an entity (the “actor”) can do.

  1. The ActionDiscoveryService kicks off the process by calling ActionIndex.getCandidateActions(actor).
  2. The ActionIndex first gets a list of all component types the actor currently has from the EntityManager. For example: ['core:stats', 'core:inventory', 'core:leading'].
  3. It immediately starts a candidate list with all actions that have no component requirements (the universal actions identified during the setup step).
  4. It then iterates through the actor’s list of components. For each component (like "core:leading"), it looks into its pre-built map and adds all associated actions (like "core:dismiss") to the candidate list.

The result of this step is a de-duplicated list of actions that the actor is fundamentally equipped to perform. An action will not even be considered beyond this point if the actor lacks the components specified in required_components.actor.

Step 2: Checking Actor State (The Prerequisite Filter)

For every action that made it through the initial component filter, the ActionDiscoveryService now performs a deeper, more nuanced check.

  1. It iterates through the candidate actions.
  2. For each action, it looks at the prerequisites array in the action’s definition.
  3. It uses the PrerequisiteEvaluationService to evaluate these rules. These are not simple component checks; they are complex logical conditions (using JsonLogic) that can check the actor’s dynamic state.

This is the filter for questions like:

  • “Do I have more than 10 mana?”
  • “Am I currently under a ‘Stunned’ status effect?”
  • “Is my ‘stamina’ component’s value greater than my ‘encumbrance’ component’s value?”

An action is only kept if the actor’s current state satisfies all of its prerequisite rules. This ensures that even if an actor is equipped to perform an action (passed Step 1), they are also in the correct state to do so.

Step 3: Finding Valid Targets (The Scope & Target Component Filter)

Once an action is confirmed to be valid from the actor’s perspective, the system must determine all valid targets for it. This entire process is handled by resolving the action’s defined scope.

  1. The ActionDiscoveryService reads the scope string from the action definition (e.g., "followers", "adjacent_chests").
  2. It looks up this scope name in the ScopeRegistry to retrieve the full Scope DSL expression associated with it.
  3. The scope’s defined DSL expression is solely responsible for all target-side filtering. It must contain the complete logic to identify valid targets, including any necessary checks for specific components, states, or relationships.
  4. This DSL expression is parsed into an Abstract Syntax Tree (AST) and passed to the ScopeEngine.

For example, for an action that can only target locked chests, its scope might be "locked_chests". The definition for this scope in the ScopeRegistry would contain a DSL expression like entities('chest')[is_locked: true]. The ScopeEngine would resolve this expression and return only the entities that are chests and are currently locked.

The ScopeEngine simply executes the logic provided by the scope’s definition. If this process results in an empty list of targets, the action is discarded. Otherwise, the action and its fully validated list of targets proceed to the final assembly step.

Step 4: Assembling the Final Commands

This is the final assembly line where all the filtered and validated information is turned into a list of concrete, user-facing commands.

  1. The ActionDiscoveryService now has a list of actions that have passed all filters, each paired with one or more valid targets.
  2. It iterates through every valid action-target pair.
  3. For each pair, it uses the formatActionCommand utility. This function takes the action’s template string (e.g., "dismiss {target}") and the specific target entity.
  4. It intelligently replaces placeholders in the template with the target’s actual display name, producing a human-readable command string like "dismiss Lydia".
  5. Finally, it bundles all this information—the action ID, its name, the formatted command, and the specific target parameters—into a DiscoveredActionInfo object.

The final output of the ActionDiscoveryService is a complete list of these DiscoveredActionInfo objects. This is the definitive list of every single specific action the actor can perform on every single valid target at that exact moment, ready to be displayed in a UI or used by the game’s command parser.

Summary of the Pipeline

  1. Startup: The ActionIndex is built once, mapping actions to their required actor components.
  2. Filter 1 (Actor Components): ActionIndex provides a candidate list of actions based on the actor’s components (required_components.actor).
  3. Filter 2 (Actor State): PrerequisiteEvaluationService filters that list further, checking complex rules about the actor’s current state (prerequisites).
  4. Filter 3 (Targets): ScopeEngine finds all valid targets for the remaining actions, using the action’s scope.
  5. Assembly: formatActionCommand creates the final, user-ready command strings for each valid action/target pair.

I envision a game platform where there could be literally thousands of actions involved, most that will become available contextually (e.g. some will only be available when a character is sitting down). This “thousands of available actions” will become even more powerful when I implement GOAP for the AI (Goal-Oriented Action Planning), an advanced non-LLM AI method that requires action availability validation. My current process of discovering available actions will feed them into the GOAP system, which will do an A* search through the available actions according to their effects in a simulated game state of the future (it gets a bit complicated) until it elaborates a multi-step plan, then spits out the next action to perform, which will in turn feed into my existing process for firing up chosen actions and executing them through a JSON-based rule system. It’s looking good.

One thought on “Living Narrative Engine #5

  1. Pingback: Living Narrative Engine #6 – The Domains of the Emperor Owl

Leave a comment