| name | ecosystem-simulation |
| description | Predator-prey dynamics, population control, stable ecosystem mechanics |
Ecosystem Simulation
Description
Master predator-prey dynamics, food chains, population control, resource cycling, and extinction prevention. Implement stable ecosystems using Lotka-Volterra equations, carrying capacity, logistic growth, and agent-based models. Balance realism with gameplay, prevent ecosystem collapse, and create engaging survival game mechanics where populations oscillate naturally without extinction or runaway growth.
When to Use This Skill
Use this skill when implementing or debugging:
- Survival games with hunting/gathering (deer, wolves, fish, birds)
- Farming simulations with crop growth and pests
- Resource management games (forests, ore regeneration)
- Dynamic wildlife systems (animal populations change over time)
- Food chain mechanics (herbivores, carnivores, apex predators)
- Environmental storytelling (ecosystem collapse as narrative device)
Do NOT use this skill for:
- Static spawning (enemies appear at fixed rate regardless of population)
- Simple resource regeneration (trees respawn on timer)
- Single-species systems (just zombies, no food chain)
- Non-interactive wildlife (decorative animals that don't eat/reproduce)
Quick Start (Time-Constrained Implementation)
If you need a working ecosystem quickly (< 4 hours), follow this priority order:
CRITICAL (Never Skip):
- Use Lotka-Volterra differential equations for predator-prey dynamics
- Implement carrying capacity for all species (grass, prey, predators)
- Add extinction prevention (auto-spawn if population < threshold)
- Test stability for 10+ minutes of game time
IMPORTANT (Strongly Recommended): 5. Use discrete time steps (1 tick = 1 second or 1 game hour) 6. Add reproduction delays (gestation period prevents instant births) 7. Implement energy budgets (eating provides energy, reproducing costs energy) 8. Debug visualization (plot population graphs over time)
CAN DEFER (Optimize Later):
- Agent-based simulation (start with equation-based)
- Spatial distribution (animals move around map)
- Seasonal effects (winter slows growth)
- Disease/starvation mechanics
Example - Stable Ecosystem in 30 Minutes:
import math
# Lotka-Volterra parameters (TUNED for stability)
PREY_GROWTH_RATE = 0.1 # α: Prey reproduction rate
PREDATION_RATE = 0.002 # β: Predator efficiency
PREDATOR_GAIN = 0.001 # δ: Predator reproduction from prey
PREDATOR_DEATH_RATE = 0.05 # γ: Predator death rate
# Carrying capacities (CRITICAL for stability)
GRASS_CAPACITY = 10000
PREY_CAPACITY = 500
PREDATOR_CAPACITY = 100
# Extinction prevention (CRITICAL)
MIN_PREY = 5
MIN_PREDATORS = 2
def simulate_ecosystem(dt=0.1):
"""
Lotka-Volterra with carrying capacity and extinction prevention
dt: Time step (smaller = more stable, but slower)
"""
grass = 5000
prey = 100
predators = 20
for tick in range(1000): # 100 seconds of game time
# Logistic growth for grass (carrying capacity)
grass_growth = 50 * (1 - grass / GRASS_CAPACITY)
grass += grass_growth * dt
grass = max(0, min(grass, GRASS_CAPACITY))
# Prey dynamics (Lotka-Volterra with carrying capacity)
prey_birth = PREY_GROWTH_RATE * prey * (1 - prey / PREY_CAPACITY)
prey_death = PREDATION_RATE * prey * predators
prey += (prey_birth - prey_death) * dt
# Predator dynamics (Lotka-Volterra)
predator_birth = PREDATOR_GAIN * prey * predators
predator_death = PREDATOR_DEATH_RATE * predators
predators += (predator_birth - predator_death) * dt
# Extinction prevention (CRITICAL)
if prey < MIN_PREY:
prey = MIN_PREY
if predators < MIN_PREDATORS:
predators = MIN_PREDATORS
# Cap populations
prey = min(prey, PREY_CAPACITY)
predators = min(predators, PREDATOR_CAPACITY)
if tick % 100 == 0:
print(f"Tick {tick}: Grass={grass:.0f}, Prey={prey:.0f}, Predators={predators:.0f}")
return grass, prey, predators
# Run simulation
simulate_ecosystem()
This gives you:
- Stable populations that oscillate naturally
- No extinction (prevention kicks in)
- No runaway growth (carrying capacity limits)
- Tunable parameters (adjust α, β, δ, γ for different dynamics)
Output Example:
Tick 0: Grass=5000, Prey=100, Predators=20
Tick 100: Grass=7200, Prey=85, Predators=22
Tick 200: Grass=6800, Prey=95, Predators=18
Tick 300: Grass=7100, Prey=90, Predators=20
... (continues stably)
Core Concepts
1. Lotka-Volterra Equations (Foundation)
What: Mathematical model of predator-prey dynamics discovered in 1920s. Describes how populations naturally oscillate.
The Equations:
Prey growth: dP/dt = αP - βPQ
- αP: Prey births (proportional to prey population)
- βPQ: Prey deaths (proportional to prey × predators)
Predator growth: dQ/dt = δβPQ - γQ
- δβPQ: Predator births (from eating prey)
- γQ: Predator deaths (natural mortality)
Parameters:
- α (alpha): Prey birth rate (e.g., 0.1 = 10% growth per time unit)
- β (beta): Predation efficiency (how often predator catches prey)
- δ (delta): Conversion efficiency (prey eaten → predator births)
- γ (gamma): Predator death rate (starvation, old age)
Python Implementation:
def lotka_volterra_step(prey, predators, dt=0.1):
"""
One step of Lotka-Volterra simulation
Returns new (prey, predators) populations
"""
# Parameters (THESE NEED TUNING)
alpha = 0.1 # Prey growth rate
beta = 0.002 # Predation rate
delta = 0.001 # Predator efficiency
gamma = 0.05 # Predator death rate
# Calculate changes
prey_change = alpha * prey - beta * prey * predators
predator_change = delta * beta * prey * predators - gamma * predators
# Apply changes
prey += prey_change * dt
predators += predator_change * dt
# Prevent negative populations
prey = max(0, prey)
predators = max(0, predators)
return prey, predators
# Example usage
prey, predators = 100, 20
for _ in range(1000):
prey, predators = lotka_volterra_step(prey, predators)
Key Insight: Pure Lotka-Volterra creates perpetual oscillations (not damped). Populations cycle forever: more prey → more predators → fewer prey → fewer predators → repeat.
Problem with Pure L-V: In games, this creates:
- Wild swings (10 deer → 200 deer → 5 deer → ...)
- Possible extinction (if swing goes to 0)
- No equilibrium (never settles)
Solution: Add carrying capacity (see next section).
2. Carrying Capacity (Prevents Runaway Growth)
What: Maximum population an environment can support. Limits exponential growth.
Why Essential:
- Pure Lotka-Volterra allows infinite prey growth when predators are low
- Real ecosystems have resource limits (food, space, water)
- Prevents 10,000 deer spawning and crashing your game
Logistic Growth Formula:
dP/dt = rP(1 - P/K)
- r: Intrinsic growth rate
- P: Current population
- K: Carrying capacity
- (1 - P/K): Slows growth as P approaches K
Behavior:
- When P << K: Growth ≈ rP (exponential)
- When P ≈ K: Growth ≈ 0 (stabilizes)
- When P > K: Growth < 0 (population decreases)
Implementation:
def logistic_growth(population, growth_rate, carrying_capacity, dt=1.0):
"""
Logistic growth with carrying capacity
"""
growth = growth_rate * population * (1 - population / carrying_capacity)
population += growth * dt
return max(0, min(population, carrying_capacity))
# Example: Grass growth
grass = 1000
GRASS_GROWTH_RATE = 50 # units per time
GRASS_CAPACITY = 10000
for tick in range(100):
grass = logistic_growth(grass, GRASS_GROWTH_RATE / GRASS_CAPACITY,
GRASS_CAPACITY, dt=1.0)
print(f"Tick {tick}: Grass = {grass:.0f}")
Carrying Capacity for Multi-Tier Food Chain:
# Grass: Environmental carrying capacity
GRASS_CAPACITY = 10000
# Herbivores: Limited by grass
# Rule of thumb: 1 deer needs 100 grass
DEER_CAPACITY = GRASS_CAPACITY / 100 # = 100 deer max
# Carnivores: Limited by herbivores
# Rule of thumb: 1 wolf needs 5 deer
WOLF_CAPACITY = DEER_CAPACITY / 5 # = 20 wolves max
Tuning Carrying Capacity:
- Start with high values (avoid constraints)
- Observe maximum populations that naturally occur
- Set capacity 20-30% above observed max
- Adjust if populations hit ceiling too often
3. Energy Budgets (Realistic Resource Flow)
What: Track energy/hunger for each animal. Eating provides energy, actions consume it.
Why: Prevents unrealistic reproduction (can't reproduce if starving).
Energy Flow Model:
class Animal:
def __init__(self):
self.energy = 100 # Max energy
self.reproduction_threshold = 80 # Need 80+ energy to reproduce
self.starvation_threshold = 10 # Die if < 10 energy
def eat(self, food_energy):
"""Eating provides energy"""
self.energy = min(100, self.energy + food_energy)
def tick(self, dt):
"""Each tick consumes energy"""
self.energy -= 5 * dt # Metabolism
if self.energy < self.starvation_threshold:
return "starve" # Animal dies
return "alive"
def can_reproduce(self):
"""Only reproduce if well-fed"""
return self.energy >= self.reproduction_threshold
def reproduce(self):
"""Reproduction costs energy"""
if self.can_reproduce():
self.energy -= 30 # Cost of birth
return True
return False
Energy Values (Rule of Thumb):
- Grass → Deer: 1 grass = 5 energy (inefficient conversion)
- Deer → Wolf: 1 deer = 50 energy (meat is energy-dense)
- Trophic efficiency: Typically 10% (only 10% of energy passes up food chain)
Example with Energy:
class Deer:
def __init__(self):
self.energy = 50
self.age = 0
def eat_grass(self, grass_amount):
"""Deer eats grass, gains energy"""
energy_gained = grass_amount * 5
self.energy = min(100, self.energy + energy_gained)
return grass_amount # Grass consumed
def try_reproduce(self):
"""Reproduce if energy > 80"""
if self.energy >= 80:
self.energy -= 30
return Deer() # New baby deer
return None
def tick(self, dt):
"""Daily energy consumption"""
self.energy -= 10 * dt
self.age += dt
if self.energy <= 0:
return "dead"
return "alive"
class Wolf:
def __init__(self):
self.energy = 70
def eat_deer(self, deer):
"""Wolf eats deer, gains energy"""
self.energy = min(100, self.energy + 50)
return True # Deer is eaten
def try_reproduce(self):
if self.energy >= 85:
self.energy -= 40
return Wolf()
return None
def tick(self, dt):
self.energy -= 8 * dt # Wolves burn energy faster
if self.energy <= 0:
return "dead"
return "alive"
Key Insight: Energy budgets create natural regulation:
- Low food → animals don't reproduce → population declines
- High food → animals reproduce → population grows
- No manual population caps needed (emerges from energy)
4. Agent-Based vs Equation-Based Models
Two Approaches:
Equation-Based (Fast, Smooth)
- Treat populations as continuous numbers (100.5 deer)
- Use differential equations (Lotka-Volterra)
- Update all at once (no individual tracking)
Pros:
- Very fast (O(1) per species, not O(N) per animal)
- Smooth behavior (no randomness)
- Easy to tune (adjust α, β, δ, γ parameters)
- Predictable (same starting conditions → same result)
Cons:
- Can't have individual differences (all deer identical)
- No spatial distribution (can't hunt specific deer)
- Less engaging for player (numbers, not animals)
- Fractional animals (23.7 deer?)
When to Use: Large populations (100+ animals), background ecosystem, performance-critical.
Agent-Based (Detailed, Spatial)
- Each animal is an object with position, energy, age
- Animals move, hunt, eat specific food
- Emergent behavior from individual rules
Pros:
- Player can interact with individuals (hunt specific deer)
- Spatial distribution (animals in different areas)
- More realistic (animals have personalities, ages)
- Visually engaging (see animals move)
Cons:
- Slower (O(N) per animal, can be O(N²) for interactions)
- More random (same start → different results)
- Harder to tune (many emergent behaviors)
- Requires spatial partitioning (quadtree, grid) for performance
When to Use: Player-visible animals (< 100), hunting mechanics, spatial gameplay.
Hybrid Approach (Recommended):
# Close to player: Agent-based (detailed)
for deer in visible_deer:
deer.move()
deer.seek_grass()
deer.avoid_wolves()
# Far from player: Equation-based (fast)
distant_deer_population += GROWTH_RATE * distant_deer_population * dt
Example Threshold:
- Within 100m of player: Agent-based (full simulation)
- 100-500m from player: Simplified agents (less frequent updates)
- Beyond 500m: Equation-based (just population numbers)
5. Time Steps and Stability
Critical: Time step size (dt) affects simulation stability.
Euler's Method (Simple but Unstable):
# Large time step (dt = 1.0)
prey += (alpha * prey - beta * prey * predators) * 1.0
predators += (delta * beta * prey * predators - gamma * predators) * 1.0
Problem: If changes are large relative to populations, can overshoot:
- Prey = 10, predators = 50 → prey change = -100 → prey = -90 (negative!)
Solution 1: Small Time Steps
# Smaller dt = more stable (but more iterations needed)
dt = 0.01 # Instead of 1.0
for _ in range(100): # 100 steps to equal 1.0 time unit
prey += (alpha * prey - beta * prey * predators) * dt
predators += (delta * beta * prey * predators - gamma * predators) * dt
Solution 2: Runge-Kutta 4th Order (RK4) - More Accurate
def rk4_step(prey, predators, dt):
"""
Runge-Kutta 4th order integration (much more stable)
"""
def derivatives(p, q):
dp = alpha * p - beta * p * q
dq = delta * beta * p * q - gamma * q
return dp, dq
# Calculate slopes
k1_p, k1_q = derivatives(prey, predators)
k2_p, k2_q = derivatives(prey + 0.5 * dt * k1_p, predators + 0.5 * dt * k1_q)
k3_p, k3_q = derivatives(prey + 0.5 * dt * k2_p, predators + 0.5 * dt * k2_q)
k4_p, k4_q = derivatives(prey + dt * k3_p, predators + dt * k3_q)
# Weighted average
prey += (dt / 6.0) * (k1_p + 2*k2_p + 2*k3_p + k4_p)
predators += (dt / 6.0) * (k1_q + 2*k2_q + 2*k3_q + k4_q)
return prey, predators
Rule of Thumb for Time Step:
- dt = 0.01 to 0.1: Safe for most simulations
- dt = 1.0: Only if changes are small relative to populations
- Adaptive dt: Reduce dt when changes are large, increase when stable
Testing Stability:
def test_stability(dt):
prey, predators = 100, 20
for _ in range(10000):
prey, predators = lotka_volterra_step(prey, predators, dt)
if prey < 0 or predators < 0:
return False # Unstable!
if math.isnan(prey) or math.isnan(predators):
return False # Exploded!
return True # Stable
# Test different dt values
for dt in [1.0, 0.1, 0.01, 0.001]:
stable = test_stability(dt)
print(f"dt={dt}: {'STABLE' if stable else 'UNSTABLE'}")
Decision Frameworks
Framework 1: Full Simulation vs Simplified Model
Question: How realistic should the ecosystem be?
Factors:
- Player interaction: Can player hunt individual animals?
- Performance budget: How many animals can you simulate?
- Gameplay importance: Is ecosystem core mechanic or background?
- Development time: Weeks or months available?
Decision Tree:
Q: Does player hunt/interact with individual animals?
├─ YES: Use agent-based (need spatial, individual tracking)
│ └─ Q: More than 100 animals?
│ ├─ YES: Hybrid (agents near player, equations far away)
│ └─ NO: Full agent-based
│
└─ NO: Use equation-based (just track population numbers)
└─ Q: Need different biomes/regions?
├─ YES: Multiple equation sets (one per biome)
└─ NO: Single global equation
Examples:
| Game Mechanic | Approach | Why |
|---|---|---|
| Background wildlife (Far Cry) | Equation-based | 100+ animals, player rarely interacts |
| Hunting game (The Hunter) | Agent-based | Track specific deer, spatial stalking |
| City builder (SimCity) | Equation-based | Abstract "population", not individuals |
| Survival game (Don't Starve) | Hybrid | Visible animals = agents, distant = numbers |
| Ecosystem collapse narrative | Equation-based | Just need population graphs declining |
Complexity Thresholds:
| Features | Implementation Time | Approach |
|---|---|---|
| Just population numbers | 2-4 hours | Equation-based |
| + Individual tracking | 1-2 days | Agent-based (simple) |
| + Spatial distribution | 3-5 days | Agent-based + quadtree |
| + Complex AI (herding) | 1-2 weeks | Agent-based + steering behaviors |
| + Genetics/evolution | 2-4 weeks | Agent-based + genetic system |
Framework 2: Deterministic vs Stochastic Populations
Question: Should populations have randomness?
Deterministic (No Randomness):
# Always produces same result with same starting conditions
prey += alpha * prey - beta * prey * predators
Pros:
- Predictable: Same start → same result
- Tunable: Easy to find stable parameters
- Reproducible: Can debug exact sequence
- Smooth: No sudden jumps
Cons:
- Boring: Feels mechanical
- Exploitable: Players learn exact patterns
- Unrealistic: Real populations have variance
Stochastic (With Randomness):
# Random variance in births/deaths
prey_births = poisson_random(alpha * prey) # Poisson distribution
prey_deaths = binomial_random(beta * prey * predators) # Binomial
prey += prey_births - prey_deaths
Pros:
- Engaging: Each playthrough different
- Realistic: Matches real population variance
- Unpredictable: Players can't exploit
- Natural: Feels organic
Cons:
- Harder to tune: Parameters vary by random seed
- Can cause extinction: Bad RNG → population dies
- Less smooth: Populations jump around
- Harder to debug: Can't reproduce exact bug
Decision Guide:
| Game Type | Approach | Randomness Amount |
|---|---|---|
| Puzzle game (requires predictability) | Deterministic | 0% |
| Strategy game (needs planning) | Mostly deterministic | 5-10% variance |
| Survival game (replayability) | Balanced stochastic | 20-30% variance |
| Roguelike (each run unique) | Highly stochastic | 40-50% variance |
Balanced Approach (Recommended):
def balanced_reproduction(base_births, variance=0.2):
"""
Deterministic core with controlled randomness
variance: 0.2 = ±20% random variation
"""
random_factor = random.uniform(1 - variance, 1 + variance)
return base_births * random_factor
# Example
base_deer_births = alpha * deer_population
actual_births = balanced_reproduction(base_deer_births, variance=0.15)
deer_population += actual_births
Red Flag: Variance > 50% creates chaotic, untunable systems.
Framework 3: When to Intervene (Preventing Collapse)
Question: Should you let ecosystems collapse naturally or intervene?
Philosophy:
1. Simulation Purist (Let Nature Run Its Course)
- No intervention: If all deer die, they die
- Teaches player consequences: Overhunting → extinction
- Narrative potential: Ecosystem collapse as story beat
- Risk: Permanent ecosystem failure, unwinnable state
2. Gameplay Pragmatist (Prevent Unfun Outcomes)
- Auto-balance: Respawn animals if population too low
- Soft boundaries: Migration brings new animals
- Invisible hand: Adjust parameters dynamically
- Risk: Feels artificial, reduces player agency
Decision Framework:
Q: Is ecosystem core gameplay mechanic?
├─ YES (survival game, ecosystem manager)
│ └─ Q: Should player failure end game?
│ ├─ YES: Allow collapse (but warn player!)
│ │ - Show "Deer population critical!" warnings
│ │ - Provide recovery mechanisms (reintroduction)
│ │ - Make collapse recoverable (not instant death)
│ │
│ └─ NO: Soft intervention
│ - Auto-spawn if population < 5 (migration)
│ - Slow recovery (not instant fix)
│ - Player notices but it's not jarring
│
└─ NO (background wildlife)
└─ Always intervene (prevent collapse)
- Player shouldn't notice ecosystem management
- Just ensure world feels alive
Intervention Techniques:
Technique 1: Extinction Prevention (Invisible)
MIN_POPULATION = 5
if deer < MIN_POPULATION:
deer = MIN_POPULATION # Instant fix
# Justification: "Migration from neighboring territory"
Pros: Simple, effective, invisible Cons: Can feel artificial if player notices
Technique 2: Slow Recovery (Visible)
MIN_POPULATION = 5
RECOVERY_RATE = 1.0 # 1 animal per time unit
if deer < MIN_POPULATION:
deer += RECOVERY_RATE * dt
show_notification("Deer migrating into area")
Pros: Feels natural, player sees recovery Cons: Slower, player might notice pattern
Technique 3: Dynamic Parameter Tuning
# Adjust predation rate based on prey population
if deer < 50:
# Reduce predation when prey is low
effective_beta = beta * 0.5
else:
effective_beta = beta
predation = effective_beta * prey * predators
Pros: Soft, invisible, maintains balance Cons: Hard to tune, can feel arbitrary
Technique 4: Reintroduction (Player Action)
# Player can manually reintroduce species
if deer == 0:
show_quest("Wildlife Crisis: Reintroduce Deer")
# Player must travel to neighboring area, bring back breeding pair
if player_completes_quest():
deer = 10 # Player action, feels earned
Pros: Player agency, narrative potential Cons: Requires quest system, can interrupt gameplay
Red Flags for Intervention:
- ❌ Instant population resets (deer: 0 → 100 in one tick) - Jarring
- ❌ Obvious patterns (deer always spawn at exactly 5) - Exploitable
- ❌ No player feedback (population mysteriously stable) - Confusing
- ✅ Gradual recovery (deer: 5 → 10 → 20 over 5 minutes) - Natural
- ✅ Contextual (migration event, seasonal breeding) - Believable
Implementation Patterns
Pattern 1: Lotka-Volterra with Carrying Capacity (Stable Ecosystem)
Complete, production-ready implementation:
import math
import matplotlib.pyplot as plt # For visualization
class EcosystemSimulation:
def __init__(self):
# Lotka-Volterra parameters (TUNED for stability)
self.prey_growth_rate = 0.1 # α: Prey birth rate
self.predation_rate = 0.002 # β: Predation efficiency
self.predator_gain = 0.001 # δ: Predator birth from prey
self.predator_death_rate = 0.05 # γ: Predator death rate
# Carrying capacities
self.grass_capacity = 10000
self.prey_capacity = 500
self.predator_capacity = 100
# Extinction prevention
self.min_prey = 5
self.min_predators = 2
# Initial populations
self.grass = 5000
self.prey = 100
self.predators = 20
# History for plotting
self.history = {
'time': [],
'grass': [],
'prey': [],
'predators': []
}
def step(self, dt=0.1):
"""
One simulation step using Lotka-Volterra with modifications
dt: Time step size (0.1 recommended for stability)
"""
# 1. Grass growth (logistic growth with carrying capacity)
grass_growth = 50 * (1 - self.grass / self.grass_capacity)
self.grass += grass_growth * dt
self.grass = max(0, min(self.grass, self.grass_capacity))
# 2. Prey consumption of grass (limits prey growth)
grass_eaten = min(self.grass, self.prey * 10 * dt)
self.grass -= grass_eaten
prey_fed_ratio = grass_eaten / (self.prey * 10 * dt) if self.prey > 0 else 0
# 3. Prey dynamics (Lotka-Volterra with carrying capacity)
prey_birth = self.prey_growth_rate * self.prey * (1 - self.prey / self.prey_capacity) * prey_fed_ratio
prey_death = self.predation_rate * self.prey * self.predators
self.prey += (prey_birth - prey_death) * dt
# 4. Predator dynamics (Lotka-Volterra)
predator_birth = self.predator_gain * self.prey * self.predators
predator_death = self.predator_death_rate * self.predators
self.predators += (predator_birth - predator_death) * dt
# 5. Extinction prevention (soft boundaries)
if self.prey < self.min_prey:
self.prey += (self.min_prey - self.prey) * 0.1 * dt # Gradual recovery
if self.predators < self.min_predators:
self.predators += (self.min_predators - self.predators) * 0.1 * dt
# 6. Cap populations at carrying capacity
self.prey = min(self.prey, self.prey_capacity)
self.predators = min(self.predators, self.predator_capacity)
# Ensure non-negative
self.grass = max(0, self.grass)
self.prey = max(0, self.prey)
self.predators = max(0, self.predators)
def run(self, duration=100, dt=0.1):
"""
Run simulation for specified duration
duration: Total game time to simulate
dt: Time step size
"""
time = 0
while time < duration:
self.step(dt)
# Record history
self.history['time'].append(time)
self.history['grass'].append(self.grass)
self.history['prey'].append(self.prey)
self.history['predators'].append(self.predators)
time += dt
def plot(self):
"""Visualize population dynamics"""
plt.figure(figsize=(12, 6))
plt.plot(self.history['time'], self.history['grass'], label='Grass', alpha=0.7)
plt.plot(self.history['time'], self.history['prey'], label='Prey (Deer)', alpha=0.7)
plt.plot(self.history['time'], self.history['predators'], label='Predators (Wolves)', alpha=0.7)
plt.xlabel('Time')
plt.ylabel('Population')
plt.title('Ecosystem Population Dynamics')
plt.legend()
plt.grid(True, alpha=0.3)
plt.show()
def check_stability(self):
"""
Analyze last 100 samples for stability
Returns True if ecosystem is stable (small oscillations)
"""
if len(self.history['time']) < 100:
return False
recent_prey = self.history['prey'][-100:]
recent_predators = self.history['predators'][-100:]
prey_variance = max(recent_prey) - min(recent_prey)
predator_variance = max(recent_predators) - min(recent_predators)
# Stable if variance is < 30% of mean
prey_mean = sum(recent_prey) / len(recent_prey)
predator_mean = sum(recent_predators) / len(recent_predators)
prey_stable = prey_variance < 0.3 * prey_mean
predator_stable = predator_variance < 0.3 * predator_mean
return prey_stable and predator_stable
# Usage
sim = EcosystemSimulation()
sim.run(duration=100, dt=0.1)
print(f"Final populations:")
print(f" Grass: {sim.grass:.0f}")
print(f" Prey: {sim.prey:.0f}")
print(f" Predators: {sim.predators:.0f}")
print(f"Ecosystem stable: {sim.check_stability()}")
# Visualize (requires matplotlib)
# sim.plot()
Key Features:
- ✅ Lotka-Volterra foundation (natural oscillations)
- ✅ Carrying capacity (prevents runaway growth)
- ✅ Grass depletion (prey can't grow infinitely)
- ✅ Extinction prevention (gradual recovery)
- ✅ Stability analysis (check if tuned correctly)
- ✅ Visualization (debug population dynamics)
Tuning Parameters:
- Run simulation for 100 time units
- Plot populations (use
sim.plot()) - If oscillations too wild: Reduce
prey_growth_rateor increasepredation_rate - If predators die out: Increase
predator_gainor reducepredator_death_rate - If prey die out: Reduce
predation_rateor increaseprey_growth_rate - Target: Oscillations of ±20% around equilibrium
Pattern 2: Agent-Based Simulation (Spatial Ecosystem)
When: Player hunts individual animals, need spatial distribution.
import random
import math
class Animal:
def __init__(self, x, y, species):
self.x = x
self.y = y
self.species = species
self.energy = 100
self.age = 0
self.alive = True
def distance_to(self, other):
"""Calculate distance to another entity"""
return math.sqrt((self.x - other.x)**2 + (self.y - other.y)**2)
def move_toward(self, target_x, target_y, speed):
"""Move toward target position"""
dx = target_x - self.x
dy = target_y - self.y
dist = math.sqrt(dx**2 + dy**2)
if dist > 0:
self.x += (dx / dist) * speed
self.y += (dy / dist) * speed
def move_away(self, target_x, target_y, speed):
"""Move away from target position"""
dx = self.x - target_x
dy = self.y - target_y
dist = math.sqrt(dx**2 + dy**2)
if dist > 0:
self.x += (dx / dist) * speed
self.y += (dy / dist) * speed
def random_wander(self, speed):
"""Random movement"""
angle = random.uniform(0, 2 * math.pi)
self.x += math.cos(angle) * speed
self.y += math.sin(angle) * speed
class Grass:
def __init__(self, x, y):
self.x = x
self.y = y
self.amount = 100 # Grass density
class Deer(Animal):
def __init__(self, x, y):
super().__init__(x, y, "deer")
self.energy = 50
self.reproduction_cooldown = 0
def update(self, dt, grass_patches, wolves):
"""
Deer behavior:
1. Flee from nearby wolves
2. Seek nearby grass
3. Wander if nothing to do
"""
self.age += dt
self.energy -= 3 * dt # Metabolism
self.reproduction_cooldown = max(0, self.reproduction_cooldown - dt)
# Check if starving
if self.energy <= 0:
self.alive = False
return None
# 1. Flee from wolves (highest priority)
nearest_wolf = None
min_wolf_dist = float('inf')
for wolf in wolves:
if not wolf.alive:
continue
dist = self.distance_to(wolf)
if dist < 30 and dist < min_wolf_dist:
min_wolf_dist = dist
nearest_wolf = wolf
if nearest_wolf:
# Flee!
self.move_away(nearest_wolf.x, nearest_wolf.y, speed=5 * dt)
return None
# 2. Seek grass (if hungry)
if self.energy < 80:
nearest_grass = None
min_grass_dist = float('inf')
for grass in grass_patches:
if grass.amount < 10:
continue # Skip depleted grass
dist = self.distance_to(grass)
if dist < min_grass_dist:
min_grass_dist = dist
nearest_grass = grass
if nearest_grass:
if min_grass_dist < 2:
# Eat grass
eaten = min(20, nearest_grass.amount)
nearest_grass.amount -= eaten
self.energy = min(100, self.energy + eaten * 0.5)
else:
# Move toward grass
self.move_toward(nearest_grass.x, nearest_grass.y, speed=3 * dt)
return None
# 3. Reproduce (if well-fed and cooldown expired)
if self.energy > 80 and self.reproduction_cooldown == 0:
self.energy -= 30
self.reproduction_cooldown = 20 # 20 time units between births
# Create baby deer nearby
baby = Deer(
self.x + random.uniform(-2, 2),
self.y + random.uniform(-2, 2)
)
return baby
# 4. Wander
self.random_wander(speed=2 * dt)
return None
class Wolf(Animal):
def __init__(self, x, y):
super().__init__(x, y, "wolf")
self.energy = 70
self.reproduction_cooldown = 0
def update(self, dt, deer_list):
"""
Wolf behavior:
1. Hunt nearby deer
2. Wander if no prey
"""
self.age += dt
self.energy -= 5 * dt # Wolves burn more energy
self.reproduction_cooldown = max(0, self.reproduction_cooldown - dt)
# Check if starving
if self.energy <= 0:
self.alive = False
return None
# 1. Hunt deer
nearest_deer = None
min_deer_dist = float('inf')
for deer in deer_list:
if not deer.alive:
continue
dist = self.distance_to(deer)
if dist < 50 and dist < min_deer_dist:
min_deer_dist = dist
nearest_deer = deer
if nearest_deer:
if min_deer_dist < 2:
# Catch deer!
nearest_deer.alive = False
self.energy = min(100, self.energy + 50)
# Reproduce if well-fed
if self.energy > 85 and self.reproduction_cooldown == 0:
self.energy -= 40
self.reproduction_cooldown = 30
baby = Wolf(
self.x + random.uniform(-2, 2),
self.y + random.uniform(-2, 2)
)
return baby
else:
# Chase deer
self.move_toward(nearest_deer.x, nearest_deer.y, speed=4 * dt)
return None
# 2. Wander
self.random_wander(speed=3 * dt)
return None
class AgentBasedEcosystem:
def __init__(self, world_size=100):
self.world_size = world_size
self.grass_patches = []
self.deer = []
self.wolves = []
# Initialize grass patches (grid)
for x in range(0, world_size, 10):
for y in range(0, world_size, 10):
self.grass_patches.append(Grass(x, y))
# Initialize deer
for _ in range(50):
self.deer.append(Deer(
random.uniform(0, world_size),
random.uniform(0, world_size)
))
# Initialize wolves
for _ in range(10):
self.wolves.append(Wolf(
random.uniform(0, world_size),
random.uniform(0, world_size)
))
def step(self, dt=0.1):
"""One simulation step"""
# 1. Grass regrowth
for grass in self.grass_patches:
grass.amount = min(100, grass.amount + 5 * dt)
# 2. Update deer
new_deer = []
for deer in self.deer:
if not deer.alive:
continue
baby = deer.update(dt, self.grass_patches, self.wolves)
if baby:
new_deer.append(baby)
# Remove dead deer
self.deer = [d for d in self.deer if d.alive]
self.deer.extend(new_deer)
# 3. Update wolves
new_wolves = []
for wolf in self.wolves:
if not wolf.alive:
continue
baby = wolf.update(dt, self.deer)
if baby:
new_wolves.append(baby)
# Remove dead wolves
self.wolves = [w for w in self.wolves if w.alive]
self.wolves.extend(new_wolves)
# 4. Extinction prevention
if len(self.deer) < 5:
# Spawn deer at random locations
for _ in range(5 - len(self.deer)):
self.deer.append(Deer(
random.uniform(0, self.world_size),
random.uniform(0, self.world_size)
))
if len(self.wolves) < 2:
for _ in range(2 - len(self.wolves)):
self.wolves.append(Wolf(
random.uniform(0, self.world_size),
random.uniform(0, self.world_size)
))
def run(self, steps=1000, dt=0.1):
"""Run simulation"""
for i in range(steps):
self.step(dt)
if i % 100 == 0:
print(f"Step {i}: Deer={len(self.deer)}, Wolves={len(self.wolves)}")
# Usage
ecosystem = AgentBasedEcosystem(world_size=100)
ecosystem.run(steps=1000, dt=0.1)
Key Features:
- ✅ Individual animals with position, energy, behavior
- ✅ Spatial interactions (deer flee from nearby wolves)
- ✅ Emergent herding (deer near grass, wolves chase deer)
- ✅ Reproduction with cooldowns (prevents explosions)
- ✅ Starvation (animals die if energy depletes)
- ✅ Extinction prevention (respawn if too few)
Performance Optimization: For > 100 animals, add spatial partitioning:
class SpatialGrid:
def __init__(self, world_size, cell_size=10):
self.cell_size = cell_size
self.cells = {}
def add(self, animal):
cell_x = int(animal.x / self.cell_size)
cell_y = int(animal.y / self.cell_size)
key = (cell_x, cell_y)
if key not in self.cells:
self.cells[key] = []
self.cells[key].append(animal)
def get_nearby(self, x, y, radius):
"""Get animals within radius of (x, y)"""
nearby = []
cell_x = int(x / self.cell_size)
cell_y = int(y / self.cell_size)
cell_radius = int(radius / self.cell_size) + 1
for dx in range(-cell_radius, cell_radius + 1):
for dy in range(-cell_radius, cell_radius + 1):
key = (cell_x + dx, cell_y + dy)
if key in self.cells:
nearby.extend(self.cells[key])
return nearby
This reduces neighbor search from O(N²) to O(N).
Pattern 3: Hybrid Approach (LOD System)
Best of Both Worlds: Agents near player, equations far away.
class HybridEcosystem:
def __init__(self):
# Agent-based (near player)
self.visible_deer = []
self.visible_wolves = []
# Equation-based (distant)
self.distant_deer_population = 200
self.distant_wolf_population = 40
# Parameters
self.visibility_radius = 100
self.player_x = 0
self.player_y = 0
def update_player_position(self, x, y):
"""Player moved, update what's visible"""
self.player_x = x
self.player_y = y
# Check if distant animals should become visible
self.spawn_visible_animals()
# Check if visible animals moved too far
self.despawn_distant_animals()
def spawn_visible_animals(self):
"""Convert distant population to visible agents"""
# Probability based on distant population
spawn_chance = self.distant_deer_population / 1000.0
if random.random() < spawn_chance:
# Spawn deer at edge of visibility
angle = random.uniform(0, 2 * math.pi)
x = self.player_x + math.cos(angle) * self.visibility_radius
y = self.player_y + math.sin(angle) * self.visibility_radius
self.visible_deer.append(Deer(x, y))
self.distant_deer_population -= 1
def despawn_distant_animals(self):
"""Convert visible agents back to distant population"""
for deer in self.visible_deer[:]:
dist = math.sqrt((deer.x - self.player_x)**2 + (deer.y - self.player_y)**2)
if dist > self.visibility_radius * 1.5:
self.visible_deer.remove(deer)
if deer.alive:
self.distant_deer_population += 1
def step(self, dt=0.1):
"""Hybrid update"""
# 1. Update visible agents (agent-based)
for deer in self.visible_deer:
deer.update(dt, grass_patches=[], wolves=self.visible_wolves)
for wolf in self.visible_wolves:
wolf.update(dt, deer_list=self.visible_deer)
# 2. Update distant populations (equation-based)
# Lotka-Volterra for distant populations
alpha = 0.1
beta = 0.002
delta = 0.001
gamma = 0.05
prey_change = alpha * self.distant_deer_population - beta * self.distant_deer_population * self.distant_wolf_population
predator_change = delta * beta * self.distant_deer_population * self.distant_wolf_population - gamma * self.distant_wolf_population
self.distant_deer_population += prey_change * dt
self.distant_wolf_population += predator_change * dt
# Cap and prevent negatives
self.distant_deer_population = max(5, min(500, self.distant_deer_population))
self.distant_wolf_population = max(2, min(100, self.distant_wolf_population))
When to Use:
- Open world games (Skyrim, Far Cry)
- Large maps with 500+ animals
- Player can hunt but can't see all at once
- Performance budget limited
Benefits:
- Fast (equations handle 90% of population)
- Immersive (player sees/interacts with individuals)
- Scalable (can have 1000s of "distant" animals)
Common Pitfalls
Pitfall 1: Ecosystem Collapse Within Minutes
The Mistake:
# ❌ No carrying capacity, no extinction prevention
prey += alpha * prey - beta * prey * predators
predators += delta * beta * prey * predators - gamma * predators
# Result: Prey goes to 0 within 5 ticks, predators starve
Why This Fails:
- Pure Lotka-Volterra allows extinction (prey → 0)
- No recovery mechanism
- Predators overfish prey instantly
Real-World Example: Player starts game, hunts 20 deer in first 10 minutes. Deer population: 100 → 80. Without recovery, wolves eat remaining 80 deer. Deer extinct. Wolves starve. 30 minutes in, world is dead.
The Fix:
# ✅ Extinction prevention + carrying capacity
MIN_PREY = 10
PREY_CAPACITY = 500
prey += alpha * prey * (1 - prey / PREY_CAPACITY) - beta * prey * predators
if prey < MIN_PREY:
prey += (MIN_PREY - prey) * 0.1 * dt # Gradual recovery
show_notification("Deer migrating from neighboring forest")
Testing:
def test_no_extinction():
prey, predators = 100, 50
for _ in range(10000):
prey, predators = simulate_step(prey, predators)
assert prey >= MIN_PREY, f"Extinction! Prey={prey}"
assert predators >= MIN_PREDATORS, f"Extinction! Predators={predators}"
Pitfall 2: Runaway Population Explosion
The Mistake:
# ❌ No cap on population, exponential growth
for deer in deer_list:
if deer.energy > 80:
deer_list.append(Deer()) # Infinite growth!
Result:
- Tick 0: 50 deer
- Tick 10: 200 deer
- Tick 20: 2,000 deer
- Tick 30: 50,000 deer (game crashes)
Why This Fails:
- No carrying capacity
- No resource limits (grass infinite)
- Reproduction has no cost
The Fix:
# ✅ Carrying capacity + resource limits
PREY_CAPACITY = 500
for deer in deer_list[:]: # Copy list to avoid mutation during iteration
if deer.energy > 80 and len(deer_list) < PREY_CAPACITY:
# Reproduce only if below carrying capacity
deer.energy -= 30 # Reproduction cost
deer_list.append(Deer())
Pitfall 3: Too Chaotic (No Stable Equilibrium)
The Mistake:
# ❌ Too much randomness, no damping
births = random.uniform(0, alpha * prey * 2) # 0-200% variance!
deaths = random.uniform(0, beta * prey * predators * 2)
prey += births - deaths
Result:
- Tick 0: 100 deer
- Tick 1: 150 deer (random spike)
- Tick 2: 30 deer (random crash)
- Tick 3: 200 deer (random spike)
- Never settles into stable oscillation
Why This Fails:
- Variance > 50% creates chaos
- No negative feedback (crashes beget more crashes)
- Can't tune (every run different)
The Fix:
# ✅ Controlled randomness (±15% max)
base_births = alpha * prey
actual_births = base_births * random.uniform(0.85, 1.15) # ±15%
# Or use deterministic core with stochastic sampling
if random.random() < (alpha * prey - int(alpha * prey)):
births = int(alpha * prey) + 1
else:
births = int(alpha * prey)
Red Flag: If population graph looks like random noise (not smooth oscillations), reduce randomness.
Pitfall 4: No Understanding of Lotka-Volterra Theory
The Mistake:
# ❌ Made-up rules with no ecological foundation
prey += 10 # Prey grows by constant 10
if predators > prey:
prey -= 5 # Arbitrary rule
if wolves_are_hungry:
wolves += 2 # Another arbitrary rule
Why This Fails:
- No basis in population dynamics theory
- Rules don't create natural oscillations
- Impossible to tune (no parameters to adjust)
Real Example: Agent tries to create "balanced" ecosystem by adding random rules:
- "If deer > 100, deer -= 10"
- "If wolves < 10, wolves += 5"
- "If grass < 500, grass = 1000"
Result: Feels mechanical, artificial. Populations jump around with no natural flow.
The Fix: Learn and apply Lotka-Volterra:
# ✅ Based on proven ecological theory
# Prey equation: dP/dt = αP - βPQ
prey_change = alpha * prey - beta * prey * predators
# Predator equation: dQ/dt = δβPQ - γQ
predator_change = delta * beta * prey * predators - gamma * predators
prey += prey_change * dt
predators += predator_change * dt
Key Insight: Lotka-Volterra creates natural oscillations without manual intervention. Prey increases → Predators increase (more food) → Prey decreases (overpredation) → Predators decrease (starvation) → Prey recovers → cycle repeats.
Pitfall 5: Instant Reproduction (No Time Delays)
The Mistake:
# ❌ Deer reproduces immediately after eating
deer.eat(grass)
if deer.energy > 80:
deer_list.append(Deer()) # Instant baby!
Result:
- Deer eats → energy 100 → spawns baby → baby eats → spawns baby → ...
- Population doubles every tick (exponential explosion)
Why This Fails:
- Real animals have gestation periods (months)
- No biological delay
- Positive feedback loop (more deer → more births → more deer)
The Fix:
# ✅ Reproduction cooldown (gestation + maturation)
class Deer:
def __init__(self):
self.energy = 50
self.reproduction_cooldown = 0
self.age = 0
def try_reproduce(self, dt):
self.reproduction_cooldown = max(0, self.reproduction_cooldown - dt)
if self.energy > 80 and self.reproduction_cooldown == 0 and self.age > 10:
self.energy -= 30
self.reproduction_cooldown = 20 # Can't reproduce for 20 time units
return Deer()
return None
Realistic Time Scales:
- Deer gestation: 6-7 months → 20-30 game time units
- Deer maturation: 1-2 years → 50-100 game time units
- Wolf gestation: 2 months → 10-15 game time units
Pitfall 6: No Starvation Mechanics
The Mistake:
# ❌ Animals never die from hunger
if grass == 0:
pass # Deer just stops reproducing, but doesn't die
Result:
- Grass depleted → deer can't eat → but deer live forever
- Population frozen (no deaths, no births)
- Unrealistic (animals should starve)
The Fix:
# ✅ Energy depletion leads to death
class Deer:
def tick(self, dt):
self.energy -= 5 * dt # Metabolism costs energy
if self.energy <= 0:
self.alive = False
return "starved"
return "alive"
Pitfall 7: Ignoring Spatial Distribution
The Mistake:
# ❌ All animals exist at same "location" (no space)
prey_population = 100
predator_population = 20
# Predators instantly catch prey (no chase)
prey_population -= predation_rate * prey_population * predator_population
Why This Fails:
- Player hunts specific deer, but they're just numbers
- No herding behavior (deer cluster near food)
- No territorial behavior (wolves patrol territory)
- Less immersive (can't see animals move)
When Acceptable:
- Background populations (distant areas)
- Performance-critical (1000+ animals)
- No player interaction with individuals
When Problematic:
- Hunting game (player targets specific deer)
- Stealth mechanics (sneak past wolves)
- Territory control (protect area from predators)
The Fix: Use agent-based or hybrid approach (Pattern 2 & 3 above).
Real-World Examples
Example 1: Minecraft - Simple Spawn System
Architecture: Spawn-based (not true ecosystem simulation)
How It Works:
# Minecraft's approach (simplified)
def spawn_animals(chunk):
"""Spawn animals in chunk if below mob cap"""
animal_count = count_animals_in_chunk(chunk)
if animal_count < 10: # Mob cap per chunk
if random.random() < 0.01: # 1% chance per tick
animal_type = random.choice(['cow', 'pig', 'chicken', 'sheep'])
spawn_position = find_grass_block(chunk)
spawn_animal(animal_type, spawn_position)
# Breeding (player-driven)
def breed_animals(animal1, animal2):
"""Player feeds two animals, they breed"""
if animal1.fed and animal2.fed:
baby = spawn_animal(animal1.type, animal1.position)
animal1.fed = False
animal2.fed = False
Not a True Ecosystem:
- No predator-prey dynamics (no wolves eating cows)
- No natural reproduction (only player-triggered breeding)
- No food chains (animals don't eat grass)
- No population balance (just spawn caps)
Why It Works for Minecraft:
- Simplicity (easy to understand)
- Player control (breeding is gameplay mechanic)
- Predictability (animals don't disappear mysteriously)
- Performance (cheap to implement)
Lessons:
- Don't need full ecosystem for every game
- Spawn caps prevent runaway growth
- Player-driven breeding gives agency
Example 2: Don't Starve - Food Chain Simulation
Architecture: Agent-based with food chains
Food Chain:
Grass/Seeds → Rabbits → Spiders
↓ ↓
Player Player
How It Works (Conceptual):
class Rabbit:
def update(self):
# 1. Flee from player and spiders
if see_threat():
flee()
# 2. Seek food (grass, carrots)
elif hungry():
food = find_nearest_food()
if food:
move_toward(food)
if near(food):
eat(food)
# 3. Reproduce (if well-fed)
if energy > 80 and can_reproduce():
spawn_rabbit_hole()
# 4. Return to burrow at night
if is_night():
return_to_burrow()
class Spider:
def update(self):
# 1. Hunt rabbits and birds
prey = find_nearest_prey(['rabbit', 'bird', 'player'])
if prey:
chase(prey)
if near(prey):
attack(prey)
# 2. Return to nest
else:
return_to_nest()
Key Mechanics:
- Burrows: Rabbits spawn from burrows (replenishment)
- Player impact: Overhunting rabbits → more spiders (less prey)
- Seasonal: Winter reduces food, animals starve
- Extinction prevention: Burrows slowly spawn new rabbits
Lessons:
- Agent-based works for < 100 animals
- Burrows/nests provide spawn points (extinction prevention)
- Player actions affect balance (hunting creates scarcity)
Example 3: Eco - Full Ecosystem Simulation
Architecture: Agent-based + nutrient cycles + player economy
Features:
- Plant succession: Grass → shrubs → trees (over days)
- Herbivores: Deer eat plants, need calories
- Carnivores: Foxes eat deer, need protein
- Nutrient cycling: Dead animals → fertilize plants
- Player impact: Deforestation → herbivores starve → carnivores starve → ecosystem collapse
Nutrient Cycle (Simplified):
class EcoSystem:
def __init__(self):
self.soil_nutrients = 1000
self.plants = []
self.herbivores = []
self.carnivores = []
def update(self, dt):
# 1. Plants grow using soil nutrients
for plant in self.plants:
if self.soil_nutrients > 0:
plant.grow(dt)
self.soil_nutrients -= plant.nutrient_uptake * dt
# 2. Herbivores eat plants
for herbivore in self.herbivores:
plant = herbivore.find_nearest_plant()
if plant:
herbivore.eat(plant)
plant.mass -= herbivore.bite_size
# 3. Carnivores eat herbivores
for carnivore in self.carnivores:
prey = carnivore.find_nearest_prey()
if prey:
carnivore.hunt(prey)
if carnivore.catches(prey):
prey.alive = False
carnivore.eat(prey)
# 4. Decomposition returns nutrients
for corpse in self.dead_animals:
self.soil_nutrients += corpse.mass * 0.5 # 50% nutrient recovery
# 5. Player actions
if player.chops_tree():
tree = self.find_tree()
self.plants.remove(tree)
# Less plants → less food → herbivores starve
Goal: Teach players about ecosystem balance. If you over-harvest, species go extinct.
Lessons:
- Full simulation is HARD (2-4 weeks implementation)
- Nutrient cycling adds depth
- Player education requires visible consequences
- Extinction is a feature (teaches lesson)
Example 4: Spore - Evolutionary Ecosystem
Architecture: Agent-based with genetics
Features:
- Creatures evolve: Traits pass from parent to offspring
- Natural selection: Weak creatures die, strong survive
- Predator-prey arms race: Prey evolves speed → predators evolve speed
Genetic System (Simplified):
class Creature:
def __init__(self, genes=None):
if genes:
self.speed = genes['speed']
self.strength = genes['strength']
self.diet = genes['diet'] # 'herbivore' or 'carnivore'
else:
# Random starting genes
self.speed = random.uniform(1, 10)
self.strength = random.uniform(1, 10)
self.diet = random.choice(['herbivore', 'carnivore'])
def reproduce(self):
"""Pass genes to offspring with mutation"""
baby_genes = {
'speed': self.speed + random.uniform(-0.5, 0.5), # Mutation
'strength': self.strength + random.uniform(-0.5, 0.5),
'diet': self.diet
}
return Creature(genes=baby_genes)
def simulate_evolution():
creatures = [Creature() for _ in range(100)]
for generation in range(1000):
# Natural selection
survivors = []
for creature in creatures:
if creature.survives(): # Depends on speed, strength
survivors.append(creature)
# Reproduction
creatures = []
for survivor in survivors:
creatures.append(survivor.reproduce())
# Result: Over time, creatures evolve to be faster/stronger
Lessons:
- Evolution = reproduction + mutation + selection
- Emergent complexity (arms race without explicit code)
- Very hard to tune (emergent behavior unpredictable)
- Cool but not necessary for most games
Example 5: The Sims - Abstract Resource Ecosystem
Not animals, but same principles:
Resources:
- Happiness: Decreases over time, replenished by fun activities
- Hunger: Decreases over time, replenished by eating
- Social: Decreases over time, replenished by socializing
Ecosystem Analogy:
# Similar to predator-prey dynamics
# Sims = "predators" consuming resources
# Resources = "prey" being depleted
class Sim:
def __init__(self):
self.hunger = 50
self.fun = 50
self.social = 50
def update(self, dt):
# Resources decrease (like prey being eaten)
self.hunger -= 5 * dt
self.fun -= 3 * dt
self.social -= 2 * dt
# Sims seek resources (like predators hunting)
if self.hunger < 30:
self.go_eat()
elif self.fun < 30:
self.go_play()
elif self.social < 30:
self.go_socialize()
def go_eat(self):
# Eating replenishes hunger (like prey reproducing)
self.hunger = min(100, self.hunger + 30)
Lesson: Ecosystem principles apply beyond wildlife. Any resource depletion/replenishment system can use Lotka-Volterra ideas.
Cross-References
Use This Skill WITH:
- ai-and-agent-simulation: Agent-based ecosystems need AI (deer flee, wolves hunt)
- physics-simulation-patterns: Animal movement, collision detection
- economic-simulation-patterns: Resource extraction (hunting) affects ecosystem
Use This Skill BEFORE:
- procedural-generation: Populate procedurally generated worlds with wildlife
- quest-systems: Quests involving hunting, conservation
- survival-mechanics: Food chains, hunting gameplay
Related Skills:
- systems-as-experience: Ecosystems as narrative/gameplay systems
- player-driven-economy: Player hunting affects ecosystem balance
- difficulty-balancing: Predator danger scales with player progress
Testing Checklist
Stability Validation
- Ecosystem runs for 10+ minutes without extinction
- Populations oscillate (not exponential growth or crash)
- Oscillations dampen over time (settle into stable range)
- Carrying capacity prevents runaway growth (populations cap out)
- Extinction prevention activates when populations drop below threshold
Parameter Tuning
- Tested with 3+ different starting conditions (all stable)
- Adjusted α, β, δ, γ to achieve desired oscillation period
- Verified populations settle within ±20% of equilibrium
- Carrying capacities set 20-30% above typical max populations
- Reproduction cooldowns prevent instant population doubling
Edge Cases
- What if all predators die? (Prey should cap at carrying capacity, not explode)
- What if all prey die? (Predators should starve, then prey respawn from extinction prevention)
- What if player hunts 50% of prey? (Population recovers over time)
- What if grass depleted? (Prey starve until grass regrows)
- What if 1000 predators spawned? (System handles it without crash, populations rebalance)
Performance
- Runs at 60 FPS with max animal count
- Agent-based: < 100 animals or use spatial partitioning
- Equation-based: Can handle 10,000+ population numbers
- Hybrid: Smoothly transitions between agent/equation modes
- No memory leaks (dead animals removed from lists)
Visualization
- Population graph shows oscillations over time
- Can plot prey, predators, resources on same graph
- Stability metric calculated (variance < 30% of mean)
- Debug mode shows individual animal states (energy, hunger)
- Notification when populations critical
Realism
- Energy budgets implemented (eating gains energy, actions cost energy)
- Reproduction has cooldown (gestation period)
- Starvation kills animals (energy <= 0 → death)
- Age-based mortality (animals die of old age eventually)
- Spatial distribution makes sense (prey near food, predators near prey)
Gameplay Integration
- Player hunting reduces prey population (visible impact)
- Ecosystem recovers from player over-hunting (migration, respawn)
- Extinction warning shown to player ("Deer population critical!")
- Player can observe ecosystem health (population stats, graphs)
- Ecosystem state saved/loaded correctly (populations persist)
Summary
Ecosystem simulation for games requires understanding Lotka-Volterra equations, carrying capacity, energy budgets, and extinction prevention. The core principles are:
- Use Lotka-Volterra as foundation - Provides natural predator-prey oscillations
- Add carrying capacity - Prevents runaway growth and crashes
- Implement extinction prevention - Soft boundaries keep ecosystem alive
- Choose right approach - Equation-based for speed, agent-based for detail, hybrid for scale
- Add time delays - Reproduction cooldowns prevent instant explosions
- Energy budgets - Animals need food to survive and reproduce
- Test stability rigorously - Run for 10+ minutes, check for explosions/extinctions
- Balance realism with fun - Don't let ecosystem collapse ruin gameplay
Most Common Failures:
- ❌ No carrying capacity → runaway growth
- ❌ No extinction prevention → collapse within minutes
- ❌ No Lotka-Volterra understanding → chaotic, untunable
- ❌ Instant reproduction → exponential explosions
- ❌ No starvation mechanics → unrealistic immortal animals
Success Pattern:
# Lotka-Volterra + carrying capacity + extinction prevention
prey += (alpha * prey * (1 - prey/K_prey) - beta * prey * predators) * dt
predators += (delta * beta * prey * predators - gamma * predators) * dt
if prey < MIN_PREY: prey += recovery_rate * dt
if predators < MIN_PREDATORS: predators += recovery_rate * dt
prey = min(prey, K_prey)
predators = min(predators, K_predators)
Master these patterns, avoid the pitfalls, and your ecosystem will be stable, engaging, and scalable.