diff --git a/src/game.py b/src/game.py index b11f088..a2925e4 100644 --- a/src/game.py +++ b/src/game.py @@ -11,6 +11,13 @@ class GameState(Enum): GAME_OVER = auto() PAUSED = auto() +class GameRules: + def __init__(self): + self.wrap_around = False # Whether snake wraps around screen edges + self.speed_increase = True # Whether snake speeds up as it grows + self.min_move_cooldown = 50 # Minimum movement delay in milliseconds + self.initial_move_cooldown = 100 # Initial movement delay + class Game: def __init__(self): # Initialize pygame and font system @@ -29,6 +36,9 @@ class Game: self.clock = pygame.time.Clock() self.fps = 60 + # Game rules + self.rules = GameRules() + # Game objects self.block_size = 20 self.menu = Menu(self.width, self.height) @@ -49,30 +59,9 @@ class Game: self.food.spawn(self.width, self.height, self.snake.body) self.score = 0 - def draw_grid(self): - """Draw the game grid (debug mode)""" - for x in range(0, self.width, self.block_size): - pygame.draw.line(self.screen, (50, 50, 50), (x, 0), (x, self.height)) - for y in range(0, self.height, self.block_size): - pygame.draw.line(self.screen, (50, 50, 50), (0, y), (self.width, y)) - - def draw_debug_info(self): - """Draw debug information""" - font = pygame.font.Font(None, 24) - debug_info = [ - f"FPS: {int(self.clock.get_fps())}", - f"Snake Length: {len(self.snake.body)}", - f"Snake Head: {self.snake.body[0]}", - f"Food Pos: {self.food.position}", - f"Game State: {self.state.name}", - f"Game Mode: {self.game_mode.name if self.game_mode else 'None'}" - ] - - for i, text in enumerate(debug_info): - surface = font.render(text, True, (0, 255, 0)) - self.screen.blit(surface, (10, self.height - 150 + i * 25)) - def handle_events(self): + current_time = pygame.time.get_ticks() + for event in pygame.event.get(): if event.type == pygame.QUIT: self.running = False @@ -86,6 +75,9 @@ class Game: self.running = False elif event.key == pygame.K_F3: # Toggle debug mode self.debug = not self.debug + elif event.key == pygame.K_w: # Toggle wrap-around mode in debug + if self.debug: + self.rules.wrap_around = not self.rules.wrap_around if self.state == GameState.MENU: # Handle menu input @@ -103,13 +95,13 @@ class Game: elif self.state == GameState.PLAYING: # Handle snake direction if event.key == pygame.K_UP: - self.snake.change_direction(Direction.UP) + self.snake.change_direction(Direction.UP, current_time) elif event.key == pygame.K_DOWN: - self.snake.change_direction(Direction.DOWN) + self.snake.change_direction(Direction.DOWN, current_time) elif event.key == pygame.K_LEFT: - self.snake.change_direction(Direction.LEFT) + self.snake.change_direction(Direction.LEFT, current_time) elif event.key == pygame.K_RIGHT: - self.snake.change_direction(Direction.RIGHT) + self.snake.change_direction(Direction.RIGHT, current_time) elif self.state == GameState.GAME_OVER: # Any key to return to menu in game over state @@ -120,8 +112,12 @@ class Game: if self.state == GameState.PLAYING: # Move snake if self.snake.move(pygame.time.get_ticks()): - # Check wall collision - if self.snake.check_collision(self.width, self.height): + # Handle wrap-around + if self.rules.wrap_around: + self.snake.wrap_position(self.width, self.height) + + # Check collisions + if self.snake.check_collision(self.width, self.height, self.rules.wrap_around): self.state = GameState.GAME_OVER return @@ -130,6 +126,38 @@ class Game: self.snake.grow() self.score += 1 self.food.spawn(self.width, self.height, self.snake.body) + + # Increase speed if enabled + if self.rules.speed_increase: + self.snake.move_cooldown = max( + self.rules.min_move_cooldown, + self.rules.initial_move_cooldown - self.score * 2 + ) + + def draw_grid(self): + """Draw the game grid (debug mode)""" + for x in range(0, self.width, self.block_size): + pygame.draw.line(self.screen, (50, 50, 50), (x, 0), (x, self.height)) + for y in range(0, self.height, self.block_size): + pygame.draw.line(self.screen, (50, 50, 50), (0, y), (self.width, y)) + + def draw_debug_info(self): + """Draw debug information""" + font = pygame.font.Font(None, 24) + debug_info = [ + f"FPS: {int(self.clock.get_fps())}", + f"Snake Length: {len(self.snake.body)}", + f"Snake Head: {self.snake.body[0]}", + f"Food Pos: {self.food.position}", + f"Game State: {self.state.name}", + f"Game Mode: {self.game_mode.name if self.game_mode else 'None'}", + f"Wrap Around: {self.rules.wrap_around}", + f"Move Cooldown: {self.snake.move_cooldown}ms" + ] + + for i, text in enumerate(debug_info): + surface = font.render(text, True, (0, 255, 0)) + self.screen.blit(surface, (10, self.height - 200 + i * 25)) def render(self): # Clear screen diff --git a/src/menu.py b/src/menu.py index 019b1c4..fc92a93 100644 --- a/src/menu.py +++ b/src/menu.py @@ -2,100 +2,128 @@ import pygame from enum import Enum, auto from typing import List, Tuple, Callable -class MenuItem: - def __init__(self, text: str, action: Callable, position: Tuple[int, int] = (0, 0)): - self.text = text - self.action = action - self.position = position - self.is_selected = False - self.color = (255, 255, 255) # Default white - self.selected_color = (0, 255, 0) # Green when selected - self.font = pygame.font.Font(None, 64) - - def draw(self, screen: pygame.Surface): - """Draw the menu item""" - color = self.selected_color if self.is_selected else self.color - text_surface = self.font.render(self.text, True, color) - text_rect = text_surface.get_rect(center=self.position) - screen.blit(text_surface, text_rect) - class GameMode(Enum): PLAYER = auto() AI_EASY = auto() AI_MEDIUM = auto() AI_HARD = auto() + QUIT = auto() + +class MenuItem: + def __init__(self, text, position, action, size=36, color=(255, 255, 255)): + self.text = text + self.position = position + self.action = action + self.size = size + self.color = color + self.hover = False + self._setup_font() + + def _setup_font(self): + self.font = pygame.font.Font(None, self.size) + self.surface = self.font.render(self.text, True, self.color) + self.rect = self.surface.get_rect(center=self.position) + + def update_hover(self, mouse_pos): + self.hover = self.rect.collidepoint(mouse_pos) + if self.hover: + self.color = (255, 255, 0) # Yellow on hover + else: + self.color = (255, 255, 255) # White normally + self._setup_font() + + def draw(self, screen): + screen.blit(self.surface, self.rect) class Menu: - def __init__(self, screen_width: int, screen_height: int): - self.width = screen_width - self.height = screen_height - self.selected_index = 0 - self.items: List[MenuItem] = [] + def __init__(self, width, height): + self.width = width + self.height = height self.setup_menu_items() - + self.title_font = pygame.font.Font(None, 72) + self.subtitle_font = pygame.font.Font(None, 36) + + # Create title surfaces + self.title_surface = self.title_font.render("AI Snake Game", True, (0, 255, 0)) + self.title_rect = self.title_surface.get_rect(center=(width//2, height//4)) + + self.subtitle_surface = self.subtitle_font.render("Choose Game Mode", True, (200, 200, 200)) + self.subtitle_rect = self.subtitle_surface.get_rect(center=(width//2, height//4 + 50)) + def setup_menu_items(self): - """Initialize menu items with their positions""" - # Calculate vertical spacing - start_y = self.height // 3 - spacing = 80 + # Calculate positions for menu items + start_y = self.height // 2 + spacing = 50 center_x = self.width // 2 - - # Create menu items - items_data = [ - ("Player Game", lambda: GameMode.PLAYER), - ("AI Easy", lambda: GameMode.AI_EASY), - ("AI Medium", lambda: GameMode.AI_MEDIUM), - ("AI Hard", lambda: GameMode.AI_HARD), - ("Quit", lambda: None) - ] - - for i, (text, action) in enumerate(items_data): - position = (center_x, start_y + i * spacing) - self.items.append(MenuItem(text, action, position)) - # Select first item - self.items[0].is_selected = True - - def handle_input(self, event: pygame.event.Event) -> GameMode: - """Handle menu navigation and selection""" - if event.type == pygame.KEYDOWN: - if event.key == pygame.K_UP: - # Move selection up - self.items[self.selected_index].is_selected = False - self.selected_index = (self.selected_index - 1) % len(self.items) - self.items[self.selected_index].is_selected = True - elif event.key == pygame.K_DOWN: - # Move selection down - self.items[self.selected_index].is_selected = False - self.selected_index = (self.selected_index + 1) % len(self.items) - self.items[self.selected_index].is_selected = True - elif event.key == pygame.K_RETURN: - # Execute selected item's action - return self.items[self.selected_index].action() + self.menu_items = [ + MenuItem("Player Mode", (center_x, start_y), GameMode.PLAYER), + MenuItem("AI Easy", (center_x, start_y + spacing), GameMode.AI_EASY), + MenuItem("AI Medium", (center_x, start_y + spacing * 2), GameMode.AI_MEDIUM), + MenuItem("AI Hard", (center_x, start_y + spacing * 3), GameMode.AI_HARD), + MenuItem("Quit", (center_x, start_y + spacing * 4), GameMode.QUIT, color=(255, 100, 100)) + ] + + def handle_input(self, event): + if event.type == pygame.MOUSEBUTTONDOWN and event.button == 1: + mouse_pos = pygame.mouse.get_pos() + for item in self.menu_items: + if item.rect.collidepoint(mouse_pos): + return item.action + elif event.type == pygame.KEYDOWN: + if event.key == pygame.K_RETURN: + # Find the currently hovered item + for item in self.menu_items: + if item.hover: + return item.action + elif event.key in (pygame.K_UP, pygame.K_DOWN): + # Find current hover index + current_hover = -1 + for i, item in enumerate(self.menu_items): + if item.hover: + current_hover = i + break + + # Move hover up or down + if current_hover == -1: + new_hover = 0 + else: + if event.key == pygame.K_UP: + new_hover = (current_hover - 1) % len(self.menu_items) + else: + new_hover = (current_hover + 1) % len(self.menu_items) + + # Update hover states + for i, item in enumerate(self.menu_items): + item.hover = (i == new_hover) + item._setup_font() + return None - - def draw(self, screen: pygame.Surface): - """Draw the menu""" - # Draw title - title_font = pygame.font.Font(None, 100) - title_text = title_font.render("AI Snake Game", True, (0, 255, 0)) - title_rect = title_text.get_rect(center=(self.width // 2, 100)) - screen.blit(title_text, title_rect) - - # Draw menu items - for item in self.items: - item.draw(screen) - - # Draw instructions - instruction_font = pygame.font.Font(None, 36) - instructions = [ - "Use UP/DOWN arrows to navigate", - "Press ENTER to select", - "Press ESC to quit" - ] + + def update(self): + mouse_pos = pygame.mouse.get_pos() + for item in self.menu_items: + item.update_hover(mouse_pos) + + def draw(self, screen): + # Draw background + screen.fill((0, 0, 0)) - start_y = self.height - 150 - for i, instruction in enumerate(instructions): - text = instruction_font.render(instruction, True, (128, 128, 128)) - rect = text.get_rect(center=(self.width // 2, start_y + i * 30)) - screen.blit(text, rect) \ No newline at end of file + # Draw title and subtitle + screen.blit(self.title_surface, self.title_rect) + screen.blit(self.subtitle_surface, self.subtitle_rect) + + # Draw menu items + for item in self.menu_items: + item.draw(screen) + + # Draw version and controls + version_text = "v0.1.0" + controls_text = "Arrow keys to navigate, Enter to select" + font = pygame.font.Font(None, 24) + + version_surface = font.render(version_text, True, (100, 100, 100)) + controls_surface = font.render(controls_text, True, (100, 100, 100)) + + screen.blit(version_surface, (10, self.height - 30)) + screen.blit(controls_surface, (self.width - controls_surface.get_width() - 10, self.height - 30)) \ No newline at end of file diff --git a/src/snake.py b/src/snake.py index 27227d7..3daae80 100644 --- a/src/snake.py +++ b/src/snake.py @@ -18,12 +18,22 @@ class Snake: # Movement cooldown self.move_cooldown = 100 # milliseconds self.last_move_time = 0 + + # Direction change cooldown + self.direction_change_cooldown = 50 # milliseconds + self.last_direction_change = 0 + self.queued_direction = None def move(self, current_time: int) -> bool: """Move the snake if enough time has passed. Returns True if moved.""" if current_time - self.last_move_time < self.move_cooldown: return False + # Apply queued direction change if it exists and is valid + if self.queued_direction and current_time - self.last_direction_change >= self.direction_change_cooldown: + self._apply_direction_change(self.queued_direction) + self.queued_direction = None + # Update position head = self.body[0] new_head = self._get_new_head_position(head) @@ -55,14 +65,22 @@ class Snake: """Mark the snake to grow on next move""" self.growing = True - def check_collision(self, width: int, height: int) -> bool: - """Check if snake has collided with walls or itself""" + def check_collision(self, width: int, height: int, wrap_around: bool = False) -> bool: + """ + Check if snake has collided with walls or itself. + + Args: + width: Game area width + height: Game area height + wrap_around: If True, snake wraps around screen edges instead of colliding + """ head = self.body[0] - # Check wall collision - if (head[0] < 0 or head[0] >= width or - head[1] < 0 or head[1] >= height): - return True + if not wrap_around: + # Check wall collision + if (head[0] < 0 or head[0] >= width or + head[1] < 0 or head[1] >= height): + return True # Check self collision (skip head) if head in self.body[1:]: @@ -70,8 +88,17 @@ class Snake: return False - def change_direction(self, new_direction: Direction): - """Change direction if it's not opposite to current direction""" + def wrap_position(self, width: int, height: int): + """Wrap snake's head position around screen edges""" + head_x, head_y = self.body[0] + wrapped_head = ( + head_x % width if head_x != width else 0, + head_y % height if head_y != height else 0 + ) + self.body[0] = wrapped_head + + def _apply_direction_change(self, new_direction: Direction) -> bool: + """Internal method to actually change direction""" opposites = { Direction.UP: Direction.DOWN, Direction.DOWN: Direction.UP, @@ -81,12 +108,36 @@ class Snake: if opposites[new_direction] != self.direction: self.direction = new_direction + return True + return False + + def change_direction(self, new_direction: Direction, current_time: int): + """ + Queue a direction change if it's valid and cooldown has passed. + + Args: + new_direction: The desired new direction + current_time: Current game time in milliseconds + """ + if current_time - self.last_direction_change < self.direction_change_cooldown: + # Queue the direction change for next movement + self.queued_direction = new_direction + return + + if self._apply_direction_change(new_direction): + self.last_direction_change = current_time def draw(self, screen: pygame.Surface): """Draw the snake on the screen""" - for segment in self.body: + # Draw head in a slightly different color + head_color = (0, 200, 0) # Darker green for head + body_color = (0, 255, 0) # Regular green for body + + # Draw body segments + for i, segment in enumerate(self.body): + color = head_color if i == 0 else body_color pygame.draw.rect( screen, - (0, 255, 0), # Green color + color, pygame.Rect(segment[0], segment[1], self.block_size, self.block_size) ) \ No newline at end of file