diff --git a/index.html b/index.html new file mode 100644 index 0000000..347f6ec --- /dev/null +++ b/index.html @@ -0,0 +1,39 @@ + + + + + + P2P Snake Game + + + + +
+

P2P Snake Game

+
+ + +
+
+ + +
+
+

+
+ + + + + + + \ No newline at end of file diff --git a/script.js b/script.js new file mode 100644 index 0000000..cb4d7cf --- /dev/null +++ b/script.js @@ -0,0 +1,306 @@ + +document.addEventListener('DOMContentLoaded', () => { + const lobby = document.getElementById('lobby'); + const createGameBtn = document.getElementById('create-game'); + const gameIdDisplay = document.getElementById('game-id-display'); + const gameIdInput = document.getElementById('game-id'); + const joinIdInput = document.getElementById('join-id-input'); + const joinGameBtn = document.getElementById('join-game'); + const statusEl = document.getElementById('status'); + const gameContainer = document.getElementById('game-container'); + const canvas = document.getElementById('game-canvas'); + const ctx = canvas.getContext('2d'); + + let peer; + let conn; + let isHost = false; + let lastTickTime = 0; + let gameSpeed = 100; // Initial speed: 100ms per tick + + const GRID_SIZE = 20; + const CANVAS_WIDTH = canvas.width; + const CANVAS_HEIGHT = canvas.height; + + let gameState = { + players: [], + fruit: {}, + score: 0, + gameOver: false, + winner: null + }; + + function initializePeer() { + createGameBtn.disabled = true; + joinGameBtn.disabled = true; + statusEl.textContent = 'Connecting to signaling server...'; + + peer = new Peer(); + peer.on('open', (id) => { + statusEl.textContent = 'Connected! Ready to create or join a game.'; + createGameBtn.disabled = false; + joinGameBtn.disabled = false; + }); + peer.on('connection', (newConn) => { + if (conn) { + newConn.close(); + return; + } + conn = newConn; + statusEl.textContent = 'Friend connected!'; + conn.on('open', () => { + startGame(); + }); + conn.on('data', handleClientInput); + conn.on('close', handleDisconnect); + }); + peer.on('error', (err) => { + statusEl.textContent = `Error: ${err.message}`; + }); + } + + createGameBtn.addEventListener('click', () => { + isHost = true; + createGameBtn.disabled = true; + joinGameBtn.disabled = true; + + if (peer && peer.id) { + gameIdInput.value = peer.id; + gameIdDisplay.classList.remove('hidden'); + statusEl.textContent = 'Waiting for a friend to join...'; + } else { + statusEl.textContent = 'Error: Peer ID not available. Please refresh.'; + } + }); + + joinGameBtn.addEventListener('click', () => { + const hostId = joinIdInput.value.trim(); + if (!hostId) { + statusEl.textContent = 'Please enter a valid ID.'; + return; + } + conn = peer.connect(hostId); + statusEl.textContent = 'Connecting to host...'; + conn.on('open', () => { + statusEl.textContent = 'Connected to host!'; + // The game will start for the client when it receives the first game state + }); + conn.on('data', (data) => { + // First message will start the game for the client + if (gameContainer.classList.contains('hidden')) { + startGame(); + } + updateGameState(data); + }); + conn.on('close', handleDisconnect); + }); + + function handleDisconnect() { + statusEl.textContent = 'Peer disconnected. Game over.'; + gameState.gameOver = true; + } + + function startGame() { + lobby.classList.add('hidden'); + gameContainer.classList.remove('hidden'); + if (isHost) { + initializeGameState(); + } + document.addEventListener('keydown', handleKeydown); + requestAnimationFrame(gameLoop); + } + + function initializeGameState() { + gameState = { + players: [ + { id: peer.id, snake: [{ x: 5, y: 5 }], direction: 'right', color: 'lightgreen' }, + { id: conn.peer, snake: [{ x: 15, y: 15 }], direction: 'left', color: 'lightblue' } + ], + fruit: generateFruit(), + score: { [peer.id]: 0, [conn.peer]: 0 }, + gameOver: false, + winner: null + }; + } + + function generateFruit() { + return { + x: Math.floor(Math.random() * (CANVAS_WIDTH / GRID_SIZE)), + y: Math.floor(Math.random() * (CANVAS_HEIGHT / GRID_SIZE)), + color: 'red' + }; + } + + function handleKeydown(e) { + const key = e.key.toLowerCase(); + let direction; + if (['w', 'arrowup'].includes(key)) direction = 'up'; + if (['s', 'arrowdown'].includes(key)) direction = 'down'; + if (['a', 'arrowleft'].includes(key)) direction = 'left'; + if (['d', 'arrowright'].includes(key)) direction = 'right'; + + if (direction) { + if (isHost) { + const hostPlayer = gameState.players[0]; + if ((direction === 'up' && hostPlayer.direction !== 'down') || + (direction === 'down' && hostPlayer.direction !== 'up') || + (direction === 'left' && hostPlayer.direction !== 'right') || + (direction === 'right' && hostPlayer.direction !== 'left')) { + hostPlayer.direction = direction; + } + } else { + conn.send({ type: 'input', direction }); + } + } + } + + function handleClientInput(data) { + if (data.type === 'input') { + const clientPlayer = gameState.players[1]; + if ((data.direction === 'up' && clientPlayer.direction !== 'down') || + (data.direction === 'down' && clientPlayer.direction !== 'up') || + (data.direction === 'left' && clientPlayer.direction !== 'right') || + (data.direction === 'right' && clientPlayer.direction !== 'left')) { + clientPlayer.direction = data.direction; + } + } + } + + function gameLoop(timestamp) { + if (gameState.gameOver) { + draw(); // Draw final game over screen + return; + } + + requestAnimationFrame(gameLoop); + + if (isHost) { + const elapsed = timestamp - lastTickTime; + if (elapsed > gameSpeed) { + lastTickTime = timestamp; + + updateSnakes(); + checkCollisions(); + checkFruit(); + + if (conn && conn.open) { + conn.send(gameState); + } + } + } + + draw(); + } + + function updateSnakes() { + gameState.players.forEach(player => { + const head = { ...player.snake[0] }; + if (player.direction === 'up') head.y--; + if (player.direction === 'down') head.y++; + if (player.direction === 'left') head.x--; + if (player.direction === 'right') head.x++; + player.snake.unshift(head); + }); + } + + function checkCollisions() { + if (!gameState.players || gameState.players.length < 2) return; + const [player1, player2] = gameState.players; + const head1 = player1.snake[0]; + const head2 = player2.snake[0]; + + if (!head1 || !head2) { + gameState.gameOver = true; + return; + } + + if (head1.x < 0 || head1.x >= CANVAS_WIDTH / GRID_SIZE || head1.y < 0 || head1.y >= CANVAS_HEIGHT / GRID_SIZE) { + gameState.gameOver = true; + gameState.winner = player2.id; + } + if (head2.x < 0 || head2.x >= CANVAS_WIDTH / GRID_SIZE || head2.y < 0 || head2.y >= CANVAS_HEIGHT / GRID_SIZE) { + gameState.gameOver = true; + gameState.winner = player1.id; + } + + for (let i = 1; i < player1.snake.length; i++) { + if (head1.x === player1.snake[i].x && head1.y === player1.snake[i].y) { + gameState.gameOver = true; + gameState.winner = player2.id; + } + } + for (let i = 1; i < player2.snake.length; i++) { + if (head2.x === player2.snake[i].x && head2.y === player2.snake[i].y) { + gameState.gameOver = true; + gameState.winner = player1.id; + } + } + + for (let i = 0; i < player2.snake.length; i++) { + if (head1.x === player2.snake[i].x && head1.y === player2.snake[i].y) { + gameState.gameOver = true; + gameState.winner = player2.id; + } + } + for (let i = 0; i < player1.snake.length; i++) { + if (head2.x === player1.snake[i].x && head2.y === player1.snake[i].y) { + gameState.gameOver = true; + gameState.winner = player1.id; + } + } + } + + function checkFruit() { + gameState.players.forEach(player => { + const head = player.snake[0]; + if (head.x === gameState.fruit.x && head.y === gameState.fruit.y) { + gameState.score[player.id]++; + gameState.fruit = generateFruit(); + // Increase speed by reducing the tick interval, with a minimum speed + if (gameSpeed > 30) { + gameSpeed -= 5; + } + } else { + player.snake.pop(); + } + }); + } + + function updateGameState(newState) { + gameState = newState; + } + + function draw() { + ctx.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT); + + if (!gameState.fruit.x) return; // Don't draw if state is not initialized + + ctx.fillStyle = gameState.fruit.color; + ctx.fillRect(gameState.fruit.x * GRID_SIZE, gameState.fruit.y * GRID_SIZE, GRID_SIZE, GRID_SIZE); + + gameState.players.forEach(player => { + ctx.fillStyle = player.color; + player.snake.forEach(segment => { + ctx.fillRect(segment.x * GRID_SIZE, segment.y * GRID_SIZE, GRID_SIZE, GRID_SIZE); + }); + }); + + if (gameState.gameOver) { + ctx.fillStyle = 'rgba(0, 0, 0, 0.5)'; + ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT); + ctx.fillStyle = 'white'; + ctx.font = '50px Arial'; + ctx.textAlign = 'center'; + ctx.fillText('Game Over', CANVAS_WIDTH / 2, CANVAS_HEIGHT / 2 - 25); + ctx.font = '30px Arial'; + let winnerText; + if (gameState.winner === (peer ? peer.id : '')) { + winnerText = 'You Win!'; + } else { + winnerText = isHost ? 'Friend Wins!' : 'Host Wins!'; + } + ctx.fillText(winnerText, CANVAS_WIDTH / 2, CANVAS_HEIGHT / 2 + 25); + } + } + + initializePeer(); +}); diff --git a/styles.css b/styles.css new file mode 100644 index 0000000..e8be29c --- /dev/null +++ b/styles.css @@ -0,0 +1,4 @@ + +#game-canvas { + border: 2px solid #4a5568; +}