/** * Multiplayer Snake Game Web Client */ class GameClient { constructor() { this.ws = null; this.playerId = null; this.gameState = null; this.canvas = document.getElementById('game-canvas'); this.ctx = this.canvas.getContext('2d'); // Game constants (matching Python) this.GRID_WIDTH = 40; this.GRID_HEIGHT = 30; this.CELL_SIZE = 20; // Colors (matching Python) this.COLOR_BACKGROUND = '#000000'; this.COLOR_GRID = '#282828'; this.COLOR_FOOD = '#ff0000'; this.COLOR_SNAKES = [ '#00ff00', // Green - Player 1 '#0000ff', // Blue - Player 2 '#ffff00', // Yellow - Player 3 '#ff00ff' // Magenta - Player 4 ]; // Setup canvas this.canvas.width = this.GRID_WIDTH * this.CELL_SIZE; this.canvas.height = this.GRID_HEIGHT * this.CELL_SIZE; // Bind UI elements this.connectBtn = document.getElementById('connect-btn'); this.disconnectBtn = document.getElementById('disconnect-btn'); this.playerNameInput = document.getElementById('player-name'); this.serverUrlInput = document.getElementById('server-url'); this.connectionStatus = document.getElementById('connection-status'); this.connectionPanel = document.getElementById('connection-panel'); this.gamePanel = document.getElementById('game-panel'); this.gameOverlay = document.getElementById('game-overlay'); this.playersList = document.getElementById('players-list'); // Setup event listeners this.setupEventListeners(); // Start render loop this.render(); } setupEventListeners() { this.connectBtn.addEventListener('click', () => this.connect()); this.disconnectBtn.addEventListener('click', () => this.disconnect()); // Keyboard controls document.addEventListener('keydown', (e) => this.handleKeyPress(e)); // Enter key in inputs triggers connect this.playerNameInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') this.connect(); }); this.serverUrlInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') this.connect(); }); } connect() { const serverUrl = this.serverUrlInput.value.trim(); const playerName = this.playerNameInput.value.trim() || 'Player'; if (!serverUrl) { this.showStatus('Please enter a server URL', 'error'); return; } this.showStatus('Connecting...', 'info'); try { this.ws = new WebSocket(serverUrl); this.ws.onopen = () => { console.log('WebSocket connected'); this.showStatus('Connected! Joining game...', 'success'); // Send JOIN message const joinMsg = createJoinMessage(playerName); this.ws.send(joinMsg.toJSON()); }; this.ws.onmessage = (event) => { try { const message = Message.fromJSON(event.data); this.handleMessage(message); } catch (error) { console.error('Error parsing message:', error); } }; this.ws.onerror = (error) => { console.error('WebSocket error:', error); this.showStatus('Connection error', 'error'); }; this.ws.onclose = () => { console.log('WebSocket closed'); this.showStatus('Disconnected from server', 'error'); this.showConnectionPanel(); }; } catch (error) { console.error('Connection error:', error); this.showStatus('Failed to connect: ' + error.message, 'error'); } } disconnect() { if (this.ws) { this.ws.close(); this.ws = null; } this.gameState = null; this.playerId = null; this.showConnectionPanel(); } handleMessage(message) { switch (message.type) { case MessageType.WELCOME: this.playerId = message.data.player_id; console.log('Assigned player ID:', this.playerId); this.showGamePanel(); break; case MessageType.STATE_UPDATE: this.gameState = message.data.game_state; this.updatePlayersList(); if (this.gameState.game_running) { this.hideOverlay(); } break; case MessageType.PLAYER_JOINED: console.log('Player joined:', message.data.player_name); break; case MessageType.PLAYER_LEFT: console.log('Player left:', message.data.player_id); break; case MessageType.GAME_STARTED: console.log('Game started!'); this.hideOverlay(); break; case MessageType.GAME_OVER: console.log('Game over! Winner:', message.data.winner_id); this.showOverlay('Game Over!', message.data.winner_id ? `Winner: ${message.data.winner_id}` : 'No winner'); break; case MessageType.ERROR: console.error('Server error:', message.data.error); this.showStatus('Error: ' + message.data.error, 'error'); break; } } handleKeyPress(event) { if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { return; } let direction = null; switch (event.key.toLowerCase()) { case 'arrowup': case 'w': direction = Direction.UP; event.preventDefault(); break; case 'arrowdown': case 's': direction = Direction.DOWN; event.preventDefault(); break; case 'arrowleft': case 'a': direction = Direction.LEFT; event.preventDefault(); break; case 'arrowright': case 'd': direction = Direction.RIGHT; event.preventDefault(); break; case ' ': // Start game const startMsg = createStartGameMessage(); this.ws.send(startMsg.toJSON()); event.preventDefault(); break; } if (direction) { const moveMsg = createMoveMessage(direction); this.ws.send(moveMsg.toJSON()); } } render() { // Clear canvas this.ctx.fillStyle = this.COLOR_BACKGROUND; this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); // Draw grid this.drawGrid(); if (this.gameState) { // Draw food if (this.gameState.food) { this.ctx.fillStyle = this.COLOR_FOOD; for (const food of this.gameState.food) { const [x, y] = food.position; this.drawCell(x, y, this.COLOR_FOOD); } } // Draw snakes if (this.gameState.snakes) { this.gameState.snakes.forEach((snake, index) => { const color = this.COLOR_SNAKES[index % this.COLOR_SNAKES.length]; if (snake.body && snake.alive) { // Draw body for (let i = 0; i < snake.body.length; i++) { const [x, y] = snake.body[i]; // Make head brighter if (i === 0) { const brightColor = this.brightenColor(color, 50); this.drawCell(x, y, brightColor); } else { this.drawCell(x, y, color); } } } }); } } requestAnimationFrame(() => this.render()); } drawGrid() { this.ctx.strokeStyle = this.COLOR_GRID; this.ctx.lineWidth = 1; for (let x = 0; x <= this.GRID_WIDTH; x++) { this.ctx.beginPath(); this.ctx.moveTo(x * this.CELL_SIZE, 0); this.ctx.lineTo(x * this.CELL_SIZE, this.canvas.height); this.ctx.stroke(); } for (let y = 0; y <= this.GRID_HEIGHT; y++) { this.ctx.beginPath(); this.ctx.moveTo(0, y * this.CELL_SIZE); this.ctx.lineTo(this.canvas.width, y * this.CELL_SIZE); this.ctx.stroke(); } } drawCell(x, y, color) { this.ctx.fillStyle = color; this.ctx.fillRect( x * this.CELL_SIZE, y * this.CELL_SIZE, this.CELL_SIZE, this.CELL_SIZE ); // Border this.ctx.strokeStyle = this.COLOR_BACKGROUND; this.ctx.lineWidth = 1; this.ctx.strokeRect( x * this.CELL_SIZE, y * this.CELL_SIZE, this.CELL_SIZE, this.CELL_SIZE ); } brightenColor(hex, amount) { // Convert hex to RGB const r = parseInt(hex.slice(1, 3), 16); const g = parseInt(hex.slice(3, 5), 16); const b = parseInt(hex.slice(5, 7), 16); // Brighten const newR = Math.min(255, r + amount); const newG = Math.min(255, g + amount); const newB = Math.min(255, b + amount); // Convert back to hex return `#${newR.toString(16).padStart(2, '0')}${newG.toString(16).padStart(2, '0')}${newB.toString(16).padStart(2, '0')}`; } updatePlayersList() { if (!this.gameState || !this.gameState.snakes) { return; } this.playersList.innerHTML = ''; this.gameState.snakes.forEach((snake, index) => { const playerItem = document.createElement('div'); playerItem.className = `player-item ${snake.alive ? 'alive' : 'dead'}`; playerItem.style.borderLeftColor = this.COLOR_SNAKES[index % this.COLOR_SNAKES.length]; const nameSpan = document.createElement('span'); nameSpan.className = 'player-name'; nameSpan.textContent = snake.player_id === this.playerId ? `You (${snake.player_id.substring(0, 8)})` : snake.player_id.substring(0, 8); const scoreSpan = document.createElement('span'); scoreSpan.className = 'player-score'; scoreSpan.textContent = snake.score; playerItem.appendChild(nameSpan); playerItem.appendChild(scoreSpan); this.playersList.appendChild(playerItem); }); } showStatus(message, type) { this.connectionStatus.textContent = message; this.connectionStatus.className = `status ${type}`; } showConnectionPanel() { this.connectionPanel.style.display = 'block'; this.gamePanel.style.display = 'none'; } showGamePanel() { this.connectionPanel.style.display = 'none'; this.gamePanel.style.display = 'flex'; } hideOverlay() { this.gameOverlay.classList.add('hidden'); } showOverlay(title, message) { this.gameOverlay.classList.remove('hidden'); this.gameOverlay.querySelector('h2').textContent = title; this.gameOverlay.querySelector('p').textContent = message; } } // Initialize game when DOM is ready document.addEventListener('DOMContentLoaded', () => { new GameClient(); });