Add player names to snake display

Show human-readable player names instead of player IDs in both desktop
and web clients. Player names are now stored in the Snake model and
synchronized across all clients.

Changes:
- Added player_name field to Snake model
- Updated create_snake() to accept player_name parameter
- Desktop client shows "YOU:" or "PlayerName:"
- Web client shows "You (Name)" or "Name"

🤖 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 17:21:53 +03:00
parent b28d78575f
commit 97d6df1896
6 changed files with 16 additions and 8 deletions

View File

@@ -6,7 +6,10 @@
"Bash(python:*)", "Bash(python:*)",
"Bash(pip install:*)", "Bash(pip install:*)",
"Bash(git log:*)", "Bash(git log:*)",
"Bash(git add:*)" "Bash(git add:*)",
"Bash(git commit -m \"$(cat <<''EOF''\nImplement stuck snake mechanics, persistent colors, and length display\n\nMajor gameplay changes:\n- Snakes no longer die from collisions\n- When blocked, snakes get \"stuck\" - head stays in place, tail shrinks by 1 per tick\n- Snakes auto-unstick when obstacle clears (other snakes move/shrink away)\n- Minimum snake length is 1 (head-only)\n- Game runs continuously without rounds or game-over state\n\nColor system:\n- Each player gets a persistent color for their entire connection\n- Colors assigned on join, rotate through available colors\n- Color follows player even after disconnect/reconnect\n- Works for both desktop and web clients\n\nDisplay improvements:\n- Show snake length instead of score\n- Length accurately reflects current snake size\n- Updates in real-time as snakes grow/shrink\n\nServer fixes:\n- Fixed HTTP server initialization issues\n- Changed default host to 0.0.0.0 for network multiplayer\n- Improved file serving with proper routing\n\nTesting:\n- Updated all collision tests for stuck mechanics\n- Added tests for stuck/unstick behavior\n- Added tests for color persistence\n- All 12 tests passing\n\n🤖 Generated with [Claude Code](https://claude.com/claude-code)\n\nCo-Authored-By: Claude <noreply@anthropic.com>\nEOF\n)\")",
"Bash(git push:*)",
"Bash(git commit:*)"
], ],
"deny": [], "deny": [],
"ask": [] "ask": []

View File

@@ -109,7 +109,7 @@ class Renderer:
color = COLOR_SNAKES[snake.color_index % len(COLOR_SNAKES)] color = COLOR_SNAKES[snake.color_index % len(COLOR_SNAKES)]
# Prepare length text # Prepare length text
prefix = "YOU: " if snake.player_id == player_id else f"Player: " prefix = "YOU: " if snake.player_id == player_id else f"{snake.player_name}: "
status = "" if snake.alive else " (DEAD)" status = "" if snake.alive else " (DEAD)"
text = f"{prefix}Length {len(snake.body)}{status}" text = f"{prefix}Length {len(snake.body)}{status}"

View File

@@ -21,12 +21,13 @@ class GameLogic:
"""Initialize game logic.""" """Initialize game logic."""
self.state = GameState() self.state = GameState()
def create_snake(self, player_id: str, color_index: int = 0) -> Snake: def create_snake(self, player_id: str, color_index: int = 0, player_name: str = "") -> Snake:
"""Create a new snake for a player. """Create a new snake for a player.
Args: Args:
player_id: Unique identifier for the player player_id: Unique identifier for the player
color_index: Index in COLOR_SNAKES array for this snake's color color_index: Index in COLOR_SNAKES array for this snake's color
player_name: Human-readable name for the player
Returns: Returns:
New Snake instance New Snake instance
@@ -41,7 +42,7 @@ class GameLogic:
for i in range(INITIAL_SNAKE_LENGTH) for i in range(INITIAL_SNAKE_LENGTH)
] ]
snake = Snake(player_id=player_id, body=body, direction=RIGHT, color_index=color_index) snake = Snake(player_id=player_id, body=body, direction=RIGHT, color_index=color_index, player_name=player_name)
return snake return snake
def spawn_food(self) -> Food: def spawn_food(self) -> Food:

View File

@@ -169,7 +169,7 @@ class GameServer:
# Add snake to game if game is running # Add snake to game if game is running
if self.game_logic.state.game_running: if self.game_logic.state.game_running:
color_index = self.player_colors[player_id] color_index = self.player_colors[player_id]
snake = self.game_logic.create_snake(player_id, color_index) snake = self.game_logic.create_snake(player_id, color_index, player_name)
self.game_logic.state.snakes.append(snake) self.game_logic.state.snakes.append(snake)
print(f"Player {player_name} ({player_id}) joined via {client_type.value}. Total players: {len(self.clients)}") print(f"Player {player_name} ({player_id}) joined via {client_type.value}. Total players: {len(self.clients)}")
@@ -193,7 +193,8 @@ class GameServer:
self.game_logic.state.snakes = [] self.game_logic.state.snakes = []
for player_id in self.clients.keys(): for player_id in self.clients.keys():
color_index = self.player_colors.get(player_id, 0) color_index = self.player_colors.get(player_id, 0)
snake = self.game_logic.create_snake(player_id, color_index) player_name = self.player_names.get(player_id, f"Player_{player_id[:8]}")
snake = self.game_logic.create_snake(player_id, color_index, player_name)
self.game_logic.state.snakes.append(snake) self.game_logic.state.snakes.append(snake)
# Spawn initial food # Spawn initial food

View File

@@ -34,6 +34,7 @@ class Snake:
score: int = 0 score: int = 0
stuck: bool = False # True when snake is blocked and shrinking stuck: bool = False # True when snake is blocked and shrinking
color_index: int = 0 # Index in COLOR_SNAKES array for persistent color color_index: int = 0 # Index in COLOR_SNAKES array for persistent color
player_name: str = "" # Human-readable player name
def get_head(self) -> Position: def get_head(self) -> Position:
"""Get the head position of the snake.""" """Get the head position of the snake."""
@@ -49,6 +50,7 @@ class Snake:
"score": self.score, "score": self.score,
"stuck": self.stuck, "stuck": self.stuck,
"color_index": self.color_index, "color_index": self.color_index,
"player_name": self.player_name,
} }
@classmethod @classmethod
@@ -61,6 +63,7 @@ class Snake:
snake.score = data["score"] snake.score = data["score"]
snake.stuck = data.get("stuck", False) # Default to False for backward compatibility 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.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
return snake return snake

View File

@@ -322,8 +322,8 @@ class GameClient {
const nameSpan = document.createElement('span'); const nameSpan = document.createElement('span');
nameSpan.className = 'player-name'; nameSpan.className = 'player-name';
nameSpan.textContent = snake.player_id === this.playerId ? nameSpan.textContent = snake.player_id === this.playerId ?
`You (${snake.player_id.substring(0, 8)})` : `You (${snake.player_name})` :
snake.player_id.substring(0, 8); snake.player_name;
const scoreSpan = document.createElement('span'); const scoreSpan = document.createElement('span');
scoreSpan.className = 'player-score'; scoreSpan.className = 'player-score';