From f82f28208aa3a2dd3ac2363926b084c112ed298a Mon Sep 17 00:00:00 2001 From: Szymon Cichowski Date: Mon, 26 Jan 2026 11:50:09 +0100 Subject: [PATCH 1/3] Add signaling mechanism --- src/agent.py | 40 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/src/agent.py b/src/agent.py index 411e170..52ebb3b 100644 --- a/src/agent.py +++ b/src/agent.py @@ -118,6 +118,15 @@ def __init__(self, position: tuple, environment : Environment,/, decision_matrix self.last_action = self.actions["ACTION_MOVE"] + self.signal_active = False + self.signal_timer = 0 + self.signal_duration = 20 + self.signal_range = 50.0 + + self.suggested_action = None + self.suggested_action_timer = 0 + self.suggested_action_duration = 50 + self.input_size = len(self.sense()) self.action_count = len(self.actions) self.output_size = self.action_count + 2 @@ -228,12 +237,29 @@ def _attack(self): best_enemy : Agent = a if best_enemy: best_enemy.hp -= self.attack_power - + best_enemy.signal() def _mate(self): self.mate_module.mate() + def signal(self): + """Send signal to call nearby teammates for help.""" + self.signal_active = True + self.signal_timer = self.signal_duration + + nearby_agents = self.environment.get_nearby_agents(self, self.signal_range) + same_species = [agent for agent in nearby_agents + if agent is not self and agent.group_id == self.group_id] + + affected_agents = random.sample(same_species, min(3, len(same_species))) + + for agent in affected_agents: + agent.signal_active = True + agent.signal_timer = self.signal_duration + agent.suggested_action = self.actions["ACTION_MOVE"] + agent.suggested_action_timer = agent.suggested_action_duration + def _tick_body(self): self.age += 1 @@ -242,6 +268,16 @@ def _tick_body(self): self.hp = max(0.0, self.hp - 0.03) if self.age >= self.max_age: self.hp = 0.0 + + if self.signal_active and self.signal_timer > 0: + self.signal_timer -= 1 + if self.signal_timer <= 0: + self.signal_active = False + + if self.suggested_action_timer > 0: + self.suggested_action_timer -= 1 + if self.suggested_action_timer <= 0: + self.suggested_action = None def is_alive(self) -> bool: return self.hp > 0.0 and self.energy > 0 @@ -261,7 +297,7 @@ def update(self, speed_modifier): if action == self.actions["ACTION_MOVE"]: self.logger.debug(f"Agent - {self.uuid}; action - move") - self._move(turn, intensity) + self._move(turn, intensity, speed_modifier) elif action == self.actions["ACTION_MATE"]: self.logger.debug(f"Agent - {self.uuid}; action - mate") self._mate() From d2d7a33f8e05983ed9ca2dc80db13d724154ca5d Mon Sep 17 00:00:00 2001 From: Szymon Cichowski Date: Mon, 26 Jan 2026 11:58:42 +0100 Subject: [PATCH 2/3] Clean up code a bit --- src/agent.py | 18 +----------------- src/charts.py | 4 ---- src/environment.py | 5 ----- src/food.py | 5 +---- src/mating.py | 2 +- src/run.py | 3 +-- src/sense.py | 3 --- 7 files changed, 4 insertions(+), 36 deletions(-) diff --git a/src/agent.py b/src/agent.py index 52ebb3b..0b3abaa 100644 --- a/src/agent.py +++ b/src/agent.py @@ -169,9 +169,6 @@ def sense(self, foods=None, agents=None): def think(self, inputs): """Forward pass through neural network.""" - - - out = [0.0 for _ in range(self.output_size)] for i in range(self.input_size): xi = inputs[i] @@ -183,6 +180,7 @@ def think(self, inputs): return out def decide(self, outputs): + """Decide action, turn, and intensity from neural network outputs.""" logits = outputs[: self.action_count] best_i = 0 best_v = logits[0] @@ -194,24 +192,16 @@ def decide(self, outputs): intensity = outputs[self.action_count + 1] return best_i, turn, intensity - def _move(self, turn: float, intensity: float, speed_modifier: float = 1.0): - turn = (turn + 1) * math.pi - inten = _clamp(0.5 + 0.5 * intensity, 0.0, 1.0) sp = self.base_speed * (0.20 + 1.30 * inten) * speed_modifier - self.x = (self.x + math.cos(turn) * sp) % (self.environment.grid_width * self.environment.cell_size) self.y = (self.y - math.sin(turn) * sp) % (self.environment.grid_height * self.environment.cell_size) - cost = 0.0075 + 0.035 * inten self.energy = max(0.0, self.energy - cost) - self.angle = math.degrees(turn) - - def _attack(self): self.energy = max(0.0, self.energy - 0.16) agents = self.environment.agents @@ -219,7 +209,6 @@ def _attack(self): r2 = self.sight * self.sight best_enemy_d2 = 1e18 best_enemy = None - for a in agents: if a is self: continue @@ -238,7 +227,6 @@ def _attack(self): if best_enemy: best_enemy.hp -= self.attack_power best_enemy.signal() - def _mate(self): self.mate_module.mate() @@ -260,7 +248,6 @@ def signal(self): agent.suggested_action = self.actions["ACTION_MOVE"] agent.suggested_action_timer = agent.suggested_action_duration - def _tick_body(self): self.age += 1 self.energy = max(0.0, self.energy - 0.01) @@ -304,10 +291,7 @@ def update(self, speed_modifier): else: self.logger.debug(f"Agent - {self.uuid}; action - attack") self._attack() - self.last_action = action / 3 - - self._tick_body() def render(self, window: pygame.window, cell_size: int, offset: tuple): diff --git a/src/charts.py b/src/charts.py index 2eb00eb..329cbdf 100644 --- a/src/charts.py +++ b/src/charts.py @@ -154,22 +154,18 @@ def register_chart(name: str, value_name: str, callback: ChartCallback) -> Chart """Registers a chart to be sampled and rendered.""" return _MANAGER.register_chart(name, value_name, callback) - def update(dt: float) -> None: """Advances the polling timer.""" _MANAGER.update(dt) - def render(surface: pygame.Surface, rect: pygame.Rect) -> None: """Renders all registered charts inside the given rect.""" _MANAGER.render(surface, rect) - def scroll(dy: float) -> None: """Scrolls the chart viewport horizontally (dy is mouse wheel delta).""" _MANAGER.scroll(dy) - def first_sample_age() -> float: """Returns seconds since the oldest datapoint across charts was sampled.""" oldest = None diff --git a/src/environment.py b/src/environment.py index 3e331fa..b4478c6 100644 --- a/src/environment.py +++ b/src/environment.py @@ -125,7 +125,6 @@ def _cells_for_area(self, area: Area) -> List[Tuple[int, int]]: if self.terrain[y][x] == area ] - def _spawn_initial_food_sources(self): spawn_plan = { Area.PLAINS: (SimpleGrassPatch, 4), @@ -157,8 +156,6 @@ def _spawn_initial_food_sources(self): ) self.area_food_sources[area] += 1 - - def _spawn_initial_agents(self): Agent.bound_x = self.pixel_width Agent.bound_y = self.pixel_height @@ -264,7 +261,6 @@ def _simulation_loop(self): new_agents.append(agent) self.agents = new_agents - def set_grid_cell(self, x: int, y: int, value: int): if 0 <= x < self.grid_width and 0 <= y < self.grid_height: self.grid[y][x] = value @@ -326,7 +322,6 @@ def reset(self): self._spawn_initial_food_sources() self._spawn_initial_agents() - def pause(self): with self.data_lock: self.pause_sim = True diff --git a/src/food.py b/src/food.py index 95fc8a5..10d8ef8 100644 --- a/src/food.py +++ b/src/food.py @@ -7,7 +7,6 @@ class Food: """Represents a single piece of food that an agent can consume.""" - def __init__(self, position: tuple, value: int, food_type: str, expiry_time: int): self.x, self.y = position # Grid position (x, y) self.value = value # Energy provided @@ -52,7 +51,6 @@ def update(self) -> Food | None: def render(self, window, cell_size: int, food_items: list, panel_offset: tuple): """Renders the food source.""" - def destroy(self): """Marks the source as destroyed.""" if not self.is_destroyed: @@ -67,10 +65,9 @@ def increment_age(self): if self.age >= self.lifespan: self.destroy() -# Example + class SimpleGrassPatch(FoodSource): """A simple herbivore food source that regenerates and periodically drops food.""" - FONT = None def __init__(self, position: tuple, area, env_area_counters): diff --git a/src/mating.py b/src/mating.py index 8688369..983d482 100644 --- a/src/mating.py +++ b/src/mating.py @@ -17,6 +17,7 @@ def mate(self): close_agents = self._get_nearby_agents() if not close_agents: return + matrix, vector = self.get_new_genome(choice(close_agents)) new_agent = Agent((self.parent.x, self.parent.y), self.parent.environment, decision_matrix= matrix, genome= vector, species = self.parent.group_id) energy_level = self.parent.energy / self.parent.max_energy @@ -66,7 +67,6 @@ def get_new_genome(self, second : Agent) -> typing.Tuple[typing.List[typing.List for key,value in vector.items(): vector[key] = value * normalisation - return matrix, vector def create_new_decision_matrix(self, second : Agent) -> typing.List[typing.List[float]]: diff --git a/src/run.py b/src/run.py index dd138ea..c051b92 100644 --- a/src/run.py +++ b/src/run.py @@ -17,7 +17,6 @@ manager = pygame_gui.UIManager((config.WINDOW_WIDTH, config.WINDOW_HEIGHT)) hint_font = pygame.font.Font(None, 22) - def _format_first_sample_age() -> str: age = charts.first_sample_age() if age <= 0: @@ -28,11 +27,11 @@ def _format_first_sample_age() -> str: seconds = int(age % 60) return f"Oldest sample is {minutes}m {seconds}s old" - def _chart_hint_text() -> str: base = "Scroll over the chart area to pan charts" age_text = _format_first_sample_age() return f"{base} — {age_text}" if age_text else base + env_ui = UI( manager=manager, x=config.PANEL_WIDTH + 2*config.PANEL_X, diff --git a/src/sense.py b/src/sense.py index d9c576a..bed91df 100644 --- a/src/sense.py +++ b/src/sense.py @@ -117,14 +117,11 @@ def get_area_type(self): return ground def sense(self, foods=None, agents=None ): - - hp_n, en_n, age_n = self.get_agent_stats() food_sin, food_cos, food_distance, food_in_sight = self.get_closest_food(foods) friend_count_n, friend_cord_sin, friend_cord_cos, friend_distance, friend_in_sight, enemy_count_n, enemy_cord_sin, enemy_cord_cos, enemy_distance, enemy_in_sight = self.get_close_agents(agents) area_type = self.get_area_type() - return [ hp_n, en_n, From 03c60ee3d7123348f9823f54a44f5e918f778741 Mon Sep 17 00:00:00 2001 From: Szymon Cichowski Date: Mon, 26 Jan 2026 12:07:18 +0100 Subject: [PATCH 3/3] Update README.md --- README.md | 79 ++++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 66 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 41fe0ad..0e5cb82 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,71 @@ -# Simulation project (real name TBD) +# **Evolution Simulation** -# Notes: -* there is a github action that runs pylint on every pull request +A Python-based evolutionary simulation where autonomous agents navigate a grid-based environment, consume food sources, reproduce, and evolve over time. The project uses neural network decision making for agent behavior and tracks population statistics through real time charts. -# Libraries used -- pygame -- pygame_gui -- yaml +## Features + +- **Agent-Based Simulation**: Autonomous agents with neural network brains making movement decisions +- **Evolutionary Dynamics**: Agents reproduce with genetic crossover and mutations, leading to different behaviors +- **Dynamic Environment**: Multiple food sources (grass patches, berry bushes, fruit trees, cactus pads) with regeneration +- **Real Time Visualization**: Pygame-based UI showing the grid state and population charts +- **Statistics & Monitoring**: Live charts tracking population age, energy levels, and other metrics +- **Configurable Parameters**: YAML-based configuration for world size, mating rules, mutation rates, and more + +## How It Works + +1. **Agents** spawn in the environment and consume food to gain energy +2. **Decision Making** is governed by neural networks with inputs from food and agent sensing +3. **Mating** occurs when nearby agents of the same group have sufficient energy and age +4. **Evolution** happens through genetic crossover and mutations in offspring +5. **Visualization** displays the grid with agents and food, plus real time population statistics + +## Setup -# Set up ```bash -# (in project folder) -python3 -m venv venv # only the first time +# Create virtual environment (first time only) +python3 -m venv venv + +# Activate virtual environment +# On Linux/Mac: source venv/bin/activate -pip3 install -r requirements.txt # only the first time and when requirements.txt changes -python3 src/run.py # to run -``` \ No newline at end of file +# On Windows: +venv\Scripts\activate + +# Install dependencies +pip3 install -r requirements.txt + +# Run the simulation +python3 src/run.py +``` + +## Dependencies + +- **pygame** - Graphics and event handling +- **pygame_gui** - UI components +- **PyYAML** - Configuration file parsing +- **python-i18n** - Internationalization support + +## Configuration + +Edit [`config.yaml`](config.yaml) to adjust: +- Window and panel dimensions +- Grid cell size +- Mating parameters (age threshold, energy level, mutation rates) +- Chart display settings + +## Project Structure + +- `src/agent.py` - Agent class with neural network decision making and genetics +- `src/environment.py` - Main simulation state and update logic +- `src/area.py` - Grid-based spatial representation +- `src/food.py` - Food source types and behavior +- `src/mating.py` - Reproduction and genetic inheritance logic +- `src/terrain.py` - World generation and topology +- `src/ui.py` - User interface management +- `src/charts.py` - Statistics and visualization +- `src/sense.py` - Agent sensory input processing + +## Notes + +- GitHub Actions runs pylint checks on all pull requests +- The simulation uses torus topology (wraps at edges)