Football Game with JavaScript [Part 3] Making the CPU move

Football Game with JavaScript [Part 3] Making the CPU move

Part 3

This part is interesting, is my favorite, and probably the most fun to do of the whole project, which is to give life to the rival bot player. The rival is going to steal the ball from us and is going to score goals if we don't defend our gate.

You can check the GitHub repository by clicking here

You can play the last version of the game by clicking here

What do I want in Part 3? I want the game to be more complex.

  • Giving the players ability to shoot and run
  • Improve the draw of the field
  • Make the goalkeepers cover their position.
  • Add a home goal gate and the possibility for the rival to go after the ball, and score a goal.

Giving the players ability to shoot and run

I'll give the user the possibility to shoot if the player is close enough to the ball. The player will also have the chance to run, which means the speed will be increased.

Also, I'll add a restart button option, when the user presses the R key, the match would restart the scores and the positions.

I'll start by adding this three global variables:

var shoot = false;
var run = false;
var restart = false;

//Also I need to add this to the reset function

I need to add the document.onkeyup function this three options:

if (e.keyCode === 88) {
  shoot = false;
}
if (e.keyCode === 90) {
  run = false;
}
if (e.keyCode === 82) {
  restart = false;
}

And I also need to add the document.onkeydown function this three options:

if (e.keyCode === 88) {
  shoot = true;
}
if (e.keyCode === 90) {
  run = true;
}
if (e.keyCode === 82) {
  restart = true;
}

Now, what I need to do to achieve this is to update the keyboardMoves function:

First I added these two vars and a playerShoot function.

  var dx = (players[0].x - ball.x) / players[0].size;
  var dy = (players[0].y - ball.y) / players[0].size;
  function playerShoot() {
    ball.xVel = 3 * -dx;
    ball.yVel = 3 * -dy;
    players[0].xVel = dx;
    players[0].yVel = dy;
    console.log("Shoots");
  }

I added a var called isAble this variable will store a number, using the getDistance function is going to tell me how close the player is from the ball. So If the player is close enough then can make a shoot. If the player presses the button, then will shoot.

On the other hand, I wrote that if the Z key is pressed down then the player speed will increase. if not then it'll be normal.

//this var will allow me to shoot if the player can (if the player is close enough)
  var isAble =
    getDistance(players[0].x, players[0].y, ball.x, ball.y) -
    players[0].size -
    ball.size;

  if (isAble < 15) {
    if (shoot) {
      playerShoot();
    }
  }

  if (run) {
    players[0].maxSpeed = 3;
  } else {
    players[0].maxSpeed = 2;
  }

And, I need also to update the HTML file, I'll add this to the body after the score elements.

<p id="yellowteam_score" class="text-1xl"></p>
<p id="greenteam_score" class="text-1xl" ></p>
<p class="text-1xl">
      Press Z to run, press X to shoot! <br />
      Press R to restart the match
</p>

So here you can check, that if I get close enough, I can console.log(isAble), and when I press the 'x key' my player will shoot the ball

https://cdn.hashnode.com/res/hashnode/image/upload/v1608718462754/xQgW3cjd8.png

Now that my player has the new abilities I'll inform the user by updating the HTML file. Below everything inside the body section I'll add the instructions:

<p class="text-1xl" id="instructions">
        Press Z to run, press X to shoot! <br />
        Press R to restart the match
</p>

Updating the field

Okay, my player can move and have two new functions. Now let's add a new goal gate for the rival team.

And the function looks like this:

function renderBackground() {
  context.save();

  // Outer lines
  context.beginPath();
  context.rect(0, 0, canvas.width, canvas.height);
  context.fillStyle = "#a0bb9e";
  context.fill();
  context.lineWidth = 1;
  context.strokeStyle = "#FFF";
  context.stroke();
  context.closePath();
  context.fillStyle = "#FFF";

  // Mid line
  context.beginPath();
  context.moveTo(canvas.width / 2, 0);
  context.lineTo(canvas.width / 2, canvas.height);
  context.stroke();
  context.closePath();

  //Mid circle
  context.beginPath();
  context.arc(canvas.width / 2, canvas.height / 2, 53, 0, 2 * Math.PI, false);
  context.stroke();
  context.closePath();

  //Local penalty box

  context.beginPath();
  context.rect(0, (canvas.height - 146) / 2, 44, 146);
  context.stroke();
  context.closePath();

  //Local goal
  context.beginPath();
  context.moveTo(1, canvas.height / 2 - 73);
  context.lineTo(1, canvas.height / 2 + 73);
  context.lineWidth = 5;
  context.stroke();
  context.closePath();
  context.lineWidth = 1;

  //Local penalty point
  context.beginPath();
  context.arc(88, canvas.height / 2, 1, 0, 2 * Math.PI, true);
  context.fill();
  context.closePath();

  //Rival goal box
  context.beginPath();
  context.rect(canvas.width - 44, (canvas.height - 146) / 2, 44, 146);
  context.stroke();
  context.closePath();
  //Rival goal
  context.beginPath();
  context.moveTo(canvas.width - 1, canvas.height / 2 - 73);
  context.lineTo(canvas.width - 1, canvas.height / 2 + 73);
  context.lineWidth = 5;
  context.stroke();
  context.closePath();
  context.lineWidth = 1;
  //Rival penalty point
  context.beginPath();
  context.arc(canvas.width - 88, canvas.height / 2, 1, 0, 2 * Math.PI, true);
  context.fill();
  context.closePath();
}

And the renderPlayers function which replaces the renderPlayer one, looks like this:

function renderPlayers() {
  for (let i = 0; i < 2; i++) {
    context.beginPath();
    context.fillStyle = "yellow"; //player color
    context.arc(players[i].x, players[i].y, players[i].size, 0, Math.PI * 2);
    context.fill();
    context.closePath();
  }
  for (let i = 2; i < 4; i++) {
    context.beginPath();
    context.fillStyle = "green";
    context.arc(players[i].x, players[i].y, players[i].size, 0, Math.PI * 2);
    context.fill();
    context.closePath();
  }

  context.restore();
}

The background now looks quite better:

https://cdn.hashnode.com/res/hashnode/image/upload/v1608717189283/di8bATn1A.png

Making the CPU move

This is an interesting section of the article. My favorite part. What I want exactly is, the goalkeepers to cover their position, if the ball is near, they should throw it as far as they can The forward player, should try to steal the ball and score a goal.

To start I started a new function called directions. Which inside has two more functions

coverPosition()

goalkeeperDirections()

forwardDirections()

function directions(){

 function coverPosition(){}
 function goalkeeperDirections(){}
 function forwardDirections(){}

coverPosition()
goalkeeperDirections()
forwardDirections()
}

The code's a little longer, but one can follow with the comments of it.

The coverPosition() function

// Making goalkeepers cover their position
  function coverPosition() {
    players[1].y--;
    players[3].y++;
    var dy_player1 = (players[1].y - (players[1].y - 25)) / players[1].size;
    var dy_player3 = (players[3].y - (players[3].y - 25)) / players[3].size;

    if (players[1].y < upon_theGoal) {
      players[1].yVel = dy_player1;
    }
    if (players[1].y > down_theGoal) {
      players[1].yVel = -dy_player1;
    }

    if (players[3].y < upon_theGoal) {
      players[3].yVel = dy_player3;
    }
    if (players[3].y > down_theGoal) {
      players[3].yVel = -dy_player3;
    }

    // A limit for the goalkeeper in the x-axis
    if (players[3].x < 825) {
      var dx = (players[3].x - (players[3].x - 25)) / players[3].size;
      players[3].xVel = dx;
    }

    if (players[1].x > 75) {
      var dx = (players[1].x - (players[1].x - 25)) / players[1].size;
      players[1].xVel = -dx;
    }
  }

The goalkeeperDirections() function:

//making the yellow goalkeeper go after the ball when it's close enough
  function goalkeeperDirections() {
    for (i = 1; i < 4; i++) {
      if (i === 2) {
        i++;
      }
      var dx = (players[i].x - ball.x) / players[i].size;
      var dy = (players[i].y - ball.y) / players[i].size;

      var ball_distance =
        getDistance(players[i].x, players[i].y, ball.x, ball.y) -
        players[i].size -
        ball.size;

      // if the player is close to the ball he will go for it

      if (i === 1) {
        if (ball_distance < 75) {
          console.log("Goalkeeper player is looking for the ball");
          players[i].xVel = (1 / 2) * -dx;
          players[i].yVel = (1 / 2) * -dy;
          if (ball_distance < 10) {
            console.log("throws the ball!");
            ball.xVel = 5 * -dx;
            ball.yVel = 0.15 - dy;
            players[i].xVel = 3 * dx;
          }
          if (players[i].x > 150) {
            players[i].xVel = 2 * dx;
          }
        }
      }

      if (i === 3) {
        if (ball_distance < 75) {
          console.log("Goalkeeper player is looking for the ball");
          players[i].xVel = (1 / 2) * -dx;
          players[i].yVel = (1 / 2) * -dy;

          if (ball_distance < 10) {
            console.log("throws the ball!");
            ball.xVel = 5 * -dx;
            ball.yVel = 0.15 - dy;
            players[i].xVel = 3 * -dx;
          }
          if (players[i].x < 750) {
            players[i].xVel = 2 * dx;
          }
        }
      }
    }
  }

And I finished this section with the forwardDirections() function:

function forwardDirections() {
    var dx = (players[2].x - ball.x) / players[2].size;
    var dy = (players[2].y - ball.y) / players[2].size;

    var ball_distance =
      getDistance(players[2].x, players[2].y, ball.x, ball.y) -
      players[2].size -
      ball.size;

    // if the green player is close to the ball he will go for it
    if (ball_distance < 100) {
      players[2].xVel = (1 / 2) * -dx;
      players[2].yVel = (1 / 2) * -dy;
      console.log("green player is looking for the ball");

      // if the player is in the right position to shoot and score a goal, the he will do it
      if (
        players[2].y < down_theGoal &&
        players[2].y > upon_theGoal &&
        players[2].x < 100
      ) {
        players[2].x = players[2].x++;
        ball.xVel = (7 / 2) * -dx;
        ball.yVel = (7 / 2) * -dy;
        players[2].xVel = 3 * dx;
        players[2].yVel = 3 * dy;
      }
      // check if the player is going in the right direction, if it's not then fix it

      // Here I check if the player goes in the right direction related to axis x

      if (players[2].x > 700) {
        console.log("nohe right direction axis-x");
        players[2].xVel = -dx;
        players[2].yVel = -dy;
        ball.x--;
        ball.y--;
      }

      if (players[2].x < upon_theGoal) {
        players[2].x = players[2].x++;
        ball.x = ball.x - 3;
        if (players[2].y < upon_theGoal) {
          ball.y = ball.y++;
        } else if (players[2].y > upon_theGoal) {
          ball.y = ball.y--;
        }
      }
      // Here I check if the player goes in the right direction related to axis-y
      if (players[2].y < upon_theGoal || players[2].y > down_theGoal) {
        console.log("not in the right direction axis-y");
        if (players[2].y < upon_theGoal) {
          ball.y++;
        } else if (players[2].y > down_theGoal) {
          ball.y--;
          players[2].x--;
        }
      }
    }
  }

When I update the reset function, ends like this:

function reset() {
  players = [
    //yellow
    new Player(canvas.width / 3, canvas.height / 2),
    new Player(30, 300),

    //green
    new Player((canvas.width / 3) * 2, canvas.height - canvas.height / 2),
    new Player(1170, 300),
  ];

  ball = new Ball(canvas.width / 2, canvas.height / 2);
  up = false;
  down = false;
  left = false;
  right = false;
  shoot = false;
  run = false;
  restart = false;
}

Finally, I need to update the start function to add the restart match possibility. It'll look like this:

function start() {
  $(document).ready(
    function()
    {
      document.getElementById("greenteam_score").innerHTML = "Green Team Score: " + greenteam;
      document.getElementById("yellowteam_score").innerHTML = "Yellow Team Score: " + yellowteam;
    }
);

  if (restart) {
    reset();
  }

  //Render functions
  clear();
  renderBackground();
  renderPlayers();
  renderBall();

  //Moves functions
  movePlayers();
  moveBall();
  keyboardMoves();
  directions();

  //Bounce functions
  players_Ball_Collision();
  playersCollision();
  playersBounds();
  ballBounds();

  requestAnimationFrame(start);
};

Here is a playable result of part 3:

(codepen.io/gonzalo-simon/full/ExgwwBq)]

Alright, now I have a game, and the CPU seems to play, at least it's trying. Now the main idea is done, the game is working, as a final step I want the game the look better, the objectives for the next part are:

  • I want a better-looking score counter.
  • I want to counter that will set up the full time of the match.
  • I want a Main Menu with the following options:
    • Quick Game Mode
    • Settings
    • About (credits)
  • Sound (Crowd, and a piece of music, for the Main Menu)
  • To draw a player for the local team.
  • To draw a player for the rival team.
  • To draw a ball.

End of the third part.