Mejoras y optimizaciones en general.
This commit is contained in:
@@ -3,13 +3,25 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Pong Game</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<h1>Pong Clásico</h1>
|
||||
<h1 id="title">Pong Clásico</h1>
|
||||
<p>Usa las flechas arriba/abajo para mover tu pala. ¡No dejes que la bola pase!</p>
|
||||
<canvas id="pong" width="600" height="400"></canvas>
|
||||
<div id="score"></div>
|
||||
|
||||
<div id="controls">
|
||||
<label for="difficulty">Dificultad:</label>
|
||||
<select id="difficulty">
|
||||
<option value="facil">Fácil</option>
|
||||
<option value="normal" selected>Normal</option>
|
||||
<option value="dificil">Difícil</option>
|
||||
</select>
|
||||
<button id="pause-btn">Pausar</button>
|
||||
</div>
|
||||
|
||||
<canvas id="pong" width="600" height="400" aria-label="Tablero Pong"></canvas>
|
||||
<div id="score" aria-live="polite"></div>
|
||||
<button id="restart-btn">Reiniciar</button>
|
||||
<script src="script.js"></script>
|
||||
</body>
|
||||
|
387
pong/script.js
387
pong/script.js
@@ -1,125 +1,332 @@
|
||||
'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;
|
||||
const paddleHeight = 80, paddleWidth = 14;
|
||||
let playerY = h/2 - paddleHeight/2, aiY = h/2 - paddleHeight/2;
|
||||
|
||||
// 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,
|
||||
size: 12,
|
||||
speed: 5,
|
||||
velX: 5,
|
||||
velY: -4
|
||||
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;
|
||||
|
||||
// Dibujo de todo (juego, paletas, bola, marcador)
|
||||
function draw() {
|
||||
// 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);
|
||||
ctx.fillStyle = "#f2e9e4";
|
||||
// Jugador (izq)
|
||||
|
||||
// Palas
|
||||
ctx.fillStyle = '#f2e9e4';
|
||||
ctx.fillRect(16, playerY, paddleWidth, paddleHeight);
|
||||
// AI (der)
|
||||
ctx.fillRect(w-16-paddleWidth, aiY, paddleWidth, paddleHeight);
|
||||
ctx.fillRect(w - 16 - paddleWidth, aiY, paddleWidth, paddleHeight);
|
||||
|
||||
// Bola
|
||||
ctx.beginPath();
|
||||
ctx.arc(ball.x, ball.y, ball.size, 0, 2 * Math.PI);
|
||||
ctx.fillStyle = "#f6c90e";
|
||||
ctx.arc(ball.x, ball.y, ball.r, 0, Math.PI * 2);
|
||||
ctx.fillStyle = '#f6c90e';
|
||||
ctx.fill();
|
||||
|
||||
// Red central
|
||||
for(let i=15;i<h;i+=30){
|
||||
ctx.fillRect(w/2-2,i,4,14);
|
||||
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);
|
||||
}
|
||||
// Score
|
||||
scoreDiv.innerHTML = `<b>Tú:</b> ${playerScore} | <b>Máquina:</b> ${aiScore}`;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
// Jugador
|
||||
if (up) playerY -= 7;
|
||||
if (down) playerY += 7;
|
||||
playerY = Math.max(0, Math.min(h-paddleHeight, playerY));
|
||||
// AI: sigue la bola
|
||||
if (aiY + paddleHeight/2 < ball.y - 12) aiY += 5;
|
||||
else if (aiY + paddleHeight/2 > ball.y + 12) aiY -= 5;
|
||||
aiY = Math.max(0, Math.min(h-paddleHeight, aiY));
|
||||
handlePlayerMovement();
|
||||
handleAIMovement();
|
||||
|
||||
// Bola
|
||||
ball.x += ball.velX;
|
||||
ball.y += ball.velY;
|
||||
stepBall();
|
||||
collideBallWithWalls();
|
||||
|
||||
// Colisión con pared arriba/abajo
|
||||
if (ball.y - ball.size < 0 || ball.y + ball.size > h) ball.velY *= -1;
|
||||
// Colisiones con palas
|
||||
// Jugador (izquierda)
|
||||
collideBallWithPaddle(16, playerY, paddleWidth, paddleHeight, false);
|
||||
// IA (derecha)
|
||||
collideBallWithPaddle(w - 16 - paddleWidth, aiY, paddleWidth, paddleHeight, true);
|
||||
|
||||
// Colisión con paleta jugador
|
||||
if (
|
||||
ball.x - ball.size < 16 + paddleWidth &&
|
||||
ball.y > playerY && ball.y < playerY + paddleHeight
|
||||
) {
|
||||
ball.velX = Math.abs(ball.velX);
|
||||
ball.velY += (Math.random() - 0.5) * 2.5;
|
||||
if (checkGoals()) {
|
||||
drawScene();
|
||||
return;
|
||||
}
|
||||
|
||||
// Colisión con paleta AI
|
||||
if (
|
||||
ball.x + ball.size > w-16-paddleWidth &&
|
||||
ball.y > aiY && ball.y < aiY + paddleHeight
|
||||
) {
|
||||
ball.velX = -Math.abs(ball.velX);
|
||||
ball.velY += (Math.random() - 0.5) * 2.5;
|
||||
}
|
||||
|
||||
// Gol jugador
|
||||
if (ball.x - ball.size < 0) {
|
||||
aiScore++;
|
||||
resetBall();
|
||||
}
|
||||
// Gol máquina
|
||||
if (ball.x + ball.size > w) {
|
||||
playerScore++;
|
||||
resetBall();
|
||||
}
|
||||
|
||||
draw();
|
||||
requestAnimationFrame(gameLoop);
|
||||
drawScene();
|
||||
rafId = requestAnimationFrame(gameLoop);
|
||||
}
|
||||
|
||||
function resetBall() {
|
||||
ball.x = w/2; ball.y = h/2;
|
||||
ball.velX = (Math.random() > 0.5 ? 5 : -5);
|
||||
ball.velY = (Math.random() - 0.5) * 7;
|
||||
running = false;
|
||||
setTimeout(() => {
|
||||
running = true;
|
||||
requestAnimationFrame(gameLoop);
|
||||
}, 900);
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', e => {
|
||||
if (e.key === 'ArrowUp') up = true;
|
||||
if (e.key === 'ArrowDown') down = true;
|
||||
});
|
||||
document.addEventListener('keyup', e => {
|
||||
if (e.key === 'ArrowUp') up = false;
|
||||
if (e.key === 'ArrowDown') down = false;
|
||||
});
|
||||
|
||||
restartBtn.onclick = () => {
|
||||
playerScore = aiScore = 0;
|
||||
playerY = h/2 - paddleHeight/2;
|
||||
aiY = h/2 - paddleHeight/2;
|
||||
resetBall();
|
||||
function resetGame() {
|
||||
playerScore = 0;
|
||||
aiScore = 0;
|
||||
playerY = h / 2 - paddleHeight / 2;
|
||||
aiY = h / 2 - paddleHeight / 2;
|
||||
resetBall(true);
|
||||
running = true;
|
||||
requestAnimationFrame(gameLoop);
|
||||
};
|
||||
updateScoreHUD();
|
||||
if (rafId) cancelAnimationFrame(rafId);
|
||||
rafId = requestAnimationFrame(gameLoop);
|
||||
}
|
||||
|
||||
draw();
|
||||
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();
|
@@ -33,4 +33,53 @@ h1 {
|
||||
}
|
||||
#restart-btn:hover {
|
||||
background: #232946;
|
||||
}
|
||||
/* Controles y accesibilidad */
|
||||
#controls {
|
||||
margin: 0.8rem auto 0.4rem auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
}
|
||||
#controls select#difficulty {
|
||||
font-size: 1em;
|
||||
padding: 6px 10px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #ddd;
|
||||
background: #232946;
|
||||
color: #f2e9e4;
|
||||
}
|
||||
#pause-btn {
|
||||
font-size: 1em;
|
||||
padding: 6px 16px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
background: #4a4e69;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
#pause-btn:hover {
|
||||
background: #232946;
|
||||
}
|
||||
|
||||
/* Foco accesible */
|
||||
#pause-btn:focus-visible,
|
||||
#restart-btn:focus-visible,
|
||||
#controls select#difficulty:focus-visible {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 4px rgba(246, 201, 14, 0.35);
|
||||
}
|
||||
|
||||
/* Estado deshabilitado para botones */
|
||||
#pause-btn:disabled,
|
||||
#restart-btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Ajuste del marcador */
|
||||
#score {
|
||||
font-weight: 600;
|
||||
}
|
Reference in New Issue
Block a user