Hello! Welcome back to our journey building a life simulation game.
In our last lesson, we took a critical step towards a moddable architecture by defining our social interactions as data in an interactions.json file. We created the Rust structs needed to load and represent this data, separating the "what" (the interaction's properties) from the "how" (the game engine's logic).
Today, we're going to build that "how." Our learning outcome is to update relationship scores based on the outcome of social interactions, modified by personality traits. This is where the simulation truly comes to life, as character personalities will now directly influence the social dynamics of your game world.
1. The Principle of Reciprocity and Trust
At the heart of any relationship system is the concept of reciprocity. An action, whether positive or negative, elicits a response, and this exchange builds or erodes the relationship. In game design, we can model this by treating relationship scores as a measure of "trust."
To get a clear perspective on this from a game design standpoint, let's watch a segment from a GDC talk on designing friendship systems.
Game Design Patterns for Building Friendships
In this clip from 'Game Design Patterns for Building Friendships', the speaker explains how reciprocity acts as a fundamental mechanism for building trust. Think of each social interaction in our game as one of these 'reciprocation loops'.
Watch the segment from 38:01 to 43:01. Pay close attention to the idea that trust grows slowly with positive interactions and can be destroyed quickly. Our system of adding and subtracting points from relationship scores is a direct simulation of this principle.
2. Implementing the Core Interaction Logic
Let's start by writing the Rust function that applies the effects from our interactions.json file. We'll create a central place to manage how interactions resolve between two characters.
For this, we'll need a representation of the game world that holds all our characters. Let's define a GameState struct. In your main Rust file (e.g., lib.rs or main.rs), add the following:
// Add these to your existing struct definitions from the previous lessons
// Assuming you have Character, CharacterId, RelationshipScores, etc. defined
pub struct GameState {
pub characters: HashMap<CharacterId, Character>,
// We will add more fields here later, like world objects
}
Now, let's add the method to process an interaction. We'll implement this on GameState. This function will take the initiator, the target, and the interaction data, and then apply the effects.
// In an impl block for GameState
impl GameState {
pub fn apply_social_interaction(
&mut self,
initiator_id: &CharacterId,
target_id: &CharacterId,
interaction: &SocialInteraction,
) {
// Find the initiator's relationship scores for the target
let initiator = self.characters.get_mut(initiator_id).unwrap();
let initiator_scores = initiator
.relationship_scores
.entry(target_id.clone())
.or_default(); // If no relationship exists, create a default one
// Iterate over the effects defined in the JSON
for effect in &interaction.effects {
if let InteractionEffect::Relationship(rel_effect) = effect {
let base_value = rel_effect.value;
// For now, a simple update. We will add personality modifiers next.
let final_value = base_value;
match rel_effect.score {
RelationshipScoreType::Friendship => {
initiator_scores.friendship += final_value;
println!(
"Updating {}'s friendship towards {} by {}",
initiator.name,
self.characters.get(target_id).unwrap().name,
final_value
);
}
RelationshipScoreType::Romance => {
initiator_scores.romance += final_value;
println!(
"Updating {}'s romance towards {} by {}",
initiator.name,
self.characters.get(target_id).unwrap().name,
final_value
);
}
}
}
}
}
}
This function currently only updates the initiator's view of the target. This is a good first step, but true reciprocity means the target's view also changes.
3. Modifying Outcomes with Personality
Now for the most interesting part. A character's personality should affect how they perform and receive social actions. A friendly character might get more out of a "Friendly Chat," while a quarrelsome one might not appreciate a "Joke."
We will use the HEXACO personality model you implemented in Module 4 to create multipliers that modify the base_value of an interaction.

Let's design a simple formula:
final_change = base_value * personality_modifier
The personality_modifier will be calculated based on the traits of the character whose relationship score is being updated.
This approach is directly inspired by academic social simulation architectures. The Comme il Faut architecture, which powered the social game Prom Week, uses this exact concept.
CiF-CK: An Architecture for Social NPCs
The paper 'CiF-CK: An Architecture for Social NPCs' describes adapting the Comme il Faut (CiF) social model for the game Skyrim. It provides the theoretical justification for our implementation.
First, read section 'B. Characters' and look at Table II. Notice how 'Traits' are permanent values that affect social exchanges. This is what we're about to implement. Then, read section 'E. Beliefs and Social Networks', which states that feelings are 'non-reciprocal'. This is key: how Character A feels about B is independent of how B feels about A. Finally, skim Table IV in the 'Implementation' section to see examples of social moves ('Flirt', 'Insult') and traits ('Friendly', 'Hostile') that influence them.
Implementing the Personality Modifier
Let's create a function to calculate this modifier. For now, we'll use Agreeableness and Extraversion from the initiator to modify how they perform an action, and Agreeableness and Emotionality from the target to modify how they receive it.
Since the HEXACO scores are floats from 0.0 to 1.0, we can create a simple linear modifier. For example, a modifier that ranges from 0.5x to 1.5x can be calculated as 0.5 + personality_score.
Let's modify our apply_social_interaction function to incorporate this.
// In your GameState impl block
pub fn apply_social_interaction(
&mut self,
initiator_id: &CharacterId,
target_id: &CharacterId,
interaction: &SocialInteraction,
) {
// We need to clone the personalities to avoid multiple mutable borrows
let initiator_personality = self.characters.get(initiator_id).unwrap().personality.clone();
let target_personality = self.characters.get(target_id).unwrap().personality.clone();
let target_name = self.characters.get(target_id).unwrap().name.clone();
for effect in &interaction.effects {
if let InteractionEffect::Relationship(rel_effect) = effect {
let base_value = rel_effect.value;
// --- Initiator's Perspective ---
// How well did the initiator perform the action?
// High Extraversion makes one better at positive social interactions.
let initiator_modifier = if base_value > 0.0 {
0.5 + initiator_personality.extraversion // Modifier from 0.5x to 1.5x
} else {
// For negative interactions, low Agreeableness could make them more cutting
0.5 + (1.0 - initiator_personality.agreeableness) // More disagreeable = stronger insult
};
let initiator_change = base_value * initiator_modifier;
// Update initiator's scores
let initiator = self.characters.get_mut(initiator_id).unwrap();
let initiator_scores = initiator.relationship_scores.entry(target_id.clone()).or_default();
match rel_effect.score {
RelationshipScoreType::Friendship => initiator_scores.friendship += initiator_change,
RelationshipScoreType::Romance => initiator_scores.romance += initiator_change,
}
println!("Updating {}'s view of {}: friendship change {}, romance change {}",
initiator.name, target_name, initiator_change, 0.0); // Simplified for one effect type
// --- Target's Perspective ---
// How well did the target receive the action?
// High Agreeableness makes one receive positive interactions better.
// High Emotionality makes one receive negative interactions worse.
let target_modifier = if base_value > 0.0 {
0.5 + target_personality.agreeableness
} else {
1.0 + target_personality.emotionality // Modifier from 1.0x to 2.0x
};
let target_change = base_value * target_modifier;
// Update target's scores
let target = self.characters.get_mut(target_id).unwrap();
let target_scores = target.relationship_scores.entry(initiator_id.clone()).or_default();
match rel_effect.score {
RelationshipScoreType::Friendship => target_scores.friendship += target_change,
RelationshipScoreType::Romance => target_scores.romance += target_change,
}
println!("Updating {}'s view of {}: friendship change {}, romance change {}",
target.name, initiator.name, target_change, 0.0);
}
}
}
This data-driven, rule-based approach is common in modern game engines and AI research. The following paper on using LLMs for RPGs uses a very similar structure.
Evaluating Large Language Models as Role-Playing Game Engines
This paper, 'Evaluating Large Language Models as Role-Playing Game Engines', uses a JSON-based event system that is highly relevant to our work. It shows how events have conditions and effects that modify state variables.
Read Section 3.1, 'Game Design', focusing on the definitions of 'State Variables' and 'Game Events'. Our relationship scores are State Variables, and our Social Interactions are Game Events. Then, look at the 'Event Details' table in Appendix F.3. Notice how the 'Success Effect' for event E004 is v.trust_with_bruce += 10. This is exactly what we are doing, but we've added personality as a dynamic modifier.
Conclusion
Excellent work. You have now implemented the logical core of your social simulation. You've created a system where data-driven interactions have dynamic outcomes based on the unique personalities of the characters involved. This is a huge step toward creating the emergent, nuanced behavior you're aiming for.
Key Takeaways:
- Reciprocity in Code: You translated the design principle of reciprocity into a Rust function that updates relationship scores for both participants in an interaction.
- Non-Reciprocal Scores: You implemented the concept of non-reciprocal feelings, where each character maintains their own unique perspective on a relationship, influenced by their personality.
- Personality-Driven Outcomes: You used the HEXACO model to create modifiers, making social interactions feel less static and more dependent on who is interacting.
- Data-Driven Logic: You completed the loop started in the last lesson, where external JSON data now directly drives the logic of the game's social engine.
In our next lesson, we will make these raw numbers more meaningful. We will learn how to classify relationships into types (e.g., friend, acquaintance) based on score thresholds. This will allow us to change how characters behave towards each other and what interactions are available to them, adding another layer of depth to the simulation.
