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>
111 lines
3.8 KiB
Python
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
|