Living Narrative Engine #8

Perhaps some of you fine folks would like to follow the development of this app of mine, see the regular blog posts about the matter in an orderly manner, or just chat about its development or whatever, so I’ve created a discord community. Perhaps in the future I’ll have randos cloning the repository and offering feedback.

Discord invite below:

https://discord.gg/6N2qHacK75

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.

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.

Living Narrative Engine, #1

This past week I’ve been in my equivalent of a drug binge. Out of nowhere, I became obsessed with the notion of implementing a text-based immersive sim relying on “vibe coding,” as has come to be known the extremely powerful approach of relying on very competent large-language models to code virtually everything in your app. Once I tasted Google’s Gemini 2.5 Pro’s power, I fell in love. The few times it makes mistakes, it’s usually my fault for not expressing my requirements correctly. Curiously enough, OpenAI released a more powerful model just a couple of days ago: o3. Sadly it’s under usage limits.

Anyway, let me explain about the project, named Living Narrative Engine. You can clone the repository from its GitHub page.

It’s a browser-based engine to play text adventures. My intention was to make it as moddable and data-driven as possible, to the extent that one could define actions in JSON files, indicating prerequisites for the action, the domain of applicability, what events it would fire on completion, etc, and the action-agnostic code would just run with it. I mention the actions because that’s the last part of the core of this app that I’m about to delve into; currently actions such as “look”, “hit”, “move”, “unlock” and such are harcoded in the system: each has a dedicated action handler. That’s terrible for the purposes of making it data-driven, so I’ve requested deep-search research documents and product requirement documents from ChatGPT, which look hella good. Before I start tearing apart the action system of the app, which may take a couple of days, I wanted to put this working version out there.

Currently the app does the minimum for a demo: it spawns you in a room, lets you move from room to room, kill a goblin, open doors, take items, equip items (and drop and unequip them), and also unlock doors (which was the hardest part of the app to codify). I have introduced quest and objective systems that listen to conditions; for example, there’s no key in the demo to open the door where the goblin is located, but when the event “event:entity_died” fires with that goblin as the subject, the door opens mysteriously. The single JSON file that drives that is below:

{
  "id": "demo:trigger_unlock_treasure_door_on_goblin_death",
  "listen_to": {
    "event_type": "event:entity_died",
    "filters": {
      "deceasedEntityId": "demo:enemy_goblin"
    }
  },
  "effects": [
    {
      "type": "trigger_event",
      "parameters": {
        "eventName": "event:unlock_entity_force",
        "payload": {
          "targetEntityId": "demo:door_treasure_room"
        }
      }
    }
  ],
  "one_shot": true
}

The goal is to make everything as data-driven and agnostic as possible.

Everything in the game world is an entity: an identifier and a bunch of components. For example, if any entity has the Item component, it can be picked up. If it has the Openable component, it can be opened and closed. If it has the Lockable component and also the Openable component, the entity cannot be opened if it’s locked. This leads to fascinating combinations of behavior that change as long as you add or remove components, or change the internal numbers of components.

The biggest hurdle involved figuring out how to represent doors and other passage blockers. All rooms are simple entities with a ConnectionsComponent. The ConnectionsComponent indicates possible exits. Initially the user could only interact with entities with a PositionComponent pointing to the user’s room, but doors aren’t quite in one room, are they? They’re at the threshold of two rooms. So I had to write special code to target them.

Anyway, this is way too much fun. Sadly for the writing aspect of my self, I haven’t written anything in about five days. I’ll return to it shortly, for sure; these binges of mine tend to burn out by themselves.

My near-future goal of this app is to involve large-language models. I want to populate rooms with sentient AIs, given them a list of valid options to choose from regarding their surroundings (such as “move north”, “eat cheesecake”, or “kick baboon”), and have them choose according to their written-in personalities. I want to find myself playing through RPG, text-based campaigns along with a harem of AI-controlled isekai hotties.

I’m going back to it. See ya.

Neural narratives in Python #37

I recommend you to check out the previous parts if you don’t know what this “neural narratives” thing is about. In short, I wrote in Python a system to have multi-character conversations with large language models (like Llama 3.3), in which the characters are isolated in terms of memories and bios, so no leakage to other participants. The large language models are fantastic at staying in character. Here’s the GitHub repo.

These days, as part of my worship of Dibella, I tend to use the Python app to set up erotic scenarios that usually finish when I finish. For this one, though, I wanted to create a more normal character than the larger-than-life ones that populate most other scenarios. I ended up just having a good time with her. Although it feels somewhat sacrilegious, I decided to mix her story of grief with another story of grief. Note: even though I’ve put myself as the player character, it’s an improved version. I’m not tooting my own horn here.

Pictured: Ainhoa Mendizabal, resilient single mom.


The fluorescent lights hum faintly overhead, casting a sterile glow over the orderly aisles of Greenleaf Grocery. A faint whiff of freshly baked bread mingles with the sharp tang of citrus from the produce section, while the rhythmic beep of scanners at the checkout counters punctuates the air. A shopping cart rattles faintly as a customer pushes it past, its wheels squeaking against the polished linoleum floor.

Jon’s large frame moves with deliberate precision as he reaches for a chilled bottle of decaf coffee, his calloused fingers brushing against the condensation on the glass. He places it carefully in his basket, the faint clink of the bottle against the metal frame echoing softly in the aisle. As he turns, his sharp eyes catch sight of an older woman maneuvering her cart toward him. Without hesitation, he steps aside, his broad shoulders pressing against the shelf to give her ample space. The woman offers a grateful nod, but Jon’s gaze remains fixed ahead, his expression unreadable, as if the small act of courtesy is just another part of his solitary routine.

Ainhoa Mendizabal adjusts her shopping cart’s trajectory, deliberately maneuvering closer to the tall stranger while pretending to examine items on the nearby shelf. Her fingers absently trace the edge of her cropped jacket as she steals another glance in his direction.

“Sorry, could I just… reach past you for the…” She trails off, as she realizes she hasn’t actually chosen what she’s reaching for.

Jon steps aside, leaving space for Ainhoa.

“No problem.”

She’s a woman in her maybe mid-thirties, with a body sculpted in the gym: her arms look toned even in her cropped jacket, her midriff has defined abs, and her legs are firm. Jon observes her as she stands there, apparently not knowing what to pick.

Ainhoa Mendizabal brushes a strand of hair behind her ear, her fingers lingering nervously at her neck as she glances at the shelves, then back at the tall stranger.

“Oh, I…” Laughs softly. “I actually don’t know what I’m reaching for. Silly, right? I just… well, it’s been a while since I’ve done this. The shopping, I mean. Usually I have two little ones pulling me in different directions, making it impossible to think straight. But they’re at school now, so…” She trails off, then straightens her shoulders. “I’m Ainhoa, by the way.”

Jon smiles softly at Ainhoa as his deep gaze explores her face.

“Life would be easier if we just could approach strangers and introduce ourselves without excuses, right? My name is Jon. Jon Ureña. Nice to meet you, Ainhoa. Let me say that I wouldn’t have thought you have children.”

Ainhoa Mendizabal laughs softly, a faint blush coloring her cheeks as she absently touches the toned muscles of her arms.

“Oh, thank you! The kids… they keep me busy enough that sometimes I forget I don’t look like the typical mom. But you’re right about the excuses thing. I guess old habits die hard when you’ve been out of practice this long. Ureña… that’s Basque too, isn’t it? Do you live around here?”

Jon smiles kindly at Ainhoa.

“Ureña actually comes from the town of Urueña, founded during the Reconquista. It’s from mainland Spain. Nevertheless, I’m part Navarrean, part Basque, part from the east coast of Spain. Born here, though. You’ve been out of practice, you said. Out of practice on what, talking to strangers?”

Ainhoa Mendizabal shifts her weight slightly, her fingers playing with the handle of her shopping cart as a mix of vulnerability and subtle flirtation crosses her face.

“Out of practice with… well, talking to interesting people in general, I suppose. After my husband passed away, my world kind of shrank to just me and the kids. You know how it is – or maybe you don’t. But lately I’ve been trying to… expand my horizons again. Though I have to admit, I’m not usually this forward about it. Something about you just seems… familiar? Maybe it’s the Basque connection.”

Jon turns toward Ainhoa. At her mention of being a widow, a cloud passes over his eyes, and he narrows them slightly.

“I’m sorry to hear that, Ainhoa. That is a pain I wouldn’t wish on anybody… and that in many ways separates humanity into those who have experienced and those who haven’t. You must be very strong, apart from your physical strength, to keep so in shape while taking care of two children after what happened to you.”

Ainhoa Mendizabal touches the lean muscle of her shoulder self-consciously, her eyes meeting Jon’s with a mixture of vulnerability and quiet strength.

“Strong…” Let’s out a soft laugh. “I suppose that’s one way to put it. Though most days it feels less like strength and more like… necessity? You learn to keep moving forward because standing still isn’t an option when tiny humans are counting on you. The gym helps though – it’s become my sanctuary in a way. Twenty minutes with a barbell and suddenly the world makes sense again, you know? Even when nothing else does. But I have to ask… you seem to understand that kind of pain a little too well. The way you said that about humanity being divided…”

Jon averts his gaze for a moment. His eyes have taken a faraway look. He then returns his gaze to Ainhoa.

“I do understand that kind of pain, but I’m not… that readily available to speak about it. However, I also understand the drive to pump weights at the gym, particularly after such an experience. I have done quite a bit of that myself, not sure if anyone can tell anymore… But while lifting heavy weights, that pain that courses through the body, added to… other pain… it feels like the sole genuine language, you know? Each pang of pain speaks the truth.”

Ainhoa Mendizabal shifts closer to Jon, her toned arms tensing slightly as she grips the shopping cart handle. Her eyes soften with recognition of shared pain.

“The truth… yes, that’s exactly it. Like each rep is this… confession of sorts? All that weight you’re carrying inside somehow makes more sense when you’re lifting actual weight outside.” Soft exhale. “But you don’t have to tell me about it if you’re not ready. Sometimes just knowing someone else understands is… well, it’s enough, isn’t it? Though I have to say, those hours at the gym have definitely paid off for you too.”

Jon glances at the width of his left arm as if he hadn’t paid attention to his physique in a good while. He then returns a warm gaze to Ainhoa.

“I suppose we are kindred spirits of a sort…” He gestures toward the end of the aisle. “Do you want us to keep shopping while we talk?”

Ainhoa Mendizabal brushes her fingertips along the shelf’s edge as she walks beside Jon, her toned muscles flexing unconsciously as she reaches to examine random items.

“Yes, let’s keep walking. Though I have to admit, I’m not even sure what I came here to buy anymore.” Soft laugh. “Funny how that happens, right? You walk in with a whole list in your head and then… poof! All gone the moment something more interesting comes along. But maybe that’s not such a bad thing. Sometimes the best conversations happen when you’re not rushing to check items off a list.”

Jon grabs a box of cereal and places it in his shopping basket. He waits for Ainhoa to approach him as she pushes her own shopping cart, that is mostly empty.

“You seem interesting to me as well, Ainhoa. If you don’t mind the question… do you identify with people that, many years or even decades after their partners pass away, they still consider themselves married?”

Ainhoa Mendizabal’s fingers trace the edge of her shopping cart as she considers Jon’s question, her expression softening with vulnerability mixed with thoughtful consideration.

“That’s… that’s a complicated question, isn’t it?” Soft exhale. “I think part of me will always feel married to Iñaki in some way – he was my children’s father, my first love, the man who helped shape who I am today. But…” Ainhoa pauses, meeting Jon’s gaze. “I also know he wouldn’t want me to stop living just because he had to. It took me a long time to accept that loving him and being open to new possibilities aren’t mutually exclusive. Though I have to admit, even saying that out loud feels… strange. Like I’m confessing something I’ve only ever whispered to myself late at night when the kids are asleep.”

Jon offers Ainhoa a deep, somber gaze from his intense eyes, then he reaches for a packet of serrano ham.

“Dealing with regret… it’s like being tied to a signpost you can’t even locate, let alone visit anymore… Sometimes I think still that it would have been better to never wake up again after… after what happened. But turns out that if you put one foot in front of the other, you keep moving along with the rest of the world. Even though it makes no sense to live when the sole person who mattered in it is gone.”

Ainhoa Mendizabal instinctively reaches out and touches Jon’s forearm, before she catches herself and withdraws her hand.

“I understand that feeling more than you know… that desperate wish to just… stop. But you said something there that really hit home – about putting one foot in front of the other. That’s exactly what it is, isn’t it? Some days those steps feel impossibly heavy, like you’re wading through concrete. But then you look up and somehow you’ve made it through another day, another week…” Soft exhale. “And maybe that’s the point? Not trying to make sense of any of it, but just… keeping those feet moving. Though I have to admit, having someone who truly gets it… someone who knows that particular kind of darkness… it makes those steps feel a little less lonely, doesn’t it?”

After Ainhoa reaches to touch his forearm, Jon stops shopping, and turns to face Ainhoa. His expression remains guarded. His gaze is unafraid, but also weary.

“Leaving aside pretense, that this is about two strangers meeting for no particular reason… You’re looking for something. Is it truly a good idea for you to look for someone who is also tied to a ghost? Perhaps it would be better for your sanity and well-being to meet up, perhaps in apps or bars, someone who doesn’t know what you and I know.”

Ainhoa Mendizabal leans against her shopping cart, her muscled shoulders tensing slightly as she considers Jon’s words. Her fingers drum thoughtfully against the metal handle.

“Maybe you’re right about the apps and bars… they feel so artificial, don’t they? Like everyone’s wearing these masks, pretending grief doesn’t exist. But here’s the thing – I’m not looking for someone to magically fix everything or make the pain disappear. That’s not how it works, is it? What I’m looking for… what I think we both might need… is someone who understands that some ghosts stay with us, and that’s okay. Someone who knows that healing isn’t about forgetting, but about making room for new memories alongside the old ones. Though I have to admit, I didn’t exactly plan on having this conversation in the middle of the grocery store between the cereal and the serrano ham.”

Jon fixes a deep gaze that bores through Ainhoa’s eyes. His eyes hint at a deep pain that’s almost ancient.

“Ainhoa, I’ve been mourning my other half for longer than she lived. Even after all this time, the world doesn’t make sense. That’s the kind of person I am. I lift weights, I go to work, I am… entertained at times. But I don’t know how to be happy.”

Ainhoa Mendizabal steps closer to Jon, her athletic frame tensing slightly as vulnerability flickers across her features. Her fingers absently touch the necklace at her throat – the one that was Iñaki’s.

“Maybe that’s exactly why we should take this chance.” Soft exhale. “Look, I’m not saying we have to figure everything out right now between the serrano ham and breakfast cereals. But there’s something… real here. Something honest. And honestly? I’d rather spend time with someone who knows that happiness isn’t always possible than pretend with someone who thinks grief has an expiration date. Besides…” slight smile, “I’ve learned that sometimes the best things in life come from the most unexpected places. Like finding a kindred spirit in aisle three of a grocery store on a random Tuesday morning.”

Jon smiles softly, but genuinely, at the intriguing woman.

“Well, I’m done warning you for now… Truth is, Ainhoa, I find you very interesting, and it’s rare for me to get interested in anyone. You also look great. Do you want us to exchange numbers, or even go for a coffee afterwards? Who knows, maybe soon enough we’ll find ourselves heading to the gym together. It would be good to have a reliable spotter.”

Ainhoa Mendizabal’s face lights up with a genuine smile, her athletic frame relaxing visibly at Jon’s suggestion. Her fingers brush against her necklace briefly before dropping to adjust the strap of her gym bag.

“Now that’s the kind of warning I don’t mind hearing. And yes to both – coffee and exchanging numbers. Though I have to warn you too… I take my spotting duties very seriously. No half-hearted encouragement from me – when I say ‘one more rep,’ I mean it!” Ainhoa pulls out her phone. “Here, let me give you my number. And there’s this little café nearby that makes these amazing pintxos… unless you had somewhere else in mind?”

Jon slips his phone back into his pocket after saving Ainhoa’s number, his movements deliberate yet slightly hesitant, as if unsure of this new step. They move through the aisles together, their carts now filled with items they’ve helped each other choose, a quiet camaraderie forming between them. Outside, the midday sun bathes the cobblestone streets in golden light, and Jon instinctively reaches for Ainhoa’s grocery bags, his broad hands brushing against hers. She smiles, her toned arms flexing slightly as she resists for a moment before relenting, her playful smirk betraying her amusement at his old-fashioned gesture. They walk side by side, the rhythmic clatter of their footsteps blending with the distant hum of the city, the promise of coffee and conversation drawing them closer to the café.

Ainhoa leads Jon through the sunlit streets, her confident stride matching the rhythm of their conversation. The Blue Moon Café welcomes them with the rich aroma of roasted coffee beans and the soft hum of acoustic music. At the counter, Ainhoa gestures toward the display of pintxos, her toned arm brushing against Jon’s as they lean in to choose. They settle into a corner table, their shopping bags resting at their feet, the cozy nook offering a sense of privacy amidst the café’s gentle bustle. The warm light from the overhead fixture catches the faint sheen of sweat on Jon’s brow, a reminder of the weight he carries, both physically and emotionally.

Ainhoa Mendizabal traces her finger along the rim of her untouched coffee cup, her athletic frame leaning forward slightly as she speaks. Her dark eyes remain fixed on Jon, studying the way the café light catches the pain etched in his features.

“You know what’s funny? Before today, I probably would’ve laughed if someone told me I’d find a real connection while shopping for serrano ham. But there’s something… different about this. About you. The way you just get it – the weight of carrying those memories, the struggle to find meaning when everything’s been turned upside down. And maybe… maybe that’s exactly what makes this feel right? Not despite our broken pieces, but because of them. Though I have to warn you – if we’re going to be workout partners, you should know I take my training seriously. No slacking allowed, even on leg day!”

Jon takes a sip of his coffee as his features light up with amusement.

“A grocery shop, or a park, are better places to find genuine connection than bars or, God forbid, dating apps. Nobody should try the apps unless they only want a connection of the physical variety. In grocery shops and parks you find people just living their normal lives. And regarding our shared struggle to find meaning… One must be careful about that, because the darkness also seeks company, right? And it prefers to stay down there, in the hole… Regarding the gym…” He smirks with a hint of mischief. “I can tell that you train your lower half very thoroughly. Maybe you didn’t notice passersby staring as we headed here, but I did.”

Ainhoa Mendizabal blushes slightly at Jon’s comment about her lower body, but her athletic confidence doesn’t waver. She shifts in her seat, unconsciously straightening her already perfect posture, while her fingers play with the necklace at her throat.

“Oh, so you noticed that, did you?” Soft laugh. “Well, those squats and deadlifts don’t lie. Though I have to say, your form isn’t bad either – I caught a glimpse of those shoulders when you were reaching for the top shelf earlier. But you’re right about the apps and bars… there’s something artificial about them. This though?” Ainhoa gestures between them. “This feels more… authentic. Real. Speaking of which, how do you usually structure your training? I typically hit the gym around 6am, after dropping the kids at their grandmother’s. Though I suppose I should warn you – I tend to get pretty intense during my workouts. It’s become my therapy of sorts, you know?”

Jon takes another sip of his coffee, then puts the cup down.

“I usually also hit the gym before work. I prefer to get the important things done before… well, before the often mind-numbing routine of the job squeezes me out. It would be nice to meet up for workouts regularly. I must say, though… You’re much stronger than me, being able to juggle your two kids, the gym, and a job.”

Ainhoa Mendizabal leans back slightly in her chair, her athletic frame relaxing as a genuine smile plays across her features. Her fingers brush against Iñaki’s necklace before dropping to wrap around her coffee cup.

“You know what? That means more than you realize. Sometimes I feel like I’m barely keeping my head above water, juggling everything. But the gym… it’s become my anchor. Those early morning sessions are sometimes the only moments I get that are truly mine. Though having the kids actually makes it easier in a way – they give me a reason to keep pushing forward when everything else feels impossible.” Soft laugh. “But hey, don’t sell yourself short. That kind of dedication to training before work? That takes its own kind of strength. What would you say to meeting up tomorrow morning? I usually start with compound lifts – squats, deadlifts, the works. Unless you’re one of those guys who skips leg day?”

Jon shoots Ainhoa a sideways glance, somewhat playfully.

“I must not be doing it right if you can’t tell that I never skip leg day. Let’s meet tomorrow morning then. I do mainly High-Intensity Training for bulking up, focusing on compound exercises. I need to be careful with the squats, as my right knee isn’t as strong as it used to be.”

Ainhoa Mendizabal tilts her head playfully, her athletic frame shifting forward as she reaches across the table to tap Jon’s forearm gently. Her dark eyes dance with amusement, though they retain that hint of carefully guarded vulnerability.

“Oh, you do those legs justice, trust me – I’ve got a trained eye for proper form! But I like that you’re careful with the knee. Smart training is sustainable training, that’s what I always say.” Soft laugh. “How about we meet at GazteluFit at 6:15? They’ve got that amazing squat rack in the corner – perfect lighting too, in case either of us feels inspired to add to our social media stories. Though fair warning – I tend to grunt when I’m pushing through those last few reps. Some people find it intimidating, but hey, that’s what headphones are for, right?”

When Ainhoa reaches to tap his forearm, he narrows his eyes warmly, welcoming the intimacy.

“GazteluFit, huh? I’ve gone there from time to time, I guess never at the same time as you… But I won’t wear headphones. In fact,” he smirks, “I look forward to hearing you grunt.”

Ainhoa Mendizabal’s cheeks flush a deeper shade of pink as she leans forward, her athletic frame emphasizing the curve of her shoulders. Her fingers toy with the necklace at her throat as a playful smirk tugs at her lips.

“Oh, so you want to hear me grunt, do you?” Ainhoa’s voice drops to a teasing whisper. “Be careful what you wish for – I’ve been known to intimidate even the burliest guys when I’m in the zone. But I like that confidence… and that you’re not afraid of a little intensity. 6:15 tomorrow then? I’ll even try to tone down my battle cries… maybe.”

* * *

The early morning sunlight streams through the large glass windows of GazteluFit, casting long, golden beams across the polished wooden floors. The rhythmic clang of weights echoes through the space, mingling with the steady hum of treadmills and the occasional grunt of exertion. A faint scent of fresh sweat and cleaning solution lingers in the air, grounding the scene in the raw energy of disciplined effort.

Jon meets Ainhoa at the entrance of the vast exercise room. He’s wearing a black tanktop that shows off his sculpted shoulders, as well as his prominent biceps and triceps.

“Good morning, Ainhoa. I’m glad we could do this.” He eyes her outfit with an unreadable expression, and doesn’t comment on it even though it feels that he would like to. “Shall we head in?”

Ainhoa Mendizabal nervously adjusts her yellow sports bra and tucks a loose strand of hair behind her ear. Her fingers briefly brush against Iñaki’s necklace before dropping to her side.

“Good morning, Jon!” She beams at him, her heart racing from more than just pre-workout anticipation. “I hope you’re ready for an intense session. I usually start with compound movements – you know, the big lifts that really get your heart pumping.” Her eyes drift briefly over his muscular arms before quickly returning to his face. “Have you worked with a training partner before?”

Jon and Ainhoa walk into the exercise room, past a few people running on treadmills.

“Yes, I was going for compound first. Do you want to do squats? I was thinking of alternating between deadlifts and squats myself; perhaps we can switch. And no, I’ve never worked with a training partner before… I’m afraid I have always been a lone wolf when it comes to weightlifting, but I can already tell that it will be more interesting with you.”

Ainhoa Mendizabal leads Jon to the squat racks, her hips swaying slightly as she walks. She adjusts her ponytail and touches Iñaki’s necklace before settling into position beside the weights.

“A lone wolf, hmm? Well, you’re in for a treat then – having someone to spot you and push your limits makes all the difference! And yes, let’s start with squats. I usually do five sets, increasing weight each time until failure.” She grins playfully. “Just don’t be intimidated when you see how much this mama can lift. But switching between exercises sounds perfect – keeps the blood pumping, you know? Plus it gives us a chance to catch our breath… and chat a bit more.”

Ainhoa keeps her form perfect as she pushes through her first set of squats. Jon, beside her, does deadlifts, his leg muscles and triceps rippling and bulging as he controls his breathing. He glances at the way Ainhoa’s leggings highlight her firm glutes and thighs.

“I’m right here for when you want me to spot you.”

Ainhoa Mendizabal completes her set of squats with controlled movements, her face flushed from exertion. She catches her breath and adjusts Iñaki’s necklace before responding.

“Thanks, Jon. Having someone ready to spot really helps push those limits.” She exhales deeply. “Though I have to admit, your form with those deadlifts is quite impressive. Looks like I’m not the only one who takes their training seriously! Want to switch exercises after this set? I’d love to see if your squat game matches your deadlift prowess.”

After Jon’s set of deadlifts, they switch. Jon sets the barbell on his broad shoulders. Jon is considerably taller than Ainhoa, so when she stands behind him to spot him, she feels that the barbell is quite high. As Jon lowers himself for the first repetition, Ainhoa realizes that she was standing too close to Jon when his glutes bump against her crotch, which makes her step back. Jon finishes his rep, then chuckles over his shoulder.

“Well, I guess I should be glad that our positions weren’t switched, or else this would have been much more awkward.”

Ainhoa Mendizabal lets out a nervous laugh, her hand instinctively reaching for Iñaki’s necklace as her cheeks flush a deeper shade of pink. She takes another step back to maintain proper spotting distance.

“Oh god, I’m so sorry about that! Guess I need to work on my spotting position as much as my squats.” She laughs softly. “Though you’re right – at least with our height difference, you won’t have that problem spotting me. Unless you’re planning on squatting really low!”

Jon lowers himself into a deep squat, his powerful thighs straining against the weight of the barbell as Ainhoa stands close behind, her hands hovering near his shoulders, ready to assist. Her eyes flicker between his form and the two veteran weightlifters nearby, their muscular frames and confident postures exuding an air of dominance. The man, with arms like tree trunks, casually refers to the woman as ‘Amazon,’ her towering presence and chiseled physique commanding attention. Ainhoa feels a mix of admiration and unease as the pair exchange knowing glances, their conversation laced with the kind of gym jargon that suggests they live and breathe this world. Jon, oblivious to their presence, focuses solely on his breathing and the burn in his legs, his determination a quiet counterpoint to the unspoken challenge radiating from the veterans.

(Pictured: Garazi “Amazon” Ibarrola, a larger-than-life teenage-boy-eater that I produced for when the mood strikes that way. In my system, you can just copy characters from other scenarios, and they behave naturally.)

Jon finishes his set. He straightens up, his face flushed, and breathes out. He stands aside to let Ainhoa do her next set, then he moves to her back, looming over her smaller form, to spot her if necessary.

“There you go, Ainhoa. You can go as hard as you need; I will be here to take care of the barbell if you fail.”

Ainhoa Mendizabal positions herself under the barbell, adjusting her stance while her fingers briefly touch the necklace at her throat. A light sheen of sweat makes her skin glow in the morning light filtering through the gym windows.

“Thank you, Jon. Having a strong spotter really helps push those limits!” She takes a deep breath, centering herself. “Though I have to warn you – I’ve been known to surprise people with how much I can lift.” Her voice carries a playful challenge as she grips the barbell. “Ready when you are, handsome… I mean, ready to spot!” She quickly corrects herself, cheeks flushing deeper than just from exertion.

Jon chuckles behind Ainhoa.

“I’m ready, gorgeous. Right behind you.”

Ainhoa starts her set while Jon’s big, solid mass looms behind her, his breath reaching her nape at times. She steadies herself under the barbell, her muscles tensing as she works through her set. A bead of sweat trickles down her temple as she maintains perfect form through each repetition while she breathes steadily through her squats.

“You know what’s funny? Yesterday I could barely imagine myself getting back to working out with someone else. But this…” she exhales deeply as she completes another rep, “this feels natural somehow. Though I have to admit, your compliments aren’t helping my concentration!” She lets out a breathy laugh as she pushes through another rep. “At this rate, I might need that spot sooner than I planned.”

Ainhoa’s legs tremble as she pushes to complete her set, and Jon hurries to step closer to her back and place his hands under the barbell in case he’ll need to lift it. Ainhoa finishes the set, and puts down the barbell.

“You are very strong, Ainhoa. Those thighs of yours, dare I say, could crush a watermelon at this point.”

They both notice that the woman someone had referred to as “Amazon” is standing close by, observing them like a veteran gym-goer, an expression of pride in her eyes. Her huge arms are crossed under her muscular breasts.

“Well well, what do we have here?” She laughs heartily as she flexes her biceps by instinct. “Look at you two lovebirds, getting all hot and sweaty together! Mmm, and those thighs of yours, chica…” Garazi breaks into an impish grin. “They’re coming along nicely, though they’ve got nothing on these babies yet.” She flexes her quads dramatically. “But hey, it’s always good to see fresh meat… I mean, new faces at the gym taking their training seriously. Especially when they’ve got such an… attentive spotter.” Garazi winks at Jon. “Keep it up, you two. Maybe I’ll even share some of my special training techniques… if you’re brave enough to handle them.”

Jon wipes the sweat from his forehead as he raises his eyebrows at Garazi.

“It’s a bit unfair to compare us regular folks to you, miss… Amazon, someone called you? You are, and I say it respectfully, a beast. Past a certain point of training, one needs to have been blessed my the right genes, I’m afraid.”

Garazi Ibarrola throws her head back and laughs heartily, her muscular frame rippling as she flexes her impressive biceps.

“Genes? Oh honey, you’re too kind! But let me tell you something – these magnificent muscles weren’t just handed to me on a silver platter. I worked my ass off for every inch of this physique.” She flexes dramatically. “Though I suppose being blessed with superior genetics doesn’t hurt… especially when it comes to attracting cute young things to train with. Speaking of which, you two make quite the pair! The way you spot each other, all that sexual tension… mmm, it takes me back to yesterday with my sweet boy Iker.” She winks suggestively. “But hey, don’t let me interrupt your workout. Keep pushing those limits, and maybe one day you’ll be half as strong as this Amazon warrior. Though I doubt it!”

Ainhoa Mendizabal wipes sweat from her brow and instinctively touches Iñaki’s necklace, her cheeks flushed from both the workout and the attention. She straightens her posture, highlighting her athletic frame while maintaining a respectful distance from both Jon and Garazi.

“Superior genetics?” She laughs nervously. “I’d say it’s more about determination and consistency. Though I have to admit, seeing results like yours is… inspiring.” Ainhoa glances briefly at Jon before returning her attention to Garazi. “But maybe we should focus on finishing our sets? Unless you’re offering to demonstrate some of those special techniques right now?”

Garazi excuses herself, and with a knowing smirk, she wanders off. Ainhoa and Jon switch positions. He’s about to do a set of squats while Ainhoa spots him.

“That Amazon… one imposing woman,” he says. “And a boy-eater, it seems. I don’t know if I should feel sorry for the teenagers that come here looking to start their training…”

Ainhoa Mendizabal adjusts her stance to spot Jon properly, her hand briefly touching the necklace at her throat as she watches Garazi’s retreating form.

“Sorry about those poor teenage boys, but they’re the least of my concerns right now!” She laughs nervously. “Though I have to admit, her dedication to training is impressive, even if her… approach is a bit intense.” Ainhoa steadies herself behind Jon. “But enough about the Amazon warrior – let’s focus on your form. These legs of yours aren’t going to sculpt themselves into watermelon crushers without proper technique!”

Jon and Ainhoa move seamlessly from the squat racks to the dumbbell area, their bodies glistening with sweat under the gym’s bright lights. Jon adjusts the bench for Ainhoa, his hands lingering slightly longer than necessary as he ensures her comfort. Ainhoa, in turn, offers Jon a towel and a playful smile, her fingers brushing against his as she hands it over. The clatter of weights and the hum of machines around them fade into the background as they focus on each other, their movements synchronized and deliberate. The gym’s energy seems to amplify their connection, each rep and set bringing them closer, both physically and emotionally.

It’s almost seven in the morning. Jon rolls his shoulder. His skin, as well as the hair on his arms and legs, is sheened with sweat.

“That was one hell of a workout, Ainhoa, but I’m afraid I’m beat for today… And I also have to head to work in a while.”

Ainhoa Mendizabal wipes her brow with a towel, her fingers instinctively finding Iñaki’s necklace as she catches her breath. Her cheeks remain flushed from the intensity of their shared workout.

“Already?” She laughs softly. “And here I was just getting warmed up! But you’re right, it’s been quite the session. I haven’t pushed myself like this in… well, longer than I care to admit.” Ainhoa pauses, gathering courage. “Listen, Jon… I was thinking maybe we could make this a regular thing? Same time tomorrow? I mean, if you’re not too sore from today’s beating!” She touches her necklace nervously. “Having a reliable training partner really does make all the difference.”

Jon places a warm, strong hand on Ainhoa’s sweaty shoulder, her fibers solid against his palm.

“Sure, we can make these sessions a regular thing, but don’t hesitate to break it off if there’s some trouble with your kids. And you know, we could also meet outside of the gym.” His eyes are warm and honest as he looks down into hers. “No need to keep the charade that we aren’t interested in each other.”

Ainhoa Mendizabal’s fingers instinctively reach for Iñaki’s necklace as her breath catches. Her cheeks flush deeper, not entirely from the workout, as she meets Jon’s intense gaze.

“Outside the gym? I… yes, you’re right. There’s no point pretending this is just about proper form and spotting techniques, is there?” Ainhoa takes a steadying breath. “Though I have to warn you – dating a widow with two kids isn’t exactly what most men sign up for. But if you’re sure… maybe we could grab coffee after tomorrow’s session? That is, assuming you survive another round with these watermelon crushers in training!”

Jon pulls back his hand from Ainhoa’s shoulder, but he keeps holding her gaze with an intimate, warm look in his eyes.

“Until we met yesterday, I wouldn’t have guessed that I would look forward to hanging out with another person again… But I’m interested in you, Ainhoa, and I want to get to know you more. I’m willing to go ahead step by step.” They head to the exercise room’s exit. “After we hit our respective showers, do you want me to drive you somewhere? I have to head to the hospital, where I work. I’m not a doctor, I’m afraid, but a programmer.”

Ainhoa Mendizabal adjusts her gym bag on her shoulder, her fingers absently playing with Iñaki’s necklace as a mix of nervousness and excitement flickers across her face.

“A programmer?” She laughs softly. “Well, that explains the intensity in your eyes when you’re counting reps! And yes, a ride would be… nice. I need to head home to get the kids ready for school anyway. Though I should warn you – my car chatter tends to bounce between workout techniques and embarrassing mom stories. But since you’re willing to risk it…” Ainhoa pauses, gathering courage. “You know, Jon, what you said about looking forward to spending time with someone again? I feel the same way. It’s been so long since I’ve felt this… comfortable with anyone. Even if it does make me nervous as hell!”

The gym’s clamor fades behind them as Jon and Ainhoa step into the crisp morning air, their damp hair and fresh scents mingling with the salty breeze. Jon opens the door of his sleek BMW for her, his hand lingering on the frame as she slides into the passenger seat, her fingers brushing against Iñaki’s necklace. The car hums to life, and they glide through the winding streets of Donostia, the city’s hills rising like silent sentinels around them. Ainhoa directs him with quiet confidence, her voice soft but steady, as the road climbs higher, offering glimpses of the bay sparkling in the distance. The silence between them is comfortable yet charged, a shared understanding of the unspoken possibilities that lie ahead.

As Jon drives, he glances at Ainhoa, seated beside him on the passenger seat, wearing her form-fitting yellow sports bra and her leggings.

“I thought you would tell me mom stories. What are your kids names, if you don’t mind me knowing?”

Ainhoa Mendizabal’s fingers trace the outline of Iñaki’s necklace as she settles into the passenger seat, her athletic frame relaxing slightly despite her nervous energy.

“Oh, my little warriors?” Soft laugh. “Itziar is my eldest – she’s nine going on thirty, I swear! Takes after her father with how stubborn she can be. And then there’s Julen, my seven-year-old tornado. He’s…” touches necklace, “he’s got his father’s smile, you know? Both of them are handling everything better than I could’ve hoped, though some days are harder than others. But they’re resilient, just like their mama had to learn to be.” Ainhoa pauses “What about you? I mean, I know you work at the hospital as a programmer, but what exactly do you do there? Must be quite different from debugging code at a regular tech company.”

Jon smiles warmly at Ainhoa, but not without a hint of deep pain in his eyes.

“Your kids sound healthy and well-adjusted. I’m really sorry that they had to go through… what nobody should experience. I can’t imagine losing a father at that age.” He keeps driving for a few moments in silence. “At work? I program systems to figure out how much the resources of the hospital are being used, to properly reorganize medical schedules, what professionals gets assigned where, and such things. We also program the intranet systems so doctors and nurses can input their schedules. Boring stuff, but at least it’s actually necessary.”

Ainhoa Mendizabal’s fingers drift to her necklace as she processes Jon’s words about his work, her eyes softening at the mention of her children.

“Necessary? Jon, what you do helps save lives, even if indirectly. A hospital is like… well, like a really complicated gym, you know? Everything has to flow just right or the whole system falls apart. And speaking of systems…” Ainhoa laughs softly. “I should probably mention that my morning routine with the kids is pretty systematic too. I’ll need to be home by 7:30 to make sure Itziar doesn’t try convincing Julen that cookies count as breakfast again. Though between you and me? Some mornings I’m tempted to just let her win that argument!” She pauses, touching necklace. “It means a lot, what you said about the kids. About understanding what they’ve been through. Most people just… they don’t know how to approach that part of our lives.”

“Did the other guys you dated after… well, after your terrible loss… did those guys bail on you when they found out you have kids?”

Ainhoa Mendizabal’s hand tightens around Iñaki’s necklace as she looks out the car window, a mix of vulnerability and determination crossing her face.

“Actually… you’re the first since Iñaki.” Nervous laugh. “I mean, there have been a few who showed interest, but I never… I couldn’t… It just didn’t feel right, you know? Like I was betraying his memory somehow. Plus, the thought of introducing someone new to the kids – that terrified me more than any weight I’ve ever lifted. But yesterday at the store, when I saw you… Something felt different. Maybe it’s because you understand loss in a way most people don’t. I can see it in your eyes, Jon. That same shadow I see in mine when I look in the mirror some mornings.”

Jon continues driving in silence, his knuckles tightening around the steering wheel as his eyes seem both to darken and get sadder, perhaps weighing if he should bring up something from his depths. When he speaks, it sounds as if he’s dragging words out of himself.

“Her name was Izar Lizarraga. My best friend since we were twelve. We dated throughout our teens. She dreamed of becoming a motocross rider. One night she came to my parents’ home in the rain. Her father had slapped her hard because she had dropped out of school and only cared about training to become a professional rider. That night we promised each other that we would run away from home and travel the breadth of Spain. After we parted ways, Izar, instead of heading home, took to the highway. She crashed and died.”

Ainhoa Mendizabal’s hand drops from her necklace as she turns sharply toward Jon, her eyes widening with recognition of shared pain. Her voice softens to barely above a whisper.

“Oh, Jon… I…” Deep breath. “That kind of loss – losing someone so young, so full of dreams – it changes you forever, doesn’t it? Makes you question everything. I can’t imagine carrying that pain since you were teenagers. With Iñaki, at least we had years together, built a life, had our children… but to lose Izar right at the beginning of your journey together? Sometimes I wonder if that’s worse somehow – losing all those possible futures before they even had a chance to happen. Did you… did you ever let yourself imagine what that trip across Spain might have been like?”

Jon keeps his heavy gaze fixed on the road ahead. His Adam’s apple bobs up and down.

“If I let myself imagine what that trip might have been like? I dream about her almost nightly. I have tried to forget her, but it’s like I’m actually haunted. It’s been 21 years, longer than she lived. And there’s not a single day that I don’t wish I could turn back time and make her get off her motorcycle instead of driving away to her death.”

Ainhoa Mendizabal gently places her hand on Jon’s forearm, her touch light but steady, before withdrawing it respectfully.

“Twenty-one years… that’s longer than some people get to live at all. And yet here you are, still carrying her with you.” Soft exhale. “You know what the hardest part is? Those dreams – they never really stop coming, do they? With Iñaki, sometimes I wake up thinking he’s just in the other room, or that I can still smell his cologne on his pillow. But your dreams of Izar… they must be different. All those ‘what-ifs’ packed into them.” Ainhoa pauses. “I don’t think we ever really stop being haunted by the people we’ve loved, Jon. Maybe we’re not meant to. Maybe carrying their memory – even when it hurts like hell – is just another way of honoring how much they meant to us. Though I bet Izar would want you to find some peace… just like I know Iñaki would want that for me.”

“I don’t know if grief of this sort, the kind you and I have, ever really goes away. I think the people afflicted from such burdens usually retire from society, lest they become eyesores to others… Quietly disappear. I have done that to a certain extent in these last twenty years. I’ve rejected women quite a few times. I was still a married man, except that the girl I never actually married died 21 years ago. Iñaki died two or three years ago, is that right? I wish I could offer you hope in that regard, tell you that it’s going to fade in the future… but it hasn’t for me. Maybe the only solution is to love someone else.”

Ainhoa Mendizabal leans forward slightly, her fingers instinctively finding Iñaki’s necklace as she processes Jon’s words about lasting grief.

“Three years. It’s been three years since the accident took Iñaki. And you’re right – there’s no magical timeline where grief just… disappears. But Jon, retiring from life? That’s not living – that’s just existing. And maybe… maybe that’s why we found each other yesterday. Two broken people who understand that some wounds never fully heal. When you say you’ve rejected women because you still feel married to Izar’s memory… I get it. Gods, do I get it. For the longest time, I couldn’t even look at another man without feeling like I was betraying Iñaki. But yesterday at the store… something shifted. Maybe because I saw that same haunted look in your eyes that I see in mine. Or maybe because after three years of just… surviving, some part of me is finally ready to try living again. Not to replace what we’ve lost – nothing could ever do that. But to maybe create something new alongside those memories we carry.”