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:
parent
766355a2b9
commit
be7b7caf28
168
src/game.py
168
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
|
||||
@ -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)
|
||||
|
16
src/menu.py
16
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)
|
||||
@ -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):
|
||||
|
40
src/snake.py
40
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
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user