Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>P2P Snake Game</title>
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
<link rel="stylesheet" href="styles.css">
</head>
<body class="bg-gray-900 text-white flex items-center justify-center h-screen">
<div id="lobby" class="text-center">
<h1 class="text-4xl font-bold mb-8">P2P Snake Game</h1>
<div class="space-y-4">
<button id="create-game" class="bg-green-500 hover:bg-green-600 text-white font-bold py-2 px-4 rounded">
Create Game
</button>
<div id="game-id-display" class="hidden">
<p>Share this ID with your friend:</p>
<input id="game-id" type="text" readonly class="bg-gray-800 text-white text-center p-2 rounded w-full">
</div>
<hr class="border-gray-700 my-4">
<div>
<input id="join-id-input" type="text" placeholder="Enter Friend's ID" class="bg-gray-800 text-white p-2 rounded">
<button id="join-game" class="bg-blue-500 hover:bg-blue-600 text-white font-bold py-2 px-4 rounded ml-2">
Join Game
</button>
</div>
</div>
<p id="status" class="mt-4 text-gray-400"></p>
</div>

<div id="game-container" class="hidden">
<canvas id="game-canvas" width="600" height="600" class="bg-gray-800"></canvas>
</div>

<script src="https://unpkg.com/peerjs@1.3.1/dist/peerjs.min.js"></script>
<script src="script.js"></script>
</body>
</html>
306 changes: 306 additions & 0 deletions script.js
Original file line number Diff line number Diff line change
@@ -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();
});
4 changes: 4 additions & 0 deletions styles.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@

#game-canvas {
border: 2px solid #4a5568;
}