From 26ea5861302239950c7bd78a3300dc8bb73c8e01 Mon Sep 17 00:00:00 2001 From: madison Date: Wed, 15 Apr 2026 14:25:23 +0100 Subject: [PATCH] Add tennis minigame --- css/styles.css | 3 +- index.html | 5 +- js/tennis.js | 228 +++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 232 insertions(+), 4 deletions(-) create mode 100644 js/tennis.js diff --git a/css/styles.css b/css/styles.css index 010b263..4f941c8 100644 --- a/css/styles.css +++ b/css/styles.css @@ -111,8 +111,7 @@ dialog.window button { background-color: rgba(0, 0, 0, 0.3); cursor: pointer; } -dialog.window iframe { - border-top: none; +dialog.window canvas { border-bottom: solid; border-left: solid; border-right: solid; diff --git a/index.html b/index.html index 573159c..20a0dc1 100644 --- a/index.html +++ b/index.html @@ -6,16 +6,17 @@ + www.madstuff.net
- Window + Legally Distinct Tennis Game
- +
diff --git a/js/tennis.js b/js/tennis.js new file mode 100644 index 0000000..58e035a --- /dev/null +++ b/js/tennis.js @@ -0,0 +1,228 @@ +/** + * @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();