Files
claudePySnake/src/shared/models.py
Vladyslav Doloman ce492b0dc2 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>
2025-10-04 19:11:20 +03:00

111 lines
3.8 KiB
Python

"""Data models shared between client and server."""
from dataclasses import dataclass, field
from typing import List, Tuple
@dataclass
class Position:
"""Represents a position on the game grid."""
x: int
y: int
def __add__(self, other: Tuple[int, int]) -> "Position":
"""Add a direction tuple to position."""
return Position(self.x + other[0], self.y + other[1])
def to_tuple(self) -> Tuple[int, int]:
"""Convert to tuple for serialization."""
return (self.x, self.y)
@classmethod
def from_tuple(cls, pos: Tuple[int, int]) -> "Position":
"""Create Position from tuple."""
return cls(pos[0], pos[1])
@dataclass
class Snake:
"""Represents a snake in the game."""
player_id: str
body: List[Position] = field(default_factory=list)
direction: Tuple[int, int] = (1, 0) # Default: moving right
alive: bool = True
score: int = 0
stuck: bool = False # True when snake is blocked and shrinking
color_index: int = 0 # Index in COLOR_SNAKES array for persistent color
player_name: str = "" # Human-readable player name
input_buffer: List[Tuple[int, int]] = field(default_factory=list) # Buffer for pending direction changes (max 3)
def get_head(self) -> Position:
"""Get the head position of the snake."""
return self.body[0] if self.body else Position(0, 0)
def to_dict(self) -> dict:
"""Convert to dictionary for serialization."""
return {
"player_id": self.player_id,
"body": [pos.to_tuple() for pos in self.body],
"direction": self.direction,
"alive": self.alive,
"score": self.score,
"stuck": self.stuck,
"color_index": self.color_index,
"player_name": self.player_name,
"input_buffer": self.input_buffer,
}
@classmethod
def from_dict(cls, data: dict) -> "Snake":
"""Create Snake from dictionary."""
snake = cls(player_id=data["player_id"])
snake.body = [Position.from_tuple(pos) for pos in data["body"]]
snake.direction = tuple(data["direction"])
snake.alive = data["alive"]
snake.score = data["score"]
snake.stuck = data.get("stuck", False) # Default to False for backward compatibility
snake.color_index = data.get("color_index", 0) # Default to 0 for backward compatibility
snake.player_name = data.get("player_name", "") # Default to empty string for backward compatibility
snake.input_buffer = [tuple(d) for d in data.get("input_buffer", [])] # Default to empty list for backward compatibility
return snake
@dataclass
class Food:
"""Represents food on the game grid."""
position: Position
def to_dict(self) -> dict:
"""Convert to dictionary for serialization."""
return {"position": self.position.to_tuple()}
@classmethod
def from_dict(cls, data: dict) -> "Food":
"""Create Food from dictionary."""
return cls(position=Position.from_tuple(data["position"]))
@dataclass
class GameState:
"""Represents the complete game state."""
snakes: List[Snake] = field(default_factory=list)
food: List[Food] = field(default_factory=list)
game_running: bool = False
def to_dict(self) -> dict:
"""Convert to dictionary for serialization."""
return {
"snakes": [snake.to_dict() for snake in self.snakes],
"food": [f.to_dict() for f in self.food],
"game_running": self.game_running,
}
@classmethod
def from_dict(cls, data: dict) -> "GameState":
"""Create GameState from dictionary."""
state = cls()
state.snakes = [Snake.from_dict(s) for s in data["snakes"]]
state.food = [Food.from_dict(f) for f in data["food"]]
state.game_running = data["game_running"]
return state