Improve game mechanics and UI: - Add wrap-around mode and speed increase - Implement proper movement cooldown - Enhance menu UI with better layout and interactions - Add visual feedback and mouse support
This commit is contained in:
parent
31e9771ba0
commit
766355a2b9
86
src/game.py
86
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
|
||||
|
||||
@ -131,6 +127,38 @@ class Game:
|
||||
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
|
||||
self.screen.fill((0, 0, 0)) # Black background
|
||||
|
176
src/menu.py
176
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)
|
||||
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))
|
||||
]
|
||||
|
||||
for i, (text, action) in enumerate(items_data):
|
||||
position = (center_x, start_y + i * spacing)
|
||||
self.items.append(MenuItem(text, action, position))
|
||||
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
|
||||
|
||||
# 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:
|
||||
# Move hover up or down
|
||||
if current_hover == -1:
|
||||
new_hover = 0
|
||||
else:
|
||||
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()
|
||||
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)
|
||||
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))
|
||||
|
||||
# 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.items:
|
||||
for item in self.menu_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"
|
||||
]
|
||||
# Draw version and controls
|
||||
version_text = "v0.1.0"
|
||||
controls_text = "Arrow keys to navigate, Enter to select"
|
||||
font = pygame.font.Font(None, 24)
|
||||
|
||||
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)
|
||||
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))
|
63
src/snake.py
63
src/snake.py
@ -19,11 +19,21 @@ class Snake:
|
||||
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,10 +65,18 @@ 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]
|
||||
|
||||
if not wrap_around:
|
||||
# Check wall collision
|
||||
if (head[0] < 0 or head[0] >= width or
|
||||
head[1] < 0 or head[1] >= height):
|
||||
@ -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)
|
||||
)
|
Loading…
Reference in New Issue
Block a user