diff --git a/main.py b/main.py index 4875e31..ba665ca 100644 --- a/main.py +++ b/main.py @@ -12,6 +12,12 @@ from src.genome import Genome from src.globals import SCREEN_WIDTH, SCREEN_HEIGHT from src.environment import Environment, GroundType +from src.render_object import RenderObject +from src.interface import Button, Interface +from src.agent_parts.limb import Limb +from src.agent_parts.rectangle import Point, Rectangle, rectangle_factory + + from src.agent_parts.rectangle import Point from src.genetic_algoritm import GeneticAlgorithm from src.globals import ( @@ -86,10 +92,99 @@ def create_creature(in_nodes: int,out_nodes: int): screen_width, screen_height = SCREEN_WIDTH, SCREEN_HEIGHT screen = pygame.display.set_mode((screen_width, screen_height)) pygame.display.set_caption("Pymunk Rectangle Physics") - + interface = Interface() + + + # Track whether physics is on or off + physics_on = False + physics_value = 0 + + def handle_physics(): + nonlocal physics_on + nonlocal physics_value + physics_value = 1/60.0 if physics_value == 0 else 0 + physics_on = True if physics_value != 0 else False + print("Physics Enabled" if physics_value != 0 else "Physics Disabled") + + font = pygame.font.Font(None, 20) + + pause_button = Button( + text="Pause", + pos=(10, 10), + width=100, + height=30, + font=font, + color=(70, 130, 180), + hover_color=(100, 149, 237), + text_color=(255, 255, 255), + active_color=(200, 100, 100), + callback=handle_physics + ) + + make_limb_mode = False + + def make_limb(): + nonlocal make_limb_mode + make_limb_mode = not make_limb_mode + + limb_button = Button( + text="Add limb", + pos=(10, 50), + width=100, + height=30, + font=font, + color=(70, 130, 180), + hover_color=(100, 149, 237), + text_color=(255, 255, 255), + active_color=(200, 100, 100), + callback=make_limb + ) + + interface.add_button(pause_button) + + + environment = Environment(screen) + environment.ground_type = GroundType.BASIC_GROUND + + # Set up the Pymunk space space = pymunk.Space() space.gravity = (0, 981) # Gravity pointing downward + for environment_segment in environment.ground.terrain_segments: + environment_segment.init_pymunk_polygon(space) + + creature = Creature(space) + + # Add limbs to the creature, placing them above the ground + #limb1 = creature.add_limb(100, 20, (300, 100), mass=1) # Positioned above the ground + #limb2 = creature.add_limb(100, 20, (350, 100), mass=1) # Positioned above the ground + #limb3 = creature.add_limb(110, 20, (400, 100), mass=5) + + # Add a motor between limbs + #creature.add_motor(limb1, limb2, (50, 0), (-25, 0), rate=2, tolerance=30) + #creature.add_motor(limb2, limb3, (37, 0), (-23, 0), rate=-2, tolerance=50) + + # Add limbs to the creature + limb1 = creature.add_limb(100, 20, (300, 300), mass=1) + limb2 = creature.add_limb(100, 20, (350, 300), mass=3) + limb3 = creature.add_limb(80, 40, (400, 300), mass=5) + + # Add motors between limbs + #creature.add_motor(limb1, limb2, (25, 0), (-25, 0), rate=2) + #creature.add_motor(limb2, limb3, (37, 0), (-23, 0), rate=-2) + creature.add_motor_on_limbs(limb1, limb2, (325, 300)) + creature.add_motor_on_limbs(limb2, limb3, (375, 300)) + + #dragging creature properties + dragging = False + dragged_limb = None + drag_offset = [] + + # creating rectangles properties + start_pos = None + end_pos = None + limbs = [] + environment = Environment(screen, space) environment.ground_type = GroundType.PERLIN @@ -109,18 +204,71 @@ def create_creature(in_nodes: int,out_nodes: int): for event in pygame.event.get(): if event.type == pygame.QUIT: running = False + interface.handle_events(event) if event.type == pygame.KEYDOWN: if event.key == pygame.K_LEFT: print("Left arrow pressed") if event.key == pygame.K_RIGHT: print("Right arrow pressed") - - space.step(1/60.0) + if event.key == pygame.K_SPACE: + handle_physics() + elif event.type == pygame.MOUSEBUTTONDOWN: + if not physics_on: + mouse_x, mouse_y = event.pos + mouse_pos = (mouse_x, mouse_y) + # For dragging creature: Check if the mouse is over any limb + if not make_limb_mode: + for limb in creature.limbs: + if limb.contains_point(mouse_pos): + dragging = True + dragged_limb = limb + creature.start_dragging(dragged_limb) + drag_offset = (limb.body.position.x - mouse_x, limb.body.position.y - mouse_y) + break + # For creating rectangles + elif make_limb_mode: + start_pos = mouse_pos + + elif event.type == MOUSEMOTION and make_limb_mode: + mouse_x, mouse_y = event.pos + mouse_pos = (mouse_x, mouse_y) + end_pos = mouse_pos + + elif event.type == pygame.MOUSEBUTTONUP: + dragging = False + if make_limb_mode and start_pos and end_pos: + width = abs(start_pos[0] - end_pos[0]) + height = abs(start_pos[1] - end_pos[1]) + position = ((start_pos[0] + end_pos[0]) / 2, (start_pos[1] + end_pos[1]) / 2) + limb = creature.add_limb(width, height, position) + + # Reset start and end positions + start_pos = None + end_pos = None + + + space.step(physics_value) + + if dragging and dragged_limb and not make_limb_mode: + mouse_x, mouse_y = pygame.mouse.get_pos() + new_position = (mouse_x + drag_offset[0], mouse_y + drag_offset[1]) + creature.update_creature_position(dragged_limb, new_position) screen.fill((135, 206, 235)) - environment.update() environment.render() + + #creature.set_joint_rates([random.random()*2, random.random()*2]) + # Render the creature + creature.render(screen) + + if not physics_on: + interface.add_button(limb_button) + else: + interface.remove_button(limb_button) + interface.render(screen) + + # TODO: vision should be part of a creature, and not environment inputs = np.array([environment.vision.get_near_periphery().x, @@ -170,9 +318,5 @@ def create_creature(in_nodes: int,out_nodes: int): - - - - if __name__ == "__main__": main() \ No newline at end of file diff --git a/src/agent_parts/creature.py b/src/agent_parts/creature.py index a99bb30..4b44c81 100644 --- a/src/agent_parts/creature.py +++ b/src/agent_parts/creature.py @@ -11,18 +11,52 @@ def __init__(self, space): self.space = space self.limbs = [] self.motors = [] + self.relative_vectors = [] def add_limb(self, width: float, height: float, position: tuple[float,float], mass=1, color=(0, 255, 0)) -> Limb: """Add a limb to the creature.""" limb = Limb(self.space, width, height, position, mass, color) self.limbs.append(limb) return limb + + def start_dragging(self, dragged_limb: Limb): + for limb in self.limbs: + if limb != dragged_limb: + vector = (limb.body.position.x - dragged_limb.body.position.x, + limb.body.position.y - dragged_limb.body.position.y) + self.relative_vectors.append((limb, vector)) + + + def update_creature_position(self, dragged_limb: Limb, new_position: tuple[float, float]): + dragged_limb.body.position = new_position[0], new_position[1] + for limb, vector in self.relative_vectors: + new_position = (dragged_limb.body.position.x + vector[0], + dragged_limb.body.position.y + vector[1]) + limb.body.position = new_position + - def add_motor(self, limb_a: Limb, limb_b: Limb, anchor_a: tuple[float,float], anchor_b: tuple[float,float], rate: float, tolerance: float) -> MotorJoint|None: + + def add_motor_on_limbs(self, limb_a: Limb, limb_b: Limb, position: tuple[float, float]) -> MotorJoint|None: + if(limb_a.contains_point(position) and limb_b.contains_point(position)): + anchor1 = limb_a.global_to_local(position) + anchor2 = limb_b.global_to_local(position) + print("true") + return self.add_motor(limb_a, limb_b, anchor1, anchor2, 2.0) + else: + print("false") + return None + + + def add_motor(self, limb_a: Limb, limb_b: Limb, anchor_a: tuple[float,float], anchor_b: tuple[float,float], rate = 0.0, tolerance = 30) -> MotorJoint|None: """Add a motor connecting two limbs.""" - if(abs(limb_a.body.position + anchor_a - limb_b.body.position + anchor_b) < tolerance): + global_a = self.local_to_global(limb_a, anchor_a) + global_b = self.local_to_global(limb_b, anchor_b) + + # Check if the global points are within the tolerance + if abs(global_a[0] - global_b[0]) < tolerance and abs(global_a[1] - global_b[1]) < tolerance: motor = MotorJoint(self.space, limb_a.body, limb_b.body, anchor_a, anchor_b, rate) self.motors.append(motor) + print("add_motor: true") return motor def render(self, screen: pygame.display): diff --git a/src/agent_parts/limb.py b/src/agent_parts/limb.py index e49b572..b1da1c1 100644 --- a/src/agent_parts/limb.py +++ b/src/agent_parts/limb.py @@ -34,6 +34,6 @@ def render(self, screen): vertices = self.shape.get_vertices() vertices = [v.rotated(angle) + pos for v in vertices] # Convert pymunk Vec2d vertices to pygame coordinates - vertices = [(int(v.x), int(v.y)) for v in vertices] + vertices = [(float(v.x), float(v.y)) for v in vertices] # Draw the polygon onto the screen pygame.draw.polygon(surface=screen, color=(0, 255, 0), points=vertices, width=0) \ No newline at end of file diff --git a/src/interface.py b/src/interface.py new file mode 100644 index 0000000..2d536d9 --- /dev/null +++ b/src/interface.py @@ -0,0 +1,65 @@ +import pygame + +class Button: + """General button functions""" + + def __init__(self, text, pos, width, height, font, color, hover_color, text_color, active_color = None, callback=None): + """Initializes a button with the function name as input""" + self.text = text + self.pos = pos + self.width = width + self.height = height + self.font = font + self.color = color + self.hover_color = hover_color + self.text_color = text_color + self.active_color = active_color if active_color else color + self.rect = pygame.Rect(pos[0], pos[1], width, height) + self.callback = callback + self.toggled = False + + def render(self, screen): + """mouse_pos = pygame.mouse.get_pos() + is_hovered = self.rect.collidepoint(mouse_pos) + color = self.hover_color if is_hovered else self.color""" + current_color = self.active_color if self.toggled else self.color + pygame.draw.rect(screen, current_color, self.rect) + text_surf = self.font.render(self.text, True, self.text_color) + text_rect = text_surf.get_rect(center=self.rect.center) + screen.blit(text_surf, text_rect) + + def is_clicked(self, event): + if event.type == pygame.MOUSEBUTTONDOWN and event.button == 1: + if self.rect.collidepoint(event.pos): + self.toggled = not self.toggled + if self.callback: # Check if there’s a function to call + self.callback() # Call the button's callback function + return True + return False + + +class Interface: + def __init__(self): + """Initializes the elements in the interface""" + self.elements = [] + + def add_button(self, button: Button) -> Button: + self.elements.append(button) + return button + + def remove_button(self, button: Button) -> Button: + self.elements.remove(button) + return button + + def render(self, screen): + """Render all UI elements.""" + for element in self.elements: + element.render(screen) + + def handle_events(self, event): + """Handle events for all UI elements.""" + for element in self.elements: + if isinstance(element, Button): + element.is_clicked(event) # This will call the callback if the button is clicked + + \ No newline at end of file diff --git a/tests/test_enviroment.py b/tests/test_enviroment.py index 4ca390b..13e04f9 100644 --- a/tests/test_enviroment.py +++ b/tests/test_enviroment.py @@ -1,7 +1,7 @@ -from src.enviroment import Enviroment +from src.environment import Environment def test_create_enviroment(): - env = Enviroment(state="Env") + env = Environment(state="Env") assert env is not None