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.
Let’s start with the basic HTML document and some CSS specifications.
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.
Let’s create the board layout in JavaScript.
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.
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
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.
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.
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:
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.
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.
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).
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:
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.
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.
Let’s start by checking for a win with four disks in a column. Here’s the modified code:
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
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:
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.
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:
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.
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.
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
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.
Free Resources