Source code for simulation.engine

from core.organism import Consumer, Producer
from core.foodweb import FoodWeb
import logic.behavior as behavior
from visualizer.plot import plot_organisms
import random
from logic.reproduction import reproduce
from collections import defaultdict
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from statistic_tools.heatmap import export_heatmaps
from statistic_tools.population import export_population_chart

[docs]class SimulationEngine: """ Controls and runs the ecosystem simulation. Handles grid setup, population initialization, step-wise organism behavior, reproduction, and visualization including population tracking and heatmaps. """
[docs] def __init__(self, grid_size=20, steps=30, foodweb_path="configs/foodweb_config.json"): """ Initialize simulation parameters and state. Parameters: grid_size (int): Size of the simulation grid. steps (int): Total number of simulation steps. foodweb_path (str): Path to food web configuration JSON. """ self.grid_size = grid_size self.steps = steps self.foodweb = FoodWeb(foodweb_path) self.organisms = [] self.heatmaps = defaultdict(lambda: np.zeros((self.grid_size, self.grid_size), dtype=int)) self.population_history = defaultdict(list) self.terrain = None
[docs] def setup(self): """ Populate the grid with initial organisms based on food web configuration. Organisms are placed randomly avoiding blocked or occupied terrain. Sets up internal reproduction and decomposition parameters. """ level_counts = { "primary": 5, "secondary": 2, "tertiary": 1, "omnivore": 3, "unknown": 1 } for species in self.foodweb.all_species(): org_type = self.foodweb.get_type(species) if org_type == "Producer": for _ in range(4): while True: x = random.randint(0, self.grid_size - 1) y = random.randint(0, self.grid_size - 1) occupied = any(o.x == x and o.y == y for o in self.organisms) blocked = self.terrain and (self.terrain.is_water(x, y) or self.terrain.is_blocked(x, y)) if not occupied and not blocked: break self.organisms.append(Producer(species, x, y)) elif org_type == "Consumer": trophic_level = self.foodweb.get_trophic_level(species) count = self.foodweb.organisms[species].get("initial_count", level_counts.get(trophic_level, 1)) for _ in range(count): while True: x = random.randint(0, self.grid_size - 1) y = random.randint(0, self.grid_size - 1) occupied = any(o.x == x and o.y == y for o in self.organisms) blocked = self.terrain and (self.terrain.is_water(x, y) or self.terrain.is_blocked(x, y)) if not occupied and not blocked: break self.organisms.append(Consumer(species, x, y, trophic_level=trophic_level)) reproduce._foodweb = self.foodweb reproduce._terrain = self.terrain decomposer_species = [ s for s in self.foodweb.all_species() if self.foodweb.get_type(s) == "Decomposer" ] self.decomposition_interval = min( [self.foodweb.get_decomposition_rate(s) for s in decomposer_species], default=20 )
[docs] def run(self): """ Execute the simulation over the predefined number of steps. Organisms act each step based on their roles (Producer/Consumer), terrain effects are applied, population data is updated, and visual outputs such as organism plots and heatmaps are generated. """ for step in range(self.steps): print(f"\n--- Step {step} ---") living = [org for org in self.organisms if org.alive] for org in living: if isinstance(org, Consumer): org.step(self.grid_size, self.organisms, self.foodweb, behavior, self.terrain) elif isinstance(org, Producer): org.step(self.grid_size) if self.terrain: self.terrain.apply_terrain_effects(org, step) if 0 <= org.x < self.grid_size and 0 <= org.y < self.grid_size: self.heatmaps[org.species][org.y, org.x] += 1 if self.terrain: self.terrain.update_shelters(self.organisms) for org in self.organisms: status = "X" if not org.alive else "" print(f"{org} {status}") newbies = reproduce(self.organisms, self.grid_size, step) self.organisms.extend(newbies) species_counts = defaultdict(int) for org in self.organisms: if org.alive: species_counts[org.species] += 1 for species in self.foodweb.all_species(): self.population_history[species].append(species_counts.get(species, 0)) if step > 0 and step % self.decomposition_interval == 0: for corpse in self.organisms: if not corpse.alive: print(f"💀 Decomposed: {corpse}") self.organisms.remove(corpse) break plot_organisms(step, self.organisms, self.grid_size, foodweb=self.foodweb, terrain=self.terrain) export_population_chart(self.population_history) for species, heatmap_data in self.heatmaps.items(): plt.figure(figsize=(8, 6)) plt.imshow(heatmap_data, cmap="YlOrRd", interpolation='bilinear') plt.title(f"Heatmap: {species}") plt.xlabel("X coordinate") plt.ylabel("Y coordinate") plt.colorbar() plt.tight_layout() plt.close() export_heatmaps(self.heatmaps)