import mesa
import random
import math
import numpy as np
from mesa.experimental.continuous_space import ContinuousSpaceAgent
from mesa import Model
from mesa.experimental.continuous_space import ContinuousSpace
from mesa.datacollection import DataCollector
from prey import PreyAgent
from predator import PredatorAgent
def space_distance(self, pos1, pos2):
dx = abs(pos1[0] - pos2[0])
dy = abs(pos1[1] - pos2[1])
if self.torus:
dx = min(dx, self.width - dx)
dy = min(dy, self.height - dy)
return math.sqrt(dx**2 + dy**2)
# Attach the custom distance function to ContinuousSpace.
ContinuousSpace.get_distance = space_distance
def generate_terrain(width, height, border_thickness=22, mountain_fraction=0.1):
"""
Generates terrain for a grid with the following layout:
- Outer border cells (with thickness border_thickness) are forest.
- The interior is divided vertically:
* The left portion (mountain_fraction of the interior width) is mountains.
* The remaining interior cells are grassland.
Args:
width (int): The grid's total width.
height (int): The grid's total height.
border_thickness (int): Thickness of the forest border.
mountain_fraction (float): Fraction (0 to 1) of the interior allocated to mountains.
Returns:
dict: A mapping from (x, y) tuple to terrain type ("forest", "mountains", "grassland").
"""
terrain = {}
# Initialize the entire grid as forest.
for x in range(width):
for y in range(height):
terrain[(x, y)] = "forest"
# Define the interior region boundaries.
interior_min_x = border_thickness
interior_max_x = width - border_thickness
interior_min_y = border_thickness
interior_max_y = height - border_thickness
interior_width = interior_max_x - interior_min_x
# Determine the column index dividing mountains and grassland.
mountain_division = interior_min_x + int(interior_width * mountain_fraction)
# Fill the interior region with mountains on the left and grassland on the right.
for x in range(interior_min_x, interior_max_x):
for y in range(interior_min_y, interior_max_y):
if x < mountain_division:
terrain[(x, y)] = "mountains"
else:
terrain[(x, y)] = "grassland"
return terrain
class PreyPredatorModel(Model):
def __init__(self,
width=50,
height=50,
num_predators=5,
num_prey=20,
sensor_noise=2.0,
use_kalman_filter=True,
seed=None):
super().__init__(seed=seed)
self.num_predators = num_predators
self.num_prey = num_prey
self.sensor_noise = sensor_noise
self.use_kalman_filter = use_kalman_filter
self.kalman_Q = 0.1 # Process noise for Kalman filter
self.kalman_R = 0.1 # Measurement noise for Kalman filter
# Set up the continuous space.
self.space = ContinuousSpace([[0, width], [0, height]], torus=False, random=self.random, n_agents=num_predators + num_prey)
# Generate structured terrain: a forest border, a mountain range on the left, and grassland on the right.
self.terrain = generate_terrain(width, height, border_thickness=10, mountain_fraction=0.1)
# Create Prey Agents
positions = self.rng.random(size=(num_prey, 2)) * self.space.size
self.prey_agents = PreyAgent.create_agents(model=self, n=self.num_prey, space=self.space, position=positions)
# Create Predator Agents
positions = self.rng.random(size=(num_predators, 2)) * self.space.size
self.predator_agents = PredatorAgent.create_agents(model=self, n=self.num_predators, space=self.space, position=positions)
# Initialize DataCollector
self.datacollector = DataCollector(
model_reporters={
"Prey_Count": lambda m: len(m.prey_agents),
"Predator_Count": lambda m: len(m.predator_agents),
"Average_Energy": lambda m: np.mean([agent.energy for agent in m.prey_agents]) if m.prey_agents else 0,
"Average_Hunger": lambda m: np.mean([agent.hunger for agent in m.predator_agents]) if m.predator_agents else 0,
}
)
def step(self):
self.agents.shuffle_do("step")
self.datacollector.collect(self)