Fix game issues: - Prevent rapid direction changes - Add proper settings menu - Fix menu highlighting - Add wrap-around toggle in settings

This commit is contained in:
Rbanh 2025-02-23 12:58:32 -05:00
parent 766355a2b9
commit be7b7caf28
3 changed files with 200 additions and 24 deletions

View File

@ -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
@ -52,6 +197,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"""
self.snake = Snake((self.width // 2, self.height // 2), self.block_size)
@ -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)

View File

@ -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)
@ -50,6 +54,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
start_y = self.height // 2
@ -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):

View File

@ -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