diff --git a/src/game.py b/src/game.py index a2925e4..f044484 100644 --- a/src/game.py +++ b/src/game.py @@ -7,6 +7,7 @@ from src.menu import Menu, GameMode class GameState(Enum): MENU = auto() + SETTINGS = auto() # New state for settings menu PLAYING = auto() GAME_OVER = auto() PAUSED = auto() @@ -18,6 +19,150 @@ class GameRules: self.min_move_cooldown = 50 # Minimum movement delay in milliseconds self.initial_move_cooldown = 100 # Initial movement delay +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.default_color = color + 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): + old_hover = self.hover + self.hover = self.rect.collidepoint(mouse_pos) + if self.hover: + self.color = (255, 255, 0) # Yellow on hover + else: + self.color = self.default_color + if old_hover != self.hover: + self._setup_font() + + def draw(self, screen): + screen.blit(self.surface, self.rect) + +class SettingsMenu: + def __init__(self, width, height, rules): + self.width = width + self.height = height + self.rules = rules + 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("Settings", True, (0, 255, 0)) + self.title_rect = self.title_surface.get_rect(center=(width//2, height//4)) + + # Initialize first item as hovered + if self.menu_items: + self.menu_items[0].hover = True + self.menu_items[0]._setup_font() + + def setup_menu_items(self): + start_y = self.height // 2 + spacing = 50 + center_x = self.width // 2 + + self.menu_items = [ + MenuItem(f"Wrap Around: {'On' if self.rules.wrap_around else 'Off'}", + (center_x, start_y), + 'toggle_wrap'), + MenuItem(f"Speed Increase: {'On' if self.rules.speed_increase else 'Off'}", + (center_x, start_y + spacing), + 'toggle_speed'), + MenuItem("Back to Menu", + (center_x, start_y + spacing * 3), + 'back') + ] + + def update(self): + mouse_pos = pygame.mouse.get_pos() + for item in self.menu_items: + item.update_hover(mouse_pos) + + 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): + if item.action == 'toggle_wrap': + self.rules.wrap_around = not self.rules.wrap_around + item.text = f"Wrap Around: {'On' if self.rules.wrap_around else 'Off'}" + item._setup_font() + elif item.action == 'toggle_speed': + self.rules.speed_increase = not self.rules.speed_increase + item.text = f"Speed Increase: {'On' if self.rules.speed_increase else 'Off'}" + item._setup_font() + elif item.action == 'back': + return 'back' + elif event.type == pygame.KEYDOWN: + if event.key == pygame.K_ESCAPE: + return 'back' + elif event.key == pygame.K_RETURN: + for item in self.menu_items: + if item.hover: + if item.action == 'toggle_wrap': + self.rules.wrap_around = not self.rules.wrap_around + item.text = f"Wrap Around: {'On' if self.rules.wrap_around else 'Off'}" + item._setup_font() + elif item.action == 'toggle_speed': + self.rules.speed_increase = not self.rules.speed_increase + item.text = f"Speed Increase: {'On' if self.rules.speed_increase else 'Off'}" + item._setup_font() + elif item.action == 'back': + return 'back' + 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): + # Draw background + screen.fill((0, 0, 0)) + + # Draw title + screen.blit(self.title_surface, self.title_rect) + + # Draw menu items + for item in self.menu_items: + item.draw(screen) + + # Draw controls + controls_text = "Arrow keys to navigate, Enter to select, Esc to go back" + font = pygame.font.Font(None, 24) + controls_surface = font.render(controls_text, True, (100, 100, 100)) + screen.blit(controls_surface, + (self.width - controls_surface.get_width() - 10, + self.height - 30)) + class Game: def __init__(self): # Initialize pygame and font system @@ -51,6 +196,9 @@ class Game: # Debug mode self.debug = False + + # Add settings menu + self.settings_menu = SettingsMenu(self.width, self.height, self.rules) def reset_game(self): """Reset the game state for a new game""" @@ -73,17 +221,18 @@ class Game: self.state = GameState.PLAYING elif self.state == GameState.MENU: self.running = False + elif self.state == GameState.SETTINGS: + self.state = GameState.MENU 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 selected_mode = self.menu.handle_input(event) if selected_mode is not None: - if selected_mode == GameMode.PLAYER: + if selected_mode == GameMode.SETTINGS: + self.state = GameState.SETTINGS + elif selected_mode == GameMode.PLAYER: self.game_mode = GameMode.PLAYER self.state = GameState.PLAYING elif selected_mode in (GameMode.AI_EASY, GameMode.AI_MEDIUM, GameMode.AI_HARD): @@ -92,6 +241,12 @@ class Game: else: # Quit selected self.running = False + elif self.state == GameState.SETTINGS: + # Handle settings menu input + result = self.settings_menu.handle_input(event) + if result == 'back': + self.state = GameState.MENU + elif self.state == GameState.PLAYING: # Handle snake direction if event.key == pygame.K_UP: @@ -164,12 +319,13 @@ class Game: self.screen.fill((0, 0, 0)) # Black background # Draw grid in debug mode - if self.debug and self.state != GameState.MENU: + if self.debug and self.state not in (GameState.MENU, GameState.SETTINGS): self.draw_grid() if self.state == GameState.MENU: self.menu.draw(self.screen) - + elif self.state == GameState.SETTINGS: + self.settings_menu.draw(self.screen) elif self.state == GameState.PLAYING or self.state == GameState.PAUSED: # Draw game objects self.snake.draw(self.screen) diff --git a/src/menu.py b/src/menu.py index fc92a93..54adce5 100644 --- a/src/menu.py +++ b/src/menu.py @@ -7,6 +7,7 @@ class GameMode(Enum): AI_EASY = auto() AI_MEDIUM = auto() AI_HARD = auto() + SETTINGS = auto() QUIT = auto() class MenuItem: @@ -15,6 +16,7 @@ class MenuItem: self.position = position self.action = action self.size = size + self.default_color = color self.color = color self.hover = False self._setup_font() @@ -25,12 +27,14 @@ class MenuItem: self.rect = self.surface.get_rect(center=self.position) def update_hover(self, mouse_pos): + old_hover = self.hover 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() + self.color = self.default_color + if old_hover != self.hover: + self._setup_font() def draw(self, screen): screen.blit(self.surface, self.rect) @@ -49,6 +53,11 @@ class Menu: 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)) + + # Initialize first item as hovered + if self.menu_items: + self.menu_items[0].hover = True + self.menu_items[0]._setup_font() def setup_menu_items(self): # Calculate positions for menu items @@ -61,7 +70,8 @@ class Menu: 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)) + MenuItem("Settings", (center_x, start_y + spacing * 4), GameMode.SETTINGS), + MenuItem("Quit", (center_x, start_y + spacing * 5), GameMode.QUIT, color=(255, 100, 100)) ] def handle_input(self, event): diff --git a/src/snake.py b/src/snake.py index 3daae80..40fcdbd 100644 --- a/src/snake.py +++ b/src/snake.py @@ -23,6 +23,7 @@ class Snake: self.direction_change_cooldown = 50 # milliseconds self.last_direction_change = 0 self.queued_direction = None + self.last_valid_move_time = 0 # Track when we last actually moved def move(self, current_time: int) -> bool: """Move the snake if enough time has passed. Returns True if moved.""" @@ -30,8 +31,10 @@ class Snake: 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) + if self.queued_direction: + if current_time - self.last_valid_move_time >= self.direction_change_cooldown: + if self._apply_direction_change(self.queued_direction): + self.last_direction_change = current_time self.queued_direction = None # Update position @@ -48,6 +51,7 @@ class Snake: self.growing = False self.last_move_time = current_time + self.last_valid_move_time = current_time return True def _get_new_head_position(self, head: Tuple[int, int]) -> Tuple[int, int]: @@ -98,18 +102,23 @@ class Snake: 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, - Direction.LEFT: Direction.RIGHT, - Direction.RIGHT: Direction.LEFT - } + """ + Internal method to actually change direction. + Returns True if direction was changed, False otherwise. + """ + # Prevent 180-degree turns when snake is longer than 1 + if len(self.body) > 1: + opposites = { + Direction.UP: Direction.DOWN, + Direction.DOWN: Direction.UP, + Direction.LEFT: Direction.RIGHT, + Direction.RIGHT: Direction.LEFT + } + if opposites[new_direction] == self.direction: + return False - if opposites[new_direction] != self.direction: - self.direction = new_direction - return True - return False + self.direction = new_direction + return True def change_direction(self, new_direction: Direction, current_time: int): """ @@ -119,11 +128,12 @@ class Snake: 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 + # If we haven't moved since the last direction change, queue it + if current_time - self.last_valid_move_time < self.direction_change_cooldown: self.queued_direction = new_direction return + # Try to change direction immediately if self._apply_direction_change(new_direction): self.last_direction_change = current_time