From eb060589fe188d6837762a22833edbf256833a7f Mon Sep 17 00:00:00 2001 From: Trimutex Date: Fri, 27 Dec 2024 10:25:28 -0600 Subject: [PATCH] pong: rewrite into typescript --- src/pong.js | 226 ----------------------------------------- src/pong.ts | 287 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 287 insertions(+), 226 deletions(-) delete mode 100644 src/pong.js create mode 100644 src/pong.ts diff --git a/src/pong.js b/src/pong.js deleted file mode 100644 index 69b5906..0000000 --- a/src/pong.js +++ /dev/null @@ -1,226 +0,0 @@ -const canvas = document.getElementById('pong'); -const context = canvas.getContext('2d'); -const grid = 15; -const paddleHeight = grid * 5; // 80 -const maxPaddleY = canvas.height - grid - paddleHeight; - -var paddleSpeed = 6; -var ballSpeed = 5; - -var leftScore = 0; -var rightScore = 0; - -const leftPaddle = { - // start in the middle of the game on the left side - x: grid * 2, - y: canvas.height / 2 - paddleHeight / 2, - width: grid, - height: paddleHeight, - - // paddle velocity - dy: 0 -}; -const rightPaddle = { - // start in the middle of the game on the right side - x: canvas.width - grid * 3, - y: canvas.height / 2 - paddleHeight / 2, - width: grid, - height: paddleHeight, - - // paddle velocity - dy: 0 -}; -const ball = { - // start in the middle of the game - x: canvas.width / 2, - y: canvas.height / 2, - width: grid, - height: grid, - - // keep track of when need to reset the ball position - resetting: false, - - // ball velocity (start going to the top-right corner) - dx: ballSpeed, - dy: -ballSpeed -}; - -// check for collision between two objects using axis-aligned bounding box (AABB) -// @see https://developer.mozilla.org/en-US/docs/Games/Techniques/2D_collision_detection -function collides(obj1, obj2) { - return obj1.x < obj2.x + obj2.width && - obj1.x + obj1.width > obj2.x && - obj1.y < obj2.y + obj2.height && - obj1.y + obj1.height > obj2.y; -} - -function gameOver() { - context.clearRect(0, 0, canvas.width, canvas.height); - let restartGame = document.getElementById("restart"); - let gameOverText = document.getElementById("gameover"); - restartGame.hidden = false; - gameOverText.hidden = false; -} - -function restartGame() { - leftScore = 0; - rightScore = 0; - let restartGame = document.getElementById("restart"); - restartGame.hidden = true; - requestAnimationFrame(loop); -} - -function BotMovesPaddle(paddle) { - ballRegion = Math.floor(ball.y / 15); - paddleRegion = Math.floor(paddle.y / 15); - if (ballRegion < paddleRegion) paddle.dy = -paddleSpeed; - if (ballRegion > paddleRegion) paddle.dy = paddleSpeed; - if (ballRegion === paddleRegion) paddle.dy = 0; -} - -// game loop -function loop() { - //if (leftScore < 7 && rightScore < 7) { - requestAnimationFrame(loop); - //} else if (leftScore >= 7 || rightScore >= 7) { - // gameOver(); - //} - context.clearRect(0,0,canvas.width,canvas.height); - - if (ball.dx < 0) { - BotMovesPaddle(leftPaddle); - document.getElementById("leftPaddle").innerHTML = leftPaddle.y; - } - if (ball.dx > 0) { - BotMovesPaddle(rightPaddle); - document.getElementById("rightPaddle").innerHTML = rightPaddle.y; - } - - // move paddles by their velocity - leftPaddle.y += leftPaddle.dy; - rightPaddle.y += rightPaddle.dy; - - // prevent paddles from going through walls - if (leftPaddle.y < grid) { - leftPaddle.y = grid; - } - else if (leftPaddle.y > maxPaddleY) { - leftPaddle.y = maxPaddleY; - } - - if (rightPaddle.y < grid) { - rightPaddle.y = grid; - } - else if (rightPaddle.y > maxPaddleY) { - rightPaddle.y = maxPaddleY; - } - - // draw paddles - context.fillStyle = 'white'; - context.fillRect(leftPaddle.x, leftPaddle.y, leftPaddle.width, leftPaddle.height); - context.fillRect(rightPaddle.x, rightPaddle.y, rightPaddle.width, rightPaddle.height); - - // move ball by its velocity - ball.x += ball.dx; - ball.y += ball.dy; - - // prevent ball from going through walls by changing its velocity - if (ball.y < grid) { - ball.y = grid; - ball.dy *= -1; - } - else if (ball.y + grid > canvas.height - grid) { - ball.y = canvas.height - grid * 2; - ball.dy *= -1; - } - - // reset ball if it goes past paddle (but only if we haven't already done so) - if ( (ball.x < 0 || ball.x > canvas.width) && !ball.resetting) { - ball.resetting = true; - - if (ball.x < 0) { - rightScore++; - } - if (ball.x > canvas.width) { - leftScore++; - } - - // give some time for the player to recover before launching the ball again - setTimeout(() => { - ball.resetting = false; - ball.x = canvas.width / 2; - ball.y = canvas.height / 2; - }, 400); - } - - // check to see if ball collides with paddle. if they do change x velocity - if (collides(ball, leftPaddle)) { - ball.dx *= -1; - leftPaddle.dy = 0; - - // move ball next to the paddle otherwise the collision will happen again - // in the next frame - ball.x = leftPaddle.x + leftPaddle.width; - } - else if (collides(ball, rightPaddle)) { - ball.dx *= -1; - rightPaddle.dy = 0; - - // move ball next to the paddle otherwise the collision will happen again - // in the next frame - ball.x = rightPaddle.x - ball.width; - } - - // draw score - context.font = "30px Arial"; - context.fillText(leftScore, 200, 50); - context.fillText(rightScore, 560, 50); - - // draw ball - context.fillRect(ball.x, ball.y, ball.width, ball.height); - - // draw walls - context.fillStyle = "#f4dbd6"; - context.fillRect(0, 0, canvas.width, grid); - context.fillRect(0, canvas.height - grid, canvas.width, canvas.height); - - // draw dotted line down the middle - for (let i = grid; i < canvas.height - grid; i += grid * 2) { - context.fillRect(canvas.width / 2 - grid / 2, i, grid, grid); - } -} - -// listen to keyboard events to move the paddles -document.addEventListener('keydown', function(e) { - // up arrow key - if (e.which === 38) { - rightPaddle.dy = -paddleSpeed; - } - // down arrow key - else if (e.which === 40) { - rightPaddle.dy = paddleSpeed; - } - - // w key - if (e.which === 87) { - leftPaddle.dy = -paddleSpeed; - } - // a key - else if (e.which === 83) { - leftPaddle.dy = paddleSpeed; - } -}); - -// listen to keyboard events to stop the paddle if key is released -document.addEventListener('keyup', function(e) { - if (e.which === 38 || e.which === 40) { - rightPaddle.dy = 0; - } - if (e.which === 83 || e.which === 87) { - leftPaddle.dy = 0; - } -}); - -// start the game -requestAnimationFrame(loop); - diff --git a/src/pong.ts b/src/pong.ts new file mode 100644 index 0000000..0cc6efa --- /dev/null +++ b/src/pong.ts @@ -0,0 +1,287 @@ +class Box { + x: number; + y: number; + width: number; + height: number; + + constructor(_x: number, _y: number, _width: number, _height: number) { + this.x = _x; + this.y = _y; + this.width = _width; + this.height = _height; + } + + collides(other: Box) { + return this.x < other.x + other.width && + this.x + this.width > other.x && + this.y < other.y + other.height && + this.y + this.height > other.y; + } +} + +class Paddle { + geometry: Box; + speed: number; + isBot: boolean; + isLeft: boolean; + score: number; + dy: number; + + constructor() { + this.speed = 6; + this.isBot = true; + this.score = 0; + this.dy = 0; + } + + scored() { + this.score++; + } + + updateGeometry(_geometry: Box) { + this.geometry = _geometry; + } + + updateSide(_isLeft: boolean) { + this.isLeft = _isLeft; + } + + input() { + if (!this.isBot) + return; + let ballRegion = Math.floor(g_pong.ball.geometry.y / 15); + let paddleRegion = Math.floor(this.geometry.y / 15); + if (ballRegion < paddleRegion) this.dy = -this.speed; + if (ballRegion > paddleRegion) this.dy = this.speed; + if (ballRegion === paddleRegion) this.dy = 0; + } +} + +class Ball { + geometry: Box; + speed: number; + dx: number; + dy: number; + resetting: boolean; + + constructor() { + // start in the middle of the game + this.geometry = new Box(g_pong.width / 2, g_pong.height / 2, g_pong.grid, g_pong.grid); + this.speed = 5; + // ball velocity (start going to the top-right corner) + this.dx = this.speed; + this.dy = -this.speed; + // keep track of when need to reset the ball position + this.resetting = false; + } +} + +class PongCore { + canvas: HTMLCanvasElement; + context: CanvasRenderingContext2D; + grid: number; + timeout: number; + width: number; + height: number; + gameover: boolean; + ball: Ball; + left: Paddle; + right: Paddle; + paddleHeight: number; + paddleHeightMax: number; + paddleSpeed: number; + + constructor() { + this.canvas = document.getElementById('pong') as HTMLCanvasElement; + this.context = this.canvas.getContext('2d') as CanvasRenderingContext2D; + this.grid = 15; // size of grid squares + this.paddleHeight = this.grid * 5; + this.paddleHeightMax = this.canvas.height - this.grid - this.paddleHeight; + this.timeout = 32; // speed in ms + this.width = this.canvas.width; + this.height = this.canvas.height; + this.gameover = false; + this.paddleSpeed = 6; + this.left.updateGeometry(new Box(this.width / 2, this.height / 2 - this.paddleHeight / 2, this.grid, this.paddleHeight)); + this.right.updateGeometry(new Box(this.width - this.grid * 3, this.height / 2 - this.paddleHeight / 2, this.grid, this.paddleHeight)); + } + + start() { + setInterval(this.loop, this.timeout); + } + + reset() { + this.context.clearRect(0, 0, this.canvas.width, this.canvas.height); + let restartGame = document.getElementById("restart"); + let gameOverText = document.getElementById("gameover"); + restartGame.hidden = false; + gameOverText.hidden = false; + console.debug("[TRACE] Reset was triggered"); + this.gameover = false; + this.left.score = 0; + this.right.score = 0; + this.draw(); + } + + // Game loop + loop() { + // Reset of needed + if (this.gameover) + this.reset(); + + this.simulate(); + + this.draw(); + this.updatePageText(); + } + + // Simulate game logic + simulate() { + this.left.input(); + this.right.input(); + + // move paddles by their velocity + this.left.geometry.y += this.left.dy; + this.right.geometry.y += this.right.dy; + + // prevent paddles from going through walls + if (this.left.geometry.y < this.grid) { + this.left.geometry.y = this.grid; + } + else if (this.left.geometry.y > this.paddleHeightMax) { + this.left.geometry.y = this.paddleHeightMax; + } + + if (this.right.geometry.y < this.grid) { + this.right.geometry.y = this.grid; + } + else if (this.right.geometry.y > this.paddleHeightMax) { + this.right.geometry.y = this.paddleHeightMax; + } + + // move ball by its velocity + this.ball.geometry.x += this.ball.dx; + this.ball.geometry.y += this.ball.dy; + + // prevent ball from going through walls by changing its velocity + if (this.ball.geometry.y < this.grid) { + this.ball.geometry.y = this.grid; + this.ball.dy *= -1; + } + else if (this.ball.geometry.y + this.grid > this.canvas.height - this.grid) { + this.ball.geometry.y = this.canvas.height - this.grid * 2; + this.ball.dy *= -1; + } + + // reset ball if it goes past paddle (but only if we haven't already done so) + if ( (this.ball.geometry.x < 0 || this.ball.geometry.x > this.canvas.width) && !this.ball.resetting) { + this.ball.resetting = true; + + if (this.ball.geometry.x < 0) { + this.right.scored(); + } + if (this.ball.geometry.x > this.canvas.width) { + this.left.scored(); + } + + // give some time for the player to recover before launching the ball again + setTimeout(() => { + this.ball.resetting = false; + this.ball.geometry.x = this.canvas.width / 2; + this.ball.geometry.y = this.canvas.height / 2; + }, 400); + } + + // check to see if ball collides with paddle. if they do change x velocity + if (this.left.geometry.collides(this.ball.geometry)) { + this.ball.dx *= -1; + this.left.dy = 0; + + // move ball next to the paddle otherwise the collision will happen again + // in the next frame + this.ball.geometry.x = this.left.geometry.x + this.left.geometry.width; + } + else if (this.right.geometry.collides(this.ball.geometry)) { + this.ball.dx *= -1; + this.right.dy = 0; + + // move ball next to the paddle otherwise the collision will happen again + // in the next frame + this.ball.geometry.x = this.right.geometry.x - this.ball.geometry.width; + } + + } + + // Draw game to canvas + draw() { + // Clear the screen + this.context.clearRect(0, 0, this.canvas.width, this.canvas.height); + + // draw score + this.context.font = "30px Arial"; + this.context.fillText(this.left.score.toString(), 200, 50); + this.context.fillText(this.right.score.toString(), 560, 50); + + // draw paddles + this.context.fillStyle = 'white'; + this.fillRect(this.left.geometry); + this.fillRect(this.right.geometry); + + // draw ball + this.fillRect(this.ball.geometry); + + // draw walls + this.context.fillStyle = "#f4dbd6"; + this.fillRect(new Box(0, 0, this.canvas.width, this.grid)); + this.fillRect(new Box(0, this.canvas.height - this.grid, this.canvas.width, this.canvas.height)); + + // draw dotted line down the middle + for (let i = this.grid; i < this.canvas.height - this.grid; i += this.grid * 2) { + this.fillRect(new Box(this.canvas.width / 2 - this.grid / 2, i, this.grid, this.grid); + } + } + + fillRect(_box: Box) { + this.context.fillRect(_box.x, _box.y, _box.width, _box.height); + } + + // Update text on page to match game + updatePageText() { + document.getElementById("leftPaddle").innerHTML = this.left.geometry.y.toString(); + document.getElementById("rightPaddle").innerHTML = this.right.geometry.y.toString(); + } +} + +// listen to keyboard events to move the paddles +document.addEventListener('keydown', function(e) { + // up arrow key + if (e.which === 38) { + g_pong.right.dy = -g_pong.paddleSpeed; + } + // down arrow key + else if (e.which === 40) { + g_pong.right.dy = g_pong.paddleSpeed; + } + + // w key + if (e.which === 87) { + g_pong.left.dy = -g_pong.paddleSpeed; + } + // a key + else if (e.which === 83) { + g_pong.left.dy = g_pong.paddleSpeed; + } +}); + +// listen to keyboard events to stop the paddle if key is released +document.addEventListener('keyup', function(e) { + if (e.which === 38 || e.which === 40) { + g_pong.right.dy = 0; + } + if (e.which === 83 || e.which === 87) { + g_pong.left.dy = 0; + } +}); + +const g_pong: PongCore = new PongCore(); +g_pong.start();