Source code for core.terrain

import json
import random
from collections import defaultdict

[docs]class Terrain: """ Represents the terrain grid used in the ecosystem simulation, including terrain types and their effects. Attributes: grid_size (int): The size of the square simulation grid. map (defaultdict): A mapping of (x, y) positions to terrain types. shelter_occupants (dict): Tracks the occupancy of shelters by organisms. water_counters (defaultdict): Tracks energy boosts from water for individual organisms. """
[docs] def __init__(self, grid_size): """ Initializes the terrain grid with default terrain type "plain". Args: grid_size (int): Size of the simulation grid. """ self.grid_size = grid_size self.map = defaultdict(lambda: "plain") self.shelter_occupants = {} self.water_counters = defaultdict(lambda: defaultdict(int))
[docs] def load_from_config(self, config_path): """ Loads terrain layout from a configuration file. Supports 'random' or 'manual' terrain placement. Args: config_path (str): Path to the JSON configuration file. """ with open(config_path, 'r') as f: data = json.load(f) mode = data.get("mode", "random") if mode == "random": self.generate_water() self.generate_trees() self.generate_hills() self.generate_shelters() elif mode == "manual": for obj in data.get("terrain_objects", []): ttype = obj["type"] x0 = obj["x"] y0 = obj["y"] width = obj.get("width", 1) height = obj.get("height", 1) for dx in range(width): for dy in range(height): x = x0 + dx y = y0 + dy if 0 <= x < self.grid_size and 0 <= y < self.grid_size: self.map[(x, y)] = ttype
def _place_random(self, terrain_type): """ Randomly places a terrain type on an unoccupied plain tile. Args: terrain_type (str): The type of terrain to place (e.g., 'tree', 'shelter'). """ tries = 0 while tries < 100: x = random.randint(0, self.grid_size - 1) y = random.randint(0, self.grid_size - 1) if self.map[(x, y)] == "plain": self.map[(x, y)] = terrain_type return tries += 1
[docs] def generate_water(self): """ Generates water patches randomly across the terrain. """ patches = max(1, round(self.grid_size / 7)) for _ in range(patches): cx = random.randint(2, self.grid_size - 3) cy = random.randint(2, self.grid_size - 3) for dx in range(-2, 3): for dy in range(-2, 3): x, y = cx + dx, cy + dy if 0 <= x < self.grid_size and 0 <= y < self.grid_size: if self.map[(x, y)] == "plain": self.map[(x, y)] = "water"
[docs] def generate_trees(self): """ Randomly places individual trees throughout the grid. """ for _ in range(self.grid_size - 7): self._place_random("tree")
[docs] def generate_hills(self): """ Generates small clusters of 'hill' terrain types. """ patches = round(self.grid_size / 6) for _ in range(patches): cx = random.randint(2, self.grid_size - 3) cy = random.randint(2, self.grid_size - 3) for dx in range(-2, 3): for dy in range(-2, 3): x, y = cx + dx, cy + dy if 0 <= x < self.grid_size and 0 <= y < self.grid_size: if self.map[(x, y)] == "plain": self.map[(x, y)] = "hill"
[docs] def generate_shelters(self): """ Randomly places multiple 'shelter' locations on the terrain. """ for _ in range(max(1, round(self.grid_size / 10))): for _ in range(4): self._place_random("shelter")
[docs] def get_type(self, x, y): """ Returns the terrain type at a given coordinate. Args: x (int): X-coordinate. y (int): Y-coordinate. Returns: str: Terrain type at the specified location. """ return self.map.get((x, y), "plain")
[docs] def is_blocked(self, x, y): """ Checks if a tile is blocked due to a tree. Args: x (int): X-coordinate. y (int): Y-coordinate. Returns: bool: True if tile contains a tree. """ return self.map.get((x, y)) == "tree"
[docs] def is_shelter(self, x, y): """ Checks if a tile is a shelter. Args: x (int): X-coordinate. y (int): Y-coordinate. Returns: bool: True if tile is a shelter. """ return self.map.get((x, y)) == "shelter"
[docs] def is_in_shelter(self, x, y): """ Alias for is_shelter() for compatibility. Returns: bool: True if the position is a shelter. """ return self.is_shelter(x, y)
[docs] def is_hill(self, x, y): """ Checks if a tile is a hill. Args: x (int): X-coordinate. y (int): Y-coordinate. Returns: bool: True if tile is a hill. """ return self.map.get((x, y)) == "hill"
[docs] def is_water(self, x, y): """ Checks if a tile is water. Args: x (int): X-coordinate. y (int): Y-coordinate. Returns: bool: True if tile is water. """ return self.map.get((x, y)) == "water"
[docs] def apply_terrain_effects(self, org, step_counter): """ Applies terrain-based effects to an organism, such as energy gain near water. Args: org (Organism): The organism affected. step_counter (int): Current simulation step counter. """ if step_counter % 10 != 0 or not org.alive or hasattr(org, "is_edible"): return adjacent = [(org.x + dx, org.y + dy) for dx in [-1, 0, 1] for dy in [-1, 0, 1] if (dx != 0 or dy != 0) and 0 <= org.x + dx < self.grid_size and 0 <= org.y + dy < self.grid_size] for pos in adjacent: if self.is_water(*pos): counter = self.water_counters[pos][id(org)] if counter < 2: org.energy = min(org.max_energy, org.energy + 5) self.water_counters[pos][id(org)] += 1 break
[docs] def update_shelters(self, organisms): """ Updates organism interactions with shelter tiles. Carnivores are pushed out, while herbivores gain temporary protection or die if they stay too long. Args: organisms (list): List of organisms to update. """ for org in organisms: pos = (org.x, org.y) if self.is_shelter(org.x, org.y) and getattr(org, "trophic_level", None) != "primary": # Carnivores get ejected from shelters for _ in range(20): new_x = random.randint(0, self.grid_size - 1) new_y = random.randint(0, self.grid_size - 1) if not self.is_shelter(new_x, new_y) and not self.is_blocked(new_x, new_y): org.x = new_x org.y = new_y break continue if self.is_shelter(org.x, org.y) and getattr(org, "trophic_level", None) == "primary": # Herbivores in shelter if pos not in self.shelter_occupants: self.shelter_occupants[pos] = {} count = self.shelter_occupants[pos].get(id(org), 0) + 1 self.shelter_occupants[pos][id(org)] = count if count > 3: org.alive = False else: # Clear shelter counter if not in shelter for data in self.shelter_occupants.values(): data.pop(id(org), None)
[docs] def can_enter_shelter(self, org): """ Checks if a given organism is allowed to enter the current shelter tile. Args: org (Organism): The organism to check. Returns: bool: True if organism is a primary consumer in a shelter. """ return self.is_shelter(org.x, org.y) and getattr(org, "trophic_level", None) == "primary"