Life simulations, often designed to explore the confluence of chance and agency, present a compelling domain for technical analysis. "The Lottery of Life," as described, encapsulates a system where player choices and stochastic events collectively shape a simulated individual's trajectory across various life dimensions. This article delves into the technical architecture required to implement such a system, focusing on core simulation mechanics, event generation, attribute management, and the computational modeling of choice and consequence.
Core Simulation Architecture
At its foundation, "The Lottery of Life" operates as a discrete-time simulation. The progression of a simulated life is broken down into distinct time steps, or "turns," each representing a period during which events occur, choices are made, and attributes are updated.
The Life Entity Model
The central entity in this simulation is the "Life Entity," representing the player's simulated self. This entity is defined by a comprehensive set of attributes that quantify various aspects of its existence. These attributes are not merely scalar values but often exhibit complex interdependencies and dynamic ranges.
Consider a foundational set of attributes:
- Financial:
money,debt,income,expenses,assets - Health:
physical_health,mental_health,stress_level - Well-being:
happiness,social_connections - Career/Skills:
education_level,job_satisfaction,skill_points - Relationships:
family_status,relationship_quality - Time:
age,turns_remaining
Each attribute typically has a defined range (e.g., 0-100 for health/happiness, arbitrary for money), and may have thresholds that trigger specific events or states (e.g., physical_health < 20 could trigger a "critical illness" state).
A Python class can serve as a suitable representation for the Life Entity:
import uuid
class LifeEntity:
def __init__(self, initial_age=18):
self.entity_id = str(uuid.uuid4())
self.age = initial_age
self.turns_remaining = 100 # Example: simulation ends after 100 turns
# Financial Attributes
self.money = 1000.0
self.debt = 0.0
self.income = 0.0
self.expenses = 100.0 # Base expenses per turn
self.assets = {} # e.g., {'house': 200000}
# Health Attributes (0-100 scale)
self.physical_health = 80
self.mental_health = 75
self.stress_level = 30 # 0-100, higher is worse
# Well-being Attributes (0-100 scale)
self.happiness = 70
self.social_connections = 50
# Career/Skills Attributes
self.education_level = 'High School'
self.job_satisfaction = 60
self.skill_points = 10
# Relationships
self.family_status = 'Single' # Could be 'Single', 'Married', 'Divorced'
self.relationship_quality = 50 # Applicable if in a relationship
# State flags or derived attributes
self.is_employed = False
self.has_illness = False
self.events_taken = [] # Log of past events/choices
def update_age(self, years=1):
self.age += years
self.turns_remaining -= years # Assuming each turn is a year
def apply_financial_update(self):
# Basic income/expense calculation
self.money += self.income - self.expenses
if self.money < 0:
self.debt += abs(self.money) * 0.1 # Accumulate debt interest
self.money = 0
def check_health_thresholds(self):
if self.physical_health < 20 and not self.has_illness:
# Trigger a critical illness event
print("CRITICAL: Physical health is severely low!")
self.has_illness = True # Prevent multiple triggers
def to_dict(self):
return {attr: getattr(self, attr) for attr in dir(self) if not attr.startswith('__') and not callable(getattr(self, attr))}
The Turn-Based Engine
The core loop orchestrates the progression of the simulation. Each turn involves several phases:
- Passive Attribute Updates: Recalculate and apply changes to attributes that accrue over time (e.g., age, base expenses, passive income).
- State-Dependent Adjustments: Adjust attributes based on interdependencies (e.g., high stress negatively impacts mental health).
- Event Generation: Determine and present new events to the player.
- Player Choice: Solicit and process player decisions for available events.
- Outcome Resolution: Apply the consequences of player choices and event outcomes.
- End-of-Turn Checks: Evaluate win/loss conditions, game termination, or trigger scheduled events.
class SimulationEngine:
def __init__(self, life_entity, event_manager):
self.life = life_entity
self.event_manager = event_manager
self.game_over = False
def run_turn(self):
if self.game_over:
print("Simulation ended.")
return
print(f"\n--- Age: {self.life.age} ---")
# Phase 1: Passive Attribute Updates
self.life.update_age()
self.life.apply_financial_update()
# Phase 2: State-Dependent Adjustments
self._apply_interdependencies()
self.life.check_health_thresholds()
# Phase 3 & 4: Event Generation & Player Choice
possible_events = self.event_manager.get_eligible_events(self.life)
if possible_events:
event_to_handle = self.event_manager.select_event(possible_events)
if event_to_handle:
print(f"EVENT: {event_to_handle.description}")
choice = self._get_player_choice(event_to_handle)
if choice:
# Phase 5: Outcome Resolution
self.event_manager.resolve_event(event_to_handle, choice, self.life)
self.life.events_taken.append(event_to_handle.id)
else:
print("No valid choice made for this event.")
else:
print("No significant events this turn.")
# Phase 6: End-of-Turn Checks
if self.life.turns_remaining <= 0 or self.life.physical_health <= 0 or self.life.mental_health <= 0:
self.game_over = True
print("Game Over condition met.")
self._display_current_status()
def _apply_interdependencies(self):
# Example: High stress reduces mental and physical health
if self.life.stress_level > 70:
self.life.mental_health -= 5
self.life.physical_health -= 2
elif self.life.stress_level < 30:
self.life.mental_health += 2
self.life.physical_health += 1
# Example: Low money increases stress
if self.life.money < 500:
self.life.stress_level += 5
elif self.life.money > 5000:
self.life.stress_level -= 2
# Ensure attributes stay within bounds
self.life.physical_health = max(0, min(100, self.life.physical_health))
self.life.mental_health = max(0, min(100, self.life.mental_health))
self.life.stress_level = max(0, min(100, self.life.stress_level))
self.life.happiness = max(0, min(100, self.life.happiness))
def _get_player_choice(self, event):
print("Choices:")
for i, choice in enumerate(event.choices):
print(f" {i+1}. {choice['description']}")
while True:
try:
choice_index = int(input("Enter your choice number: ")) - 1
if 0 <= choice_index < len(event.choices):
return event.choices[choice_index]
else:
print("Invalid choice. Please try again.")
except ValueError:
print("Invalid input. Please enter a number.")
def _display_current_status(self):
print("\n--- Current Status ---")
print(f"Money: ${self.life.money:.2f}, Debt: ${self.life.debt:.2f}")
print(f"Health: P:{self.life.physical_health}, M:{self.life.mental_health}, Stress:{self.life.stress_level}")
print(f"Happiness: {self.life.happiness}, Social: {self.life.social_connections}")
The Event System
The event system is the primary mechanism for introducing variability and interactive elements into the simulation. It encompasses event definition, generation, and outcome resolution.
Event Definition and Structure
An event must encapsulate all necessary information for its presentation and processing. This includes a unique identifier, a descriptive text, a set of available choices, and the specific outcomes associated with each choice. Outcomes are typically attribute modifications, but can also trigger new events, set flags, or modify game rules.
class Event:
def __init__(self, event_id, description, choices, prerequisites=None, type='general'):
self.id = event_id
self.description = description
self.choices = choices # List of dicts, each with 'description' and 'outcomes'
self.prerequisites = prerequisites if prerequisites is not None else {} # e.g., {'min_money': 500, 'min_age': 25}
self.type = type
def check_prerequisites(self, life_entity):
for attr, value in self.prerequisites.items():
current_attr_value = getattr(life_entity, attr, None)
if current_attr_value is None:
return False # Prerequisite attribute not found
# Simple greater than/less than checks
if isinstance(value, dict): # For more complex conditions, e.g., {'min': 500, 'max': 1000}
if 'min' in value and current_attr_value < value['min']: return False
if 'max' in value and current_attr_value > value['max']: return False
elif current_attr_value < value: # Default: current value must be >= prerequisite value
return False
return True
# Example Event
event_template = {
"id": "JOB_OFFER_STARTUP",
"description": "You receive a job offer from a risky startup.",
"choices": [
{
"description": "Accept the offer (high risk, high reward potential)",
"outcomes": [
{"attribute": "money", "change": 500, "chance": 0.7, "on_fail": -200},
{"attribute": "job_satisfaction", "change": 20, "chance": 0.8, "on_fail": -10},
{"attribute": "stress_level", "change": 15},
{"attribute": "is_employed", "set_value": True},
{"event_trigger": "STARTUP_SUCCESS_EVENT", "chance": 0.2},
{"event_trigger": "STARTUP_FAILURE_EVENT", "chance": 0.4}
]
},
{
"description": "Decline the offer (maintain current stability)",
"outcomes": [
{"attribute": "happiness", "change": 5},
{"attribute": "stress_level", "change": -5}
]
}
],
"prerequisites": {
"education_level": "University",
"age": {"min": 22, "max": 35}
}
}
Event Generation and Selection
Events can be generated in various ways:
- Purely Random: Events are drawn uniformly from a pool.
- Weighted Random: Events have different probabilities of being selected, potentially based on
life_entitystate. - State-Dependent: Events are only eligible if specific conditions (prerequisites) on the
life_entityattributes are met. - Sequential/Chained: One event's outcome triggers a subsequent, specific event.
- Scheduled: Events occur at predefined
ageorturnnumbers.
An EventManager class manages the pool of all possible events and determines which ones are eligible and presented to the player.
import random
class EventManager:
def __init__(self, event_data_list):
self.all_events = {event['id']: Event(**event) for event in event_data_list}
self.event_weights = {event['id']: 1 for event in event_data_list} # Default uniform weight
def get_eligible_events(self, life_entity):
eligible = []
for event_id, event in self.all_events.items():
if event.check_prerequisites(life_entity) and event_id not in life_entity.events_taken:
eligible.append(event)
return eligible
def select_event(self, eligible_events, num_to_select=1):
if not eligible_events:
return None
# Simple selection: pick one at random from eligible events
# More complex: weight by type, player's current 'luck' attribute, etc.
return random.choice(eligible_events)
def resolve_event(self, event, chosen_option, life_entity):
for outcome in chosen_option['outcomes']:
if 'attribute' in outcome:
change = outcome['change']
# Handle probabilistic outcomes for attribute changes
if 'chance' in outcome:
if random.random() < outcome['chance']:
setattr(life_entity, outcome['attribute'], getattr(life_entity, outcome['attribute']) + change)
else:
# Apply 'on_fail' if specified and probabilistic outcome fails
if 'on_fail' in outcome:
setattr(life_entity, outcome['attribute'], getattr(life_entity, outcome['attribute']) + outcome['on_fail'])
else:
# Direct change
setattr(life_entity, outcome['attribute'], getattr(life_entity, outcome['attribute']) + change)
if 'set_value' in outcome:
setattr(life_entity, outcome['attribute'], outcome['set_value'])
if 'event_trigger' in outcome:
if 'chance' in outcome:
if random.random() < outcome['chance']:
# This would queue a new event for the next turn or immediate processing
print(f"DEBUG: Triggering chained event: {outcome['event_trigger']}")
else:
print(f"DEBUG: Triggering chained event: {outcome['event_trigger']}")
# Ensure all attributes stay within their defined bounds after changes
life_entity.physical_health = max(0, min(100, life_entity.physical_health))
life_entity.mental_health = max(0, min(100, life_entity.mental_health))
life_entity.stress_level = max(0, min(100, life_entity.stress_level))
life_entity.happiness = max(0, min(100, life_entity.happiness))
life_entity.job_satisfaction = max(0, min(100, life_entity.job_satisfaction))
life_entity.relationship_quality = max(0, min(100, life_entity.relationship_quality))
This EventManager demonstrates a mechanism for applying outcomes. A more sophisticated system might use a dispatcher pattern to handle different outcome types (e.g., AttributeChangeOutcome, EventTriggerOutcome) as distinct objects, promoting extensibility.
Attribute Management and Interdependencies
The realism and strategic depth of the simulation heavily rely on the intricate relationships between attributes. Attributes rarely exist in isolation; a change in one often cascades to others.
Attribute Modeling
Beyond simple numerical values, attributes can possess additional properties:
- Min/Max Bounds: Defines the operational range (e.g., health cannot go below 0 or above 100).
- Decay/Growth Rates: Some attributes might naturally decay (e.g., happiness, physical health) or grow (e.g., age, skill points) over time.
- Severity Tiers: Health might have "minor," "serious," "critical" states, each triggering different effects or event probabilities.
Implementing these can be achieved by adding helper methods to the LifeEntity or by defining a separate Attribute class.
class LifeAttribute:
def __init__(self, name, value, min_val=0, max_val=100, decay_rate=0, affects=None):
self.name = name
self._value = value
self.min_val = min_val
self.max_val = max_val
self.decay_rate = decay_rate # Per turn decay
self.affects = affects if affects is not None else {} # {attribute_name: {'modifier': 0.1, 'threshold': 70}}
@property
def value(self):
return self._value
@value.setter
def value(self, new_value):
self._value = max(self.min_val, min(self.max_val, new_value))
def apply_decay(self):
self.value -= self.decay_rate
def get_impact_on_others(self):
impacts = {}
for affected_attr, rules in self.affects.items():
if 'threshold' in rules and self.value >= rules['threshold']:
impacts[affected_attr] = rules['modifier']
elif 'threshold' not in rules: # Always affects
impacts[affected_attr] = rules['modifier']
return impacts
The LifeEntity would then manage a dictionary of LifeAttribute objects instead of raw numbers. This increases complexity but provides a more robust and configurable system for attribute behavior.
Inter-Attribute Dynamics
The heart of a compelling life simulation lies in its interdependencies. These relationships model how different facets of life influence one another. Examples include:
- Stress and Health: High
stress_leveldirectly depletesphysical_healthandmental_health. - Money and Happiness: Low
moneycan reducehappinessand increasestress_level. Conversely, highmoneymight reducestress_levelbut not necessarily increasehappinessbeyond a certain point. - Education and Career: Higher
education_levelunlocks better jobincomeandjob_satisfactionopportunities. - Relationships and Mental Health: Strong
social_connectionsand positiverelationship_qualitycan bolstermental_healthandhappiness.
These dependencies can be modeled using:
- Direct Modifiers: Simple additive or subtractive changes based on thresholds (as shown in
_apply_interdependencies). - Functions: More complex non-linear relationships, where the impact is calculated using a mathematical function (e.g., a sigmoid function for happiness saturation).
- Conditional Event Triggers: Specific attribute states (e.g.,
money < -1000) might trigger unique "crisis" events.
A dedicated "dependency manager" or integrating these rules into the SimulationEngine's turn logic is crucial. This logic ensures that attribute changes propagate throughout the system consistently.
# Example of a more explicit dependency update (could be moved to LifeEntity or a separate manager)
def update_attribute_dependencies(life_entity):
# Stress impacts health
if life_entity.stress_level > 70:
life_entity.physical_health -= 5 * ((life_entity.stress_level - 70) / 30) # Scaled impact
life_entity.mental_health -= 10 * ((life_entity.stress_level - 70) / 30)
elif life_entity.stress_level < 30:
life_entity.physical_health += 1
life_entity.mental_health += 2
# Financial stress
if life_entity.money < 0:
life_entity.stress_level += 5
life_entity.happiness -= 3
elif life_entity.money > 10000:
life_entity.stress_level -= 2
life_entity.happiness += 1 # Diminishing returns on happiness
# Physical health impacts mental health
if life_entity.physical_health < 40:
life_entity.mental_health -= 5
# Social connections and happiness
if life_entity.social_connections < 20:
life_entity.happiness -= 5
life_entity.mental_health -= 3
elif life_entity.social_connections > 80:
life_entity.happiness += 5
life_entity.mental_health += 3
# Ensure all attributes are clamped to their valid ranges after all updates
life_entity.physical_health = max(0, min(100, life_entity.physical_health))
life_entity.mental_health = max(0, min(100, life_entity.mental_health))
life_entity.stress_level = max(0, min(100, life_entity.stress_level))
life_entity.happiness = max(0, min(100, life_entity.happiness))
life_entity.job_satisfaction = max(0, min(100, life_entity.job_satisfaction))
life_entity.relationship_quality = max(0, min(100, life_entity.relationship_quality))
# Integrate this into SimulationEngine.run_turn()
# In SimulationEngine._apply_interdependencies():
# update_attribute_dependencies(self.life)
Decision Making and Strategic Depth
The "Lottery of Life" is not purely deterministic; it introduces a layer of player agency through choices. The technical design must facilitate meaningful decisions where players weigh risks and rewards, often under uncertainty.
Choice Impact Modeling
Each choice presented to the player results in a set of outcomes. These outcomes can be:
- Deterministic: Always happen as described.
- Probabilistic: Have a chance of happening, potentially with alternative outcomes on failure (
on_fail). - Conditional: Only apply if certain
life_entityconditions are met. - Delayed: Outcomes that manifest after several turns or trigger future events.
The outcomes structure within an Event needs to support these variations. Probabilistic outcomes are crucial for modeling the "lottery" aspect, where even well-intentioned choices might not yield the desired result.
{
"attribute": "money",
"change": 1000,
"chance": 0.6,
"on_fail": -500,
"description_success": "Your investment paid off!",
"description_failure": "The investment tanked."
}
The EventManager.resolve_event method would interpret these parameters to apply changes. A robust outcome system would allow for multiple parallel probabilistic outcomes from a single choice, each resolved independently.
Risk/Reward Balancing
The design of events and choices naturally creates a risk/reward dynamic. Technically, this translates to:
- Variance in Outcomes: High-risk choices might offer large positive attribute changes (
change) with lowchance, or large negativeon_failvalues. Low-risk choices would have smaller changes but higher certainties. - Attribute Cost/Benefit: Choices often require a trade-off (e.g., spending
moneyforhappiness, sacrificinghealthformoney). - Hidden Information: The game might deliberately obscure exact probabilities or future consequences, simulating real-world uncertainty. This could be achieved by only displaying qualitative descriptions (e.g., "potentially lucrative, but risky") rather than exact percentages.
Long-Term Strategy vs. Short-Term Gains
A well-designed simulation encourages diverse player strategies. This requires:
- Delayed Consequences: Some choices should not have immediate, obvious impacts but rather set up conditions for future events or subtly shift attribute trajectories over many turns. For instance,
education_levelorskill_pointsinvestments might not pay off for several years. - Resource Management: Players must balance immediate needs (e.g.,
moneyforexpenses) against long-term goals (e.g., saving forassets, improvinghealth). - Emergent Narratives: The interplay of player choices and random events should ideally create unique "life stories" rather than a linear path. This implies a large, diverse event pool and flexible event chaining.
Technical Implementation Considerations
Building such a simulation involves practical considerations for data management, randomness, and maintainability.
Data Structures for Events and Attributes
Using data-driven design is paramount for manageability. Events, choices, and their outcomes should be defined in external files (e.g., JSON, YAML) rather than hardcoded. This allows for easy expansion, balancing, and modding.
# events.yaml
- id: "INVEST_STOCK_MARKET"
description: "You have some spare capital. Consider investing in the stock market."
prerequisites:
money: {"min": 500}
choices:
- description: "Invest a significant amount (high risk)"
outcomes:
- attribute: "money"
change: 2000
chance: 0.3
on_fail: -1000
- attribute: "stress_level"
change: 10
- description: "Invest a small amount (moderate risk)"
outcomes:
- attribute: "money"
change: 300
chance: 0.6
on_fail: -100
- attribute: "stress_level"
change: 5
- description: "Don't invest"
outcomes:
- attribute: "happiness"
change: 2
This YAML data can be loaded and
Originally published in Spanish at www.mgatc.com/blog/the-lottery-of-life/
Top comments (0)