Files
claudePySnake/tests/test_game_logic.py
Vladyslav Doloman 84a58038f7 Implement stuck snake mechanics, persistent colors, and length display
Major gameplay changes:
- Snakes no longer die from collisions
- When blocked, snakes get "stuck" - head stays in place, tail shrinks by 1 per tick
- Snakes auto-unstick when obstacle clears (other snakes move/shrink away)
- Minimum snake length is 1 (head-only)
- Game runs continuously without rounds or game-over state

Color system:
- Each player gets a persistent color for their entire connection
- Colors assigned on join, rotate through available colors
- Color follows player even after disconnect/reconnect
- Works for both desktop and web clients

Display improvements:
- Show snake length instead of score
- Length accurately reflects current snake size
- Updates in real-time as snakes grow/shrink

Server fixes:
- Fixed HTTP server initialization issues
- Changed default host to 0.0.0.0 for network multiplayer
- Improved file serving with proper routing

Testing:
- Updated all collision tests for stuck mechanics
- Added tests for stuck/unstick behavior
- Added tests for color persistence
- All 12 tests passing

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-04 16:39:30 +03:00

255 lines
8.1 KiB
Python

"""Tests for game logic."""
import pytest
from src.server.game_logic import GameLogic
from src.shared.models import Position, Snake
from src.shared.constants import GRID_WIDTH, GRID_HEIGHT, RIGHT, LEFT, UP, DOWN
class TestGameLogic:
"""Test suite for GameLogic class."""
def test_create_snake(self) -> None:
"""Test snake creation."""
logic = GameLogic()
snake = logic.create_snake("player1")
assert snake.player_id == "player1"
assert len(snake.body) == 3
assert snake.alive is True
assert snake.score == 0
assert snake.direction == RIGHT
def test_spawn_food(self) -> None:
"""Test food spawning."""
logic = GameLogic()
food = logic.spawn_food()
assert 0 <= food.position.x < GRID_WIDTH
assert 0 <= food.position.y < GRID_HEIGHT
def test_update_snake_direction(self) -> None:
"""Test updating snake direction."""
logic = GameLogic()
snake = logic.create_snake("player1")
logic.state.snakes.append(snake)
# Valid direction change
logic.update_snake_direction("player1", UP)
assert snake.direction == UP
# Invalid 180-degree turn (should be ignored)
logic.update_snake_direction("player1", DOWN)
assert snake.direction == UP # Should remain UP
def test_move_snakes(self) -> None:
"""Test snake movement."""
logic = GameLogic()
snake = Snake(player_id="player1", body=[
Position(5, 5),
Position(4, 5),
Position(3, 5),
], direction=RIGHT)
logic.state.snakes.append(snake)
initial_length = len(snake.body)
logic.move_snakes()
# Snake should have moved one cell to the right
assert snake.get_head().x == 6
assert snake.get_head().y == 5
assert len(snake.body) == initial_length
def test_collision_with_wall(self) -> None:
"""Test collision detection with walls - snake gets stuck."""
logic = GameLogic()
# Snake at left wall
snake = Snake(player_id="player1", body=[
Position(0, 5),
Position(1, 5),
Position(2, 5),
], direction=LEFT)
logic.state.snakes.append(snake)
initial_length = len(snake.body)
head_pos = snake.get_head()
logic.move_snakes()
# Snake should be stuck, not dead
assert snake.stuck is True
assert snake.alive is True
# Head position should not change
assert snake.get_head().x == head_pos.x
assert snake.get_head().y == head_pos.y
# Tail should shrink by 1
assert len(snake.body) == initial_length - 1
def test_collision_with_self(self) -> None:
"""Test collision detection with self - snake gets stuck then auto-unsticks."""
logic = GameLogic()
# Create a snake that will hit itself
snake = Snake(player_id="player1", body=[
Position(5, 5),
Position(5, 6),
Position(6, 6),
Position(6, 5),
], direction=DOWN)
logic.state.snakes.append(snake)
# First move: should get stuck
logic.move_snakes()
assert snake.stuck is True
assert snake.alive is True
assert len(snake.body) == 3 # Shrunk by 1
# Continue moving: tail shrinks, eventually unsticks
logic.move_snakes()
assert len(snake.body) == 2 # Shrunk again
logic.move_snakes()
# After enough shrinking, blocking segment is gone, snake unsticks
assert len(snake.body) >= 1
def test_food_eating(self) -> None:
"""Test snake eating food and growing."""
logic = GameLogic()
snake = Snake(player_id="player1", body=[
Position(5, 5),
Position(4, 5),
Position(3, 5),
], direction=RIGHT)
logic.state.snakes.append(snake)
# Place food in front of snake
from src.shared.models import Food
food = Food(position=Position(6, 5))
logic.state.food.append(food)
initial_length = len(snake.body)
initial_score = snake.score
logic.move_snakes()
# Snake should have grown and scored
assert len(snake.body) == initial_length + 1
assert snake.score == initial_score + 10
assert food not in logic.state.food
def test_stuck_minimum_length(self) -> None:
"""Test that snake cannot shrink below length 1."""
logic = GameLogic()
# Create length-1 snake stuck against wall
snake = Snake(player_id="player1", body=[Position(0, 5)], direction=LEFT)
logic.state.snakes.append(snake)
# Move multiple times - should stay stuck, not shrink below 1
for _ in range(5):
logic.move_snakes()
assert len(snake.body) == 1
assert snake.stuck is True
assert snake.alive is True
def test_other_snake_blocks(self) -> None:
"""Test snake getting stuck on another snake."""
logic = GameLogic()
# Snake A trying to move into Snake B
snake_a = Snake(player_id="player_a", body=[
Position(5, 5),
Position(4, 5),
], direction=RIGHT)
snake_b = Snake(player_id="player_b", body=[
Position(6, 5), # Blocking Snake A
Position(7, 5),
], direction=RIGHT)
logic.state.snakes.extend([snake_a, snake_b])
logic.move_snakes()
# Snake A should be stuck
assert snake_a.stuck is True
assert len(snake_a.body) == 1 # Shrunk by 1
def test_other_snake_moves_away(self) -> None:
"""Test snake auto-unsticks when other snake moves away."""
logic = GameLogic()
# Snake A stuck behind Snake B
snake_a = Snake(player_id="player_a", body=[
Position(5, 5),
Position(4, 5),
Position(3, 5),
], direction=RIGHT)
snake_b = Snake(player_id="player_b", body=[
Position(6, 5), # Blocking at (6,5)
Position(6, 6), # Tail at (6,6)
], direction=UP) # Will move up
logic.state.snakes.extend([snake_a, snake_b])
# Tick 1: Snake A gets stuck, Snake B moves to (6,4)-(6,5)
logic.move_snakes()
assert snake_a.stuck is True
assert len(snake_a.body) == 2 # Shrunk by 1
# Tick 2: Snake B moves to (6,3)-(6,4), still stuck (shrunk to length 1)
logic.move_snakes()
assert snake_a.stuck is True
assert len(snake_a.body) == 1 # Shrunk to minimum
# Tick 3: Position (6,5) clear, Snake A finally unsticks!
logic.move_snakes()
assert snake_a.stuck is False
assert snake_a.get_head().x == 6 # Moved forward to (6,5)
def test_direction_change_unsticks(self) -> None:
"""Test changing direction to clear path unsticks snake."""
logic = GameLogic()
# Snake stuck against wall facing left
snake = Snake(player_id="player1", body=[
Position(0, 5),
Position(1, 5),
], direction=LEFT)
logic.state.snakes.append(snake)
# Get stuck
logic.move_snakes()
assert snake.stuck is True
# Change direction to up (clear path)
logic.update_snake_direction("player1", UP)
# Next move should unstick
logic.move_snakes()
assert snake.stuck is False
assert snake.get_head().y == 4 # Moved up
def test_two_snakes_head_to_head(self) -> None:
"""Test two snakes stuck head-to-head both shrink."""
logic = GameLogic()
snake_a = Snake(player_id="player_a", body=[
Position(5, 5),
Position(4, 5),
], direction=RIGHT)
snake_b = Snake(player_id="player_b", body=[
Position(6, 5),
Position(7, 5),
], direction=LEFT)
logic.state.snakes.extend([snake_a, snake_b])
logic.move_snakes()
# Both snakes should be stuck
assert snake_a.stuck is True
assert snake_b.stuck is True
# Both should shrink
assert len(snake_a.body) == 1
assert len(snake_b.body) == 1