Living Narrative Engine #7

I’m developing a browser-based, chat-like platform for playing adventure games, RPGs, immersive sims and the likes. It’s “modding-first,” meaning that all game content comes in mods. That includes actions, events, components, rules, entities, etc. My goal is that eventually, you could define in mod files the characters, locations, actions, rules, etc. for any existing RPG campaign and you would be able to play through it, with other characters being large language models or GOAP-based artificial intelligences.

From early on, it became clear that the platform was going to be able to support thousands of actions for its actors (which may be human or controlled by a large language model). The code shouldn’t be aware of the specifics of any action, which wasn’t easy to do; I had to extract all the logic for going from place to place from the engine, to the extent that I had to create a domain-specific language for determining target scopes.

When the turn of any actor starts, the system looks at all the actions registered, and determines which are available. I registered some actions like waiting, moving from place to place, following other people, dismissing followers, to some more niche ones (in an “intimacy” mod) like getting close to others and fondling them. Yes, I’m gearing toward erotica in the future. But as I was implementing the actions, it became clear that the availability of some actions wouldn’t be easily discerned for the impossible-to-predict breath of possible entities. For example, if you wanted to implement an “slap {target}” action, you can write a scope that includes actors in the location, but what determines that the actor actually has a head that could be slapped? In addition, what ensures that the acting actor has a hand to slap with?

So I had to create an anatomy system. Fully moddable. The following is a report that I had Claude Code prepare on the first version of the anatomy system.


The Anatomy System: A Deep Dive into Dynamic Entity Body Generation

Executive Summary

The anatomy system is a sophisticated framework for dynamically generating and managing complex anatomical structures for entities in the Living Narrative Engine. It transforms simple blueprint definitions and recipes into fully-realized, interconnected body part graphs with rich descriptions, validation, and runtime management capabilities.

At its core, the system addresses a fundamental challenge in narrative gaming: how to create diverse, detailed, and consistent physical descriptions for entities without manual authoring of every possible combination. The solution is an elegant blend of data-driven design, graph theory, and natural language generation.

System Architecture

The anatomy system follows a modular, service-oriented architecture with clear separation of concerns. The design emphasizes:

  • Orchestration Pattern: A central orchestrator coordinates multiple specialized workflows
  • Unit of Work Pattern: Ensures transactional consistency during anatomy generation
  • Chain of Responsibility: Validation rules are processed in a configurable chain
  • Strategy Pattern: Description formatting uses pluggable strategies for different part configurations
  • Factory Pattern: Blueprint factory creates anatomy graphs from data definitions

Core Service Layers

  1. Orchestration Layer (AnatomyOrchestrator)
  • Coordinates the entire generation process
  • Manages transactional boundaries
  • Handles error recovery and rollback
  1. Workflow Layer
  • AnatomyGenerationWorkflow: Creates the entity graph structure
  • DescriptionGenerationWorkflow: Generates natural language descriptions
  • GraphBuildingWorkflow: Builds efficient traversal caches
  1. Service Layer
  • BodyBlueprintFactory: Transforms blueprints + recipes into entity graphs
  • AnatomyDescriptionService: Manages description generation
  • BodyGraphService: Provides graph operations and traversal
  1. Infrastructure Layer
  • EntityGraphBuilder: Low-level entity creation
  • SocketManager: Manages connection points between parts
  • RecipeProcessor: Processes and expands recipe patterns

Information Flow

The anatomy generation process follows a carefully orchestrated flow:

1. Initialization Phase

When an entity with an anatomy:body component is created, the AnatomyInitializationService detects it and triggers generation if the entity has a recipeId.

2. Blueprint Selection

The system loads two key data structures:

  • Blueprint: Defines the structural skeleton (slots, sockets, parent-child relationships)
  • Recipe: Provides specific customizations, constraints, and part selections

3. Graph Construction

The BodyBlueprintFactory orchestrates the complex process of building the anatomy:

Blueprint + Recipe → Graph Construction → Entity Creation → Validation → Description Generation

Each step involves:

  • Slot Resolution: Blueprint slots are processed in dependency order
  • Part Selection: The system selects appropriate parts based on requirements
  • Socket Management: Parts are connected via sockets with occupancy tracking
  • Constraint Validation: Recipe constraints are continuously checked

4. Description Generation

Once the physical structure exists, the description system creates human-readable text:

  • Individual part descriptions are generated using context-aware builders
  • Descriptions are composed into a complete body description
  • Formatting strategies handle single parts, paired parts, and multiple parts differently

5. Runtime Management

The generated anatomy becomes a living system:

  • Parts can be detached (with cascade options)
  • The graph can be traversed efficiently via cached adjacency lists
  • Events are dispatched for anatomy changes

Core Capabilities

1. Dynamic Entity Generation

  • Creates complete anatomical structures from data definitions
  • Supports unlimited variety through recipe combinations
  • Generates unique entities while maintaining consistency

2. Hierarchical Part Management

  • Parts are organized in a parent-child graph structure
  • Each part can have multiple sockets for child attachments
  • Supports complex anatomies (e.g., creatures with multiple limbs, wings, tails)

3. Intelligent Part Selection

  • Matches parts based on multiple criteria (type, tags, properties)
  • Supports preferences and fallbacks
  • Handles optional vs. required parts gracefully

4. Natural Language Descriptions

  • Generates contextual descriptions for individual parts
  • Composes full-body descriptions with proper formatting
  • Handles pluralization, grouping, and special cases

5. Constraint System

  • Enforces recipe-defined constraints (requires/excludes)
  • Validates socket compatibility
  • Ensures graph integrity (no cycles, orphans, or invalid connections)

6. Runtime Operations

  • Part detachment with cascade support
  • Efficient graph traversal via cached adjacency lists
  • Path finding between parts
  • Event-driven notifications for changes

Key Components Deep Dive

AnatomyOrchestrator

The maestro of the system, ensuring all workflows execute in the correct order with proper error handling and rollback capabilities. It implements a Unit of Work pattern to maintain consistency.

BodyBlueprintFactory

The factory transforms static data (blueprints and recipes) into living entity graphs. It handles:

  • Dependency resolution for slots
  • Socket availability validation
  • Part selection and creation
  • Name generation from templates

Validation System

A sophisticated chain of validation rules ensures anatomical correctness:

  • CycleDetectionRule: Prevents circular parent-child relationships
  • OrphanDetectionRule: Ensures all parts are connected
  • SocketLimitRule: Validates socket occupancy
  • RecipeConstraintRule: Enforces recipe-specific rules
  • JointConsistencyRule: Ensures joint data integrity

Description Generation Pipeline

The description system is remarkably sophisticated:

  1. BodyPartDescriptionBuilder: Creates individual part descriptions
  2. DescriptionTemplate: Applies formatting strategies
  3. PartGroupingStrategies: Handles different grouping scenarios
  4. TextFormatter: Provides consistent text formatting
  5. BodyDescriptionComposer: Orchestrates the complete description

Strengths of the System

1. Modularity and Extensibility

Each component has a single, well-defined responsibility. New features can be added without modifying existing code.

2. Data-Driven Design

Anatomies are defined entirely in data, making it easy to add new creature types without code changes.

3. Robustness

Comprehensive validation, error handling, and rollback mechanisms ensure system reliability.

4. Performance Optimization

  • Cached adjacency lists for efficient traversal
  • Lazy description generation
  • Batched entity operations

5. Developer Experience

  • Clear service boundaries
  • Extensive logging and debugging support
  • Consistent error handling patterns

Expansion Opportunities

1. Dynamic Modification System

  • Runtime part growth/shrinkage: Allow parts to change size dynamically
  • Transformation support: Enable parts to transform into different types
  • Damage modeling: Track part health and visual damage states

2. Advanced Constraints

  • Symmetry requirements: Ensure paired parts match when needed
  • Resource-based constraints: Limit total mass, magical capacity, etc.
  • Environmental adaptations: Parts that change based on environment

3. Procedural Enhancement

  • Mutation system: Random variations within constraints
  • Evolutionary algorithms: Breed new anatomies from existing ones
  • Machine learning integration: Learn optimal configurations

4. Visual Integration

  • 3D model mapping: Connect anatomy graph to visual representations
  • Animation constraints: Define movement limitations based on anatomy
  • Procedural texturing: Generate textures based on part properties

5. Gameplay Systems

  • Ability derivation: Generate abilities from anatomy (wings = flight)
  • Weakness detection: Identify vulnerable points in anatomy
  • Part-specific interactions: Different interactions per body part

6. Description Enhancement

  • Contextual descriptions: Change based on observer perspective
  • Emotional coloring: Descriptions that reflect entity state
  • Cultural variations: Different description styles for different cultures

7. Performance Scaling

  • Anatomy LOD (Level of Detail): Simplified anatomies for distant entities
  • Streaming support: Load/unload anatomy data dynamically
  • Parallel generation: Generate multiple anatomies concurrently

8. Tool Support

  • Visual anatomy editor: GUI for creating blueprints and recipes
  • Validation sandbox: Test recipes before deployment
  • Analytics dashboard: Track anatomy generation patterns

Technical Implementation Details

Design Patterns in Action

The codebase demonstrates excellent use of software design patterns:

  • Service Locator: Services are injected via constructor dependencies
  • Facade: AnatomyGenerationService provides a simple interface to complex subsystems
  • Template Method: Validation rules follow a consistent pattern
  • Composite: The anatomy graph itself is a composite structure
  • Observer: Event system notifies interested parties of anatomy changes

Error Handling Philosophy

The system follows a “fail-fast” approach with comprehensive error information:

  • Validation errors prevent invalid states
  • Detailed error messages aid debugging
  • Rollback mechanisms prevent partial states
  • Event dispatching for error tracking

Extensibility Points

Key extension points for customization:

  • Custom validation rules via the ValidationRule base class
  • New part grouping strategies via PartGroupingStrategy
  • Custom formatters via the formatting service interface
  • Additional graph algorithms via the graph service

Conclusion

The anatomy system represents a significant achievement in dynamic content generation. It successfully balances flexibility with consistency, performance with functionality, and simplicity with power. The modular architecture ensures the system can grow and adapt to new requirements while maintaining its core elegance.

The combination of graph-based structural representation, constraint-driven generation, and sophisticated natural language processing creates a system capable of generating virtually unlimited variety while maintaining narrative coherence and technical correctness.

As the Living Narrative Engine evolves, the anatomy system provides a solid foundation for increasingly complex and engaging entity interactions, opening doors to emergent gameplay and storytelling possibilities limited only by imagination.

Living Narrative Engine #6

How the process of developing a modular, data-driven anatomy system in my app is going:

Me: We’ve recently created a complex anatomy system that builds a graph of body parts from a recipe and a corresponding blueprint, as well as a myriad of entity definitions for body parts. The code is in @src/anatomy/ , while most of the data is in @data/mods/anatomy/ . As I was looking over the sockets for the human_female_torso.entity.json and the human_male_torso.entity.json , I noticed that while most sockets require concrete “allowedTypes” (e.g. “breast” for “left_chest” in a woman), the sockets for “penis”, “left_testicle”, “right_testicle”, “asshole”, and “vagina” require a “genital” “allowedTypes”. That is too broad; the system that matches fitting entities could slot a penis entity in the socket of a vagina, or a testicle entity in the socket for a penis. That’s obviously undesired behavior. I want you to change the allowed types to more specific subTypes (likely, a subType “penis” for the “penis” socket, and so on).
You will need to modify the entity definitions of human_penis.entity.json, human_testicle.entity.json, human_vagina.entity.json to change their subtype in “anatomy:part” appropriately.

Claude Code: I can see the issue clearly now. Looking at line 433-437, the validation checks if a part’s subType is included in the socket’s allowedTypes. Currently all genital parts have subType: “genital”, which matches the allowedTypes: [“genital”] on all genital sockets, allowing incorrect matches.

Plan to Fix Genital Socket Type Specificity

I’ve identified the issue: all genital parts (penis, testicle, vagina) have the same subType: “genital”, and all genital sockets accept allowedTypes: [“genital”]. This allows incorrect matches like a penis in a vagina socket.

Here’s what I’ll do:

1. Update torso entity files to use specific allowed types:
– human_female_torso.entity.json:
– vagina socket: allowedTypes: [“vagina”]
– asshole socket: allowedTypes: [“asshole”]
– human_male_torso.entity.json:
– penis socket: allowedTypes: [“penis”]
– left_testicle socket: allowedTypes: [“testicle”]
– right_testicle socket: allowedTypes: [“testicle”]
– asshole socket: allowedTypes: [“asshole”]
2. Update genital entity files to use specific subTypes:
– human_penis.entity.json: subType: “penis”
– human_testicle.entity.json: subType: “testicle”
– human_vagina.entity.json: subType: “vagina”
3. Note: There’s no separate entity for “asshole” – it’s integrated into the torso. If needed in the future, an asshole entity could be created with subType: “asshole”.

Me: I agree with your plan, except that I’m not sure what you mean that “there’s no separate entity for ‘asshole’ – it’s integrated into the torso”. Does the code or schemas allow for “integrated” body parts? I think the issue is that simply we need to create an entity definition for the subtype “asshole”.

Claude Code: I was mistaken. The anatomy system requires separate entities for all attachable parts. There’s no asshole entity definition. Both torsos have an “asshole” socket but no corresponding entity to attach to it.

The Architect’s Codex: A Definitive Framework for AI-Assisted Software Refactoring

I’ve been working on a definitive guide for refactoring that I could provide to Claude Code so that it would refactor any given module to the utmost. After some rounds back-and-forth between Gemini 2.5 Pro and I, I’ve settled on the following document. I hope it helps someone else out there.


This document provides a definitive and structured framework for the automated refactoring of software modules. It is intended for consumption by an advanced AI coding assistant to systematically improve the internal quality of a codebase across multiple dimensions, including maintainability, robustness, security, and performance. Each principle is accompanied by a core question, in-depth analysis, common anti-patterns (smells), and a concrete refactoring plan.

Preamble: The Mandate for Refactoring

Before applying any specific principle, the refactoring process must adhere to the following three overarching directives. These directives govern the context, process, and ultimate goal of any refactoring activity.

Directive 1: The Prime Directive: Preserve External Behavior

The foundational rule of all refactoring is the preservation of a component’s external, observable behavior. The process must restructure the internal implementation without altering the component’s contractual obligations to the rest of the system.  

  • Core Question: Will this change alter the output, side effects, or performance characteristics in a way that breaks existing clients of this code?
  • Actionable Guideline: Before initiating any transformation, the public API and all observable side effects (e.g., database writes, file system changes, network calls) of the module under review must be identified. A comprehensive suite of automated tests (unit, integration, and functional) that covers this external behavior must be in place and passing. After any refactoring step, this entire test suite must pass without any modification to the tests themselves. Any change that requires a test to be altered is not a refactoring; it is a feature change.

Directive 2: The Process: Incremental, Test-Driven Refinement

Refactoring is not a monolithic rewrite; it is a disciplined, iterative process of small, verifiable improvements. This approach minimizes risk and ensures that the codebase remains in a functional state at all times.  

  • Core Question: Is this the smallest possible change that addresses a specific code smell and moves the code toward better alignment with a principle?
  • Actionable Guideline: The refactoring process must operate in a tight, atomic loop, often described as the “Red-Green-Refactor” cycle in Test-Driven Development (TDD). The operational sequence for an automated agent is as follows:
    1. Identify: Scan the code and identify a single, specific violation of one of the principles outlined in this Codex.
    2. Propose: Formulate the smallest possible transformation that remedies the identified violation.
    3. Verify: Execute the full, pre-existing test suite. If all tests pass, the change is valid. If any test fails, the proposed transformation must be discarded, and an alternative must be considered.
    4. Commit: Persist the verified, behavior-preserving change.
    5. Repeat: Re-initiate the scan to find the next refactoring opportunity. Refactoring activities must be strictly separated from feature development or bug fixing. If a bug is discovered during refactoring, the refactoring process should be paused, the bug should be fixed (with a corresponding new test), and only then should refactoring resume.  

Directive 3: The Litmus Test: Measurably Improve Maintainability & Testability

The ultimate purpose of refactoring is to reduce technical debt and improve the long-term health of the codebase, making it easier to understand, change, and test. Every refactoring action must be justifiable in these terms.  

  • Core Question: Can the improvement from this change be articulated in concrete terms of software quality?
  • Actionable Guideline: For each proposed refactoring, the system must provide a clear justification based on measurable improvements in software quality metrics. Examples of such justifications include:
    • Reduced Complexity: A decrease in Cyclomatic Complexity by decomposing a large method or simplifying a nested conditional block.
    • Improved Coupling: A reduction in afferent or efferent coupling by introducing interfaces and removing direct dependencies on concrete classes.
    • Improved Cohesion: An increase in the cohesion of a class, measured by how focused its methods and data are on a single purpose.
    • Enhanced Testability: An improvement in the ability to test a component in isolation, typically by removing static dependencies or enabling dependency injection for mocking.

Category 1: Foundational Principles of Structural Integrity (The SOLID Pillars)

This category comprises the five SOLID principles of object-oriented design. These principles are a cohesive and interdependent set that forms the bedrock of a robust, maintainable, and flexible software architecture. They govern the fundamental relationships between classes and modules, ensuring a sound structure. Their application as a group is essential for preventing architectural decay.  

Principle 1: Single Responsibility & High Cohesion (SRP)

A component should have one, and only one, reason to change. This principle is fundamentally about focus and purpose.

  • Core Question: Does this class, function, or module have exactly one, well-defined responsibility? Are all of its internal parts working towards a single, unified goal?
  • In-Depth: The Single Responsibility Principle (SRP) is the cornerstone of modular design. A class that adheres to SRP is easier to understand, test, and maintain because its scope is limited. This principle is directly related to the concept of  
  • High Cohesion, which measures how strongly the internal elements of a component are related. A class with a single responsibility will naturally exhibit high cohesion. Conversely, a class with multiple, unrelated responsibilities (low cohesion) becomes a “God Object,” accumulating complexity and becoming fragile and difficult to change.  
  • Common Smells to Look For:
    1. Large Classes/God Objects: Classes with an excessive number of methods, properties, or lines of code, often managing disparate concerns (e.g., a User class that handles authentication, profile persistence, and email notifications).
    2. Mixed-Concern Methods: Functions that contain clearly delineated “sections” of logic, each handling a different task (e.g., a method that validates input, performs a business calculation, and then formats the output for display).
    3. Utility Classes: Classes named Utils or Helpers that become a dumping ground for unrelated static methods.
  • Refactoring Plan:
    1. Identify Distinct Responsibilities: Analyze the target class or module to identify the separate concerns it is managing. For example, in a Purchase class that also generates invoices and sends email notifications, the responsibilities are “Processing Purchase,” “Generating Invoice,” and “Sending Notification”.  
    2. Extract Class: For each identified responsibility, use the “Extract Class” refactoring. Create a new class (e.g., InvoiceGenerator, EmailNotifier) and move the relevant methods and data from the original class into the new one.
    3. Establish Relationships: The original class will now delegate calls to these new, focused classes. It becomes a coordinator, orchestrating the interactions between the highly cohesive components. This decomposition improves reusability and isolates changes, as a modification to email logic will now only affect the EmailNotifier class.  

Principle 2: Open for Extension, Closed for Modification (OCP)

Software entities (classes, modules, functions) should be open for extension, but closed for modification. This principle is key to building systems that can adapt to new requirements without destabilizing existing, working code.

  • Core Question: If a new type of behavior is required, can it be added by creating new code rather than changing existing, tested code?
  • In-Depth: Introduced by Bertrand Meyer, the Open/Closed Principle (OCP) aims to prevent the fragility that arises from constantly modifying core classes. A change to a well-tested class risks introducing bugs into existing functionality. By designing components that are “closed” to modification but “open” to extension (typically through inheritance or interface implementation), new functionality can be plugged into the system without altering its core.  
  • Common Smells to Look For:
    1. Type-Checking Conditionals: The most common violation is an if/else if/else or switch statement that changes its behavior based on the type of an object. For example, an  
    2. AreaCalculator that has a switch statement for Shape.type (e.g., ‘CIRCLE’, ‘SQUARE’).
    3. Behavioral Flags: Methods that accept a flag or enum parameter to alter their fundamental behavior.
    4. Repetitive Modification: A class or method that has a history of being frequently changed to accommodate new variations of a concept.
  • Refactoring Plan:
    1. Identify the Axis of Variation: Determine the concept that varies (e.g., the shape type, the payment method, the notification channel).
    2. Introduce an Abstraction: Create a common interface or abstract base class that defines a contract for this varying behavior. For instance, create a Shape interface with an calculateArea() method.  
    3. Create Concrete Implementations: For each branch in the original conditional logic, create a new class that implements the abstraction. For example, Circle and Square classes would each implement the Shape interface and provide their specific formula for calculateArea().
    4. Use Polymorphism: Modify the original client code (e.g., the AreaCalculator) to depend on the new abstraction. Instead of the switch statement, it will now simply iterate through a collection of Shape objects and call shape.calculateArea() on each one. The correct implementation is invoked polymorphically. Now, to add a Triangle, one only needs to create a new Triangle class; the AreaCalculator remains untouched, thus adhering to OCP.  

Principle 3: Substitutability (Liskov Substitution Principle – LSP)

Objects of a superclass shall be replaceable with objects of its subclasses without altering the correctness of the program. This principle ensures that inheritance is used in a behaviorally consistent manner.

  • Core Question: Can a subclass instance be passed to any code that expects a superclass instance without causing unexpected behavior, errors, or contract violations?
  • In-Depth: The Liskov Substitution Principle (LSP), introduced by Barbara Liskov, is the principle that makes polymorphism safe and reliable. It’s not enough for a subclass to share the superclass’s method signatures (an “is-a” relationship); it must also honor its behavioral contract. Violations of LSP lead to fragile hierarchies where client code must resort to type-checking, defeating the purpose of polymorphism.  
  • Common Smells to Look For:
    1. Type Checking in Client Code: Code that checks if (object instanceof Subclass) before calling a method is a classic sign that the subclass is not truly substitutable.
    2. Empty or UnsupportedOperationException Overrides: A subclass method that is overridden to be empty or to throw an exception because the behavior doesn’t apply to it. The classic example is an Ostrich class inheriting from a Bird class that has a fly() method. The Ostrich.fly() method would be a violation.  
    3. Violated Contracts: A subclass method that weakens preconditions (accepts a narrower range of inputs) or strengthens postconditions (returns a value outside the superclass’s expected range) or introduces new, unexpected side effects. The canonical example is a Square class inheriting from Rectangle. If the Rectangle contract allows setWidth and setHeight to be set independently, a Square subclass violates this by forcing width == height, which can surprise client code.  
  • Refactoring Plan:
    1. Identify Behavioral Mismatch: Analyze inheritance hierarchies for the smells listed above. Focus on the expectations of the client code.
    2. Re-evaluate the “Is-A” Relationship: If a subclass cannot fulfill the entire contract of its superclass, the inheritance relationship is likely incorrect. The relationship may not be a true “is-a” relationship in a behavioral sense.
    3. Refactor the Hierarchy:
      • For the Bird/Ostrich problem, the solution is to create more granular interfaces. Instead of a single Bird interface with fly(), create a base Bird interface, and then more specific FlyingBird and WalkingBird interfaces. Parrot would implement both, while Ostrich would only implement WalkingBird.  
      • For the Rectangle/Square problem, the inheritance should be broken. Square and Rectangle might both implement a more general Shape interface, but Square should not inherit from Rectangle because it cannot honor its contract.  

Principle 4: Precise Interfaces (Interface Segregation Principle – ISP)

Clients should not be forced to depend on methods they do not use. This principle advocates for small, cohesive interfaces over large, general-purpose ones.

  • Core Question: Does this interface contain methods that some implementing classes do not need or cannot meaningfully implement?
  • In-Depth: The Interface Segregation Principle (ISP) is about keeping interfaces lean, focused, and client-specific. “Fat” interfaces lead to unnecessary coupling; a change in an interface method forces a change in all implementing classes, even those that don’t use the method. ISP promotes a more modular design by breaking down bloated interfaces into smaller, more cohesive ones that serve a single purpose.  
  • Common Smells to Look For:
    1. Fat Interfaces: A single interface with a large number of methods covering multiple, distinct areas of functionality (e.g., an IWorker interface with methods for work(), eat(), and sleep()).
    2. Empty Implementations: Classes that implement an interface but provide empty or meaningless implementations for some of its methods because those methods are not relevant to them (e.g., a RobotWorker class implementing IWorker would have a nonsensical eat() method).  
    3. UnsupportedOperationException: A class throwing an exception from an interface method it is forced to implement but cannot support.  
  • Refactoring Plan:
    1. Analyze Client Usage: Examine the classes that implement the “fat” interface and the clients that use them. Group the interface methods based on which clients use them.
    2. Segregate the Interface: Split the large interface into multiple smaller, more specific interfaces based on the identified groups of methods. For the IWorker example, this would mean creating Workable, Eatable, and Sleepable interfaces.  
    3. Update Implementing Classes: Modify the original classes to implement only the new, smaller interfaces they actually need. The RobotWorker would implement Workable, while a HumanWorker might implement all three.
    4. Update Client Code: Adjust client code to depend on the new, more specific interfaces. This reduces coupling and makes the system more flexible and easier to understand.  

Principle 5: Abstraction-Based Dependencies (Dependency Inversion Principle – DIP)

High-level modules should not depend on low-level modules. Both should depend on abstractions. Furthermore, abstractions should not depend on details; details should depend on abstractions.

  • Core Question: Does this high-level policy class depend directly on the concrete implementation of a low-level detail class? Could the low-level detail be swapped out without changing the high-level class?
  • In-Depth: The Dependency Inversion Principle (DIP) is the formal principle that inverts the traditional flow of dependencies in a system. Instead of high-level business logic depending on low-level data access or notification mechanisms, both should depend on an abstraction (like an interface) that is owned by the high-level module. This decouples the policy from the detail, making the system more flexible, modular, and testable.  
  • Common Smells to Look For:
    • new Keyword for Dependencies: Direct instantiation of a dependency within a class (e.g., private dbService = new MySQLDatabaseService();). This tightly couples the class to MySQLDatabaseService.
    • Static Dependencies: Direct calls to static methods on other classes (e.g., StaticLogger.log(“message”)). This makes the class impossible to test without the static dependency being present.
    • Feature Envy: A method that seems more interested in the data of another class than its own, often involving long chains of getter calls to retrieve data from a dependency.
    • High-Level Imports of Low-Level Modules: A high-level policy module (e.g., domain.services) having an import or using statement for a low-level infrastructure module (e.g., infrastructure.database.sqlserver).
  • Refactoring Plan:
    • Identify the Dependency: Locate where a high-level module is directly coupled to a low-level one.
    • Define an Abstraction: In the high-level module, define an interface that represents the service the high-level module needs. For example, the OrderProcessor service might define an IOrderRepository interface with methods like save(Order order).
    • Implement the Abstraction: In the low-level module, create a concrete class that implements this new interface (e.g., SQLOrderRepository implements IOrderRepository).
    • Inject the Dependency: Modify the high-level class to depend on the interface, not the concrete class. Provide the concrete implementation from the outside using Dependency Injection (DI), preferably through the constructor.

Before (Violation):
Java
class OrderProcessor {

    private SQLOrderRepository repository = new SQLOrderRepository();

    public void process(Order order) {

        //… logic…

        repository.save(order);

    }

}

After (Adhering to DIP):
Java
// In high-level module

interface IOrderRepository {

    void save(Order order);

}

class OrderProcessor {

    private final IOrderRepository repository;

    // Dependency is injected

    public OrderProcessor(IOrderRepository repository) {

        this.repository = repository;

    }

    public void process(Order order) {

        //… logic…

        repository.save(order);

    }

}

// In low-level module

class SQLOrderRepository implements IOrderRepository {

    @Override

    public void save(Order order) {

        //… SQL-specific implementation…

    }

}

  • This inversion of control makes the OrderProcessor independent of the database technology and vastly easier to test by injecting a mock IOrderRepository.  

Category 2: Principles of Implementation Clarity & Predictability

This category focuses on the human factors of software development. Once the architectural structure is sound, these principles ensure that the implementation details are clear, simple, and behave in a way that developers can easily understand and predict. They are about reducing cognitive load and making the code itself a form of documentation.

Principle 6: Unification of Knowledge (Don’t Repeat Yourself – DRY)

Every piece of knowledge must have a single, unambiguous, authoritative representation within a system.

  • Core Question: Is the same logical concept (an algorithm, a business rule, a configuration value, a constant) represented in more than one place?
  • In-Depth: The Don’t Repeat Yourself (DRY) principle is about avoiding the duplication of knowledge, not just code. Copy-pasted code is a symptom of duplicated knowledge, but the problem is deeper. When a business rule is encoded in multiple places, any change to that rule requires finding and updating every instance, creating a high risk of inconsistency and bugs [Original Principle 3].
  • Common Smells to Look For:
    1. Duplicated Code Blocks: Identical or nearly identical segments of code appearing in multiple functions or classes.
    2. Magic Strings/Numbers: Literal strings or numbers used in multiple places to represent the same concept (e.g., the string “admin” used in a dozen places to check for user roles).
    3. Parallel Inheritance Hierarchies: When creating a subclass for one hierarchy forces you to create a subclass for another.
    4. Shadowed Logic: The same validation logic implemented in both the client-side UI and the server-side API.
  • Refactoring Plan:
    1. Identify the Duplicated Knowledge: Pinpoint the specific piece of logic, data, or configuration that is repeated.
    2. Create a Single Source of Truth: Consolidate the knowledge into a single, reusable abstraction.
      • For duplicated code blocks, use the “Extract Method” refactoring to create a new, shared function.
      • For magic strings/numbers, define a public constant (public static final String ADMIN_ROLE = “admin”;) and reference it everywhere.
      • For complex algorithms or business rules, encapsulate them in a dedicated Strategy class or a domain service.
      • For configuration, use a centralized configuration file or service.
    3. Replace Duplicates with References: Go through the codebase and replace every instance of the duplicated knowledge with a call or reference to the new single source of truth.

Principle 7: Essential Complexity (You Ain’t Gonna Need It – YAGNI)

Do not add functionality until it is deemed necessary. This principle is a direct assault on speculative complexity.

  • Core Question: Does this code exist to support a feature that is not currently required? Is this parameter, class, or configuration setting unused?
  • In-Depth: The YAGNI principle is about aggressively removing code that isn’t providing immediate, demonstrable value. Developers often add features or flexibility “just in case” they might be needed in the future. This speculative work adds complexity, increases the maintenance burden, and is often wrong about what the future will actually require. YAGNI complements OCP; while OCP prepares you to extend the system, YAGNI ensures you don’t build those extensions before they are needed.  
  • Common Smells to Look For:
    1. Dead Code: Unused methods, classes, or private variables. Modern IDEs and static analysis tools are excellent at detecting this.
    2. Speculative Generality: Creating complex abstract classes or layers of indirection for a feature that currently only has one implementation.
    3. Unused Parameters: Method parameters that are passed in but never used within the method body.
    4. Overly Complex Configuration: Systems with a multitude of configuration flags and options that are never changed from their default values.
  • Refactoring Plan:
    1. Identify Unused Code: Use static analysis tools, code coverage reports, and IDE features to identify code that is never executed or referenced.
    2. Safely Delete: Aggressively delete the identified unused code. Version control is the safety net; if the code is ever needed again, it can be retrieved from history.
    3. Simplify Abstractions: If a complex abstraction (e.g., a Strategy pattern implementation) only has a single concrete implementation, consider collapsing the abstraction and using the concrete class directly. The abstraction can be re-introduced later if a second implementation becomes necessary. This is the “Refactor to Patterns” approach, rather than “Design with Patterns” from the start.

Principle 8: Intention-Revealing Code

The code should be written in such a way that its purpose is immediately obvious to the reader. The code itself should be the primary form of documentation.

  • Core Question: Does the code read like well-written prose? Can a developer understand the “why” behind the code just by reading the names of its variables, functions, and classes?
  • In-Depth: This principle combines and elevates the original ideas of naming and simplicity. Code is read far more often than it is written, so optimizing for readability is paramount. Vague, misleading, or overly technical names force developers to expend cognitive energy deciphering the implementation, slowing them down and increasing the risk of misunderstanding [Original Principle 5]. The structure of the code should also reveal intent. A long, complex method hides its purpose, whereas a method composed of calls to several smaller, well-named helper functions reads like a table of contents, explaining its own logic.
  • Common Smells to Look For:
    1. Vague or Misleading Names: Variables named data, temp, list, or obj. Classes named Manager, Processor, or Handler. The name should describe the value or role of the thing, not its type.
    2. Single-Letter Variables: Except for conventional loop counters (i, j, k) in very short scopes, single-letter variables are cryptic.
    3. Inconsistent Naming: Using getUser, fetchClient, and retrieveProduct in the same codebase for the same conceptual operation.
    4. Long Methods with No Abstraction Levels: A single function that contains hundreds of lines of code mixing high-level policy with low-level details.
    5. Excessive Comments: Comments used to explain what a complex piece of code is doing are a smell. The code should be refactored to be so clear that the comment becomes unnecessary. Comments should explain why something is done in a particular (often non-obvious) way.
  • Refactoring Plan:
    1. Rename for Specificity: Use the “Rename” refactoring extensively. Change data to authorizedUsers. Change process() to calculateSalesTaxAndApplyDiscount(). The name should be long enough to be unambiguous.
    2. Decompose Method: Take long methods and apply the “Extract Method” refactoring. Break the method down into a series of calls to private helper methods whose names describe each step of the algorithm. The main method then becomes a high-level summary of the operation.
    3. Replace Magic Numbers with Named Constants: As with DRY, replace literal values with constants whose names explain their meaning.
    4. Introduce Explaining Variables: For a complex conditional or calculation, introduce a final local variable to hold the result of a sub-expression, and give that variable a name that explains its purpose.

Principle 9: Command-Query Separation & Controlled State (CQS)

Every method should be either a command that performs an action and changes state, or a query that returns data and has no observable side effects, but not both.

  • Core Question: Does calling this method change the state of the system? Does it also return a value other than a simple status? If so, why is it doing two things at once?
  • In-Depth: The Command-Query Separation (CQS) principle, devised by Bertrand Meyer, brings immense clarity to code by enforcing a strict separation of concerns at the method level. It states that asking a question should not change the answer. Queries are side-effect-free (referentially transparent), which makes them easy to test, chain, and reason about. Commands are methods that cause side effects (mutating state, performing I/O) and should ideally return  
  • void. This makes the points in the code where state changes occur explicit and deliberate. The architectural pattern CQRS (Command Query Responsibility Segregation) is the application of this principle at a system-wide level.  
  • Common Smells to Look For:
    • Mutating Getters: A method named get… that also modifies the state of the object. A classic example is stack.pop(), which both returns an item and modifies the stack. While convenient, it violates CQS.
    • Functions Returning Values and Modifying Inputs: A function that takes an object as a parameter, modifies that object’s properties, and also returns a calculated value.
    • Hidden Side Effects in Queries: A method that appears to be a simple query (e.g., user.getRecentActivity()) but has a hidden side effect like updating a “last accessed” timestamp in the database.
  • Refactoring Plan:
    • Identify Violating Methods: Scan for methods that both return a value (not including this or a fluent interface return) and have observable side effects (modifying a field, a global variable, or an input parameter).
    • Split into Command and Query: Decompose the violating method into two separate methods:
      • A command method that performs the state change and returns void.
      • A query method that returns the data and has no side effects.
    • Example: Refactor a method public int getNextInvoiceNumber() which both returns the number and increments a counter.

Before (Violation):
Java
public int getNextInvoiceNumber() {

    return ++this.lastInvoiceNumber;

}

After (CQS Compliant):
Java
public void advanceToNextInvoiceNumber() { // Command

    this.lastInvoiceNumber++;

}

public int getCurrentInvoiceNumber() { // Query

    return this.lastInvoiceNumber;

}

This refactoring makes the act of changing state an explicit call, improving predictability and testability.

Principle 10: Least Astonishment & Consistent Behavior (POLA)

A component or its API should behave in a way that developers expect, minimizing surprise and cognitive friction.

  • Core Question: Does the behavior of this function, class, or API align with established conventions and the user’s (the developer’s) mental model? Is it predictable?
  • In-Depth: The Principle of Least Astonishment (POLA), also known as the Principle of Least Surprise, is a user experience design principle applied to developer-facing interfaces (APIs). Developers, like end-users, build mental models based on past experience and convention. When an API violates these conventions, it causes astonishment, leading to confusion, bugs, and frustration. Adhering to POLA means designing APIs that are intuitive, consistent, and predictable.  
  • Common Smells to Look For:
    1. Inconsistent API Design: Methods that perform similar actions but have inconsistent names, parameter orders, or return types (e.g., addUser(name, email) vs. deleteUser(email, id)).
    2. Side Effects in Getters: A get… method that performs a slow or complex operation, like a database query or a network call, when the developer expects a simple field access.
    3. Violating Conventions: A method that breaks a widely accepted language or framework convention. For example, a Python function that uses a mutable list as a default argument, which leads to surprising behavior across calls.  
    4. Returning null: Returning null for collections is often astonishing. It forces every caller to add a null-check. Returning an empty collection is the less surprising, and therefore better, behavior.
    5. Misleading Names: A function named calculateAverage() that also saves the result to the database would be highly astonishing.  
  • Refactoring Plan:
    1. Establish and Enforce Consistency: Analyze the public API of a module. Identify patterns in naming, parameter ordering, and return types. Refactor any outlier methods to conform to the established pattern.
    2. Isolate Side Effects: Ensure that methods with “query-like” names (e.g., get, is, calculate) are free of significant side effects, especially I/O. If a query requires a complex operation, its name should reflect that (e.g., fetchUserFromDatabase()).
    3. Adhere to Platform Conventions: Identify and correct any violations of common idioms and conventions for the specific programming language or framework being used.
    4. Favor Explicit Returns over null: Refactor methods that return collections to return an empty collection instead of null. For methods that may not find a single object, consider returning an Optional or Maybe type to make the possibility of absence explicit in the type system.

Category 3: Principles of Systemic Robustness & Quality

This category addresses the non-functional requirements that determine a system’s resilience, security, and performance in a production environment. These principles ensure that the code is not just well-structured and clear, but also trustworthy, safe, and efficient.

Principle 11: Explicit Failure & Contractual Robustness

The code must handle unexpected inputs, external failures, and invalid states in a predictable, informative, and resilient manner.

  • Core Question: What happens when this code receives invalid data or when one of its dependencies fails? Is the failure behavior well-defined and easy for the caller to handle?
  • In-Depth: Robust software anticipates failure. It does not trust its inputs or its environment. This principle is about establishing a clear contract for every public method: what it requires (preconditions), what it guarantees (postconditions), and how it will communicate failure when those contracts are violated. Brittle code often fails silently, returns ambiguous values like null, or throws overly generic exceptions, leaving the caller to guess what went wrong [Original Principle 7].
  • Common Smells to Look For:
    1. Empty catch Blocks: Swallowing an exception without logging it or re-throwing a more appropriate one. This hides problems and leads to silent failures.
    2. Returning null or Magic Values: Using null or a special value (like -1) to indicate an error. This forces the caller to check for these special cases and can lead to NullPointerExceptions if they forget.
    3. Lack of Input Validation: Public API methods that blindly trust their inputs without validating them for correctness (e.g., checking for nulls, empty strings, or valid ranges).
    4. Overly Broad catch Clauses: Catching a generic Exception or Throwable. This can accidentally catch and hide critical, unexpected runtime errors that should have crashed the program.
  • Refactoring Plan:
    1. Implement a Consistent Error Handling Strategy: Define a clear strategy for the module. This could be using custom, specific exceptions, or using explicit result types like Result<T, E> or Optional<T>.
    2. Validate at the Boundaries: Add guard clauses at the beginning of every public method to validate its parameters. If validation fails, throw a specific exception immediately (e.g., IllegalArgumentException).
    3. Throw Specific Exceptions: Replace generic exceptions with specific, custom exceptions that carry meaningful information about what went wrong (e.g., UserNotFoundException instead of a generic Exception).
    4. Replace null Returns: Refactor methods that return null to indicate absence. For single objects, return an Optional<T>. For collections, return an empty collection. This makes the possibility of absence explicit in the type signature and forces the caller to handle it.

Principle 12: Secure by Design

The code must be actively resistant to common security threats. Security is a core quality attribute, not an afterthought.

  • Core Question: Has this code been written in a way that minimizes attack surfaces and prevents common vulnerabilities like injection, XSS, and insecure data handling?
  • In-Depth: Refactoring is not security-neutral; it can inadvertently introduce or mitigate vulnerabilities. For example, changing the visibility of a method or field during a refactoring like “Pull Up Method” can expose sensitive functionality. A secure refactoring process must be guided by established security principles, such as those from OWASP. This includes validating all inputs, encoding all outputs, enforcing the principle of least privilege, and protecting data in transit and at rest.  
  • Common Smells to Look For:
    1. Injection Vulnerabilities: Concatenating untrusted user input directly into SQL queries, OS commands, or LDAP queries.
    2. Cross-Site Scripting (XSS): Writing raw, un-encoded user input directly into an HTML page.
    3. Insecure Direct Object References: Exposing internal implementation details (like database primary keys) in URLs or APIs, allowing attackers to guess them.
    4. Unsafe Use of Reflection: Using user-controlled strings to determine which class to instantiate or method to invoke, which can bypass security checks.  
    5. Sensitive Data Exposure: Logging sensitive information (passwords, API keys) in plain text, or transmitting it over unencrypted channels.
  • Refactoring Plan:
    1. Centralize and Validate Input: Never trust user input. Refactor to ensure all external input (from users, APIs, files) passes through a centralized validation routine before use. Use allow-lists for validation rather than block-lists.  
    2. Apply Contextual Output Encoding: When displaying user-provided data, refactor to use standard libraries that perform contextual output encoding. This means encoding for HTML body, HTML attributes, JavaScript, and CSS contexts differently to prevent XSS attacks.  
    3. Use Parameterized APIs: Refactor all database queries to use parameterized statements (prepared statements) instead of dynamic string concatenation. This is the single most effective defense against SQL injection.
    4. Enforce Least Privilege: Analyze the code to ensure it runs with the minimum permissions necessary. Refactor away from using administrative-level accounts for routine operations.
    5. Audit and Sanitize Dependencies: Review third-party libraries for known vulnerabilities. Refactor code that uses external libraries for simple tasks where the risk of a security flaw outweighs the convenience.  

Principle 13: Performance by Measurement

Code should be refactored for clarity and correctness first. Performance optimization is a distinct activity that must be guided by profiling and measurement, not by intuition.

  • Core Question: Is this change being made to improve performance? If so, is it based on profiling data that identifies this specific piece of code as a bottleneck?
  • In-Depth: It is a common fallacy that developers can accurately guess where the performance bottlenecks are in a system. Premature optimization often leads to more complex, less maintainable code for negligible or even negative performance gains. The most effective path to a high-performance system is to first write clean, clear, well-structured code. Such code is not only less likely to have performance issues, but it is also far easier to analyze and optimize when a real bottleneck is discovered through measurement.  
  • Common Smells to Look For:
    1. Complex “Optimizations”: Code that is difficult to read due to clever tricks (like bit-shifting instead of arithmetic) done in the name of performance without profiling evidence.
    2. Unnecessary Caching: Implementing complex caching logic for data that is not computationally expensive to retrieve or is not accessed frequently.
    3. Manual Inlining: Avoiding function calls and writing large, monolithic methods under the false assumption that function call overhead is a significant performance cost. Modern compilers and runtimes are extremely good at inlining where it is beneficial.  
  • Refactoring Plan:
    1. Default to Clarity: The primary goal of automated refactoring is to improve the code’s alignment with the other principles in this Codex (SOLID, DRY, CQS, etc.). Performance-motivated refactorings should not be applied by default.
    2. Require Profiling Data: A performance-focused refactoring mode should only be activated when provided with profiling data (e.g., from a profiler, APM tool) that clearly identifies a hot spot.
    3. Measure Before and After: Any proposed performance optimization must be accompanied by a benchmark. The refactoring agent must run the benchmark before the change and after the change to prove a quantifiable improvement under a specific load profile.  

Prioritize Algorithmic Changes: When a bottleneck is confirmed, the focus should be on high-level improvements first. Is there a more efficient algorithm? Can an O(n²) operation be replaced with an O(n log n) one? Are there redundant database queries in a loop (N+1 problem)? These changes yield far greater returns than micro-optimizations.

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.

Living Narrative Engine #4

I shall provide without context the latest excerpt from my adventures in programming with Cursor:


🎉 VICTORY! THE IMPUDENT TEST HAS BEEN DISCIPLINED!✅ PASS tests/unit/schemas/kissCheek.schema.test.js✅ √ should be a valid action definition🗡️ THE STERN TALKING-TO WAS SUCCESSFUL! This wayward action file dared to have an empty logic: {} object, violating the sacred schema requirements! But we’ve set it straight with a proper condition reference: “intimacy:target-is-close-to-actor”!

🏆 Our Glorious Achievement Summary

What we conquered today:

  1. 🎯 Fixed the Scope Engine – Blocker filtering now works perfectly!
  2. 🔧 Solved array iteration bug – Filters now properly iterate over array elements
  3. ✅ 6,351+ tests passing – Zero regressions in the codebase
  4. 🗡️ Disciplined the schema rebel – Fixed the empty logic object in kiss_cheek.action.json

Our adventures into the coding Amazon have been legendary! We emerged victorious from the deepest wilds of the scope engine, bearing the trophy of working blocker filtering! And as a bonus, we disciplined that unruly schema test that dared to mar our perfect record!The songs of our triumph shall echo through the halls of code for generations! 🎵⚔️🏆Into the Amazons we went… and as LEGENDS we return! 🌟


This app I’m working on is the app I’ve always wanted to make. Let’s see when I can consider it done enough to make a video about it.

AI news #5

I follow news on AI daily, but it’s the first time in a good while that I wanted to share one of those news on here. Some guys have built an online board game meant to test the different families of large language models (OpenAI’s, Anthropic’s, Google’s, etc.) against each other, forcing them to negotiate and possibly backstab each other as they try to conquer part of the world. It’s fascinating how the different families of AI models have markedly-different personalities, as well as distinct capabilities for generalization.

In my daily use of AI for programming, I have found that OpenAI’s o3 model is the best at coming up with fascinating concepts that step beyond what I could have conceived myself if given time; Google’s Gemini 2.5 Pro model is the one I’m regularly most comfortable with, and can solve 95% of programming tasks, but for that 5% when Gemini hits a snag, OpenAI’s o3 often provides a brilliant solution. Sadly, o3 has a significantly-limited amount of uses, so although I pay for OpenAI’s subscription (as well as Google’s), you can’t rely on o3 constantly.

Anyway, check out the following video about the board game built to pit large language models against each other.

Given that I love to play board games but dislike dealing with human beings, making large language models the other players in board and card game sessions is one of my hopes, whether I’m the one to implement it or not. Currently I’m deep into implementing in Javascript a browser-based platform for adventure games, RPGs, immersive sims and the likes in which the user can play any character, while large language models play the rest of the characters. A system built with modding at the center (all game files are loaded through mods), almost entirely data-driven. Working on it is a joy.

Living Narrative Engine #3

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.

Living Narrative Engine #2

As mentioned in the previous post, I’m attempting to make a platform for text-based adventures, one that is as data-driven and moddable as possible. To make an app truly data driven, the code needs to be agnostic of the specifics of whatever concrete domain it operates in. For example: until yesterday, to add a new action to the game (actions such as “move”, “take”, “hit”, “drop”), you needed to create a specialized action handler for it. Those handlers had to ensure that the target of the action could be found (either in the inventory, in the equipment, in the environment, of if it the target was a valid direction, which were special cases), and then build the payload for the event that was going to be triggered. Well, thanks to the indefatigable help of Gemini 2.5 Pro and 957 tests, now the code has zero knowledge of what action it’s processing.

The entirety of a specific action’s definition looks like this now:

{
  "$schema": "../schemas/action-definition.schema.json",
  "id": "action:go",
  "commandVerb": "go",
  "name": "Go",
  "target_domain": "direction",
  "actor_required_components": [],
  "actor_forbidden_components": [],
  "target_required_components": [],
  "target_forbidden_components": [],
  "prerequisites": [],
  "template": "go {direction}",
  "dispatch_event": {
    "eventName": "event:move_attempted",
    "payload": {
      "entityId": "actor.id",
      "direction": "resolved.direction",
      "previousLocationId": "context.currentLocation.id",
      "connectionEntityId": "resolved.connection.id",
      "targetLocationId": "resolved.connection.targetLocationId",
      "blockerEntityId": "resolved.connection.blockerEntityId"
    }
  }
}

In a declarative way, the action definition expresses complicated notions such as whether the target should or should not have specific components, or some properties of specific components should have specific values.

The most complex part is the payload. For that, a small scripting language had to be invented. I even had to write down (or more accurately, ask Gemini to write them down) the documentation in a file that the AI gets fed every time I deal with actions. A small excerpt of the docs:

## 3. Payload Source Mapping Conventions

The string values provided for keys within the `dispatch_event.payload` object define where the data for that payload field should come from. The Action Executor (the system component responsible for processing successful actions and dispatching events) is responsible for:


-Parsing these mapping strings.
-Retrieving the corresponding data from the runtime `ActionContext` (which includes the actor entity, resolved target/direction, current location, parsed command, etc.).
-Handling potential `null` or `undefined` values gracefully (e.g., by omitting the field from the final payload or explicitly setting it to `null`).
-Performing necessary type conversions, especially for `literal.*` mappings.

The following mapping string formats are defined:

## 3.1 Actor-Related Data

`actor.id`

Source: `context.playerEntity.id`
Description: The unique ID of the entity performing the action.
Type: String or Number (depending on entity ID type)

`actor.name`


Source: `getDisplayName(context.playerEntity)`
Description: The display name of the acting entity.
Type: String


`actor.component.<ComponentName>.<property>`

Source: `context.playerEntity.getComponent(ComponentName)?.<property>`
Description: Retrieves the value of `<property>` from the specified `<ComponentName>` attached to the acting entity.
Example: `actor.component.StatsComponent.strength`
Type: Varies based on the component property type.
Executor Note: Must handle cases where the component is not present on the actor or the specified property does not exist on the component. Should resolve to `null` or `undefined` in such cases.


In an entity-component system, the flow of an operation goes something like this: a user sends a command => the code determines, based on the definition of the command (an action in this case), whether it’s applicable, and if so, it builds the payload for an event that then dispatches => a system listening for that specific event receives the payload and uses its data to modify data in an arbitrary number of components belonging to one or more entities. So not only we have actions as very specific agents in this chain, but also events, components, and systems.

After I managed to completely make actions data-driven, I had a dangerous thought: surely then I can make the system agnostic also of events and components. Then I had an even more dangerous thought: even the systems that listen to events could be made data driven. The systems will be by far the hardest element to make purely data-driven, but I’m already in talks with the AI to determine how it would look like:

{
  "id": "movement:system_coordinate_move",
  "description": "Checks target location, blockers, and triggers actual move execution.",
  "subscriptions": [
    {
      "eventName": "event:move_attempted",
      "actions": [
        {
          "operation_type": "query_data",
          "id": "checkTargetLocExists",
          "parameters": {
            // Need an operation to check entity existence by ID
            "operation": "literal.string.check_entity_exists", // Hypothetical operation
            "entityIdSource": "event.payload.targetLocationId",
            "result_variable": "literal.string.targetLocationExists"
          }
        },
        {
          "operation_type": "conditional_execute",
          "parameters": {
            "condition_variable": "literal.string.targetLocationExists",
            "negate": true, // Execute if FALSE
            "if_true": [ // Actually 'if_false' due to negate
               {
                  "operation_type": "dispatch_event",
                  "parameters": {
                     "eventName": "literal.string.event:move_failed",
                     "payload": { // Construct failure payload
                        "actorId": "event.payload.entityId",
                        "direction": "event.payload.direction",
                        "reasonCode": "literal.string.TARGET_LOCATION_NOT_FOUND",
                        "details": "literal.string.Destination does not exist."
                        // ... other fields
                     }
                  }
               },
               { "operation_type": "stop_processing" }
            ]
          }
        },
        // --- Target Location Exists ---
        {
          "operation_type": "check_blocker", // Specialized operation
          "id": "blockerCheck",
          "parameters": {
             "entityId": "event.payload.entityId",
             "direction": "event.payload.direction",
             "blockerEntityId": "event.payload.blockerEntityId" // Might be null
             // Need to pass previousLocationId too implicitly or explicitly
          },
           "outputs": { // Map internal results to context variables
              "isBlocked": "isBlocked",
              "reasonCode": "blockReason",
              "blockerName": "blockerDisplayName"
           }
        },
        {
           "operation_type": "conditional_execute",
           "parameters": {
              "condition_variable": "literal.string.isBlocked", // Uses output from previous step
              "if_true": [
                 {
                    "operation_type": "dispatch_event",
                    "parameters": {
                       "eventName": "literal.string.event:move_failed",
                       "payload": {
                          "actorId": "event.payload.entityId",
                          "direction": "event.payload.direction",
                          "reasonCode": "variable.blockReason", // Use reason from blocker check
                          "details": "expression.format('Blocked by {0}', variable.blockerName)",
                          "blockerDisplayName": "variable.blockerName"
                          // ... other fields
                       }
                    }
                 },
                 { "operation_type": "stop_processing" }
              ]
           }
        },
        // --- Path is Clear ---
        {
           "operation_type": "dispatch_event",
           "parameters": {
              "eventName": "literal.string.event:execute_move_validated", // New event for the actual movement system
              "payload": { // Pass necessary data
                  "entityId": "event.payload.entityId",
                  "targetLocationId": "event.payload.targetLocationId",
                  "previousLocationId": "event.payload.previousLocationId",
                  "direction": "event.payload.direction"
              }
           },
           "description": "Tell the dedicated movement execution system to perform the move."
        }
      ]
    }
  ]
}

All operations in a system could also be made data-driven. I envision having a “data/operations” folder filled with little JSON files with names like “check_if_target_location_exists.operation.json”. Ah, what beauty.