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 Ball { geometry: Box; speed: number; dx: number; dy: number; resetting: boolean; constructor(_geometry: Box) { // start in the middle of the game this.geometry = _geometry; 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 Paddle { geometry: Box; speed: number; isBot: boolean; isLeft: boolean; score: number; dy: number; constructor(_geometry: Box, _isLeft: boolean) { this.geometry = _geometry; this.isLeft = _isLeft; this.speed = 6; this.isBot = true; this.score = 0; this.dy = 0; } scored() { this.score++; } } 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.ball = new Ball(new Box(this.width / 2, this.height / 2, this.grid, this.grid)); this.left = new Paddle(new Box(this.width / 2, this.height / 2 - this.paddleHeight / 2, this.grid, this.paddleHeight), true); this.right = new Paddle(new Box(this.width - this.grid * 3, this.height / 2 - this.paddleHeight / 2, this.grid, this.paddleHeight), false); } 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 (g_pong.gameover) g_pong.reset(); g_pong.simulate(); g_pong.draw(); g_pong.updatePageText(); } // Simulate game logic simulate() { this.botInput(this.left); this.botInput(this.right); // 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(); } // Function for bot to play as a paddle botInput(player: Paddle) { if (!player.isBot) return; let ballRegion = Math.floor(this.ball.geometry.y / 15); let paddleRegion = Math.floor(player.geometry.y / 15); if (ballRegion < paddleRegion) player.dy = -player.speed; if (ballRegion > paddleRegion) player.dy = player.speed; if (ballRegion === paddleRegion) player.dy = 0; } } // 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();