Spaces:
Running
Running
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Classic Pong Game</title> | |
<script src="https://cdn.tailwindcss.com"></script> | |
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/peerjs.min.js"></script> | |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
<style> | |
@keyframes pulse { | |
0%, 100% { opacity: 1; } | |
50% { opacity: 0.5; } | |
} | |
.pulse-animation { | |
animation: pulse 1.5s infinite; | |
} | |
#gameCanvas { | |
background-color: #111827; | |
border-radius: 8px; | |
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.3); | |
} | |
.glow { | |
box-shadow: 0 0 10px rgba(59, 130, 246, 0.7); | |
} | |
.score-display { | |
font-family: 'Courier New', monospace; | |
text-shadow: 0 0 5px rgba(59, 130, 246, 0.7); | |
} | |
</style> | |
</head> | |
<body class="bg-gray-900 text-white min-h-screen flex flex-col items-center justify-center p-4"> | |
<div class="max-w-4xl w-full"> | |
<h1 class="text-4xl font-bold text-center mb-2 text-blue-400">Classic Pong</h1> | |
<p class="text-center text-gray-400 mb-8">Relive the arcade classic with modern multiplayer</p> | |
<div id="menu" class="flex flex-col items-center space-y-6 mb-8"> | |
<button id="singlePlayerBtn" class="bg-blue-600 hover:bg-blue-700 text-white font-bold py-3 px-6 rounded-lg transition-all duration-300 transform hover:scale-105 w-64 flex items-center justify-center space-x-2"> | |
<i class="fas fa-robot"></i> | |
<span>Play vs Computer</span> | |
</button> | |
<button id="multiplayerBtn" class="bg-purple-600 hover:bg-purple-700 text-white font-bold py-3 px-6 rounded-lg transition-all duration-300 transform hover:scale-105 w-64 flex items-center justify-center space-x-2"> | |
<i class="fas fa-users"></i> | |
<span>Multiplayer</span> | |
</button> | |
<div id="multiplayerControls" class="hidden flex-col items-center space-y-4 w-full max-w-md"> | |
<div class="flex space-x-4 w-full"> | |
<button id="createRoomBtn" class="bg-green-600 hover:bg-green-700 text-white font-bold py-2 px-4 rounded-lg transition-all duration-300 flex-1 flex items-center justify-center space-x-2"> | |
<i class="fas fa-plus"></i> | |
<span>Create Room</span> | |
</button> | |
<button id="joinRoomBtn" class="bg-yellow-600 hover:bg-yellow-700 text-white font-bold py-2 px-4 rounded-lg transition-all duration-300 flex-1 flex items-center justify-center space-x-2"> | |
<i class="fas fa-sign-in-alt"></i> | |
<span>Join Room</span> | |
</button> | |
</div> | |
<div id="roomControls" class="hidden w-full space-y-4"> | |
<div id="createRoomSection" class="hidden"> | |
<div class="bg-gray-800 p-4 rounded-lg"> | |
<p class="text-sm text-gray-400 mb-2">Share this ID with your friend:</p> | |
<div class="flex items-center space-x-2"> | |
<input id="hostPeerId" type="text" readonly class="bg-gray-700 text-white p-2 rounded flex-1 font-mono"> | |
<button id="copyHostIdBtn" class="bg-blue-600 hover:bg-blue-700 text-white p-2 rounded"> | |
<i class="fas fa-copy"></i> | |
</button> | |
</div> | |
<p class="text-xs text-gray-500 mt-2">Waiting for player to join...</p> | |
</div> | |
</div> | |
<div id="joinRoomSection" class="hidden"> | |
<div class="bg-gray-800 p-4 rounded-lg"> | |
<p class="text-sm text-gray-400 mb-2">Enter host's ID:</p> | |
<div class="flex items-center space-x-2"> | |
<input id="guestPeerId" type="text" class="bg-gray-700 text-white p-2 rounded flex-1 font-mono" placeholder="Enter host ID"> | |
<button id="connectBtn" class="bg-green-600 hover:bg-green-700 text-white p-2 rounded"> | |
<i class="fas fa-plug"></i> Connect | |
</button> | |
</div> | |
</div> | |
</div> | |
<div id="connectionStatus" class="hidden bg-gray-800 p-4 rounded-lg text-center"> | |
<p class="text-yellow-400 pulse-animation"> | |
<i class="fas fa-circle-notch fa-spin"></i> Connecting... | |
</p> | |
</div> | |
<div id="connectedStatus" class="hidden bg-gray-800 p-4 rounded-lg text-center"> | |
<p class="text-green-400"> | |
<i class="fas fa-check-circle"></i> Connected! | |
</p> | |
</div> | |
</div> | |
</div> | |
</div> | |
<div id="gameContainer" class="hidden flex flex-col items-center"> | |
<div class="flex justify-between w-full mb-4"> | |
<div class="score-display text-2xl">Player: <span id="playerScore">0</span></div> | |
<div class="score-display text-2xl">Opponent: <span id="opponentScore">0</span></div> | |
</div> | |
<canvas id="gameCanvas" width="800" height="500" class="w-full max-w-full"></canvas> | |
<div id="gameControls" class="mt-4 flex space-x-4"> | |
<button id="pauseBtn" class="bg-yellow-600 hover:bg-yellow-700 text-white font-bold py-2 px-4 rounded-lg"> | |
<i class="fas fa-pause"></i> Pause | |
</button> | |
<button id="restartBtn" class="bg-red-600 hover:bg-red-700 text-white font-bold py-2 px-4 rounded-lg"> | |
<i class="fas fa-redo"></i> Restart | |
</button> | |
<button id="backToMenuBtn" class="bg-gray-600 hover:bg-gray-700 text-white font-bold py-2 px-4 rounded-lg"> | |
<i class="fas fa-arrow-left"></i> Menu | |
</button> | |
</div> | |
<div id="gameMessage" class="mt-4 text-xl font-bold text-center hidden"></div> | |
</div> | |
</div> | |
<script> | |
// Game elements | |
const canvas = document.getElementById('gameCanvas'); | |
const ctx = canvas.getContext('2d'); | |
const playerScoreDisplay = document.getElementById('playerScore'); | |
const opponentScoreDisplay = document.getElementById('opponentScore'); | |
const gameMessage = document.getElementById('gameMessage'); | |
// Menu elements | |
const menu = document.getElementById('menu'); | |
const gameContainer = document.getElementById('gameContainer'); | |
const singlePlayerBtn = document.getElementById('singlePlayerBtn'); | |
const multiplayerBtn = document.getElementById('multiplayerBtn'); | |
const multiplayerControls = document.getElementById('multiplayerControls'); | |
const roomControls = document.getElementById('roomControls'); | |
const createRoomBtn = document.getElementById('createRoomBtn'); | |
const joinRoomBtn = document.getElementById('joinRoomBtn'); | |
const createRoomSection = document.getElementById('createRoomSection'); | |
const joinRoomSection = document.getElementById('joinRoomSection'); | |
const hostPeerId = document.getElementById('hostPeerId'); | |
const copyHostIdBtn = document.getElementById('copyHostIdBtn'); | |
const guestPeerId = document.getElementById('guestPeerId'); | |
const connectBtn = document.getElementById('connectBtn'); | |
const connectionStatus = document.getElementById('connectionStatus'); | |
const connectedStatus = document.getElementById('connectedStatus'); | |
const pauseBtn = document.getElementById('pauseBtn'); | |
const restartBtn = document.getElementById('restartBtn'); | |
const backToMenuBtn = document.getElementById('backToMenuBtn'); | |
// Game variables | |
let gameMode = null; // 'single' or 'multiplayer' | |
let isHost = false; | |
let isPaused = false; | |
let gameRunning = false; | |
let animationId; | |
let resetInProgress = false; // Flag to prevent reset loops | |
let paddleUpdateInterval; | |
// Pong game objects | |
const paddleWidth = 15; | |
const paddleHeight = 100; | |
const ballSize = 10; | |
const paddleSpeed = 8; | |
const initialBallSpeed = 5; | |
const paddleUpdateFrequency = 50; // ms between paddle position updates | |
let leftPaddle = { | |
x: 30, | |
y: canvas.height / 2 - paddleHeight / 2, | |
width: paddleWidth, | |
height: paddleHeight, | |
dy: 0, | |
score: 0 | |
}; | |
let rightPaddle = { | |
x: canvas.width - 30 - paddleWidth, | |
y: canvas.height / 2 - paddleHeight / 2, | |
width: paddleWidth, | |
height: paddleHeight, | |
dy: 0, | |
score: 0 | |
}; | |
let ball = { | |
x: canvas.width / 2, | |
y: canvas.height / 2, | |
size: ballSize, | |
dx: initialBallSpeed, | |
dy: initialBallSpeed | |
}; | |
// PeerJS variables | |
let peer; | |
let conn; | |
let myPeerId; | |
let connectionTimeout; | |
// Event listeners | |
singlePlayerBtn.addEventListener('click', () => { | |
gameMode = 'single'; | |
startGame(); | |
}); | |
multiplayerBtn.addEventListener('click', () => { | |
multiplayerControls.classList.remove('hidden'); | |
multiplayerBtn.classList.add('hidden'); | |
singlePlayerBtn.classList.add('hidden'); | |
}); | |
createRoomBtn.addEventListener('click', () => { | |
isHost = true; | |
setupPeerConnection(); | |
createRoomSection.classList.remove('hidden'); | |
joinRoomSection.classList.add('hidden'); | |
roomControls.classList.remove('hidden'); | |
}); | |
joinRoomBtn.addEventListener('click', () => { | |
isHost = false; | |
setupPeerConnection(); | |
joinRoomSection.classList.remove('hidden'); | |
createRoomSection.classList.add('hidden'); | |
roomControls.classList.remove('hidden'); | |
}); | |
copyHostIdBtn.addEventListener('click', () => { | |
hostPeerId.select(); | |
document.execCommand('copy'); | |
showMessage('Copied to clipboard!', 'green-400'); | |
}); | |
connectBtn.addEventListener('click', () => { | |
const hostId = guestPeerId.value.trim(); | |
if (!hostId) { | |
showMessage('Please enter a valid host ID', 'red-400'); | |
return; | |
} | |
if (!peer) { | |
showMessage('Peer connection not ready yet', 'red-400'); | |
return; | |
} | |
connectionStatus.classList.remove('hidden'); | |
// Set timeout for connection attempt | |
connectionTimeout = setTimeout(() => { | |
if (connectionStatus && !connectionStatus.classList.contains('hidden')) { | |
showMessage('Connection timed out. Try again.', 'red-400'); | |
connectionStatus.classList.add('hidden'); | |
if (conn) conn.close(); | |
} | |
}, 15000); // 15 seconds timeout | |
conn = peer.connect(hostId, { | |
reliable: true, | |
serialization: 'json', | |
metadata: { | |
game: 'pong', | |
version: '1.0' | |
} | |
}); | |
conn.on('open', () => { | |
clearTimeout(connectionTimeout); | |
connectionStatus.classList.add('hidden'); | |
connectedStatus.classList.remove('hidden'); | |
gameMode = 'multiplayer'; | |
// Set up data handler | |
conn.on('data', handleData); | |
// Handle connection close | |
conn.on('close', () => { | |
if (gameRunning) { | |
showMessage('Player disconnected', 'red-400'); | |
returnToMenu(); | |
} | |
}); | |
// Start sending paddle updates | |
if (!isHost) { | |
startPaddleUpdates(rightPaddle); | |
} else { | |
startPaddleUpdates(leftPaddle); | |
} | |
startGame(); | |
}); | |
conn.on('error', (err) => { | |
clearTimeout(connectionTimeout); | |
console.error('Connection error:', err); | |
showMessage('Connection failed: ' + (err.message || 'Unknown error'), 'red-400'); | |
connectionStatus.classList.add('hidden'); | |
if (conn) conn.close(); | |
}); | |
}); | |
pauseBtn.addEventListener('click', togglePause); | |
restartBtn.addEventListener('click', resetGame); | |
backToMenuBtn.addEventListener('click', returnToMenu); | |
// Keyboard controls | |
const keys = {}; | |
document.addEventListener('keydown', (e) => { | |
keys[e.key] = true; | |
// Prevent default for arrow keys and space to avoid page scrolling | |
if (['ArrowUp', 'ArrowDown', ' '].includes(e.key)) { | |
e.preventDefault(); | |
} | |
}); | |
document.addEventListener('keyup', (e) => { | |
keys[e.key] = false; | |
}); | |
// Game functions | |
function startGame() { | |
menu.classList.add('hidden'); | |
gameContainer.classList.remove('hidden'); | |
gameRunning = true; | |
resetGame(); | |
gameLoop(); | |
// Focus canvas for keyboard controls | |
canvas.focus(); | |
} | |
function gameLoop() { | |
if (isPaused || !gameRunning) return; | |
update(); | |
draw(); | |
animationId = requestAnimationFrame(gameLoop); | |
} | |
function update() { | |
// Update paddles based on keyboard input | |
if (gameMode === 'single') { | |
// Player controls (left paddle) | |
if (keys['w'] || keys['ArrowUp']) { | |
leftPaddle.dy = -paddleSpeed; | |
} else if (keys['s'] || keys['ArrowDown']) { | |
leftPaddle.dy = paddleSpeed; | |
} else { | |
leftPaddle.dy = 0; | |
} | |
// Simple AI for right paddle | |
const paddleCenter = rightPaddle.y + rightPaddle.height / 2; | |
const ballCenter = ball.y + ball.size / 2; | |
if (paddleCenter < ballCenter - 10) { | |
rightPaddle.dy = paddleSpeed * 0.7; // Slightly slower than player | |
} else if (paddleCenter > ballCenter + 10) { | |
rightPaddle.dy = -paddleSpeed * 0.7; | |
} else { | |
rightPaddle.dy = 0; | |
} | |
} else { | |
// Multiplayer controls | |
if (isHost) { | |
// Host controls left paddle | |
if (keys['w'] || keys['ArrowUp']) { | |
leftPaddle.dy = -paddleSpeed; | |
} else if (keys['s'] || keys['ArrowDown']) { | |
leftPaddle.dy = paddleSpeed; | |
} else { | |
leftPaddle.dy = 0; | |
} | |
} else { | |
// Guest controls right paddle | |
if (keys['ArrowUp']) { | |
rightPaddle.dy = -paddleSpeed; | |
} else if (keys['ArrowDown']) { | |
rightPaddle.dy = paddleSpeed; | |
} else { | |
rightPaddle.dy = 0; | |
} | |
} | |
} | |
// Move paddles | |
leftPaddle.y += leftPaddle.dy; | |
rightPaddle.y += rightPaddle.dy; | |
// Paddle boundaries | |
if (leftPaddle.y < 0) leftPaddle.y = 0; | |
if (leftPaddle.y + leftPaddle.height > canvas.height) leftPaddle.y = canvas.height - leftPaddle.height; | |
if (rightPaddle.y < 0) rightPaddle.y = 0; | |
if (rightPaddle.y + rightPaddle.height > canvas.height) rightPaddle.y = canvas.height - rightPaddle.height; | |
// Move ball | |
ball.x += ball.dx; | |
ball.y += ball.dy; | |
// Ball collision with top and bottom | |
if (ball.y - ball.size / 2 < 0 || ball.y + ball.size / 2 > canvas.height) { | |
ball.dy = -ball.dy; | |
} | |
// Ball collision with paddles | |
if ( | |
ball.x - ball.size / 2 < leftPaddle.x + leftPaddle.width && | |
ball.x + ball.size / 2 > leftPaddle.x && | |
ball.y + ball.size / 2 > leftPaddle.y && | |
ball.y - ball.size / 2 < leftPaddle.y + leftPaddle.height | |
) { | |
const hitPosition = (ball.y - (leftPaddle.y + leftPaddle.height / 2)) / (leftPaddle.height / 2); | |
ball.dx = Math.abs(ball.dx) * 1.05; // Increase speed slightly | |
ball.dy = hitPosition * 5; // Change angle based on where ball hits paddle | |
ball.x = leftPaddle.x + leftPaddle.width + ball.size / 2; | |
if (gameMode === 'multiplayer' && isHost) { | |
sendBallUpdate(); | |
} | |
} | |
if ( | |
ball.x + ball.size / 2 > rightPaddle.x && | |
ball.x - ball.size / 2 < rightPaddle.x + rightPaddle.width && | |
ball.y + ball.size / 2 > rightPaddle.y && | |
ball.y - ball.size / 2 < rightPaddle.y + rightPaddle.height | |
) { | |
const hitPosition = (ball.y - (rightPaddle.y + rightPaddle.height / 2)) / (rightPaddle.height / 2); | |
ball.dx = -Math.abs(ball.dx) * 1.05; | |
ball.dy = hitPosition * 5; | |
ball.x = rightPaddle.x - ball.size / 2; | |
if (gameMode === 'multiplayer' && !isHost) { | |
// Guest doesn't send ball updates - only host does | |
} | |
} | |
// Ball out of bounds (score) | |
if (ball.x - ball.size / 2 < 0) { | |
rightPaddle.score++; | |
opponentScoreDisplay.textContent = rightPaddle.score; | |
resetBall(); | |
if (gameMode === 'multiplayer' && isHost) { | |
sendScoreUpdate(); | |
} | |
if (rightPaddle.score >= 5) { | |
endGame(isHost ? 'You lost!' : 'You won!'); | |
} | |
} | |
if (ball.x + ball.size / 2 > canvas.width) { | |
leftPaddle.score++; | |
playerScoreDisplay.textContent = leftPaddle.score; | |
resetBall(); | |
if (gameMode === 'multiplayer' && !isHost) { | |
sendScoreUpdate(); | |
} | |
if (leftPaddle.score >= 5) { | |
endGame(isHost ? 'You won!' : 'You lost!'); | |
} | |
} | |
} | |
function draw() { | |
// Clear canvas | |
ctx.clearRect(0, 0, canvas.width, canvas.height); | |
// Draw center line | |
ctx.strokeStyle = 'rgba(255, 255, 255, 0.2)'; | |
ctx.setLineDash([10, 10]); | |
ctx.beginPath(); | |
ctx.moveTo(canvas.width / 2, 0); | |
ctx.lineTo(canvas.width / 2, canvas.height); | |
ctx.stroke(); | |
ctx.setLineDash([]); | |
// Draw paddles | |
ctx.fillStyle = '#3B82F6'; | |
ctx.fillRect(leftPaddle.x, leftPaddle.y, leftPaddle.width, leftPaddle.height); | |
ctx.fillStyle = '#EC4899'; | |
ctx.fillRect(rightPaddle.x, rightPaddle.y, rightPaddle.width, rightPaddle.height); | |
// Draw ball | |
ctx.fillStyle = '#FFFFFF'; | |
ctx.beginPath(); | |
ctx.arc(ball.x, ball.y, ball.size / 2, 0, Math.PI * 2); | |
ctx.fill(); | |
} | |
function resetBall() { | |
ball.x = canvas.width / 2; | |
ball.y = canvas.height / 2; | |
// Random direction but always towards the scoring player | |
const direction = Math.random() > 0.5 ? 1 : -1; | |
ball.dx = initialBallSpeed * direction; | |
ball.dy = (Math.random() * 4 - 2); // Random angle between -2 and 2 | |
} | |
function resetGame() { | |
if (resetInProgress) return; | |
resetInProgress = true; | |
leftPaddle.y = canvas.height / 2 - paddleHeight / 2; | |
rightPaddle.y = canvas.height / 2 - paddleHeight / 2; | |
leftPaddle.score = 0; | |
rightPaddle.score = 0; | |
playerScoreDisplay.textContent = '0'; | |
opponentScoreDisplay.textContent = '0'; | |
resetBall(); | |
// In multiplayer, only host can send reset command | |
if (gameMode === 'multiplayer' && isHost) { | |
sendGameState('reset'); | |
} | |
gameMessage.classList.add('hidden'); | |
isPaused = false; | |
pauseBtn.innerHTML = '<i class="fas fa-pause"></i> Pause'; | |
// Reset the flag after a small delay | |
setTimeout(() => { | |
resetInProgress = false; | |
}, 100); | |
} | |
function endGame(message) { | |
gameRunning = false; | |
cancelAnimationFrame(animationId); | |
clearInterval(paddleUpdateInterval); | |
gameMessage.textContent = message; | |
gameMessage.classList.remove('hidden'); | |
gameMessage.className = 'mt-4 text-xl font-bold text-center'; | |
if (message.includes('won')) { | |
gameMessage.classList.add('text-green-400'); | |
} else { | |
gameMessage.classList.add('text-red-400'); | |
} | |
if (gameMode === 'multiplayer') { | |
sendGameState('end', message); | |
} | |
} | |
function togglePause() { | |
isPaused = !isPaused; | |
if (isPaused) { | |
cancelAnimationFrame(animationId); | |
clearInterval(paddleUpdateInterval); | |
pauseBtn.innerHTML = '<i class="fas fa-play"></i> Resume'; | |
gameMessage.textContent = 'Game Paused'; | |
gameMessage.classList.remove('hidden'); | |
gameMessage.className = 'mt-4 text-xl font-bold text-center text-yellow-400'; | |
if (gameMode === 'multiplayer') { | |
sendGameState('pause'); | |
} | |
} else { | |
pauseBtn.innerHTML = '<i class="fas fa-pause"></i> Pause'; | |
gameMessage.classList.add('hidden'); | |
if (gameMode === 'multiplayer') { | |
// Restart paddle updates | |
if (isHost) { | |
startPaddleUpdates(leftPaddle); | |
} else { | |
startPaddleUpdates(rightPaddle); | |
} | |
sendGameState('resume'); | |
} | |
gameLoop(); | |
} | |
} | |
function startPaddleUpdates(paddle) { | |
// Clear any existing interval | |
clearInterval(paddleUpdateInterval); | |
// Start sending paddle position updates at regular intervals | |
paddleUpdateInterval = setInterval(() => { | |
if (gameRunning && !isPaused && conn && conn.open) { | |
sendPaddlePosition(paddle.y); | |
} | |
}, paddleUpdateFrequency); | |
} | |
function returnToMenu() { | |
// Clean up PeerJS connection | |
if (conn) { | |
conn.close(); | |
} | |
if (peer) { | |
peer.destroy(); | |
} | |
clearTimeout(connectionTimeout); | |
clearInterval(paddleUpdateInterval); | |
// Reset game state | |
cancelAnimationFrame(animationId); | |
gameRunning = false; | |
resetInProgress = false; | |
// Show menu and hide game | |
menu.classList.remove('hidden'); | |
gameContainer.classList.add('hidden'); | |
multiplayerControls.classList.add('hidden'); | |
roomControls.classList.add('hidden'); | |
connectionStatus.classList.add('hidden'); | |
connectedStatus.classList.add('hidden'); | |
multiplayerBtn.classList.remove('hidden'); | |
singlePlayerBtn.classList.remove('hidden'); | |
// Reset multiplayer UI | |
createRoomSection.classList.add('hidden'); | |
joinRoomSection.classList.add('hidden'); | |
} | |
function showMessage(message, colorClass) { | |
const messageDiv = document.createElement('div'); | |
messageDiv.className = `fixed top-4 left-1/2 transform -translate-x-1/2 bg-gray-800 text-${colorClass} px-4 py-2 rounded-lg shadow-lg z-50`; | |
messageDiv.textContent = message; | |
document.body.appendChild(messageDiv); | |
setTimeout(() => { | |
messageDiv.remove(); | |
}, 3000); | |
} | |
// PeerJS functions | |
function setupPeerConnection() { | |
// Destroy previous peer instance if exists | |
if (peer) { | |
peer.destroy(); | |
} | |
// Create PeerJS instance with TURN servers for better connectivity | |
peer = new Peer({ | |
config: { | |
iceServers: [ | |
{ urls: 'stun:stun.l.google.com:19302' } | |
] | |
}, | |
debug: 3 // Enable debug logging | |
}); | |
peer.on('open', (id) => { | |
console.log('My peer ID is: ' + id); | |
myPeerId = id; | |
if (isHost) { | |
hostPeerId.value = id; | |
// Listen for incoming connections | |
peer.on('connection', (connection) => { | |
console.log('Incoming connection from:', connection.peer); | |
conn = connection; | |
conn.on('open', () => { | |
console.log('Connection established with:', conn.peer); | |
clearTimeout(connectionTimeout); | |
connectionStatus.classList.add('hidden'); | |
connectedStatus.classList.remove('hidden'); | |
// Set up data handler | |
conn.on('data', handleData); | |
// Handle connection close | |
conn.on('close', () => { | |
console.log('Connection closed'); | |
if (gameRunning) { | |
showMessage('Player disconnected', 'red-400'); | |
returnToMenu(); | |
} | |
}); | |
gameMode = 'multiplayer'; | |
startGame(); | |
// Host starts sending paddle updates | |
startPaddleUpdates(leftPaddle); | |
}); | |
conn.on('error', (err) => { | |
console.error('Connection error:', err); | |
showMessage('Connection error: ' + (err.message || 'Unknown error'), 'red-400'); | |
}); | |
}); | |
} | |
// Reset connection status if we're joining | |
if (!isHost) { | |
connectionStatus.classList.add('hidden'); | |
connectedStatus.classList.add('hidden'); | |
} | |
}); | |
peer.on('error', (err) => { | |
console.error('PeerJS error:', err); | |
showMessage('Connection error: ' + (err.message || 'Unknown error'), 'red-400'); | |
connectionStatus.classList.add('hidden'); | |
if (conn) conn.close(); | |
if (isHost) { | |
returnToMenu(); | |
} | |
}); | |
} | |
function handleData(data) { | |
console.log('Received data:', data); | |
if (data.type === 'paddle') { | |
if (isHost) { | |
// Host receives guest's paddle position (right paddle) | |
rightPaddle.y = data.y; | |
} else { | |
// Guest receives host's paddle position (left paddle) | |
leftPaddle.y = data.y; | |
} | |
} else if (data.type === 'ball') { | |
if (!isHost) { // Only guests should update ball from host | |
ball.x = data.x; | |
ball.y = data.y; | |
ball.dx = data.dx; | |
ball.dy = data.dy; | |
} | |
} else if (data.type === 'score') { | |
if (isHost) { | |
leftPaddle.score = data.playerScore; | |
playerScoreDisplay.textContent = data.playerScore; | |
} else { | |
rightPaddle.score = data.opponentScore; | |
opponentScoreDisplay.textContent = data.opponentScore; | |
} | |
} else if (data.type === 'gameState') { | |
if (data.state === 'reset' && !isHost) { | |
// Only guests should respond to reset commands | |
leftPaddle.y = canvas.height / 2 - paddleHeight / 2; | |
rightPaddle.y = canvas.height / 2 - paddleHeight / 2; | |
leftPaddle.score = 0; | |
rightPaddle.score = 0; | |
playerScoreDisplay.textContent = '0'; | |
opponentScoreDisplay.textContent = '0'; | |
resetBall(); | |
gameMessage.textContent = 'Game reset by host'; | |
gameMessage.classList.remove('hidden'); | |
gameMessage.className = 'mt-4 text-xl font-bold text-center text-yellow-400'; | |
setTimeout(() => { | |
gameMessage.classList.add('hidden'); | |
}, 2000); | |
} else if (data.state === 'pause') { | |
isPaused = true; | |
cancelAnimationFrame(animationId); | |
clearInterval(paddleUpdateInterval); | |
pauseBtn.innerHTML = '<i class="fas fa-play"></i> Resume'; | |
gameMessage.textContent = 'Game Paused by ' + (isHost ? 'you' : 'host'); | |
gameMessage.classList.remove('hidden'); | |
gameMessage.className = 'mt-4 text-xl font-bold text-center text-yellow-400'; | |
} else if (data.state === 'resume') { | |
isPaused = false; | |
pauseBtn.innerHTML = '<i class="fas fa-pause"></i> Pause'; | |
gameMessage.classList.add('hidden'); | |
// Restart paddle updates if we're a guest | |
if (!isHost) { | |
startPaddleUpdates(rightPaddle); | |
} | |
gameLoop(); | |
} else if (data.state === 'end') { | |
endGame(data.message); | |
} | |
} | |
} | |
function sendPaddlePosition(y) { | |
if (conn && conn.open) { | |
const data = { | |
type: 'paddle', | |
y: y | |
}; | |
console.log('Sending paddle position:', data); | |
conn.send(data); | |
} | |
} | |
function sendBallUpdate() { | |
if (conn && conn.open && isHost) { // Only host should send ball updates | |
const data = { | |
type: 'ball', | |
x: ball.x, | |
y: ball.y, | |
dx: ball.dx, | |
dy: ball.dy | |
}; | |
console.log('Sending ball update:', data); | |
conn.send(data); | |
} | |
} | |
function sendScoreUpdate() { | |
if (conn && conn.open) { | |
const data = { | |
type: 'score', | |
playerScore: leftPaddle.score, | |
opponentScore: rightPaddle.score | |
}; | |
console.log('Sending score update:', data); | |
conn.send(data); | |
} | |
} | |
function sendGameState(state, message = '') { | |
if (conn && conn.open) { | |
const data = { | |
type: 'gameState', | |
state: state, | |
message: message | |
}; | |
console.log('Sending game state:', data); | |
conn.send(data); | |
} | |
} | |
</script> | |
</body> | |
</html> |