6
\$\begingroup\$

I am new to game development and took on the task to create a Pong replica due to my curiosity and interest in the field. I decided to write it in JavaScript because the game is not too labor intensive. I specially wrote it without any libraries for learning purposes.

One of the biggest issues that I seem to run into is making an AI that is good, but not too good. I can make one that is perfect, and I can also make one that is absolutely terrible but I can not seem to find any in between with my code.

The purpose of this code review is that I hope someone can suggest how I can fix my AI issue and to provide me with information on how to more efficiently code this game.

Hit the "(Re)start" button to demo the code.

// get a random signed number
window.signedRand = function (min, max, unrounded){
	let sign = Math.random() > 0.5 ? -1 : 1;
	let randRange = Math.random() * (max - min + 1);
	let minRange = (unrounded ? randRange : Math.floor(randRange))
    return sign * (minRange + min);
}

Math.addIgnoreSign = function (a, b) {
	return Math.sign(a) * Math.abs(a + b);
}

let Pong = function () {
	let canvas = document.getElementById("canvas");
	let WIDTH = canvas.width;
	let HEIGHT = canvas.height;
	let acceleration = 0.5; // sets the acceleration everytime the ball its a board
	let playing = true; // true if the game is running
	let AIHelper = true; // if this is true then we are going to help the AI out by speeding it up
	
	let ghostReaction = 6; // a higher number == slower reaction to the ghost ball
	
	// true reaction is half on the first hit (so it doubles after first hit)
	let trueReaction = 4; // a heigher number == slower reaction to the true/real ball
	let ghostSpeed = 10; // the max speed of the ghost ball
	let paddleSpeed = 7; // the speed of the paddle
		
	var ctx = canvas.getContext("2d");
	ctx.fillStyle = ctx.strokeStyle = "white";
	ctx.font = '48px serif';
	
	let Elements = {}; // hold the different drawing elements
	let Player1Score = 0;
	let Player2Score = 0;
	
	function calculateChangeInY(ballY, paddle) {
		let totalPaddle = paddle.height;
		let center = paddle.y + totalPaddle/2;
		let distanceFromCenter = ballY - center
		return 12 * Math.sin(distanceFromCenter/totalPaddle * (Math.PI/2));
	}
	
	let KeyBinding = new (function () { // keep track of keys that are pressed
		let pressing = new Set(); // ensure uniqueness
		let keyMap = { // map keys for convenience and readable while in use
			"uparrow": 38,
			"downarrow": 40,
			"w": 87,
			"s": 83
		}
		
		this.register = function (keyCode) { // add a key
			pressing.add(keyCode);
		}
		
		this.clear = function (keyCode) { // remove a key
			pressing.delete(keyCode);
		}
		
		this.pressing = function (keyname) { // check if the key is pressed by checking if it is availiable
			return pressing.has(keyMap[keyname]);
		}
		
		// Register Key Bindings
		window.addEventListener('keydown', function (event) { KeyBinding.register(event.keyCode); }, false); 
		window.addEventListener('keyup', function (event) { KeyBinding.clear(event.keyCode); }, false);
	})();
	
	// not really important to the functionality, but will add an effect for when the ball hits the paddle
	let Emitter = new (function (number) {
		let Particles = null;
		let Particle = function (particleID, xco, yco) {
			let ang = particleID/number*2*Math.PI; // get the desired angle that you want the particle to move
			let x = xco; // starting x
			let y = yco; // starting y
			let xi = Math.cos(ang); // change in x
			let yi = Math.sin(ang); // change in y
			let radius = 1;

			this.draw = function () {
				if (radius > 0) {
					ctx.beginPath();
					ctx.arc(x, y, radius, 0, Math.PI * 2, true);
					ctx.closePath();
					ctx.fill();

					x += xi;
					y += yi;
					radius -= 0.05; // make the particles fade away
				} else {
					delete Particles[particleID];
				}
			}
		}
		
		this.set = function (xco, yco) {
			Particles = {};
			for (let i = 0; i < number; i++) {
				Particles[i] = new Particle(i, xco, yco);
			}
		}
		
		this.draw = function () {
			for (let particle in Particles) {
				Particles[particle].draw();
			}
		}
		// 15 number of particles
	})(15);
	
	let Ball = function (x_coord, y_coord, radius, hidden) {
		this.xi = signedRand(7,9); //every ball starts with a unique velocity
		this.yi = signedRand(1, 5);
		this.x = x_coord;
		this.y = y_coord;
		this.radius = radius;
		
		this.hitTopBoundary = function () { // checks if the ball its the top or bottom
			return this.y + this.yi > HEIGHT - radius || this.y + this.yi < radius;
		}

		this.outOfBound = function () { // checks if the ball is out of bound
			return this.x > WIDTH + radius || this.x < -radius;
		}

		this.draw = function () {
			if (!hidden) {
				ctx.beginPath(); // draw ball
				ctx.arc(this.x, this.y, radius, 0, Math.PI * 2, true);
				ctx.closePath();
				ctx.fill();
				
				if (this.outOfBound()) {
					if (this.x > WIDTH/2) { // if on right side
						Player2Score++;
					} else { 
						Player1Score++;
					}
					playing = false;
					
					// reset the game
					setTimeout(() => {
						Elements.ball = new Ball(WIDTH/2, HEIGHT/2, 7);
						if (!AIHelper) trueReaction /= 2;
						
						acceleration = 0.5;
						paddleSpeed = 6;
						playing = true;
						AIHelper = true;
					}, 1500);
				}
			}
			if (this.outOfBound()) Elements.ghostBall = null;
			if (this.hitTopBoundary()) {
				this.yi = -this.yi;
			}
			
			// update positioning
			this.x += this.xi;
			this.y += this.yi;
		}
	}

	let Paddle = function (x) {
		this.height = 100; 
		this.width = 10;
		this.x = x; 
		this.y = HEIGHT/2 - this.height/2;  // initial position should be in the center
		let movement = function () {}; // handles movement this.init
		
		this.hitLeftPaddle = function (b1) { // detection from the left side of the paddle
			if (b1 == null) return false;
			
			let rightSide = this.width + this.x; // off by 30 and width of 10
			let top = this.y;
			let bottom = top + this.height;
			
			// We are basically trying to see if the ball has touched the box
			return b1.x + b1.xi < rightSide + b1.radius && 
			       b1.x - b1.xi > this.x &&
				   b1.y > top - b1.radius && 
				   b1.y < bottom + b1.radius
		}

		this.hitRightPaddle = function (b1) { // detection from the right side of the paddle
			if (b1 == null) return false;
			
			let leftSide = this.x; // off by 30 and width of 10
			let top = this.y;
			let bottom = top + this.height;
			
			// We are basically trying to see if the ball has touched the box
			return b1.x + b1.xi > leftSide - b1.radius &&
			       b1.x - b1.xi < this.x + this.width &&
				   b1.y > top - b1.radius &&
				   b1.y < bottom + b1.radius
		}
		
		this.bindMovement = function (movementStatement) { // bind the movement function
			movement = movementStatement;		
		}

		this.draw = function () {
			ctx.beginPath(); // draw the paddle
			ctx.rect(this.x, this.y, this.width, this.height);
			ctx.fill();
			ctx.closePath();
			
			////
			// We move the paddle based on function based in so that we have flexiblity
			// to add a computer or switch to two players.
			////
			let move = movement.call(this);
			if (move != null) this.y = this.y + move;
			if (this.y < 0) this.y = 0;
			if (this.y > (HEIGHT - this.height)) this.y = HEIGHT - this.height;
		}
	}

	let generateGhost = function () { // creates a ghost based on the 
		Elements.ghostBall = new Ball(Elements.ball.x, Elements.ball.y, 7, true);
		Elements.ghostBall.xi = Math.addIgnoreSign(Elements.ball.xi, ghostSpeed * Math.random());
		Elements.ghostBall.yi = Elements.ball.yi + Math.random()/7;
	}
	
	this.init = function () {
		////
		// Create our three moving parts to the game
		////
		Elements.ball = new Ball(WIDTH/2, HEIGHT/2, 7);
		Elements.paddle1 = new Paddle(30);
		Elements.paddle2 = new Paddle(WIDTH - 40);
		
		
		Elements.paddle1.bindMovement(function () { // binds the movement of paddle 1 to the up and down arrows
			if (KeyBinding.pressing("uparrow")) return -paddleSpeed;
			if (KeyBinding.pressing("downarrow")) return paddleSpeed;
		});
		
		// for now we are using a bot with the help of a ghost ball 
		// the ghost ball appears everytime the ball its the left paddle and goes away when it hits the right
		Elements.paddle2.bindMovement(function () { 
			let padY = this.y + this.height/2;
			if (Elements.ghostBall != null) {
				return (Elements.ghostBall.y - padY)/ghostReaction;
			} else {
				return (Elements.ball.y - padY)/trueReaction;
			}
		});
		/*function () { // optionally to make this two players, bind paddle 2 to w and s
			if (KeyBinding.pressing("w")) return -paddleSpeed;
			if (KeyBinding.pressing("s")) return paddleSpeed;
		}*/
	}
	
	this.start = function () {
		this.init();
		this.draw();
	};
	this.stop = () => playing=false;
	
	this.draw = function () {
		ctx.clearRect(0, 0, WIDTH, HEIGHT);
		Emitter.draw();
		////
		// Redraw all elements 
		////
		Elements.paddle1.draw(); // paddles can move even if the rest of the game does not
		Elements.paddle2.draw();
		
		if (playing) { // if we are playing
			Elements.ball.draw();
			// if we have a ghost ball then we should draw it
			if (Elements.ghostBall) Elements.ghostBall.draw(); 

			// save us some work while the ball is traveling since it can not possible
			// hit the paddle if it isnt below or above these ranges
			if (Elements.ball.x < 50 || Elements.ball.x > WIDTH-50) {
				if (Elements.paddle1.hitLeftPaddle(Elements.ball)) {
					////
					// calculateChangeInY will change the change in y depending on where on the paddle we hit
					////
					Elements.ball.yi = calculateChangeInY(Elements.ball.y, Elements.paddle1); // change vertical velocity of the ball
					Elements.ball.xi = -Math.addIgnoreSign(Elements.ball.xi, acceleration); // switch the x direction with acceleration

					generateGhost(); // generate our ghost
					Emitter.set(Elements.ball.x, Elements.ball.y); // emit the particle effect

					paddleSpeed += acceleration/1.5; // speed up the paddle a little less than the ball is speeding up
				} else {
					if (Elements.paddle2.hitRightPaddle(Elements.ball)) {
						if (AIHelper) {
							trueReaction *= 2;
							AIHelper = false;
						}
						Elements.ball.yi = calculateChangeInY(Elements.ball.y, Elements.paddle2); // change vertical velocity of the ball
						Elements.ball.xi = -Math.addIgnoreSign(Elements.ball.xi, acceleration); // switch the x direction with acceleration 
						Emitter.set(Elements.ball.x, Elements.ball.y); //emit the particle effect
					}
				}
			}
			if (Elements.ghostBall && Elements.ghostBall.x > WIDTH-50) {
				if (Elements.paddle2.hitRightPaddle(Elements.ghostBall)) {
					Elements.ghostBall = null;
				}
			}
		}
		
		// Some design while the game is ongoing
		ctx.fillText(Player1Score, WIDTH/2 - 100, 50);
		ctx.fillText(Player2Score, WIDTH/2 + 75, 50);
		ctx.setLineDash([10, 10]);
		ctx.moveTo(WIDTH/2, 0);
		ctx.lineTo(WIDTH/2, HEIGHT);
		ctx.stroke();

		window.requestAnimationFrame(this.draw.bind(this));
	}
};


var PongGame;
document.getElementById("restart").addEventListener('click', function () {
    if (PongGame != null) PongGame.stop();
    PongGame = new Pong();
    PongGame.start();
});
canvas{
	background: black;
}
<button id="restart">
	(Re)start
</button>

<canvas width="500" height="400" id="canvas" style="border: 1px solid"></canvas>

\$\endgroup\$
1
  • 1
    \$\begingroup\$ Welcome to Code Review! We'll be more than happy to review your code for improvements, however we can't really comment on your specific concern, AI difficulty. This is more of a game design concern, and will also vary depending on who is playing against your AI. For questions like these, feel free to check out Game Development. There is a difficulty tag on there specifically for questions like that. You may need to boil down your concerns to plain English though, not sure they would read all this code to understand it. \$\endgroup\$
    – Phrancis
    Apr 12, 2018 at 4:46

1 Answer 1

1
\$\begingroup\$

As Phrancis alluded to in the comment, this review pertains to the existing code and has no remarks about the AI.

Feedback

The game appears to function well, and the code appears to be organized somewhat well. The CSS and HTML are very simple but sufficient.

Suggestions

  • Instance/Prototype Methods: It would be much more efficient to define the methods on the prototypes instead of attaching to each instance. That way each method is declared only once instead of once per each instance. That would have huge impacts on the memory used by the browser. For more information about that topic, read about it in this post.

    The code would need to be re-worked to use either more instance variables or variables with scope larger than the current functions/closures.

    Because features like arrow functions and the let keyword are used, the class notation could be used also.

  • Indentation: some lines are indented with four spaces while others are indented with tab characters. It is simple to see when pasted into the code blocks here- e.g. the first line of the Pong() constructor looks like it is indented with 8 spaces but upon further inspection it appears that it is a tab character.
  • const vs let: - it appears that let is used exclusively. It is wise to use const for any value that shouldn't get re-assigned - especially constant values. This can help avoid unintentional re-assignment.
  • CONSTANT NAMES: Most style guides recommend constant values have all capital letters in the name - e.g. ghostspeed would be changed to GHOST_SPEED. The code already has WIDTH and HEIGHT but those almost seem excessive - the code could just use canvas.width and canvas.height in their place.
  • variable names: most JS style guides recommend using camelCase for variable names and ProperCase for class names/constructor methods.
  • for...of loops: could be used to simplify looping - e.g.

    for (let particle in Particles) {
        Particles[particle].draw();
    }
    

    could be simplified to:

    for (const particle of Particles) {
        particle.draw();
    }
    
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.