In this tutorial, we will cover how to build a fully functioning Connect Four game in HTML, CSS and Vanilla JavaScript.
Here’s the demo we’ll be working towards. Click on the available slots to place whichever colour is next in the queue!
Alright, let’s get started. In the body of your HTML file, add the code below.
1 |
|
2 |
|
3 |
Connect 4 |
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 |
|
We’ll start our styling by importing a custom font.
1 |
@import url("https://fonts.googleapis.com/css2?family=DM+Mono:ital,wght@0,300;0,400;0,500;1,300;1,400;1,500&display=swap"); |
In the body, use flex to style everything horizontally and vertically. To match the design, add a big font to the title.
1 |
body { |
2 |
font-family: Arial, sans-serif; |
3 |
background-color: #e0e0e0; |
4 |
text-align: center; |
5 |
display: flex; |
6 |
justify-content: center; |
7 |
align-items: center; |
8 |
height: 100vh; |
9 |
margin: 0; |
10 |
font-family: "DM Mono", monospace; |
11 |
}
|
12 |
|
13 |
|
14 |
h1 { |
15 |
font-size: 4.5rem; |
16 |
color: #000; |
17 |
margin-bottom: 20px; |
18 |
}
|
The red circle on the right side will show the current player. If the current player is red, it will show red, otherwise, it will show yellow. These styles achieve the shape and design.
1 |
.status { |
2 |
width: 60px; |
3 |
height: 60px; |
4 |
/* background-color: #ffcc00; */
|
5 |
border-radius: 50%; |
6 |
box-shadow: inset -5px -5px 10px rgba(0, 0, 0, 0.3); |
7 |
margin: 5px; |
8 |
}
|
This element will show the current winner
1 |
.message .winner { |
2 |
font-size: 2.5rem; |
3 |
font-weight: bold; |
4 |
color: #000; |
5 |
margin: 0; |
6 |
}
|
The most important component of the Connect Four game is the grid, which consists of 6 rows and 7 columns. Add the grid style to the board and style each cell in the board as shown below.
1 |
.board { |
2 |
display: grid; |
3 |
grid-template-columns: repeat(7, 1fr); |
4 |
gap: 10px; |
5 |
background-color: #0066cc; |
6 |
padding: 20px; |
7 |
border-radius: 10px; |
8 |
border: 10px solid #0055aa; |
9 |
margin-bottom: 20px; |
10 |
}
|
11 |
.cell { |
12 |
width: 80px; |
13 |
height: 80px; |
14 |
background-color: #ffffff; |
15 |
border-radius: 50%; |
16 |
cursor: pointer; |
17 |
|
18 |
}
|
In a Connect Four game, players usually take turns dropping discs into a grid. To simulate this effect visually, we will assign specific styles to each player. For instance, when it’s RED’s turn, we will add a red
class to the relevant element, and when it’s YELLOW’s turn, we will add a yellow
class.
These styles will change the status circle depending on the current player.
1 |
.yellow-selected { |
2 |
background-color: #ffdc00; |
3 |
}
|
4 |
|
5 |
.red-selected { |
6 |
background-color: #ff4136; |
7 |
}
|
Lastly, add these styles to the reset button.
1 |
#reset-btn { |
2 |
padding: 10px 20px; |
3 |
background-color: #060606; |
4 |
color: white; |
5 |
border: none; |
6 |
margin-top: 20px; |
7 |
|
8 |
border-radius: 5px; |
9 |
font-size: 1rem; |
10 |
cursor: pointer; |
11 |
}
|
12 |
|
13 |
#reset-btn:hover { |
14 |
background-color: #383938; |
15 |
}
|
In order to see the board, we will use JavaScript to create divs for each cell in the grid. Each cell will then be assigned the cell class and added to the board.
Let’s get the elements that need to be updated.
1 |
const board = document.querySelector(".board"); |
2 |
const status = document.querySelector(".status"); |
3 |
const resetBtn = document.getElementById("reset-btn"); |
4 |
const winner = document.querySelector(".winner"); |
Create the following variables:
1 |
let rows = 6; |
2 |
let cols = 7; |
The variables rows and columns represent the number of rows and columns in the grid.
Next, create a function called createGameBoard()
and add the code below.
1 |
function createGameBoard() { |
2 |
|
3 |
for (let r = 0; r < rows; r++) { |
4 |
for (let c = 0; c < cols; c++) { |
5 |
const disc = document.createElement("div"); |
6 |
disc.classList.add("cell"); |
7 |
disc.setAttribute("data-col", c); |
8 |
disc.setAttribute("data-row", r); |
9 |
board.appendChild(disc); |
10 |
|
11 |
}
|
12 |
}
|
13 |
}
|
In the function above, we start by looping through each row and column, for every cell. We create a element, add the cell class, and add a data attribute to store the position of each cell. Finally, we append the disc to the
board
.
The next step is to ensure that every time a player clicks on a cell, the cell receives the current player status. We also want to alternate the players and add the relevant colors to each cell. To ensure that the upper rows are not filled before the bottom one, we will start from the bottom.
Let’s start by declaring currentPlayer
and isGameOver
variables
1 |
let currentPlayer = "red"; |
2 |
let isGameOver = false; |
Let’s also initialize a gameBoard array, a 2D array representing the board grid.
1 |
let gameBoard = Array.from({ length: rows }, () => Array(cols).fill(null)); |
Update with the code below:
1 |
board.addEventListener("click", function (e) { |
2 |
status.classList.remove(`${currentPlayer}-selected`); |
3 |
let col = parseInt(e.target.getAttribute("data-col")); |
4 |
|
5 |
if (e.target.classList.contains("cell") && !isGameOver) { |
6 |
for (let row = rows - 1; row >= 0; row--) { |
7 |
let clickedCell = board.querySelector( |
8 |
`[data-row="${row}"][data-col="${col}"]` |
9 |
);
|
10 |
if ( |
11 |
!clickedCell.classList.contains("red") && |
12 |
!clickedCell.classList.contains("yellow") |
13 |
) { |
14 |
clickedCell.classList.add(currentPlayer); |
15 |
gameBoard[row][col] = currentPlayer; |
16 |
|
17 |
|
18 |
currentPlayer = currentPlayer === "red" ? "yellow" : "red"; |
19 |
|
20 |
status.classList.add(`${currentPlayer}-selected`); |
21 |
break; |
22 |
}
|
23 |
}
|
24 |
}
|
25 |
});
|
This is a good time to break down what’s happening above:
We should now have something like this when a cell is clicked.
Now that players can take turns playing the game, it’s time to check for a win.
In a Connect Four game, a win is accomplished by having four consecutive discs of the same color under the following conditions:
Create a function called checkWin()
.
1 |
function checkWin(){ |
2 |
// check win logic
|
3 |
|
4 |
}
|
In this function, we will check for each direction. Let’s start with a horizontal win.
For the horizontal win, we want to use the logic gameBoard[row][col]
that returns the current cell, and for each cell, we will check the 3 cells next to it. If we have cells belonging to the same player, then it’s considered a win for the current player.
Update your code as follows:
1 |
function checkForWin() { |
2 |
for (let r = 0; r < rows; r++) { |
3 |
for (let c = 0; c <= cols - 4; c++) { |
4 |
const player = gameBoard[r][c]; |
5 |
if (player) { |
6 |
if ( |
7 |
player === gameBoard[r][c + 1] && |
8 |
player === gameBoard[r][c + 2] && |
9 |
player === gameBoard[r][c + 3] |
10 |
) { |
11 |
// console.log("Win horizontally");
|
12 |
return true; |
13 |
}
|
14 |
}
|
15 |
}
|
16 |
}
|
17 |
|
18 |
return false; |
19 |
}
|
Let’s break down the code, we have the gameBoard
array which looks like this:
1 |
const gameBoard = [ |
2 |
[null, null, null, null, null, null, null], // row 0 |
3 |
[null, null, null, null, null, null, null], // row 1 |
4 |
[null, null, null, null, null, null, null], // row 2 |
5 |
[null, null, null, null, null, null, null], // row 3 |
6 |
[null, null, null, null, null, null, null], // row 4 |
7 |
[null, null, null, null, null, null, null], // row 5 |
8 |
];
|
When we loop through row = 0
to row = 5.
For each row, we check for four consecutive columns at a time. For example, in row = 5,
if we start at column col = 0,
we check the following cells in reference to [5][0]:
1 |
[5][0+1], [5][0+2], and [5][0+3] |
If all these cells contain the same player as the original cell we are comparing to, ((5[0]), it will be considered a win for that player. For example, if RED wins for the horizontal condition, it will look like this:
To check for a vertical win, the logic is similar to the horizontal check, but this time, we are looping through columns. For each column, we are checking for four consecutive rows in reference to each cell in that row. If we get four vertical consecutive cells, it’s considered a win
1 |
function checkForWin() { |
2 |
for (let r = 0; r < rows; r++) { |
3 |
for (let c = 0; c <= cols - 4; c++) { |
4 |
const player = gameBoard[r][c]; |
5 |
if (player) { |
6 |
if ( |
7 |
player === gameBoard[r][c + 1] && |
8 |
player === gameBoard[r][c + 2] && |
9 |
player === gameBoard[r][c + 3] |
10 |
) { |
11 |
// console.log("Win horizontally");
|
12 |
return true; |
13 |
}
|
14 |
}
|
15 |
}
|
16 |
}
|
17 |
|
18 |
for (let c = 0; c < cols; c++) { |
19 |
for (let r = 0; r <= rows - 4; r++) { |
20 |
const player = gameBoard[r][c]; |
21 |
if (player) { |
22 |
if ( |
23 |
player === gameBoard[r + 1][c] && |
24 |
player === gameBoard[r + 2][c] && |
25 |
player === gameBoard[r + 3][c] |
26 |
) { |
27 |
// console.log("Win vertically");
|
28 |
return true; |
29 |
}
|
30 |
}
|
31 |
}
|
32 |
}
|
33 |
|
34 |
return false; |
35 |
}
|
For the diagonal win, we will check for both bottom-left to top-right and top-left to bottom-right directions. We will do this by looping over 3 rows and 3 columns at a time. For example, suppose we have a win that looks like this:
1 |
const gameBoard = [ |
2 |
[null, null, null, null, null, null, null], // row 0 |
3 |
[null, null, null, null, null, null, null], // row 1 |
4 |
[null, null, null, "red", null, null, null], // row 2 |
5 |
[null, null, "red", null, null, null, null], // row 3 |
6 |
[null, "red", null, null, null, null, null], // row 4 |
7 |
["red", null, null, null, null, null, null], // row 5 |
8 |
|
9 |
];
|
If we are starting at col =0
, we will check [0][0] against the following cells.
1 |
[4][1] , [3][2] and [2][3] |
In each step, we are moving one row up (-) and one column right (+ ). Update the checkWin()
function as follows.
1 |
function checkForWin() { |
2 |
|
3 |
for (let r = 0; r < rows; r++) { |
4 |
for (let c = 0; c <= cols - 4; c++) { |
5 |
const player = gameBoard[r][c]; |
6 |
if (player) { |
7 |
if ( |
8 |
player === gameBoard[r][c + 1] && |
9 |
player === gameBoard[r][c + 2] && |
10 |
player === gameBoard[r][c + 3] |
11 |
) { |
12 |
// console.log("Win horizontally");
|
13 |
return true; |
14 |
}
|
15 |
}
|
16 |
}
|
17 |
}
|
18 |
|
19 |
|
20 |
for (let c = 0; c < cols; c++) { |
21 |
for (let r = 0; r <= rows - 4; r++) { |
22 |
const player = gameBoard[r][c]; |
23 |
if (player) { |
24 |
if ( |
25 |
player === gameBoard[r + 1][c] && |
26 |
player === gameBoard[r + 2][c] && |
27 |
player === gameBoard[r + 3][c] |
28 |
) { |
29 |
// console.log("Win vertically");
|
30 |
return true; |
31 |
}
|
32 |
}
|
33 |
}
|
34 |
}
|
35 |
|
36 |
for (let r = 3; r < rows; r++) { |
37 |
for (let c = 0; c <= cols - 4; c++) { |
38 |
const player = gameBoard[r][c]; |
39 |
if (player) { |
40 |
|
41 |
if ( |
42 |
player === gameBoard[r - 1][c + 1] && |
43 |
player === gameBoard[r - 2][c + 2] && |
44 |
player === gameBoard[r - 3][c + 3] |
45 |
) { |
46 |
|
47 |
return true; |
48 |
}
|
49 |
}
|
50 |
}
|
51 |
}
|
52 |
|
53 |
return false; |
54 |
}
|
55 |
|
Consider another win scenario which looks like this:
1 |
const gameBoard = [ |
2 |
["red", null, null, null, null, null, null], // row 0 |
3 |
[null, "red", null, null, null, null, null], // row 1 |
4 |
[null, null, "red", null, null, null, null], // row 2 |
5 |
[null, null, null, "red", null, null, null], // row 3 |
6 |
[null, null, null, null, null, null, null], // row 4 |
7 |
[null, null, null, null, null, null, null], // row 5 |
8 |
];
|
In this case, we will loop over 3 rows and 3 columns at a time, and for each step, we will be moving one row downwards (+) and one column to the right (+). The final checkWin function will look like this
1 |
function checkForWin() { |
2 |
|
3 |
for (let r = 0; r < rows; r++) { |
4 |
for (let c = 0; c <= cols - 4; c++) { |
5 |
const player = gameBoard[r][c]; |
6 |
if (player) { |
7 |
if ( |
8 |
player === gameBoard[r][c + 1] && |
9 |
player === gameBoard[r][c + 2] && |
10 |
player === gameBoard[r][c + 3] |
11 |
) { |
12 |
// console.log("Win horizontally");
|
13 |
return true; |
14 |
}
|
15 |
}
|
16 |
}
|
17 |
}
|
18 |
|
19 |
|
20 |
for (let c = 0; c < cols; c++) { |
21 |
for (let r = 0; r <= rows - 4; r++) { |
22 |
const player = gameBoard[r][c]; |
23 |
if (player) { |
24 |
if ( |
25 |
player === gameBoard[r + 1][c] && |
26 |
player === gameBoard[r + 2][c] && |
27 |
player === gameBoard[r + 3][c] |
28 |
) { |
29 |
// console.log("Win vertically");
|
30 |
return true; |
31 |
}
|
32 |
}
|
33 |
}
|
34 |
}
|
35 |
|
36 |
for (let r = 0; r <= rows - 4; r++) { |
37 |
for (let c = 0; c <= cols - 4; c++) { |
38 |
const player = gameBoard[r][c]; |
39 |
if (player) { |
40 |
|
41 |
if ( |
42 |
player === gameBoard[r + 1][c + 1] && |
43 |
player === gameBoard[r + 2][c + 2] && |
44 |
player === gameBoard[r + 3][c + 3] |
45 |
) { |
46 |
console.log("Win vertically"); |
47 |
|
48 |
return true; |
49 |
|
50 |
}
|
51 |
}
|
52 |
}
|
53 |
}
|
54 |
|
55 |
|
56 |
for (let r = 3; r < rows; r++) { |
57 |
for (let c = 0; c <= cols - 4; c++) { |
58 |
const player = gameBoard[r][c]; |
59 |
if (player) { |
60 |
|
61 |
if ( |
62 |
player === gameBoard[r - 1][c + 1] && |
63 |
player === gameBoard[r - 2][c + 2] && |
64 |
player === gameBoard[r - 3][c + 3] |
65 |
) { |
66 |
|
67 |
return true; |
68 |
}
|
69 |
}
|
70 |
}
|
71 |
}
|
72 |
|
73 |
|
74 |
return false; |
75 |
}
|
Update the board events to ensure when a win is detected, it displays the winner and ends the game.
1 |
board.addEventListener("click", function (e) { |
2 |
status.classList.remove(`${currentPlayer}-selected`); |
3 |
let col = parseInt(e.target.getAttribute("data-col")); |
4 |
|
5 |
if (e.target.classList.contains("cell") && !isGameOver) { |
6 |
for (let row = rows - 1; row >= 0; row--) { |
7 |
let clickedCell = board.querySelector( |
8 |
`[data-row="${row}"][data-col="${col}"]` |
9 |
);
|
10 |
if ( |
11 |
!clickedCell.classList.contains("red") && |
12 |
!clickedCell.classList.contains("yellow") |
13 |
) { |
14 |
clickedCell.classList.add(currentPlayer); |
15 |
gameBoard[row][col] = currentPlayer; |
16 |
|
17 |
|
18 |
if (checkForWin()) { |
19 |
status.style.display = "block"; |
20 |
console.log(currentPlayer.toUpperCase()); |
21 |
status.style.display = "none"; |
22 |
|
23 |
winner.innerText = `${currentPlayer.toUpperCase()} wins!`; |
24 |
isGameOver = true; |
25 |
return; |
26 |
}
|
27 |
|
28 |
|
29 |
currentPlayer = currentPlayer === "red" ? "yellow" : "red"; |
30 |
|
31 |
status.classList.add(`${currentPlayer}-selected`); |
32 |
break; |
33 |
}
|
34 |
}
|
35 |
}
|
36 |
});
|
The last step is to reset the game fucntionality when a win occurs.
1 |
resetBtn.addEventListener("click", function () { |
2 |
isGameOver = false; |
3 |
board.querySelectorAll(".cell").forEach((cell) => { |
4 |
cell.classList.remove("red", "yellow"); |
5 |
});
|
6 |
currentPlayer = "red"; |
7 |
winner.innerText = ""; |
8 |
|
9 |
status.classList.add(`${currentPlayer}-selected`); |
10 |
});
|
When the rest button is clicked, the game will be reset by removing all assigned colors to all the cells and removing any messages from the previous game.
As a reminder, here is the final demo!
With just HTML, CSS, and JavaScript, we’ve built a fully functional Connect Four game from scratch. I hope you enjoyed following along.
To make the game even more challenging, you can improve it by adding a timer, to add a little pressure to each move!