/** * @file Implements a tennis game. * * @licstart The following is the entire license notice for the JavaScript code * in this page. * * Copyright (C) 2026 Madison L. * * The JavaScript code in this page is free software: you can redistribute it * and/or modify it under the terms of the GNU General Public License (GNU GPL) * as published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. The code is distributed * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU GPL for more details. * * As additional permission under GNU GPL version 3 section 7, you may * distribute non-source (e.g., minimized or compacted) forms of that code * without the copy of the GNU GPL normally required by section 4, provided you * include this license notice and a URL through which recipients can access the * Corresponding Source. * * @licend The above is the entire license notice for the JavaScript code in * this page. * * Behold! My latest contribution to the site's growing technical debt, this * time with 80% more duct tape and glue. Bask in the constant recomputations of * the same values, repeated logic, and magic numbers galore! I just want to get * this working, I'll fix it later. */ const gameState = { START_MENU: 0, IN_GAME: 1, PAUSED: 2, } function rectsIntersecting(a, b) { return (a.x-(a.w/2) < b.x+(b.w/2)) && (a.x+(a.w/2) > b.x-(b.w/2)) && (a.y-(a.w/2) < b.y+(b.w/2)) && (a.y+(a.w/2) > b.y-(b.w/2)); } function initGame() { let canvas = document.getElementById("canvas"); let gameData = { canvas: canvas, canvasContext: canvas.getContext("2d"), paddleLeft: { rectangle: { x: 10, y: canvas.height/2, w: 10, h: 20 }, // px controller: updatePaddleP, }, paddleRight: { rectangle: { x: canvas.width-10, // px y: canvas.height/2, // px w: 10, // px h: 20, // px }, controller: updatePaddleCPU, }, paddleSpeed: 1, // px/s ball: { rectangle: { x: canvas.width/2, y: canvas.height/2, w: 10, h: 10 }, direction: { x: -1, y: -1 }, speed: 0.5, // px/s increment: 0.01, // px/collision paddleHit: false, }, score: { left: 0, right: 0 }, gameState: gameState.START_MENU, targetFrameTime: 1/30, // 30 FPS upPressed: false, downPressed: false, }; window.addEventListener("keydown", (event) => { if (event.code == "KeyW") gameData.upPressed = true; else if (event.code == "KeyS") gameData.downPressed = true; else if ((event.code == "Escape") && (gameData.gameState != gameState.START_MENU)) gameData.gameState = (gameData.gameState == gameState.IN_GAME) ? gameState.PAUSED : gameState.IN_GAME; else if ((event.code == "Enter") && (gameData.gameState == gameState.START_MENU)) gameData.gameState = gameState.IN_GAME; }); window.addEventListener("keyup", (event) => { if (event.code == "KeyW") gameData.upPressed = false; else if (event.code == "KeyS") gameData.downPressed = false; }); return gameData; } function updatePaddleP(paddle, gameData) { if ((gameData.upPressed) && ((paddle.rectangle.y-paddle.rectangle.w) >= 0)) paddle.rectangle.y -= gameData.paddleSpeed; if ((gameData.downPressed) && ((paddle.rectangle.y+paddle.rectangle.w) <= gameData.canvas.height)) paddle.rectangle.y += gameData.paddleSpeed; } function updatePaddleCPU(paddle, gameData) { if (gameData.ball.rectangle.x < gameData.canvas.width/2) return; if (paddle.rectangle.y < gameData.ball.rectangle.y) { paddle.rectangle.y += gameData.paddleSpeed; } if (paddle.rectangle.y > gameData.ball.rectangle.y) { paddle.rectangle.y -= gameData.paddleSpeed; } } function resetBall(ball) { ball.rectangle.x = canvas.width/2; ball.rectangle.y = canvas.height/2; ball.direction.x *= -1; ball.direction.y *= -1; ball.speed = 0.5; } function updateBall(gameData) { gameData.ball.rectangle.x += gameData.ball.direction.x*gameData.ball.speed; gameData.ball.rectangle.y += gameData.ball.direction.y*gameData.ball.speed; // r/programminghorror: if ((gameData.ball.rectangle.y <= 0) || (gameData.ball.rectangle.y >= gameData.canvas.height)) { gameData.ball.direction.y *= -1; gameData.ball.paddleHit = false; gameData.ball.speed += 0.01; } else if ((rectsIntersecting(gameData.ball.rectangle, gameData.paddleLeft.rectangle) || rectsIntersecting(gameData.ball.rectangle, gameData.paddleRight.rectangle)) && !gameData.ball.paddleHit) { /* * HACK: Ball gets stuck in paddle sometimes. This kinda fixes it, but * it results in occasional weird behaviour where the ball is * reflected when it probably shouldn't be. I think that's the * least of the problems with this function though. */ gameData.ball.paddleHit = true; gameData.ball.direction.x *= -1; gameData.ball.speed += 0.01; } else if (gameData.ball.rectangle.x <= 0) { ++gameData.score.right; resetBall(gameData.ball); } else if (gameData.ball.rectangle.x >= gameData.canvas.width) { ++gameData.score.left; resetBall(gameData.ball); } } function drawPaddle(paddle, gameData) { gameData.canvasContext.fillStyle = "white"; const halfWidth = paddle.rectangle.w/2; const halfHeight = paddle.rectangle.h/2; gameData.canvasContext.fillRect(paddle.rectangle.x-halfWidth, paddle.rectangle.y-halfHeight, paddle.rectangle.w, paddle.rectangle.h); } function drawBall(gameData) { gameData.canvasContext.fillStyle = "white"; const halfWidth = gameData.ball.rectangle.w/2; gameData.canvasContext.fillRect(gameData.ball.rectangle.x-halfWidth, gameData.ball.rectangle.y-halfWidth, gameData.ball.rectangle.w, gameData.ball.rectangle.h); } function drawUI(gameData) { gameData.canvasContext.fillStyle = "white"; gameData.canvasContext.font = "20px 'Courier New'"; if (gameData.gameState == gameState.START_MENU) { gameData.canvasContext.fillText("Press Enter to Play", 45, 90, 250); } else if (gameData.gameState == gameState.PAUSED) { gameData.canvasContext.fillText("PAUSED", 120, 100, 100); } else { gameData.canvasContext.fillRect(gameData.canvas.width/2, 0, 2, gameData.canvas.height); gameData.canvasContext.fillText(gameData.score.left, (gameData.canvas.width/2)-(20*2)-5, 20, 80); gameData.canvasContext.fillText(gameData.score.right, (gameData.canvas.width/2)+(20*2), 20, 80); } } function updateGame(gameData) { gameData.paddleLeft.controller(gameData.paddleLeft, gameData); gameData.paddleRight.controller(gameData.paddleRight, gameData); updateBall(gameData); } function drawGame(gameData) { gameData.canvasContext.fillStyle = "black"; gameData.canvasContext.fillRect(0, 0, gameData.canvas.width, gameData.canvas.height); if (gameData.gameState == gameState.IN_GAME) { drawPaddle(gameData.paddleLeft, gameData); drawPaddle(gameData.paddleRight, gameData); drawBall(gameData); } drawUI(gameData); } function runGame() { let gameData = initGame(); setInterval(function() { if (gameData.gameState == gameState.IN_GAME) updateGame(gameData); drawGame(gameData); }, gameData.targetFrameTime); } runGame();