Add input buffering, auto-start, and gameplay improvements

Input buffer system:
- Added 3-slot direction input buffer to handle rapid key presses
- Buffer ignores duplicate inputs (same key pressed multiple times)
- Opposite direction replaces last buffered input (e.g., LEFT→RIGHT replaces LEFT)
- Buffer overflow replaces last slot when full
- Multi-segment snakes skip invalid 180° turns when consuming buffer
- Head-only snakes (length=1) can turn 180° for flexibility

Gameplay improvements:
- Desktop client auto-starts game on connect (no SPACE needed)
- Field populates with 3 apples when no players connected
- HTTP server now binds to 0.0.0.0 for network access (matches game server)

Testing:
- Added 7 new tests for input buffer functionality
- Added test for zero-player apple spawning
- All 19 tests passing

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Vladyslav Doloman
2025-10-04 19:11:20 +03:00
parent 97d6df1896
commit ce492b0dc2
5 changed files with 167 additions and 15 deletions

View File

@@ -29,18 +29,20 @@ class TestGameLogic:
assert 0 <= food.position.y < GRID_HEIGHT
def test_update_snake_direction(self) -> None:
"""Test updating snake direction."""
"""Test updating snake direction via input buffer."""
logic = GameLogic()
snake = logic.create_snake("player1")
logic.state.snakes.append(snake)
# Valid direction change
# Direction changes go into buffer first
logic.update_snake_direction("player1", UP)
assert snake.direction == UP
assert snake.input_buffer == [UP]
assert snake.direction == RIGHT # Original direction unchanged
# Invalid 180-degree turn (should be ignored)
logic.update_snake_direction("player1", DOWN)
assert snake.direction == UP # Should remain UP
# Moving consumes from buffer
logic.move_snakes()
assert snake.direction == UP # Now changed after movement
assert len(snake.input_buffer) == 0
def test_move_snakes(self) -> None:
"""Test snake movement."""
@@ -252,3 +254,117 @@ class TestGameLogic:
assert len(snake_a.body) == 1
assert len(snake_b.body) == 1
def test_input_buffer_fills_to_three(self) -> None:
"""Test input buffer fills up to 3 directions."""
logic = GameLogic()
snake = logic.create_snake("player1")
logic.state.snakes.append(snake)
# Add 3 different directions
logic.update_snake_direction("player1", UP)
logic.update_snake_direction("player1", LEFT)
logic.update_snake_direction("player1", DOWN)
assert len(snake.input_buffer) == 3
assert snake.input_buffer == [UP, LEFT, DOWN]
def test_input_buffer_ignores_duplicates(self) -> None:
"""Test input buffer ignores duplicate inputs."""
logic = GameLogic()
snake = logic.create_snake("player1")
logic.state.snakes.append(snake)
logic.update_snake_direction("player1", UP)
logic.update_snake_direction("player1", UP) # Duplicate
assert len(snake.input_buffer) == 1
assert snake.input_buffer == [UP]
def test_input_buffer_opposite_replacement(self) -> None:
"""Test opposite direction replaces last in buffer."""
logic = GameLogic()
snake = logic.create_snake("player1")
logic.state.snakes.append(snake)
logic.update_snake_direction("player1", UP)
logic.update_snake_direction("player1", DOWN) # Opposite to UP
# DOWN should replace UP
assert len(snake.input_buffer) == 1
assert snake.input_buffer == [DOWN]
def test_input_buffer_overflow_replacement(self) -> None:
"""Test 4th input replaces last slot when buffer is full."""
logic = GameLogic()
snake = logic.create_snake("player1")
logic.state.snakes.append(snake)
# Fill buffer with 3 directions
logic.update_snake_direction("player1", UP)
logic.update_snake_direction("player1", LEFT)
logic.update_snake_direction("player1", DOWN)
# 4th input should replace last slot
logic.update_snake_direction("player1", RIGHT)
assert len(snake.input_buffer) == 3
assert snake.input_buffer == [UP, LEFT, RIGHT] # DOWN replaced by RIGHT
def test_input_buffer_consumption(self) -> None:
"""Test buffer is consumed during 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)
# Add direction to buffer
snake.input_buffer = [UP]
logic.move_snakes()
# Buffer should be consumed and direction applied
assert len(snake.input_buffer) == 0
assert snake.direction == UP
assert snake.get_head().y == 4 # Moved up
def test_input_buffer_skips_180_turn(self) -> None:
"""Test buffer skips 180-degree turns for multi-segment snakes."""
logic = GameLogic()
snake = Snake(player_id="player1", body=[
Position(5, 5),
Position(4, 5),
Position(3, 5),
], direction=RIGHT)
logic.state.snakes.append(snake)
# Buffer has opposite direction then valid direction
snake.input_buffer = [LEFT, UP] # LEFT is 180° from RIGHT
logic.move_snakes()
# LEFT should be skipped, UP should be applied
assert len(snake.input_buffer) == 0
assert snake.direction == UP
assert snake.get_head().y == 4 # Moved up
def test_zero_players_spawns_three_apples(self) -> None:
"""Test field populates with 3 apples when no players."""
logic = GameLogic()
logic.state.game_running = True
# Start with no snakes and no food
logic.state.snakes = []
logic.state.food = []
# Update should populate 3 apples
logic.update()
assert len(logic.state.food) == 3
# Subsequent updates should maintain 3 apples
logic.update()
assert len(logic.state.food) == 3