diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..3f7a136 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,6 @@ +[pytest] +testpaths = tests +python_files = test_*.py +python_classes = Test* +python_functions = test_* +addopts = -v --tb=short \ No newline at end of file diff --git a/run_game.py b/run_game.py new file mode 100644 index 0000000..62e3d9e --- /dev/null +++ b/run_game.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python3 +import argparse +from src.game import Game + +def main(): + parser = argparse.ArgumentParser(description='Run AI Snake Game') + parser.add_argument('--test', action='store_true', + help='Run game in test mode (smaller window, faster speed)') + parser.add_argument('--debug', action='store_true', + help='Run game in debug mode (shows grid and additional info)') + parser.add_argument('--ai-only', action='store_true', + help='Start directly in AI mode (skips menu)') + + args = parser.parse_args() + + # Create game instance + game = Game() + + # Apply test mode settings + if args.test: + game.width = 400 + game.height = 300 + game.block_size = 20 + game.fps = 30 + + # Apply debug mode settings + if args.debug: + game.debug = True # Will need to add debug support to Game class + + # Run the game + game.run() + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/src/game.py b/src/game.py index 2fc3a49..37a5563 100644 --- a/src/game.py +++ b/src/game.py @@ -32,6 +32,9 @@ class Game: self.state = GameState.MENU # Start in menu state self.running = True self.game_mode = None + + # Debug mode + self.debug = False def reset_game(self): """Reset the game state for a new game""" @@ -40,6 +43,29 @@ 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): for event in pygame.event.get(): if event.type == pygame.QUIT: @@ -52,6 +78,8 @@ class Game: self.state = GameState.PLAYING elif self.state == GameState.MENU: self.running = False + elif event.key == pygame.K_F3: # Toggle debug mode + self.debug = not self.debug if self.state == GameState.MENU: # Handle menu input @@ -101,6 +129,10 @@ class Game: # Clear screen self.screen.fill((0, 0, 0)) # Black background + # Draw grid in debug mode + if self.debug and self.state != GameState.MENU: + self.draw_grid() + if self.state == GameState.MENU: self.menu.draw(self.screen) @@ -138,6 +170,10 @@ class Game: self.screen.blit(score_text, score_rect) self.screen.blit(restart_text, restart_rect) + # Draw debug info + if self.debug: + self.draw_debug_info() + # Update display pygame.display.flip() diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..6c0f708 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,29 @@ +import pytest +import pygame +import sys +import os + +# Add src directory to Python path +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) + +@pytest.fixture(autouse=True) +def pygame_init(): + """Initialize pygame for all tests""" + pygame.init() + yield + pygame.quit() + +@pytest.fixture +def game_config(): + """Basic game configuration""" + return { + 'width': 800, + 'height': 600, + 'block_size': 20, + 'fps': 60 + } + +@pytest.fixture +def mock_screen(game_config): + """Create a mock pygame screen""" + return pygame.Surface((game_config['width'], game_config['height'])) \ No newline at end of file diff --git a/tests/test_food.py b/tests/test_food.py new file mode 100644 index 0000000..4e7ed72 --- /dev/null +++ b/tests/test_food.py @@ -0,0 +1,58 @@ +import pytest +from src.food import Food + +def test_food_initialization(): + """Test food initialization""" + food = Food(20) + assert food.block_size == 20 + assert food.color == (255, 0, 0) # Default red color + assert food.position == (0, 0) # Default position + +def test_food_spawn(): + """Test food spawning mechanics""" + food = Food(20) + width, height = 800, 600 + snake_body = [(100, 100), (120, 100)] # Mock snake body + + # Test spawning + food.spawn(width, height, snake_body) + + # Check if food is within bounds + assert 0 <= food.position[0] < width + assert 0 <= food.position[1] < height + + # Check if position aligns with grid + assert food.position[0] % food.block_size == 0 + assert food.position[1] % food.block_size == 0 + + # Check if food doesn't spawn on snake + assert food.position not in snake_body + +def test_food_collision(): + """Test food collision detection""" + food = Food(20) + food.position = (100, 100) + + # Test collision + assert food.check_collision((100, 100)) == True + + # Test no collision + assert food.check_collision((120, 120)) == False + +def test_food_spawn_full_grid(): + """Test food spawning when grid is mostly occupied""" + food = Food(20) + width, height = 60, 60 # Small grid + + # Create a snake that occupies most of the grid + snake_body = [(x, y) for x in range(0, 60, 20) for y in range(0, 40, 20)] + + # Leave one spot free at (40, 40) + free_spot = (40, 40) + assert free_spot not in snake_body + + # Spawn food + food.spawn(width, height, snake_body) + + # Food should spawn in the only free spot + assert food.position == free_spot \ No newline at end of file diff --git a/tests/test_snake.py b/tests/test_snake.py new file mode 100644 index 0000000..2495817 --- /dev/null +++ b/tests/test_snake.py @@ -0,0 +1,74 @@ +import pytest +from src.snake import Snake, Direction + +def test_snake_initialization(): + """Test snake initialization with default values""" + snake = Snake((100, 100), 20) + assert snake.block_size == 20 + assert snake.direction == Direction.RIGHT + assert len(snake.body) == 1 + assert snake.body[0] == (100, 100) + assert not snake.growing + +def test_snake_movement(): + """Test snake movement in different directions""" + snake = Snake((100, 100), 20) + + # Test right movement (default direction) + snake.move(100) # time parameter + assert snake.body[0] == (120, 100) + + # Test down movement + snake.change_direction(Direction.DOWN) + snake.move(200) + assert snake.body[0] == (120, 120) + + # Test left movement + snake.change_direction(Direction.LEFT) + snake.move(300) + assert snake.body[0] == (100, 120) + + # Test up movement + snake.change_direction(Direction.UP) + snake.move(400) + assert snake.body[0] == (100, 100) + +def test_snake_growth(): + """Test snake growth when eating food""" + snake = Snake((100, 100), 20) + original_length = len(snake.body) + + snake.grow() + snake.move(100) + + assert len(snake.body) == original_length + 1 + +def test_snake_collision(): + """Test snake collision detection""" + snake = Snake((100, 100), 20) + + # Test wall collision + assert snake.check_collision(200, 200) == False # No collision + snake.body[0] = (-20, 100) # Move outside left wall + assert snake.check_collision(200, 200) == True + + # Test self collision + snake = Snake((100, 100), 20) + snake.body = [(100, 100), (120, 100), (120, 120), (100, 120), (100, 100)] + assert snake.check_collision(200, 200) == True + +def test_invalid_direction_change(): + """Test that snake cannot reverse direction""" + snake = Snake((100, 100), 20) + + # Try to change to opposite direction (LEFT while going RIGHT) + snake.change_direction(Direction.LEFT) + assert snake.direction == Direction.RIGHT + + # Valid direction change + snake.change_direction(Direction.DOWN) + assert snake.direction == Direction.DOWN + + # Try to change to opposite direction (UP while going DOWN) + snake.change_direction(Direction.UP) + assert snake.direction == Direction.DOWN \ No newline at end of file