// ── Canvas setup ────────────────────────────────────────────── const canvas = document.getElementById('gameCanvas'); const ctx = canvas.getContext('2d'); // ── Constants ───────────────────────────────────────────────── const CANVAS_WIDTH = 800; const CANVAS_HEIGHT = 600; const WIN_SCORE = 7; const BAT_SPEED = 6; const AI_SPEED = 4; // ── Data models ─────────────────────────────────────────────── const ball = { x: 400, y: 300, radius: 8, vx: 4, vy: 3, speed: 4, baseSpeed: 4, maxSpeed: 8 }; const leftBat = { x: 20, y: 250, width: 12, height: 80, dy: 0 }; const rightBat = { x: 768, y: 250, width: 12, height: 80, dy: 0 }; const score = { left: 0, right: 0 }; let gameState = 'START'; // 'START' | 'PLAYING' | 'GAME_OVER' let gameMode = '1player'; // '1player' | '2player' // ── Game loop ───────────────────────────────────────────────── function gameLoop() { update(); render(); requestAnimationFrame(gameLoop); } function update() { if (gameState !== 'PLAYING') return; moveBats(); if (gameMode === '1player') moveAI(); moveBall(); checkCollisions(); checkScore(); } // ── AI opponent ─────────────────────────────────────────────── function moveAI() { if (ball.vx > 0) { // Ball moving toward AI — track it if (rightBat.y + rightBat.height / 2 < ball.y) rightBat.y += 4; else rightBat.y -= 4; } else { // Ball moving away — drift to center const center = 300 - rightBat.height / 2; if (rightBat.y < center) rightBat.y += 2; else rightBat.y -= 2; } rightBat.y = Math.max(0, Math.min(CANVAS_HEIGHT - rightBat.height, rightBat.y)); } // ── Bat movement ───────────────────────────────────────────── function moveBats() { leftBat.y += leftBat.dy; leftBat.y = Math.max(0, Math.min(CANVAS_HEIGHT - leftBat.height, leftBat.y)); rightBat.y += rightBat.dy; rightBat.y = Math.max(0, Math.min(CANVAS_HEIGHT - rightBat.height, rightBat.y)); } // ── Mode buttons ────────────────────────────────────────────── const modeButtons = document.getElementById('modeButtons'); const btn1Player = document.getElementById('btn1Player'); const btn2Players = document.getElementById('btn2Players'); function startGame(mode) { gameMode = mode; leftBat.y = 250; rightBat.y = 250; modeButtons.style.display = 'none'; gameState = 'PLAYING'; } btn1Player.addEventListener('click', () => startGame('1player')); btn2Players.addEventListener('click', () => startGame('2player')); // ── Keyboard input ──────────────────────────────────────────── document.addEventListener('keydown', (e) => { if (gameState === 'START') { if (e.key === '1') startGame('1player'); if (e.key === '2') startGame('2player'); } if (e.key === 'w' || e.key === 'W') leftBat.dy = -BAT_SPEED; if (e.key === 's' || e.key === 'S') leftBat.dy = BAT_SPEED; if (gameMode === '2player') { if (e.key === 'ArrowUp') { e.preventDefault(); rightBat.dy = -BAT_SPEED; } if (e.key === 'ArrowDown') { e.preventDefault(); rightBat.dy = BAT_SPEED; } } if (e.key === 'r' || e.key === 'R') { if (gameState === 'GAME_OVER') { score.left = 0; score.right = 0; resetBall('right'); modeButtons.style.display = 'flex'; gameState = 'START'; } } }); document.addEventListener('keyup', (e) => { if (e.key === 'w' || e.key === 'W') leftBat.dy = 0; if (e.key === 's' || e.key === 'S') leftBat.dy = 0; if (e.key === 'ArrowUp') rightBat.dy = 0; if (e.key === 'ArrowDown') rightBat.dy = 0; }); // ── On-screen button input ──────────────────────────────────── const btnUp = document.getElementById('btnUp'); const btnDown = document.getElementById('btnDown'); ['mousedown', 'touchstart'].forEach(evt => { btnUp.addEventListener(evt, (e) => { e.preventDefault(); leftBat.dy = -BAT_SPEED; }); btnDown.addEventListener(evt, (e) => { e.preventDefault(); leftBat.dy = BAT_SPEED; }); }); ['mouseup', 'mouseleave', 'touchend', 'touchcancel'].forEach(evt => { btnUp.addEventListener(evt, () => { leftBat.dy = 0; }); btnDown.addEventListener(evt, () => { leftBat.dy = 0; }); }); // ── Ball physics ────────────────────────────────────────────── function moveBall() { ball.x += ball.vx; ball.y += ball.vy; } function ballHitsBat(bat) { return ( ball.x - ball.radius < bat.x + bat.width && ball.x + ball.radius > bat.x && ball.y - ball.radius < bat.y + bat.height && ball.y + ball.radius > bat.y ); } function checkCollisions() { // Top/bottom wall bounce if (ball.y - ball.radius <= 0 || ball.y + ball.radius >= CANVAS_HEIGHT) { ball.vy *= -1; } // Bat collision — reflect + deflect + speed up [leftBat, rightBat].forEach(bat => { if (ballHitsBat(bat)) { ball.vx *= -1; const hitPos = (ball.y - bat.y) / bat.height; // 0 to 1 ball.vy = (hitPos - 0.5) * 8; // deflect based on hit position ball.speed = Math.min(ball.speed * 1.05, ball.maxSpeed); const dir = ball.vx > 0 ? 1 : -1; ball.vx = dir * ball.speed; } }); } // ── Scoring & reset ─────────────────────────────────────────── function checkScore() { if (ball.x < 0) { score.right++; resetBall('right'); } else if (ball.x > CANVAS_WIDTH) { score.left++; resetBall('left'); } if (score.left >= WIN_SCORE || score.right >= WIN_SCORE) gameState = 'GAME_OVER'; } function resetBall(scorer) { ball.x = 400; ball.y = 300; ball.speed = ball.baseSpeed; ball.vx = scorer === 'left' ? -ball.speed : ball.speed; ball.vy = (Math.random() > 0.5 ? 1 : -1) * 3; } // ── Draw functions ──────────────────────────────────────────── function drawBat(bat) { const isLeft = bat.x < CANVAS_WIDTH / 2; const cx = bat.x + bat.width / 2; const cy = bat.y + bat.height / 2; const headRadius = bat.width * 1.8; // Handle (black rectangle extending away from center) const handleLen = 22; const handleW = 6; ctx.fillStyle = '#222222'; if (isLeft) { ctx.fillRect(bat.x - handleLen, cy - handleW / 2, handleLen, handleW); } else { ctx.fillRect(bat.x + bat.width, cy - handleW / 2, handleLen, handleW); } // Paddle head — white circle ctx.fillStyle = 'white'; ctx.beginPath(); ctx.arc(cx, cy, headRadius, 0, Math.PI * 2); ctx.fill(); // Black border ring ctx.strokeStyle = '#222222'; ctx.lineWidth = 2; ctx.beginPath(); ctx.arc(cx, cy, headRadius, 0, Math.PI * 2); ctx.stroke(); } function drawBall() { ctx.fillStyle = 'white'; ctx.beginPath(); ctx.arc(ball.x, ball.y, ball.radius, 0, Math.PI * 2); ctx.fill(); } function drawDivider() { ctx.strokeStyle = 'white'; ctx.setLineDash([10, 10]); ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(400, 0); ctx.lineTo(400, CANVAS_HEIGHT); ctx.stroke(); ctx.setLineDash([]); // reset dash so other drawing isn't affected } function drawScore() { ctx.fillStyle = 'white'; ctx.font = '36px monospace'; ctx.textAlign = 'center'; ctx.fillText(`${score.left}/${score.right}`, 400, 40); } function renderStartScreen() { ctx.textAlign = 'center'; // Title ctx.fillStyle = 'white'; ctx.font = '72px monospace'; ctx.fillText('PING 🏓', CANVAS_WIDTH / 2, 200); // Credit ctx.font = '16px monospace'; ctx.fillStyle = '#666666'; ctx.textAlign = 'right'; ctx.fillText('Created By: TBlock', CANVAS_WIDTH - 16, CANVAS_HEIGHT - 16); ctx.textAlign = 'center'; } function renderGameOver() { ctx.fillStyle = 'white'; ctx.textAlign = 'center'; let winnerMsg; if (score.left >= WIN_SCORE) { winnerMsg = 'Left Player Wins! 🎉'; } else if (gameMode === '1player') { winnerMsg = 'Computer Wins! 🤖'; } else { winnerMsg = 'Right Player Wins! 🎉'; } ctx.font = '48px monospace'; ctx.fillText(winnerMsg, CANVAS_WIDTH / 2, CANVAS_HEIGHT / 2 - 30); ctx.font = '28px monospace'; ctx.fillText('Press R to play again!', CANVAS_WIDTH / 2, CANVAS_HEIGHT / 2 + 40); } function render() { // Clear canvas with black background ctx.fillStyle = 'black'; ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT); if (gameState === 'START') { renderStartScreen(); return; } if (gameState === 'GAME_OVER') { renderGameOver(); return; } drawBat(leftBat); drawBat(rightBat); drawBall(); drawScore(); drawDivider(); } // ── Start ───────────────────────────────────────────────────── requestAnimationFrame(gameLoop);