diff --git a/fitness_plot.png b/fitness_plot.png index 1c6c9f3..a050b16 100644 Binary files a/fitness_plot.png and b/fitness_plot.png differ diff --git a/gameobjects.py b/gameobjects.py index a7f7c34..e0aa98e 100644 --- a/gameobjects.py +++ b/gameobjects.py @@ -6,6 +6,10 @@ HUD_HEIGHT = 150 BARRIER_SEED = 20 class GameObject: + """ + Base class for all game objects. + Handles image loading, positioning, drawing, and child objects. + """ x = 0 y = 0 @@ -17,62 +21,70 @@ class GameObject: self.offset = offset def draw(self, surface): + # Draw self and all children recursively surface.blit(self.image, self.rect) - if len(self.children) == 0: + if not self.children: return for child in self.children: child.draw(surface) def set_position(self, x, y): + # Set position and update children positions with offsets self.rect.x = x self.rect.y = y self.x = x self.y = y - if len(self.children) == 0: + if not self.children: return for child in self.children: child.set_position(x + child.offset[0], y + child.offset[1]) def move(self, dx, dy): + # Move object by delta and update children accordingly self.set_position(self.rect.x + dx, self.rect.y + dy) - if len(self.children) == 0: + if not self.children: return for child in self.children: self.set_position(self.rect.x + dx, self.rect.y + dy) def set_size(self, width, height): + # Resize image and update rect self.image = pygame.transform.scale(self.image, (width, height)) self.rect = self.image.get_rect(topleft=(self.x, self.y)) def scale_by(self, scale): - self.image = pygame.transform.scale(self.image, (self.image.get_width() * scale, self.image.get_height() * scale)) + # Scale image by a factor + new_size = (self.image.get_width() * scale, self.image.get_height() * scale) + self.image = pygame.transform.scale(self.image, new_size) self.rect = self.image.get_rect(topleft=(self.x, self.y)) def get_position(self): return (self.x, self.y) def set_children(self, child): + # Add a child GameObject self.children.append(child) def recolor(self, color): + # Apply color tint to image self.image.fill(color, special_flags=pygame.BLEND_MULT) self.image.set_colorkey(None) class Horse(GameObject): + """ + Represents a horse character with vertical movement, animation, and fitness tracking. + Supports acceleration, speed, and stopping. + """ default_vacceleration = 0.2 - vspeed = 0 - vacceleration = 0 - stopped = False - color = () - ribbon_fill = None - ribbon_out = None def __init__(self, image_path, x, y): super().__init__(image_path, x, y) - self.images = [] - self.images.append(pygame.image.load("images/Horse_1.png")) - self.images.append(pygame.image.load("images/Horse_2.png")) - self.images.append(pygame.image.load("images/Horse_3.png")) + # Load animation frames + self.images = [ + pygame.image.load("images/Horse_1.png"), + pygame.image.load("images/Horse_2.png"), + pygame.image.load("images/Horse_3.png") + ] self.animation_speed = 25 self.frame_counter = 0 self.current_frame = 0 @@ -80,7 +92,8 @@ class Horse(GameObject): self.vacceleration = 0 self.stopped = False self.fitness = 0 - self.color = (random.randint(0, 255), random.randint(0, 255), random.randint(0, 255)) + # Assign random color for ribbons + self.color = (random.randint(0,255), random.randint(0,255), random.randint(0,255)) self.ribbon_fill = GameObject("images/Ribbon_fill.png", self.x, self.y, (16, -1)) self.ribbon_fill.recolor(self.color) self.ribbon_out = GameObject("images/Ribbon_out.png", self.x, self.y, (14, -2)) @@ -88,10 +101,11 @@ class Horse(GameObject): self.set_children(self.ribbon_out) def apply_vspeed(self): - pos = self.get_position() - self.set_position(pos[0], pos[1] + self.vspeed) + # Move vertically by current speed + self.set_position(self.x, self.y + self.vspeed) def apply_vacceleration(self): + # Update speed by acceleration self.vspeed += self.vacceleration def set_vspeed(self, speed): @@ -104,23 +118,25 @@ class Horse(GameObject): self.vacceleration = vacceleration def up(self): + # Accelerate upward self.set_vacceleration(-self.default_vacceleration) def down(self): + # Accelerate downward self.set_vacceleration(self.default_vacceleration) def stay(self): + # No vertical acceleration self.set_vacceleration(0) def stop(self): + # Stop all movement self.stopped = True self.set_vacceleration(0) self.set_vspeed(0) - def draw(self, surface): - super().draw(surface) - def update_animation(self): + # Animate horse by cycling frames self.frame_counter += 1 if self.frame_counter >= self.animation_speed: self.frame_counter = 0 @@ -128,21 +144,31 @@ class Horse(GameObject): self.image = self.images[self.current_frame] def count_fitness(self): - if self.stopped == False: + # Increase fitness if moving; penalize if speed zero + if not self.stopped: self.fitness += 1 if self.vspeed == 0: self.fitness -= 0.9 class Background(GameObject): + """ + Background image for the game scene. + """ def __init__(self, image_path, x, y): super().__init__(image_path, x, y) class Barrier(GameObject): + """ + Obstacles that move horizontally and interact with horses. + """ def __init__(self, image_path, x, y): super().__init__(image_path, x, y) - self.scale_by(5) + self.scale_by(5) # Make barrier larger for visibility class UI: + """ + Handles HUD display and horse status markers. + """ iteration_num = 0 horses = [] horizontal_offset = 25 @@ -155,34 +181,38 @@ class UI: self.hud_rect = self.hud_image.get_rect(topleft=(x, y)) def add_horses(self, children): - new_horses = [child for child in children if child not in self.horses] - self.horses.extend(new_horses) + # Add horses to UI tracking list + self.horses.extend([child for child in children if child not in self.horses]) def draw_marks(self, screen): - active_marks = [horse for horse in self.horses if not horse.stopped] - unactive_marks = [horse for horse in self.horses if horse.stopped] - width = 10 - xa = 0 - ya = HEIGHT + HUD_HEIGHT - self.mark_size - for i, horse in enumerate(active_marks): + # Draw colored squares representing active and stopped horses + active = [h for h in self.horses if not h.stopped] + inactive = [h for h in self.horses if h.stopped] + + xa, ya = 0, HEIGHT + HUD_HEIGHT - self.mark_size + for i, horse in enumerate(active): pygame.draw.rect(screen, horse.color, (xa, ya, self.mark_size, self.mark_size)) xa += self.horizontal_offset - if (i + 1) % width == 0: + if (i+1) % 10 == 0: ya += self.vertical_offset xa = 0 - xu = WIDTH - self.mark_size - yu = HEIGHT + HUD_HEIGHT - self.mark_size - for i, horse in enumerate(unactive_marks): + + xu, yu = WIDTH - self.mark_size, HEIGHT + HUD_HEIGHT - self.mark_size + for i, horse in enumerate(inactive): pygame.draw.rect(screen, horse.color, (xu, yu, self.mark_size, self.mark_size)) xu -= self.horizontal_offset - if (i + 1) % width == 0: + if (i+1) % 10 == 0: yu += self.vertical_offset xu = WIDTH - self.mark_size def draw(self, surface): + # Draw HUD image surface.blit(self.hud_image, self.hud_rect) class Spawner: + """ + Spawns barriers at intervals to challenge horses. + """ active = True delay = 500 tick_counter = 0 @@ -192,6 +222,7 @@ class Spawner: self.delay = delay def handle(self): + # Increment tick counter and spawn barrier when delay reached if self.active: self.tick_counter += 1 if self.tick_counter >= self.delay: @@ -199,6 +230,5 @@ class Spawner: self.tick_counter = 0 def spawn(self): - random_y = random.randint(0, HEIGHT - 100) - new_barrier = Barrier(self.barrier_image_path, WIDTH, random_y) - return new_barrier + # Create a new barrier at a random vertical position on the right edge + return Barrier(self.barrier_image_path, WIDTH, random.randint(0, HEIGHT - 100)) diff --git a/main.py b/main.py index df35227..da350e7 100644 --- a/main.py +++ b/main.py @@ -7,7 +7,7 @@ matplotlib.use('Agg') import matplotlib.pyplot as plt import random -POPULATION_SIZE = 50 # SET TO 500 IF YOU HAVE A GOOD PC +POPULATION_SIZE = 50 # Number of horses in population MUTATION_RATE = 0.5 POPULATION_NEW = 0.1 POPULATION_BEST = 0.3 @@ -42,6 +42,7 @@ last_barrier = None def init_game(): global gameobjects, horses, barriers, upper_bound_rect, lower_bound_rect, spawner, last_barrier, BARRIER_SPEED + # Initialize background and game objects for a new iteration #random.seed(BARRIER_SEED) #SET SEED grass = Background("images/Grass.jpg", 0, 0) grass.set_size(WIDTH, HEIGHT) @@ -49,6 +50,7 @@ def init_game(): gameobjects = [grass] barriers = [] + # Reset horses to starting position and state for horse in horses: horse.set_position(50, HEIGHT / 2) horse.stopped = False @@ -64,6 +66,7 @@ def init_game(): gameobjects.extend(horses) gameobjects.extend(barriers) + # Define upper and lower bounds for horse movement upper_bound_rect = pygame.Rect(0, 0, WIDTH, -10) lower_bound_rect = pygame.Rect(0, HEIGHT, WIDTH, 10) @@ -73,6 +76,7 @@ def init_game(): def get_features(horse: Horse): + # Extract normalized features for neural network input features = [last_barrier.rect.topleft[0], last_barrier.rect.topleft[1], last_barrier.rect.bottomright[0], last_barrier.rect.bottomright[1], horse.rect.topleft[0], horse.rect.topleft[1], @@ -92,6 +96,7 @@ genecticAlg = GeneticAlgorithm(POPULATION_SIZE, MUTATION_RATE, POPULATION_BEST, while True: for event in pygame.event.get(): if event.type == pygame.QUIT: + # Save fitness plot on exit plt.plot(running_fitnesses) plt.xlabel("Iteration") plt.ylabel("Current Fitness") @@ -106,6 +111,7 @@ while True: if not horse.stopped: data = get_features(horse) res = genecticAlg.predict(data, i) + # Control horse movement based on neural network output if res == 0: horse.up() elif res == 1: @@ -113,6 +119,7 @@ while True: else: horse.stay() else: + # Move stopped horses left with barriers horse.move(-BARRIER_SPEED, 0) horse.apply_vacceleration() @@ -121,6 +128,7 @@ while True: spawner.handle() if spawner.tick_counter == 0: + # Spawn new barrier periodically new_barrier = spawner.spawn() barriers.append(new_barrier) gameobjects.append(new_barrier) @@ -133,6 +141,7 @@ while True: horse.update_animation() if not horse.stopped: + # Check collisions with barriers and bounds for barrier in barriers: if horse.rect.colliderect(barrier.rect): horse.stop() @@ -146,8 +155,8 @@ while True: current_fitnesses = [horse.fitness for horse in horses] current_fitness = max(current_fitnesses) - if all(horse.stopped for horse in horses): + # All horses stopped: evolve population and start new iteration UI.iteration_num += 1 genecticAlg.learn([x.fitness for x in horses]) @@ -160,6 +169,7 @@ while True: UI.draw(screen) UI.draw_marks(screen) + # Display iteration and fitness info iteration_num_txt = font.render(f"Iteration: {current_iteration}", True, BLACK) current_fitness_txt = font.render( f"Current Fitness: {current_fitness}", True, BLACK)