Mejoras y optimizaciones en general.

This commit is contained in:
2025-10-03 00:05:08 +02:00
parent bd76741bd2
commit d1a7442ffa
32 changed files with 3336 additions and 783 deletions

View File

@@ -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>

View File

@@ -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} &nbsp; | &nbsp; <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();

View File

@@ -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;
}