Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 43

Character Memory Log

Recommended resources

Hello and welcome back!

In our last lesson, you did some excellent work building a system to classify raw relationship scores into meaningful tiers like "Friends," "Enemies," and "Crushing." This gave our game's logic a clear language to understand the state of a relationship. However, relationships are more than just their current status; they are the sum of their history.

Today, we will build that history. The learning outcome for this lesson is to store a simplified log of significant interactions between characters as a 'memory'. This system will be the foundation for making character relationships feel persistent and storied. Instead of just knowing two characters are friends, the game will remember how and when they became friends, opening the door for richer, more contextual gameplay down the line.


1. What is a "Memory" in a Game?

At a conceptual level, a memory system allows an AI agent (our character) to retain and recall information from past events. This can range from very simple logs to complex AI-driven summarization. For our purposes, a memory is a data record of a significant event that occurred between two characters.

Memory for agents (conceptual video)

To get a high-level overview of how memory can work for AI agents, let's watch a short conceptual video from LangChain. While it's aimed at LLM agents, the core ideas are directly applicable to our game simulation.

Please watch the following two clips: Short-term vs. Long-term memory (00:51 - 01:50): This introduces the key distinction between conversational (short-term) memory and a more persistent (long-term) memory store. Our interaction log is a form of long-term memory. Long-term memory structures (04:52 - 06:12): This part discusses different ways to store memories, including 'profiles' and 'lists of objects'. We will be implementing a 'list of objects' to log our significant interactions.

As the video explains, we're building a form of long-term memory. Each "memory" will be an object in a list, chronicling the history of a relationship.

Agent Interaction Model with Memory System
This diagram illustrates a social simulation where agents interact over time. Notice the 'Core User' has a dedicated 'Memory Module' for writing and retrieving interaction memories, which is conceptually similar to the system we are building for our characters.


2. Designing the Memory Data Structure

Now, let's translate this concept into Rust data structures. A memory needs to capture the "what, when, and why" of a significant interaction. We'll start by defining a MemoryEntry struct.

This struct will represent a single remembered event. What should it contain?

  • An ID for the interaction that occurred.
  • The impact it had on the relationship scores.
  • A descriptive label for the memory.
  • A timestamp for when it happened.

Let's define this in lib.rs, along with your other character-related structs.

// In lib.rs




#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MemoryEntry {
    pub interaction_id: String,
    pub description: String,
    pub friendship_change: f32,
    pub romance_change: f32,
    pub game_day: u64, // We'll use a simple day count for our timestamp
}

Now, where do we store these memories? A character will have a separate memory log for each person they have a relationship with. A great way to organize this is to create a new struct that bundles a relationship's scores and its history together.

// In lib.rs, this will replace the simple RelationshipScores in your Character struct.




#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct RelationshipData {
    pub scores: RelationshipScores,
    pub memories: Vec<MemoryEntry>,
}

Finally, we update the Character struct to use this new, more comprehensive data structure.

// In lib.rs, update your Character struct.
// You'll need to change this:
// pub relationship_scores: HashMap<CharacterId, RelationshipScores>,
// to this:
pub relationship_data: HashMap<CharacterId, RelationshipData>,

This structure ensures that every relationship is self-contained, with its current scores and its full history neatly packaged together. When serialized, this will create a clean, hierarchical JSON object.

JSON Visualizer for Hierarchical Data
This image shows how JSON can represent hierarchical data. Our `Character` will have a `relationship_data` map, where each entry contains `scores` and a `memories` array of objects, similar to how the 'Super hero squad' has a list of 'members' with their own properties.


3. Logging Significant Interactions

With our data structures in place, how do we create and log a new MemoryEntry? We should do this only when a "significant" interaction occurs. A perfect trigger for this is a change in the relationship type we defined in the last lesson (e.g., moving from Acquaintances to Friends).

This approach, where the game's state is built from a sequence of events, is a powerful pattern in game development.

Making a turn-based multiplayer game in Rust

This article about making a turn-based game in Rust provides a fantastic explanation of this event-driven pattern. While it talks about networking, the core concept of a 'sequence of actions' defining the game state is exactly what we are implementing for our memory system.

Please read these sections to grasp the pattern: Start with 'What games are "turn-based"?' and focus on the key idea that "the state of the game can be determined solely from a sequence of actions." Glance at the bullet points under that section, especially the one about saving/loading by saving the sequence of actions. Finally, look at the Rust code under 'How do we write a reducer in Rust?'. Notice how the GameState struct contains a history: Vec<GameEvent>, and the reduce function adds new events to this vector. Our memories: Vec<MemoryEntry> is a direct parallel to this.

Inspired by that pattern, let's modify the code that processes social interactions. We need to capture the state before and after the interaction to see if a significant change happened.

Let's assume you have a function that processes an interaction. Here’s how you would modify it to log memories.

// This is an example of how you might update your interaction logic.
// You will need to adapt this to your existing GameState implementation.

// In your `impl GameState`
pub fn process_social_interaction(
    &mut self,
    actor_id: &CharacterId,
    target_id: &CharacterId,
    interaction_id: &str,
    friendship_delta: f32,
    romance_delta: f32,
) {
    let actor = self.characters.get_mut(actor_id).unwrap();
    let relationship = actor.relationship_data.entry(target_id.clone()).or_default();

    // 1. Capture the state BEFORE the change
    let status_before = relationship.scores.get_status();

    // 2. Apply the score changes
    relationship.scores.friendship += friendship_delta;
    relationship.scores.romance += romance_delta;

    // Clamp scores to a [-100, 100] range
    relationship.scores.friendship = relationship.scores.friendship.clamp(-100.0, 100.0);
    relationship.scores.romance = relationship.scores.romance.clamp(-100.0, 100.0);

    // 3. Capture the state AFTER the change
    let status_after = relationship.scores.get_status();

    // 4. Check for a significant change (relationship tier changed)
    if status_before.friendship != status_after.friendship {
        // 5. Create and log the memory!
        let description = format!(
            "Became {:?} after interaction '{}'",
            status_after.friendship, interaction_id
        );

        let memory = MemoryEntry {
            interaction_id: interaction_id.to_string(),
            description,
            friendship_change: friendship_delta,
            romance_change: romance_delta,
            game_day: self.game_day, // Assuming you have a game clock
        };

        relationship.memories.push(memory);
        println!("New memory logged for {:?}: {}", actor_id, relationship.memories.last().unwrap().description);
    }
}

This implementation is simple but powerful. By comparing the RelationshipType enum before and after, we automatically detect meaningful shifts in the relationship and record them forever in the character's memory log.



4. A Note on Memory Management

As your game runs, these memory logs could grow very large, potentially slowing down save/load times and any logic that needs to parse them. In a full-scale game, you would need strategies to manage this.

Common techniques include:

  • Pruning: Keeping only the last N memories.
  • Summarization: Using an AI or rule-based system to condense older memories into a summary (e.g., "Had many pleasant conversations").
  • Forgetting: Gradually deleting old or unimportant memories.

Memory-Driven NPC Behavior: Context-Aware and Emotion-Based ...

This academic paper discusses a more advanced memory-driven NPC. While the paper is dense, a few short paragraphs clearly describe a JSON-based memory log and a simple pruning policy, which gives a glimpse into how you might manage memory growth in the future.

You don't need to read the whole paper. Just focus on these specific parts: In the Introduction, read the paragraph under 'Memory Retention' (Section 1.1, second bullet point) to see a description of a JSON-based memory engine. In the Methodology chapter, read the paragraph under 'Memory Module (JSON-Based Episodic Log)' (Section 4.3.2) for details on the structure. Finally, read the paragraph under 'Memory Pruning and Summarization Policy' (Section 5.3.2) to see a concrete example of a simple rule: keeping only the most recent 50 entries.

We won't implement pruning today, but it's a key concept to keep in mind as we continue to build more complex systems. For now, our logging system is a perfect foundation.



Conclusion

Excellent work! You have successfully implemented a memory system that gives your characters a sense of history. Relationships are no longer just a number; they are a story told through a sequence of significant events.

Key Takeaways:

  • You designed a MemoryEntry struct to act as a record of a significant interaction, storing what happened and what its impact was.
  • You integrated this into your main character data by creating a RelationshipData struct that holds both the current scores and the historical Vec<MemoryEntry>.
  • You defined a "significant interaction" as one that changes a character's relationship tier, providing a clear and efficient trigger for logging new memories.
  • You learned that thinking of game state as a sequence of events (or memories) is a powerful pattern for creating persistent, historical simulations.

This memory log is not just for show. In our next module, we'll begin building a Hierarchical Conversation System. The memories you logged today will become crucial for unlocking contextual dialogue. For example, two characters will only be able to "Reminisce About Becoming Friends" if that specific memory exists in their shared history. Get ready to make your conversations smarter and more personal