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):
|
class GameState(Enum):
|
||||||
MENU = auto()
|
MENU = auto()
|
||||||
|
SETTINGS = auto() # New state for settings menu
|
||||||
PLAYING = auto()
|
PLAYING = auto()
|
||||||
GAME_OVER = auto()
|
GAME_OVER = auto()
|
||||||
PAUSED = auto()
|
PAUSED = auto()
|
||||||
@ -18,6 +19,150 @@ class GameRules:
|
|||||||
self.min_move_cooldown = 50 # Minimum movement delay in milliseconds
|
self.min_move_cooldown = 50 # Minimum movement delay in milliseconds
|
||||||
self.initial_move_cooldown = 100 # Initial movement delay
|
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:
|
class Game:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
# Initialize pygame and font system
|
# Initialize pygame and font system
|
||||||
@ -51,6 +196,9 @@ class Game:
|
|||||||
|
|
||||||
# Debug mode
|
# Debug mode
|
||||||
self.debug = False
|
self.debug = False
|
||||||
|
|
||||||
|
# Add settings menu
|
||||||
|
self.settings_menu = SettingsMenu(self.width, self.height, self.rules)
|
||||||
|
|
||||||
def reset_game(self):
|
def reset_game(self):
|
||||||
"""Reset the game state for a new game"""
|
"""Reset the game state for a new game"""
|
||||||
@ -73,17 +221,18 @@ class Game:
|
|||||||
self.state = GameState.PLAYING
|
self.state = GameState.PLAYING
|
||||||
elif self.state == GameState.MENU:
|
elif self.state == GameState.MENU:
|
||||||
self.running = False
|
self.running = False
|
||||||
|
elif self.state == GameState.SETTINGS:
|
||||||
|
self.state = GameState.MENU
|
||||||
elif event.key == pygame.K_F3: # Toggle debug mode
|
elif event.key == pygame.K_F3: # Toggle debug mode
|
||||||
self.debug = not self.debug
|
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:
|
if self.state == GameState.MENU:
|
||||||
# Handle menu input
|
# Handle menu input
|
||||||
selected_mode = self.menu.handle_input(event)
|
selected_mode = self.menu.handle_input(event)
|
||||||
if selected_mode is not None:
|
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.game_mode = GameMode.PLAYER
|
||||||
self.state = GameState.PLAYING
|
self.state = GameState.PLAYING
|
||||||
elif selected_mode in (GameMode.AI_EASY, GameMode.AI_MEDIUM, GameMode.AI_HARD):
|
elif selected_mode in (GameMode.AI_EASY, GameMode.AI_MEDIUM, GameMode.AI_HARD):
|
||||||
@ -92,6 +241,12 @@ class Game:
|
|||||||
else: # Quit selected
|
else: # Quit selected
|
||||||
self.running = False
|
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:
|
elif self.state == GameState.PLAYING:
|
||||||
# Handle snake direction
|
# Handle snake direction
|
||||||
if event.key == pygame.K_UP:
|
if event.key == pygame.K_UP:
|
||||||
@ -164,12 +319,13 @@ class Game:
|
|||||||
self.screen.fill((0, 0, 0)) # Black background
|
self.screen.fill((0, 0, 0)) # Black background
|
||||||
|
|
||||||
# Draw grid in debug mode
|
# 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()
|
self.draw_grid()
|
||||||
|
|
||||||
if self.state == GameState.MENU:
|
if self.state == GameState.MENU:
|
||||||
self.menu.draw(self.screen)
|
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:
|
elif self.state == GameState.PLAYING or self.state == GameState.PAUSED:
|
||||||
# Draw game objects
|
# Draw game objects
|
||||||
self.snake.draw(self.screen)
|
self.snake.draw(self.screen)
|
||||||
|
16
src/menu.py
16
src/menu.py
@ -7,6 +7,7 @@ class GameMode(Enum):
|
|||||||
AI_EASY = auto()
|
AI_EASY = auto()
|
||||||
AI_MEDIUM = auto()
|
AI_MEDIUM = auto()
|
||||||
AI_HARD = auto()
|
AI_HARD = auto()
|
||||||
|
SETTINGS = auto()
|
||||||
QUIT = auto()
|
QUIT = auto()
|
||||||
|
|
||||||
class MenuItem:
|
class MenuItem:
|
||||||
@ -15,6 +16,7 @@ class MenuItem:
|
|||||||
self.position = position
|
self.position = position
|
||||||
self.action = action
|
self.action = action
|
||||||
self.size = size
|
self.size = size
|
||||||
|
self.default_color = color
|
||||||
self.color = color
|
self.color = color
|
||||||
self.hover = False
|
self.hover = False
|
||||||
self._setup_font()
|
self._setup_font()
|
||||||
@ -25,12 +27,14 @@ class MenuItem:
|
|||||||
self.rect = self.surface.get_rect(center=self.position)
|
self.rect = self.surface.get_rect(center=self.position)
|
||||||
|
|
||||||
def update_hover(self, mouse_pos):
|
def update_hover(self, mouse_pos):
|
||||||
|
old_hover = self.hover
|
||||||
self.hover = self.rect.collidepoint(mouse_pos)
|
self.hover = self.rect.collidepoint(mouse_pos)
|
||||||
if self.hover:
|
if self.hover:
|
||||||
self.color = (255, 255, 0) # Yellow on hover
|
self.color = (255, 255, 0) # Yellow on hover
|
||||||
else:
|
else:
|
||||||
self.color = (255, 255, 255) # White normally
|
self.color = self.default_color
|
||||||
self._setup_font()
|
if old_hover != self.hover:
|
||||||
|
self._setup_font()
|
||||||
|
|
||||||
def draw(self, screen):
|
def draw(self, screen):
|
||||||
screen.blit(self.surface, self.rect)
|
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_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))
|
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):
|
def setup_menu_items(self):
|
||||||
# Calculate positions for menu items
|
# Calculate positions for menu items
|
||||||
@ -61,7 +70,8 @@ class Menu:
|
|||||||
MenuItem("AI Easy", (center_x, start_y + spacing), GameMode.AI_EASY),
|
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 Medium", (center_x, start_y + spacing * 2), GameMode.AI_MEDIUM),
|
||||||
MenuItem("AI Hard", (center_x, start_y + spacing * 3), GameMode.AI_HARD),
|
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):
|
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.direction_change_cooldown = 50 # milliseconds
|
||||||
self.last_direction_change = 0
|
self.last_direction_change = 0
|
||||||
self.queued_direction = None
|
self.queued_direction = None
|
||||||
|
self.last_valid_move_time = 0 # Track when we last actually moved
|
||||||
|
|
||||||
def move(self, current_time: int) -> bool:
|
def move(self, current_time: int) -> bool:
|
||||||
"""Move the snake if enough time has passed. Returns True if moved."""
|
"""Move the snake if enough time has passed. Returns True if moved."""
|
||||||
@ -30,8 +31,10 @@ class Snake:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
# Apply queued direction change if it exists and is valid
|
# 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:
|
if self.queued_direction:
|
||||||
self._apply_direction_change(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
|
self.queued_direction = None
|
||||||
|
|
||||||
# Update position
|
# Update position
|
||||||
@ -48,6 +51,7 @@ class Snake:
|
|||||||
self.growing = False
|
self.growing = False
|
||||||
|
|
||||||
self.last_move_time = current_time
|
self.last_move_time = current_time
|
||||||
|
self.last_valid_move_time = current_time
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _get_new_head_position(self, head: Tuple[int, int]) -> Tuple[int, int]:
|
def _get_new_head_position(self, head: Tuple[int, int]) -> Tuple[int, int]:
|
||||||
@ -98,18 +102,23 @@ class Snake:
|
|||||||
self.body[0] = wrapped_head
|
self.body[0] = wrapped_head
|
||||||
|
|
||||||
def _apply_direction_change(self, new_direction: Direction) -> bool:
|
def _apply_direction_change(self, new_direction: Direction) -> bool:
|
||||||
"""Internal method to actually change direction"""
|
"""
|
||||||
opposites = {
|
Internal method to actually change direction.
|
||||||
Direction.UP: Direction.DOWN,
|
Returns True if direction was changed, False otherwise.
|
||||||
Direction.DOWN: Direction.UP,
|
"""
|
||||||
Direction.LEFT: Direction.RIGHT,
|
# Prevent 180-degree turns when snake is longer than 1
|
||||||
Direction.RIGHT: Direction.LEFT
|
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
|
||||||
self.direction = new_direction
|
return True
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def change_direction(self, new_direction: Direction, current_time: int):
|
def change_direction(self, new_direction: Direction, current_time: int):
|
||||||
"""
|
"""
|
||||||
@ -119,11 +128,12 @@ class Snake:
|
|||||||
new_direction: The desired new direction
|
new_direction: The desired new direction
|
||||||
current_time: Current game time in milliseconds
|
current_time: Current game time in milliseconds
|
||||||
"""
|
"""
|
||||||
if current_time - self.last_direction_change < self.direction_change_cooldown:
|
# If we haven't moved since the last direction change, queue it
|
||||||
# Queue the direction change for next movement
|
if current_time - self.last_valid_move_time < self.direction_change_cooldown:
|
||||||
self.queued_direction = new_direction
|
self.queued_direction = new_direction
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Try to change direction immediately
|
||||||
if self._apply_direction_change(new_direction):
|
if self._apply_direction_change(new_direction):
|
||||||
self.last_direction_change = current_time
|
self.last_direction_change = current_time
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user