# 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)**: 1. **Use Lotka-Volterra differential equations** for predator-prey dynamics 2. **Implement carrying capacity** for all species (grass, prey, predators) 3. **Add extinction prevention** (auto-spawn if population < threshold) 4. **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**: ```python 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:** ```python 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:** ```python 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:** ```python # 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:** 1. Start with high values (avoid constraints) 2. Observe maximum populations that naturally occur 3. Set capacity 20-30% above observed max 4. 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:** ```python 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:** ```python 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):** ```python # 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):** ```python # 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** ```python # 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** ```python 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:** ```python 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:** 1. **Player interaction**: Can player hunt individual animals? 2. **Performance budget**: How many animals can you simulate? 3. **Gameplay importance**: Is ecosystem core mechanic or background? 4. **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):** ```python # 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):** ```python # 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):** ```python 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)** ```python 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)** ```python 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** ```python # 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)** ```python # 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:** ```python 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:** 1. Run simulation for 100 time units 2. Plot populations (use `sim.plot()`) 3. If oscillations too wild: Reduce `prey_growth_rate` or increase `predation_rate` 4. If predators die out: Increase `predator_gain` or reduce `predator_death_rate` 5. If prey die out: Reduce `predation_rate` or increase `prey_growth_rate` 6. Target: Oscillations of ±20% around equilibrium ### Pattern 2: Agent-Based Simulation (Spatial Ecosystem) **When:** Player hunts individual animals, need spatial distribution. ```python 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: ```python 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. ```python 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:** ```python # ❌ 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:** ```python # ✅ 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:** ```python 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:** ```python # ❌ 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:** ```python # ✅ 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:** ```python # ❌ 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:** ```python # ✅ 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:** ```python # ❌ 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: ```python # ✅ 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:** ```python # ❌ 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:** ```python # ✅ 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:** ```python # ❌ 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:** ```python # ✅ 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:** ```python # ❌ 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:** ```python # 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):** ```python 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):** ```python 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):** ```python 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:** ```python # 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: 1. **Use Lotka-Volterra as foundation** - Provides natural predator-prey oscillations 2. **Add carrying capacity** - Prevents runaway growth and crashes 3. **Implement extinction prevention** - Soft boundaries keep ecosystem alive 4. **Choose right approach** - Equation-based for speed, agent-based for detail, hybrid for scale 5. **Add time delays** - Reproduction cooldowns prevent instant explosions 6. **Energy budgets** - Animals need food to survive and reproduce 7. **Test stability rigorously** - Run for 10+ minutes, check for explosions/extinctions 8. **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:** ```python # 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.