Home/Blog/Programming/Polish your JavaScript: create a connect 4 game
Home/Blog/Programming/Polish your JavaScript: create a connect 4 game

Polish your JavaScript: create a connect 4 game

15 min read
Jan 30, 2024
content
HTML and CSS starter template
Board layout
Players take turns
Handling clicks on specific columns
Implementing the disk appearance
Dropping the disk in the lowest row
Do we have a winner?
Please, stop!
That’s all, folks!

Become a Software Engineer in Months, Not Years

From your first line of code, to your first day on the job — Educative has you covered. Join 2M+ developers learning in-demand programming skills.

When learning to code, practicing by creating applications is extremely important. Early-stage learners often get discouraged by the complexity of it. Creating games is a great way to practice coding for various reasons. First, we’re familiar with the game rules, which removes some of the complexity from the process. Second, games are fun, so we enjoy ourselves coding them. This blog assumes basic knowledge of HTML, CSS, and JavaScript.

In this blog, we’ll create a clone of the classic board game Connect 4 in JavaScript. This game consists of a board with seven columns and six rows. It is a two-player game. One player has blue disks, while the other has red disks. The players take turns one by one dropping a disk into any of the columns that aren’t already full. The player who manages to have four consecutive disks in a column, a row, or diagonally adjacent wins.

HTML and CSS starter template#

Let’s start with the basic HTML document and some CSS specifications.

Starter HTML template and CSS
  • We set up a div element with an id of game (line 9) to encompass all other elements.

  • We create a div element with a span element inside it to indicate which player’s turn it is (line 10). Player 1 goes first, so that’s what we indicate.

  • We create another div with a span inside it to show the game’s outcome (line 11). Then comes the game board itself (line 12). There’s nothing inside it because we’ll populate it using JavaScript.

  • We start with a CSS reset in lines 1–5, whereby we select all the elements and set the margins and padding to zero. We also set box-sizing to border-box so that the border widths and heights are included in element sizes.

  • We define CSS variables divwidth and divheight (lines 8–9) in the :root pseudo-class. Then, we use the values of these variables to set the height and width of the #board (lines 28–29) and #board div (lines 34–35) selectors. The board is supposed to contain seven columns with six rows. So, its width and height are seven times and six times the width and height, respectively, of any of its descendant div elements. Rather than hard coding the widths and heights everywhere, we define variables and apply arithmetic to them. This way, if we want to change the game dimensions, we just have to make changes in one place. Note that we add two pixels to the calculated widths and heights in lines 28–29 to accommodate the borders on each side of the board.

Board layout#

Let’s create the board layout in JavaScript.

Code to lay out the board
  • We obtain an object corresponding to the div element with the id of board. We set up the 7 x 6 board by using nested for loops—the outer one runs seven times, while the inner one runs six times.

  • We create div elements inside the inner for loop and append these new div elements to the div element with the id of board.

In CSS, we give these new div elements a blue border (line 36) so that we can see what’s going on. Now, when we switch to the “Output” tab, we'll see a whole bunch of boxes with blue borders hanging vertically at the left edge of the game board. This is because the div elements are block elements, and each block element starts on a new line by default. To fix that, we’ll use Flexbox.

Laying out the disks using Flexbox

By setting the display property to flex and flex-wrap to wrap, we instruct Flexbox to lay out the disks in a row until the row is filled and then move the next disk to a new row. Since the dimensions of a disk are 20×2020 \times 20 pixels and the board has a width of 2020 pixels, exactly seven disks will fit in a row, and there are six rows.

Players take turns#

We need players to take turns, and the program must keep track of whose turn it is. Since people play this game with a mouse, we’ll define a mouse-click event handler.

Updating players’ turns on mouse clicks

To keep track of which player’s turn it is, we define a variable named turn and initialize it to 0 in line 7. We’ll use this variable’s value as an index into the array named player defined in line 9. This array stores the colors associated with each player—red for player 1 and blue for player 2. We obtain an object named playerSpan that orresponds to the span element that indicates whose turn it is (line 11).

For testing purposes, we register a mouse-click event listener on the web page itself in line 13. The event handler named onClick() is defined in lines 26–29. In it, we update turn to 1 - turn (line 27) and display the variable’s value (plus 1) in the playerSpan element (line 28). If the value of turn is 0, then 1 - turn is 1. If, on the other hand, the value of turn is 1, then 1 - turn is 0. So, clicking on the page repeatedly cycles the value of turn between 0 and 1.

Switch to the “Output” tab and click anywhere in the game. Notice that the game indicates a change of turns.

Handling clicks on specific columns#

That’s all great, but this click handler gets triggered no matter where we click on the page. We’d just like to respond to clicks on the board. One way to approach that is to add a click handler to all the div elements inside the div element with the id of board. Let’s go ahead and update the JavaScript to the following:

Console
Handling mouse clicks on specific columns

Since we already have a handle to the div element with the id of board, and we need access to its child div elements, we call the querySelectorAll() method on the board object with a selector of :scope > div, which means “direct children that are of type div.” Then, we iterate over the array squares, and, for every element, we use a console.log() method just to make sure that we receive the right values in the event. Switch to the “Output” tab and click any of the disk elements in the game. We should see the disk’s index in the console.

Implementing the disk appearance#

Let’s implement the logic to drop the disk in its right place. We’ll let the player click anywhere in the column they wish to drop the disk in.

Implementing disks placement at the top of the clicked column

We initialize an array named countColumns (line 11) to hold the number of disks already in a given column. Since there are seven columns, this array has a size of seven as well. The initial values are all zeros. We also obtain an object corresponding to the span element with the id of result (line 15).

Inside the click event handler, we obtain the index of the div element at the top of the column in which the user clicked (line 27). We define a helper function getColumnTopIndex() (lines 46–48) for this. Since the number of columns in a row is given by the value of the variable named columns, we can obtain the column number for a given div element’s index as divindex % columns (line 47).

After calculating the column index where the player clicked, we first check if that column is already full (line 28). If not, we increment the number of disks in the column that the user clicked (line 30). Then, we add a class of either red or blue on the top disk in the respective column (line 31), depending on the player who took the turn. These classes are defined in CSS in lines 48–51 and 53–56, respectively. Notice the border-radius property with a value equal to half the width—and height—of the disk elements. This makes the discs look circular.

Back to JavaScript now. If the column that the user clicked is full, we display a message in the span element reserved for showing the outcome (line 37). We don’t want this message to stay visible indefinitely, so we clear the text after 700 milliseconds (line 38).

Dropping the disk in the lowest row#

Let’s have the disks drop to the lowest possible row in the respective column. We know the number of disks in the respective column. We know its top index. How do we find the index of the vacant div in the lowest row in that column?

Well, we have the column’s top index in the column variable. What’s the index of the div element right below it? It is column + columns. The one below that? It is column + 2 * columns, and so on. How many rows deep do we want to go? Since the total number of rows is in the variable rows, we need to go rows — countColumns[column] rows down. With that, we have our answer. Here’s the modified code:

Implementing disks dropping in the clicked column

We calculate the index of the lowest available spot in the clicked column in line 31 and place the disk there in line 32. We remove the borders on all sides of the disks from the CSS and replace it with borders on the left and right only (lines 38–39). This reduces the visual clutter from the game.

Do we have a winner?#

Now, let’s write code to determine if we have a winner. When do we need to check this? After every disk is successfully placed. So, we’ll define a function and call it from within the click handler. Since we know which column was clicked, we know its index, and we can pass that to the win-declaring function. We’ll also pass the index of the newest-added disk to make the “calculation” more convenient.

The disks are stored in a one-dimensioinal (1D) array in our code. Let’s figure out how to locate the neighboring elements’ indexes. We start with the vertically neighboring elements.

  • As shown in slide 1, the vertically neighboring elements’ indexes differ by 7, which is the number of columns minus 1.

  • Given the index of a disk, the index of the disk above it is obtained by subtracting 7 and that of the disk below it is obtained by adding 7.

  • As shown on slide 2, the horizontally neighboring disk indexes differ by 1.

  • As shown on slide 3, the indexes of neighboring elements on the right-slanting diagonal differ by 6.

  • Finally, as shown on slide 4, the indexes of neighboring elements on the left-slanting diagonal differ by 8.

canvasAnimation-image
1 / 4

Let’s start by checking for a win with four disks in a column. Here’s the modified code:

Checking for a win with vertical matching disks

The isWin() function (lines 56–61) calls the isWinVertical() function (lines 63–73), which starts by checking if the column the player clicked has at least four disks. There’s no point in checking whether they have won if there aren’t even a total of four disks in the column. If there aren’t enough disks, we immediately return false. Then, we iterate over the disks array, starting at the location where the last disk was placed. The last disk’s location is available in the index variable. We skip columns div elements at a time in the for loop to successively access the div elements below the recently placed disk. As soon as we find a disk that does not have a class that matches the current player’s color, we return false because this disk broke the streak of consecutive disks with the same color. The loop runs only four times because there’s no point checking more than four disks deep. If the loop exits normally, it means that we found four consecutive disks of the same color and the current player has won, so we return true.

Back in the click handler, we call isWin() (line 35). If it returns true, we indicate that the current player has won.

Next, let’s implement the horizontal win check. The disk indexes increase consecutively to the right and decrease to the left. Once a disk is placed, we need to check both to its left and right to see if there are four consecutive disks of the same color. The disk that was just placed could be the leftmost disk in that set of four disks, or it could be the rightmost, or the one in the middle. So, what we’ll do is first determine the number of consecutive same-color disks to its right. If we find three, the player has won. If not, suppose that we find nn disks with a matching color to the right of the new disk. We’ll now look to the new disk’s left for 4n14-n-1 consecutive disks of the same color. Here’s the code:

Checking for a win with horizontal matching disks

We initialize a variable stepsRight to 0 and then run a while loop with a compound condition. The first condition bound checks that we don’t increment the index beyond the current row. The second condition causes a stop if we’ve located three consecutive disks of the same color. The last condition checks that the next disk to the right has the same color as the new disk. If all of these conditions are true, we increment stepsRight. At the end of the loop, this variable holds the number of consecutive disks of the same color to the right of the new disk. After the first while loop, we check if we find three consecutive disks of the same color to the right. If so, we return true. If not, we don’t lose hope and run another while loop, looking for consecutive disks of the same color to the left of the new disk. The first condition in the while loop makes sure that we don’t look beyond the top right corner of the board. The second condition checks that we don’t look to the left of the leftmost disk in any column. The next condition checks if we find enough consecutive disks to the left and right to constitute a win. The final condition checks that the next disk to the left has the same color as the new disk. If all of these conditions are true, we increment stepsLeft and continue. Outside this loop, we check if we are able to find four consecutive disks of the same color in the row. If so, we return true. Otherwise, we return false. Finally, we update the isWin() function to return true if either isWinVertical() or isWinHorizontal() returns true (line 57).

Up next are two functions to check for a win on a right-slanting or left-slanting diagonal:

Checking for a win with diagonal matching disks

Note that when moving in a diagonal, on every step, we move a multiple of either six,or eight, depending on which diagonal we are exploring. That is why we see columns — 1 being multiplied by stepsRight + 1, for example, in line 100 within isWinDiagonal1. The structure of these functions is very similar to that of isWinHorizontal(), so we wouldn’t explain these in as much detail. In line 58, we augment the isWin() function to check for wins based on diagonally neighboring matching disks.

Please, stop!#

We can keep playing our game even after a player has won. So, once a winner is declared, we need to make the game stop by removing all the click handlers. We can achieve that by setting the onclick property of all squares to null. Here’s the modified code:

Stopping the game when it is over

In line 37, we iterate over the squares collection setting the onclick attribute of each square to null, effectively getting rid of the event handler. This stops the game once a player has won. However, what if no one is able to win, and the game results in a draw? Presently, the game lets the next player click all columns, indicating that the column is full, but it doesn’t declare a draw. Let’s fix that.

Stopping the game in case of a draw
  • To check for a tie, we write a function isTie() in lines 140–142. The idea behind our implementation is that if the board is full and no one has won, then it is a tie.

  • To check that no one has won yet, we add an else clause (line 39) after the if statement that checks if a player has won (line 25).

  • To check that the board is full, we calculate the sum of the countColumns array using the Array.reduce() method (line 141) and compare it against the board’s disk capacity. The Array.reduce() method accepts a function as an argument. In this case, we use an anonymous function. This function accepts two arguments: the accumulated value and the current value. The way it works is that the reduce() method first calls the anonymous function with a zero for the accumulated value and the array’s first value as the second argument. Our anonymous function returns the sum of zero and the array’s first element, which equals the first element.

  • Then, the reduce() method calls the anonymous function again with the returned accumulated value as the first argument and the array’s second element as the second argument. This call returns the sum of the first and second elements of the array. Once the reduce() method has invoked the anonymous function on all the array elements, we have the sum of all the array elements, which replaces the call to the Array.reduce() method in line 141. We make a comparison with the total number of spots on the board. If the two values are equal, we return true; otherwise, we return false.

  • We call this function in line 39. If it returns true, we set the result span element text to say “It’s a tie!” (line 40) and remove all event listeners (line 41) so that the game is no longer playable.

That’s all, folks!#

With that, our game is complete. Do try to tweak the game’s look and feel. Implement other features, such as the ability to start a new game once the game is over and CSS animations when a disk is placed. Keep practicing, and keep learning.

Continue learning

Cover
Game Development with JavaScript: Creating Tetris

In this course, you will get hands-on game development experience with JavaScript. Using the classic game of Tetris, you are going to cover concepts like graphics, game loops, and collision detection. By the end of this course, we will have a fully functioning game with points and levels. Try it out with your friends and put it in your portfolio for employers to see.

5hrs 30mins
Beginner
12 Playgrounds
10 Quizzes


Written By:
Saqib Ilyas

Free Resources