'use strict'; const canvas = document.getElementById('pong'); const ctx = canvas.getContext('2d'); const scoreDiv = document.getElementById('score'); const restartBtn = document.getElementById('restart-btn'); const difficultySelect = document.getElementById('difficulty'); const pauseBtn = document.getElementById('pause-btn'); const w = canvas.width, h = canvas.height; // Configuraciones por dificultad const DIFFICULTIES = { facil: { ballSpeed: 5.0, aiMaxSpeed: 4.0, paddleHeight: 95 }, normal: { ballSpeed: 6.5, aiMaxSpeed: 5.5, paddleHeight: 80 }, dificil: { ballSpeed: 8.0, aiMaxSpeed: 7.0, paddleHeight: 70 } }; // Estado let paddleHeight = DIFFICULTIES.normal.paddleHeight; const paddleWidth = 14; let playerY = h / 2 - paddleHeight / 2; let aiY = h / 2 - paddleHeight / 2; let playerScore = 0, aiScore = 0; let ball = { x: w / 2, y: h / 2, r: 12, baseSpeed: DIFFICULTIES.normal.ballSpeed, velX: DIFFICULTIES.normal.ballSpeed, velY: -4.0 }; let up = false, down = false; let running = true; let rafId = null; let aiMaxSpeed = DIFFICULTIES.normal.aiMaxSpeed; // Utilidades function clamp(v, min, max) { return Math.max(min, Math.min(max, v)); } function setDifficulty() { const diff = difficultySelect && difficultySelect.value ? difficultySelect.value : 'normal'; const conf = DIFFICULTIES[diff] || DIFFICULTIES.normal; paddleHeight = conf.paddleHeight; aiMaxSpeed = conf.aiMaxSpeed; ball.baseSpeed = conf.ballSpeed; // Reposicionar palas con nueva altura playerY = clamp(playerY, 0, h - paddleHeight); aiY = clamp(aiY, 0, h - paddleHeight); // Reiniciar bola con la velocidad base de la dificultad resetBall(true); updateScoreHUD(); } function updateScoreHUD() { scoreDiv.textContent = `Tú: ${playerScore} | Máquina: ${aiScore}`; } function drawNet() { ctx.fillStyle = '#f2e9e4'; for (let i = 15; i < h; i += 30) { ctx.fillRect(w / 2 - 2, i, 4, 14); } } function drawScene() { ctx.clearRect(0, 0, w, h); // Palas ctx.fillStyle = '#f2e9e4'; ctx.fillRect(16, playerY, paddleWidth, paddleHeight); ctx.fillRect(w - 16 - paddleWidth, aiY, paddleWidth, paddleHeight); // Bola ctx.beginPath(); ctx.arc(ball.x, ball.y, ball.r, 0, Math.PI * 2); ctx.fillStyle = '#f6c90e'; ctx.fill(); // Red central drawNet(); // HUD updateScoreHUD(); } function pauseGame() { if (!running) return; running = false; if (rafId) cancelAnimationFrame(rafId); rafId = null; if (pauseBtn) pauseBtn.textContent = 'Reanudar'; } function resumeGame() { if (running) return; running = true; if (pauseBtn) pauseBtn.textContent = 'Pausar'; rafId = requestAnimationFrame(gameLoop); } function togglePause() { if (running) pauseGame(); else resumeGame(); } function resetBall(hardReset = false) { ball.x = w / 2; ball.y = h / 2; const speed = ball.baseSpeed; ball.velX = (Math.random() > 0.5 ? speed : -speed); ball.velY = (Math.random() - 0.5) * speed * 1.2; if (!hardReset) { pauseGame(); setTimeout(() => resumeGame(), 700); } } function handlePlayerMovement() { const playerSpeed = 7.0; if (up) playerY -= playerSpeed; if (down) playerY += playerSpeed; playerY = clamp(playerY, 0, h - paddleHeight); } function predictBallYAtX(targetX) { // Predicción simple: extrapola y con rebotes verticales let px = ball.x; let py = ball.y; let vx = ball.velX; let vy = ball.velY; const r = ball.r; const maxSteps = 1000; for (let i = 0; i < maxSteps; i++) { // Avanza px += vx; py += vy; // Rebote arriba/abajo if (py - r < 0) { py = r; vy = -vy; } else if (py + r > h) { py = h - r; vy = -vy; } // Si hemos cruzado el targetX if ((vx > 0 && px + r >= targetX) || (vx < 0 && px - r <= targetX)) { return py; } // Seguridad: si vx se hace cero (no debería), rompe if (Math.abs(vx) < 0.0001) break; } return py; } function handleAIMovement() { // IA solo sigue cuando la bola va hacia ella, si no recentra lentamente const aiCenter = aiY + paddleHeight / 2; let targetY; if (ball.velX > 0) { // Objetivo es el centro de la pala cuando la bola llegue al borde derecho const targetX = w - 16 - paddleWidth - ball.r; targetY = predictBallYAtX(targetX); } else { // Bola alejándose: recentrar targetY = h / 2; } const desired = targetY - aiCenter; const step = clamp(desired, -aiMaxSpeed, aiMaxSpeed); aiY += step; aiY = clamp(aiY, 0, h - paddleHeight); } function collideBallWithWalls() { // Arriba/abajo if (ball.y - ball.r < 0) { ball.y = ball.r; ball.velY *= -1; } else if (ball.y + ball.r > h) { ball.y = h - ball.r; ball.velY *= -1; } } function collideBallWithPaddle(px, py, pw, ph, reverseXSign) { // Chequeo de colisión círculo-rectángulo const nearestX = clamp(ball.x, px, px + pw); const nearestY = clamp(ball.y, py, py + ph); const dx = ball.x - nearestX; const dy = ball.y - nearestY; const dist2 = dx * dx + dy * dy; if (dist2 <= ball.r * ball.r) { // Rebote con ángulo según punto de impacto relativo const hitPos = ((nearestY - py) - ph / 2) / (ph / 2); // -1..1 con referencia al centro const maxAngle = Math.PI / 3; // 60 grados const speed = Math.max(Math.abs(ball.velX), Math.abs(ball.velY), ball.baseSpeed); const angle = hitPos * maxAngle; const newVX = speed * Math.cos(angle); const newVY = speed * Math.sin(angle); ball.velX = reverseXSign ? -Math.abs(newVX) : Math.abs(newVX); ball.velY = newVY; // Pequeño empujón para salir del rect ball.x += ball.velX * 0.2; ball.y += ball.velY * 0.2; } } function checkGoals() { // Gol IA (bola sale por izquierda) if (ball.x + ball.r < 0) { aiScore++; resetBall(); return true; } // Gol jugador (bola sale por derecha) if (ball.x - ball.r > w) { playerScore++; resetBall(); return true; } return false; } function stepBall() { ball.x += ball.velX; ball.y += ball.velY; } function gameLoop() { if (!running) return; handlePlayerMovement(); handleAIMovement(); stepBall(); collideBallWithWalls(); // Colisiones con palas // Jugador (izquierda) collideBallWithPaddle(16, playerY, paddleWidth, paddleHeight, false); // IA (derecha) collideBallWithPaddle(w - 16 - paddleWidth, aiY, paddleWidth, paddleHeight, true); if (checkGoals()) { drawScene(); return; } drawScene(); rafId = requestAnimationFrame(gameLoop); } function resetGame() { playerScore = 0; aiScore = 0; playerY = h / 2 - paddleHeight / 2; aiY = h / 2 - paddleHeight / 2; resetBall(true); running = true; updateScoreHUD(); if (rafId) cancelAnimationFrame(rafId); rafId = requestAnimationFrame(gameLoop); } function attachEvents() { // Teclado (evita scroll con flechas) window.addEventListener('keydown', (e) => { if (e.key === 'ArrowUp') { e.preventDefault(); up = true; } if (e.key === 'ArrowDown') { e.preventDefault(); down = true; } }); window.addEventListener('keyup', (e) => { if (e.key === 'ArrowUp') { e.preventDefault(); up = false; } if (e.key === 'ArrowDown') { e.preventDefault(); down = false; } }); // Pausa if (pauseBtn) { pauseBtn.addEventListener('click', togglePause); } // Dificultad if (difficultySelect) { difficultySelect.addEventListener('change', () => { setDifficulty(); }); } // Reinicio restartBtn.addEventListener('click', resetGame); } function startGame() { attachEvents(); setDifficulty(); drawScene(); rafId = requestAnimationFrame(gameLoop); } // Init startGame();