diff --git a/config.yaml b/config.yaml index ff7b957..407e53b 100644 --- a/config.yaml +++ b/config.yaml @@ -4,7 +4,7 @@ window: panel: width: 700 height: 700 - x: 50 + x: 100 y: 50 grid: cell_size: 25 \ No newline at end of file diff --git a/src/area.py b/src/area.py index 93d09e2..5daf8c3 100644 --- a/src/area.py +++ b/src/area.py @@ -1,4 +1,5 @@ from enum import Enum +import food class Area(Enum): PLAINS = {"id": 1, @@ -39,3 +40,10 @@ def __init__(self, config): self.expansion_chance = config["expansion_chance"] self.color = config["color"] self.max_food_sources = config["max_food_sources"] + +AREA_FOOD_SOURCE_MAPPING = { + Area.PLAINS: food.SimpleGrassPatch, + Area.FERTILE_VALLEY: food.FertileFruitTree, + Area.DESERT: food.CactusPads, + Area.BERRY_CORNER: food.BerryBush +} \ No newline at end of file diff --git a/src/environment.py b/src/environment.py index 52c52d7..7220f1d 100644 --- a/src/environment.py +++ b/src/environment.py @@ -5,10 +5,11 @@ import random from typing import List, Tuple import pygame -from food import SimpleGrassPatch, Food, BerryBush, FertileFruitTree, CactusPads +from food import FoodSource, SimpleGrassPatch, Food, BerryBush, FertileFruitTree, CactusPads from agent import Agent from area import Area from terrain import generate_terrain +from typing import Type class Environment: """ @@ -21,6 +22,7 @@ class Environment: def __init__(self, grid_width: int, grid_height: int, pixel_width: int, pixel_height: int): self.tick_counter = 0 self.running = True + self.pause_sim = False self.grid_width = grid_width self.grid_height = grid_height @@ -56,6 +58,46 @@ def _create_empty_grid(self): [0 for _ in range(self.grid_width)] for _ in range(self.grid_height) ] + def _cleanup_at(self, x: int, y: int): + """Deletes food source and all food it has generated at location (x, y).""" + with self.data_lock: + for fs in self.food_sources: + if fs.x == x and fs.y == y: + fs.destroy() + + self.food_sources = [fs for fs in self.food_sources if not fs.is_destroyed] + + self.food_items = [f for f in self.food_items if not (f.x == x and f.y == y)] + + def add_manual_food_source(self, grid_x: int, grid_y: int, source_type: Type[FoodSource]): + mapping = { + SimpleGrassPatch: Area.PLAINS, + BerryBush: Area.BERRY_CORNER, + FertileFruitTree: Area.FERTILE_VALLEY, + CactusPads: Area.DESERT + } + + if source_type not in mapping: + return + cls, target_area = source_type, mapping[source_type] + + self._cleanup_at(grid_x, grid_y) + + with self.data_lock: + if 0 <= grid_x < self.grid_width and 0 <= grid_y < self.grid_height: + self.terrain[grid_y][grid_x] = target_area + new_source = cls(position=(grid_x, grid_y), + area=target_area, + env_area_counters=self.area_food_sources) + self.food_sources.append(new_source) + self.area_food_sources[target_area] += 1 + + def change_area_at(self, grid_x: int, grid_y: int, new_area: Area): + self._cleanup_at(grid_x, grid_y) + + with self.data_lock: + if 0 <= grid_x < self.grid_width and 0 <= grid_y < self.grid_height: + self.terrain[grid_y][grid_x] = new_area def is_food_source_at(self, x: int, y: int) -> bool: """Returns if food_source at location""" return any(fs.x == x and fs.y == y and not fs.is_destroyed for fs in self.food_sources) @@ -126,6 +168,10 @@ def _spawn_initial_agents(self): def _simulation_loop(self): while self.running: + time.sleep(0.01) + + if self.pause_sim: + continue with self.data_lock: self.tick_counter += 1 @@ -176,7 +222,6 @@ def _simulation_loop(self): agent.update(speed_modifier) self._feed_agent(agent) - time.sleep(0.01) def set_grid_cell(self, x: int, y: int, value: int): if 0 <= x < self.grid_width and 0 <= y < self.grid_height: @@ -225,9 +270,15 @@ def shutdown(self): if self.simulation_thread.is_alive(): self.simulation_thread.join(timeout=1.0) + def pause(self): + self.pause_sim = True + + def resume(self): + self.pause_sim = False + def get_agents(self): return self.agents - + #This function should deal with creating new agents def create_agent(self, agent : Agent): pass diff --git a/src/run.py b/src/run.py index 6774300..8ad05f1 100644 --- a/src/run.py +++ b/src/run.py @@ -1,17 +1,26 @@ """Main application entry point for the simulation.""" import pygame import pygame_gui +from pygame_gui.elements import UIButton, UIPanel import config from environment import Environment - +from area import Area +from food import FoodSource +from ui import UI pygame.init() pygame.display.set_caption("Simulation") screen = pygame.display.set_mode((config.WINDOW_WIDTH, config.WINDOW_HEIGHT)) clock = pygame.time.Clock() - manager = pygame_gui.UIManager((config.WINDOW_WIDTH, config.WINDOW_HEIGHT)) +env_ui = UI( + manager=manager, + x=config.PANEL_WIDTH + 2*config.PANEL_X, + y=0, + width=config.WINDOW_WIDTH - (config.PANEL_WIDTH + 2*config.PANEL_X), + height=config.WINDOW_HEIGHT +) env = Environment( grid_width=config.GRID_WIDTH, @@ -23,13 +32,28 @@ running = True while running: dt = clock.tick(60) / 1000.0 + mouse_pos = pygame.mouse.get_pos() for event in pygame.event.get(): if event.type == pygame.QUIT: env.shutdown() running = False + if event.type == pygame.MOUSEBUTTONDOWN: + sim_rect = pygame.Rect(config.PANEL_X, + config.PANEL_Y, + config.PANEL_WIDTH, + config.PANEL_HEIGHT) + if sim_rect.collidepoint(mouse_pos): + grid_x = (mouse_pos[0] - config.PANEL_X) // config.CELL_SIZE + grid_y = (mouse_pos[1] - config.PANEL_Y) // config.CELL_SIZE + if env_ui.current_brush: + if isinstance(env_ui.current_brush, Area): + env.change_area_at(grid_x, grid_y, env_ui.current_brush) + elif isinstance(env_ui.current_brush, type(FoodSource)): + env.add_manual_food_source(grid_x, grid_y, env_ui.current_brush) manager.process_events(event) + env_ui.process_events(event, env) manager.update(dt) @@ -43,6 +67,7 @@ ) manager.draw_ui(screen) + pygame.display.flip() pygame.quit() diff --git a/src/ui.py b/src/ui.py new file mode 100644 index 0000000..453cfad --- /dev/null +++ b/src/ui.py @@ -0,0 +1,226 @@ +import pygame +import pygame_gui +from environment import Environment +from area import Area, AREA_FOOD_SOURCE_MAPPING +from agent import Agent + +class UI: + def __init__(self, manager: pygame_gui.UIManager, x: int, y: int, width: int, height: int): + self.current_brush = None + self.manager = manager + + margin = 10 + btn_height = 40 + self.panel = pygame_gui.elements.UIPanel( + relative_rect=pygame.Rect(x, y, width, height), + manager=self.manager, + margins={ + 'top': margin, + 'right': margin, + 'bottom': margin, + 'left': margin + } + ) + + # spawn ui section + self.spawn_ctrl_container = pygame_gui.elements.UIAutoResizingContainer( + relative_rect=pygame.Rect(0, 0, self.panel.get_abs_rect().width - 2 * margin, 0), + manager=self.manager, + container=self.panel + ) + + self.spawn_label = pygame_gui.elements.UILabel( + relative_rect=pygame.Rect(0, 0, -1, -1), + text='Spawn', + manager=self.manager, + container=self.spawn_ctrl_container, + anchors={ + 'centerx': 'centerx' + } + ) + + self.spawn_agent_btn = pygame_gui.elements.UIButton( + relative_rect=pygame.Rect(0, margin, self.spawn_ctrl_container.get_abs_rect().width, btn_height), + text='Agent', + manager=self.manager, + container=self.spawn_ctrl_container, + anchors={ + 'top_target': self.spawn_label + } + ) + + self.spawn_food_source_container = pygame_gui.elements.UIAutoResizingContainer( + relative_rect=pygame.Rect(0, 0, self.spawn_ctrl_container.get_abs_rect().width, 0), + manager=self.manager, + container=self.spawn_ctrl_container, + anchors={ + 'top_target': self.spawn_agent_btn + } + ) + + self.spawn_food_source_btns = dict() + y_offset = margin + for area in list(Area): + btn = pygame_gui.elements.UIButton( + relative_rect=pygame.Rect(0, y_offset, btn_height, btn_height), + text='', + manager=self.manager, + container=self.spawn_food_source_container, + + ) + btn.colours['normal_bg'] = pygame.Color(area.color) + btn.rebuild() + label = pygame_gui.elements.UILabel( + relative_rect=pygame.Rect(margin, y_offset, -1, btn_height), + text=AREA_FOOD_SOURCE_MAPPING[area].__name__, + manager=self.manager, + container=self.spawn_food_source_container, + anchors={ + 'left_target': btn + } + ) + self.spawn_food_source_btns[btn] = AREA_FOOD_SOURCE_MAPPING[area] + y_offset += margin + btn_height + + # set area ui section + self.area_ctrl_container = pygame_gui.elements.UIAutoResizingContainer( + relative_rect=pygame.Rect(0, margin, self.panel.get_abs_rect().width - 2 * margin, 200), + manager=self.manager, + container=self.panel, + anchors={ + 'top_target': self.spawn_ctrl_container + } + ) + + self.area_label = pygame_gui.elements.UILabel( + relative_rect=pygame.Rect(0, 0, -1, -1), + text='Area', + manager=self.manager, + container=self.area_ctrl_container, + anchors={ + 'centerx': 'centerx' + } + ) + + self.set_area_btns_container = pygame_gui.elements.UIAutoResizingContainer( + relative_rect=pygame.Rect(0, 0, self.area_ctrl_container.get_abs_rect().width, 0), + manager=self.manager, + container=self.area_ctrl_container, + anchors={ + 'top_target': self.area_label + } + ) + + self.set_area_btns = dict() + y_offset = margin + for area in list(Area): + btn = pygame_gui.elements.UIButton( + relative_rect=pygame.Rect(0, y_offset, btn_height, btn_height), + text='', + manager=self.manager, + container=self.set_area_btns_container, + + ) + btn.colours['normal_bg'] = pygame.Color(area.color) + btn.rebuild() + label = pygame_gui.elements.UILabel( + relative_rect=pygame.Rect(margin, y_offset, -1, btn_height), + text=area.display_name, + manager=self.manager, + container=self.set_area_btns_container, + anchors={ + 'left_target': btn + } + ) + self.set_area_btns[btn] = area + y_offset += margin + btn_height + + + # sim control ui section + self.sim_ctrl_container = pygame_gui.core.UIContainer( + relative_rect=pygame.Rect(0, -btn_height, self.panel.get_abs_rect().width - 2 * margin, btn_height), + manager=manager, + container=self.panel, + anchors={ + 'bottom': 'bottom' + } + ) + + self.sim_reset_btn = pygame_gui.elements.UIButton( + relative_rect=pygame.Rect(0, 0, (self.sim_ctrl_container.get_abs_rect().width - 2 * margin) / 3, btn_height), + text='Reset', + manager=self.manager, + container=self.sim_ctrl_container + ) + + self.sim_pause_btn = pygame_gui.elements.UIButton( + relative_rect=pygame.Rect(margin, 0, (self.sim_ctrl_container.get_abs_rect().width - 2 * margin) / 3, btn_height), + text='Pause', + manager=self.manager, + container=self.sim_ctrl_container, + anchors={ + 'left_target': self.sim_reset_btn + } + ) + + self.sim_resume_btn = pygame_gui.elements.UIButton( + relative_rect=pygame.Rect(margin, 0, (self.sim_ctrl_container.get_abs_rect().width - 2 * margin) / 3, btn_height), + text='Resume', + manager=self.manager, + container=self.sim_ctrl_container, + anchors={ + 'left_target': self.sim_pause_btn + } + ) + + # current action label + self.action_container = pygame_gui.elements.UIAutoResizingContainer( + relative_rect=pygame.Rect(0, -btn_height, self.panel.get_abs_rect().width - 2 * margin, 0), + manager=self.manager, + container=self.panel, + anchors={ + 'bottom': 'bottom', + 'bottom_target': self.sim_ctrl_container + } + ) + + self.action_label = pygame_gui.elements.UILabel( + relative_rect=pygame.Rect(0, 0, -1, -1), + text='Current brush:', + manager=self.manager, + container=self.action_container, + ) + + self.action_value = pygame_gui.elements.UILabel( + relative_rect=pygame.Rect(margin, 0, -1, -1), + text='', + manager=self.manager, + container=self.action_container, + anchors={ + 'left_target': self.action_label + } + ) + + def process_events(self, event: pygame.Event, env: Environment): + if event.type == pygame_gui.UI_BUTTON_PRESSED: + if event.ui_element == self.sim_resume_btn: + env.resume() + elif event.ui_element == self.sim_pause_btn: + env.pause() + elif event.ui_element == self.sim_reset_btn: + # TODO reset simulation + pass + elif event.ui_element == self.spawn_agent_btn: + self.change_brush(Agent, 'Agent') + elif event.ui_element in self.set_area_btns: + area = self.set_area_btns[event.ui_element] + self.change_brush(area, area.display_name) + elif event.ui_element in self.spawn_food_source_btns: + food_source = self.spawn_food_source_btns[event.ui_element] + self.change_brush(food_source, food_source.__name__) + + def change_brush(self, new_brush, brush_name: str): + self.current_brush = new_brush + self.action_value.set_text(brush_name) + +