In this post I will show you how to program the Connect 4 game by using JavaScript with HTML and Vue, with Bootstrap styles.
It is the Connect 4 game but web version with a player versus player option, as well as player versus CPU that uses a small artificial intelligence.
Throughout the post I will show you how the game works, what technologies I have used, styles, etc. I will also show you how to download the source code, as the game is totally free and open source. Finally I will leave you a demo to play connect 4 online.
The game board is a dynamic table that is drawn from an array. I have removed the borders and reduced the padding, as well as adding a blue background color to to make it look like the real game board (i.e. the real world, the physical one).
<style>
td {
background-color: #638CFF;
border: 0px ;
padding: 5px ;
}
.img-player{
max-width: 100px;
}
</style>
Inside each cell of the table (or td) I have placed an image that can be:
These images can be changed either in code or actually on the file system. The image path is returned by a function that, depending on the value, returns a different image.
As I said earlier, the board is a table that looks like this:
<table class="table table-bordered">
<thead>
<tr>
<th v-for="i in COLUMNS">
<button :disabled="!canPlay" @click="makeMove(i)" class="btn btn-warning">Make move
here <i class="fa fa-arrow-down"></i></button>
</th>
</tr>
</thead>
<tbody>
<tr v-for="row in board">
<td v-for="cell in row">
<img class="img-fluid img-player" :src="cellImage(cell)" alt="">
</td>
</tr>
</tbody>
</table>
The table header has a button that causes the piece to be placed in that column, that is, it drops the piece.
Because it is a two-dimensional array or a matrix, we first go through the entire array to get the row. And within that row we repeat a cell for each value in the row.
In each cell there will be an image that will have as source what the cellImage
method returns, which we will see next when we see the programming in Vue.
That’s all in terms of interface design, it’s time to see the JavaScript code for this Connect 4 web version game.
The method that evaluates the image and returns the path is the following:
cellImage(cell) {
if (cell === this.PLAYER_1) {
return "img/player1.png";
} else if (cell === this.PLAYER_2) {
return "img/player2.png";
} else {
return "img/empty.png"
}
},
Like I said, it is a simple if
. I left it that way to be expressive, although if you want, you can use dictionaries, concatenate the cell, etc., as well as use different images.
We can define the size of the board or the number of pieces that must be connected, in this way we could play Connect 3, Connect 5, and so on!.
const COLUMNS = 7,
ROWS = 6,
EMPTY_SPACE = " ",
PLAYER_1 = "o",
PLAYER_2 = "x",
PLAYER_CPU = PLAYER_2,
CONNECT = 4; // <-- Change this and you can play connect 5, connect 3, connect 100 and so on!
If you notice, when you play connect 4 against the CPU, it is taken as player 2.
We have the following function that resets the game. What it does first is ask the game mode: player versus player, or player versus CPU.
The next step is to clean the game board, filling it with empty spaces. Then, select a random player, who is the one who takes the first turn.
async resetGame() {
await this.askUserGameMode();
this.fillBoard();
this.selectPlayer();
this.makeCpuMove();
},
Finally it tells the CPU to make its move in case it is its turn.
When the player or user presses the button to place the piece, the following code is executed where it is validated if the column is not yet full, and it is checked if there is a tie or if the player has won.
The method receives the column number.
async makeMove(columnNumber) {
const columnIndex = columnNumber - 1;
const firstEmptyRow = this.getFirstEmptyRow(columnIndex, this.board);
if (firstEmptyRow === -1) {
Swal.fire('Cannot put here, it is full');
return;
}
Vue.set(this.board[firstEmptyRow], columnIndex, this.currentPlayer);
const status = await this.checkGameStatus();
if (!status) {
this.togglePlayer();
this.makeCpuMove();
} else {
this.askUserForAnotherMatch();
}
},
The piece is placed in the matrix, using the indicated column (minus 1) and in the empty upper row. In other words, the value in that cell is simply changed and Vue automatically takes care of refreshing it in the table.
Finally, the player is exchanged and the CPU is instructed to make its move in case it is its turn and the game mode is CPU versus player.
By the way, in case there is a draw or a winner, the user is asked if they want to play again. In that case, the game is restarted.
In this game there is a winner when there are 4 pieces connected in any direction in a straight line. Therefore, you simply have to count in all possible directions to see if there is a Connect 4 in the matrix for the player in question.
For this I have created some functions that perform the counting in several directions:
countUp(x, y, player, board) {
let startY = (y - CONNECT >= 0) ? y - CONNECT + 1 : 0;
let counter = 0;
for (; startY <= y; startY++) {
if (board[startY][x] === player) {
counter++;
} else {
counter = 0;
}
}
return counter;
},
countRight(x, y, player, board) {
let endX = (x + CONNECT < COLUMNS) ? x + CONNECT - 1 : COLUMNS - 1;
let counter = 0;
for (; x <= endX; x++) {
if (board[y][x] === player) {
counter++;
} else {
counter = 0;
}
}
return counter;
},
countUpRight(x, y, player, board) {
let endX = (x + CONNECT < COLUMNS) ? x + CONNECT - 1 : COLUMNS - 1;
let startY = (y - CONNECT >= 0) ? y - CONNECT + 1 : 0;
let counter = 0;
while (x <= endX && startY <= y) {
if (board[y][x] === player) {
counter++;
} else {
counter = 0;
}
x++;
y--;
}
return counter;
},
countDownRight(x, y, player, board) {
let endX = (x + CONNECT < COLUMNS) ? x + CONNECT - 1 : COLUMNS - 1;
let endY = (y + CONNECT < ROWS) ? y + CONNECT - 1 : ROWS - 1;
let counter = 0;
while (x <= endX && y <= endY) {
if (board[y][x] === player) {
counter++;
} else {
counter = 0;
}
x++;
y++;
}
return counter;
},
What the code does is to walk the board from one point to another and increase a counter in case it finds adjacent pieces. If it finds a piece that is not the same color, then it resets the counter to zero.
The function that decides if there’s a winner is the following:
isWinner(player, board) {
for (let y = 0; y < ROWS; y++) {
for (let x = 0; x < COLUMNS; x++) {
let count = 0;
count = this.countUp(x, y, player, board);
if (count >= CONNECT) return true;
count = this.countRight(x, y, player, board);
if (count >= CONNECT) return true;
count = this.countUpRight(x, y, player, board);
if (count >= CONNECT) return true;
count = this.countDownRight(x, y, player, board);
if (count >= CONNECT) return true;
}
}
return false;
},
What it does is count in all directions, and if it detects that there is a row followed by connecting pieces that are greater than the number needed to win, then it returns true
. Otherwise false
.
On the other hand, the function to know if there is a tie in this game uses a function that runs through the entire matrix. If an empty space is found, it means that it is not yet a tie, so false
is returned.
In case of finishing going through the entire matrix and not finding an empty space, true
is returned.
isTie(board) {
for (let y = 0; y < ROWS; y++) {
for (let x = 0; x < COLUMNS; x++) {
const currentCell = board[y][x];
if (currentCell === EMPTY_SPACE) {
return false;
}
}
}
return true;
},
As I said earlier, this game supports playing against the CPU. I have programmed a small artificial intelligence that chooses the best column based on the algorithm to try to win connect 4.
async makeCpuMove() {
if (!this.isCpuPlaying || this.currentPlayer !== PLAYER_CPU) {
return;
}
const bestColumn = this.getBestColumnForCpu();
const firstEmptyRow = this.getFirstEmptyRow(bestColumn, this.board);
console.log({ firstEmptyRow });
Vue.set(this.board[firstEmptyRow], bestColumn, this.currentPlayer);
const status = await this.checkGameStatus();
if (!status) {
this.togglePlayer();
} else {
this.askUserForAnotherMatch();
}
},
First it is verified if it is the turn of the CPU and if the game mode is player versus CPU. Then the best column is chosen and the value is placed on the board, (that is, the piece is placed).
After that, the state of the game is checked to see if there is a draw or a winner. If there is no winner or tie, the player is exchanged and it is the human’s turn.
The functions that allow you to choose the best column are shown below and are based on a series of rules:
getBestColumnForCpu() {
const winnerColumn = this.getWinnerColumn(this.board, this.currentPlayer);
if (winnerColumn !== -1) {
console.log("Cpu chooses winner column");
return winnerColumn;
}
// Check if adversary wins in the next move, if so, we take it
const adversary = this.getAdversary(this.currentPlayer);
const winnerColumnForAdversary = this.getWinnerColumn(this.board, adversary);
if (winnerColumnForAdversary !== -1) {
console.log("Cpu chooses take adversary's victory");
return winnerColumnForAdversary;
}
const cpuStats = this.getColumnWithHighestScore(this.currentPlayer, this.board);
const adversaryStats = this.getColumnWithHighestScore(adversary, this.board);
console.log({ adversaryStats });
console.log({ cpuStats });
if (adversaryStats.highestCount > cpuStats.highestCount) {
console.log("CPU chooses take adversary highest score");
// We take the adversary's best move if it is higher than CPU's
return adversaryStats.columnIndex;
} else if (cpuStats.highestCount > 1) {
console.log("CPU chooses highest count");
return cpuStats.columnIndex;
}
const centralColumn = this.getCentralColumn(this.board);
if (centralColumn !== -1) {
console.log("CPU Chooses central column");
return centralColumn;
}
// Finally we return a random column
console.log("CPU chooses random column");
return this.getRandomColumn(this.board);
},
getWinnerColumn(board, player) {
for (let i = 0; i < COLUMNS; i++) {
const boardClone = JSON.parse(JSON.stringify(board));
const firstEmptyRow = this.getFirstEmptyRow(i, boardClone);
//Proceed only if row is ok
if (firstEmptyRow !== -1) {
boardClone[firstEmptyRow][i] = player;
// If this is winner, return the column
if (this.isWinner(player, boardClone)) {
return i;
}
}
}
return -1;
},
getColumnWithHighestScore(player, board) {
const returnObject = {
highestCount: -1,
columnIndex: -1,
};
for (let i = 0; i < COLUMNS; i++) {
const boardClone = JSON.parse(JSON.stringify(board));
const firstEmptyRow = this.getFirstEmptyRow(i, boardClone);
if (firstEmptyRow !== -1) {
boardClone[firstEmptyRow][i] = player;
const firstFilledRow = this.getFirstFilledRow(i, boardClone);
if (firstFilledRow !== -1) {
let count = 0;
count = this.countUp(i, firstFilledRow, player, boardClone);
if (count > returnObject.highestCount) {
returnObject.highestCount = count;
returnObject.columnIndex = i;
}
count = this.countRight(i, firstFilledRow, player, boardClone);
if (count > returnObject.highestCount) {
returnObject.highestCount = count;
returnObject.columnIndex = i;
}
count = this.countUpRight(i, firstFilledRow, player, boardClone);
if (count > returnObject.highestCount) {
returnObject.highestCount = count;
returnObject.columnIndex = i;
}
count = this.countDownRight(i, firstFilledRow, player, boardClone);
if (count > returnObject.highestCount) {
returnObject.highestCount = count;
returnObject.columnIndex = i;
}
}
}
}
return returnObject;
},
getRandomColumn(board) {
while (true) {
const boardClone = JSON.parse(JSON.stringify(board));
const randomColumnIndex = this.getRandomNumberBetween(0, COLUMNS - 1);
const firstEmptyRow = this.getFirstEmptyRow(randomColumnIndex, boardClone);
if (firstEmptyRow !== -1) {
return randomColumnIndex;
}
}
},
getCentralColumn(board) {
const boardClone = JSON.parse(JSON.stringify(board));
const centralColumn = parseInt((COLUMNS - 1) / 2);
if (this.getFirstEmptyRow(centralColumn, boardClone) !== -1) {
return centralColumn;
}
return -1;
},
What is done is to simulate a movement on the board (first cloning the original, so we do not directly modify it) and from the movement, check:
Finally, the complete JavaScript code is as seen below (I will leave the complete project at the end of the post).
I have not detailed some functions, it seems to me that they are very simple, for example, where the winner is shown or the game mode is asked, using Sweet Alert 2.
/*
____ _____ _ _ _
| _ \ | __ \ (_) | | |
| |_) |_ _ | |__) |_ _ _ __ _____| |__ _ _| |_ ___
| _ <| | | | | ___/ _` | '__|_ / | '_ \| | | | __/ _ \
| |_) | |_| | | | | (_| | | / /| | |_) | |_| | || __/
|____/ \__, | |_| \__,_|_| /___|_|_.__/ \__, |\__\___|
__/ | __/ |
|___/ |___/
____________________________________
/ Si necesitas ayuda, contáctame en \
\ https://parzibyte.me /
------------------------------------
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||
Creado por Parzibyte (https://parzibyte.me). Este encabezado debe mantenerse intacto,
excepto si este es un proyecto de un estudiante.
*/const COLUMNS = 7,
ROWS = 6,
EMPTY_SPACE = " ",
PLAYER_1 = "o",
PLAYER_2 = "x",
PLAYER_CPU = PLAYER_2,
CONNECT = 4; // <-- Change this and you can play connect 5, connect 3, connect 100 and so on!
new Vue({
el: "#app",
data: () => ({
board: [],
COLUMNS,
ROWS,
PLAYER_1,
PLAYER_2,
PLAYER_CPU,
EMPTY_SPACE,
currentPlayer: null,
isCpuPlaying: true,
canPlay: false,
}),
async mounted() {
await Swal.fire(
'Connect 4 game',
'Brought to you by parzibyte - https://parzibyte.me',
'info'
);
this.resetGame();
},
methods: {
async resetGame() {
await this.askUserGameMode();
this.fillBoard();
this.selectPlayer();
this.makeCpuMove();
},
async askUserGameMode() {
this.canPlay = false;
const result = await Swal.fire({
title: 'Choose game mode',
text: "Do you want to play against another player or against CPU?",
icon: 'question',
showCancelButton: true,
confirmButtonColor: '#fdbf9c',
cancelButtonColor: '#4A42F3',
cancelButtonText: 'Me Vs another player',
confirmButtonText: 'Me Vs CPU'
});
this.canPlay = true;
this.isCpuPlaying = !!result.value;
},
countUp(x, y, player, board) {
let startY = (y - CONNECT >= 0) ? y - CONNECT + 1 : 0;
let counter = 0;
for (; startY <= y; startY++) {
if (board[startY][x] === player) {
counter++;
} else {
counter = 0;
}
}
return counter;
},
countRight(x, y, player, board) {
let endX = (x + CONNECT < COLUMNS) ? x + CONNECT - 1 : COLUMNS - 1;
let counter = 0;
for (; x <= endX; x++) {
if (board[y][x] === player) {
counter++;
} else {
counter = 0;
}
}
return counter;
},
countUpRight(x, y, player, board) {
let endX = (x + CONNECT < COLUMNS) ? x + CONNECT - 1 : COLUMNS - 1;
let startY = (y - CONNECT >= 0) ? y - CONNECT + 1 : 0;
let counter = 0;
while (x <= endX && startY <= y) {
if (board[y][x] === player) {
counter++;
} else {
counter = 0;
}
x++;
y--;
}
return counter;
},
countDownRight(x, y, player, board) {
let endX = (x + CONNECT < COLUMNS) ? x + CONNECT - 1 : COLUMNS - 1;
let endY = (y + CONNECT < ROWS) ? y + CONNECT - 1 : ROWS - 1;
let counter = 0;
while (x <= endX && y <= endY) {
if (board[y][x] === player) {
counter++;
} else {
counter = 0;
}
x++;
y++;
}
return counter;
},
isWinner(player, board) {
for (let y = 0; y < ROWS; y++) {
for (let x = 0; x < COLUMNS; x++) {
let count = 0;
count = this.countUp(x, y, player, board);
if (count >= CONNECT) return true;
count = this.countRight(x, y, player, board);
if (count >= CONNECT) return true;
count = this.countUpRight(x, y, player, board);
if (count >= CONNECT) return true;
count = this.countDownRight(x, y, player, board);
if (count >= CONNECT) return true;
}
}
return false;
},
isTie(board) {
for (let y = 0; y < ROWS; y++) {
for (let x = 0; x < COLUMNS; x++) {
const currentCell = board[y][x];
if (currentCell === EMPTY_SPACE) {
return false;
}
}
}
return true;
},
getRandomNumberBetween(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
},
selectPlayer() {
if (this.getRandomNumberBetween(0, 1) === 0) {
this.currentPlayer = PLAYER_1;
} else {
this.currentPlayer = PLAYER_2;
}
},
togglePlayer() {
this.currentPlayer = this.getAdversary(this.currentPlayer);
},
getAdversary(player) {
if (player === PLAYER_1) {
return PLAYER_2;
} else {
return PLAYER_1;
}
},
fillBoard() {
this.board = [];
for (let i = 0; i < ROWS; i++) {
this.board.push([]);
for (let j = 0; j < COLUMNS; j++) {
this.board[i].push(EMPTY_SPACE);
}
}
},
cellImage(cell) {
if (cell === this.PLAYER_1) {
return "img/player1.png";
} else if (cell === this.PLAYER_2) {
return "img/player2.png";
} else {
return "img/empty.png"
}
},
async makeMove(columnNumber) {
const columnIndex = columnNumber - 1;
const firstEmptyRow = this.getFirstEmptyRow(columnIndex, this.board);
if (firstEmptyRow === -1) {
Swal.fire('Cannot put here, it is full');
return;
}
Vue.set(this.board[firstEmptyRow], columnIndex, this.currentPlayer);
const status = await this.checkGameStatus();
if (!status) {
this.togglePlayer();
this.makeCpuMove();
} else {
this.askUserForAnotherMatch();
}
},
// Returns true if there's a winner or a tie. False otherwise
async checkGameStatus() {
if (this.isWinner(this.currentPlayer, this.board)) {
await this.showWinner();
return true;
} else if (this.isTie(this.board)) {
await this.showTie();
return true;
}
return false;
},
async askUserForAnotherMatch() {
this.canPlay = false;
const result = await Swal.fire({
title: 'Play again?',
text: "Do you want to play again?",
icon: 'question',
showCancelButton: true,
confirmButtonColor: '#fdbf9c',
cancelButtonColor: '#4A42F3',
cancelButtonText: 'No',
confirmButtonText: 'Yes'
});
if (result.value) {
this.resetGame();
}
},
async makeCpuMove() {
if (!this.isCpuPlaying || this.currentPlayer !== PLAYER_CPU) {
return;
}
const bestColumn = this.getBestColumnForCpu();
const firstEmptyRow = this.getFirstEmptyRow(bestColumn, this.board);
console.log({ firstEmptyRow });
Vue.set(this.board[firstEmptyRow], bestColumn, this.currentPlayer);
const status = await this.checkGameStatus();
if (!status) {
this.togglePlayer();
} else {
this.askUserForAnotherMatch();
}
},
getBestColumnForCpu() {
const winnerColumn = this.getWinnerColumn(this.board, this.currentPlayer);
if (winnerColumn !== -1) {
console.log("Cpu chooses winner column");
return winnerColumn;
}
// Check if adversary wins in the next move, if so, we take it
const adversary = this.getAdversary(this.currentPlayer);
const winnerColumnForAdversary = this.getWinnerColumn(this.board, adversary);
if (winnerColumnForAdversary !== -1) {
console.log("Cpu chooses take adversary's victory");
return winnerColumnForAdversary;
}
const cpuStats = this.getColumnWithHighestScore(this.currentPlayer, this.board);
const adversaryStats = this.getColumnWithHighestScore(adversary, this.board);
console.log({ adversaryStats });
console.log({ cpuStats });
if (adversaryStats.highestCount > cpuStats.highestCount) {
console.log("CPU chooses take adversary highest score");
// We take the adversary's best move if it is higher than CPU's
return adversaryStats.columnIndex;
} else if (cpuStats.highestCount > 1) {
console.log("CPU chooses highest count");
return cpuStats.columnIndex;
}
const centralColumn = this.getCentralColumn(this.board);
if (centralColumn !== -1) {
console.log("CPU Chooses central column");
return centralColumn;
}
// Finally we return a random column
console.log("CPU chooses random column");
return this.getRandomColumn(this.board);
},
getWinnerColumn(board, player) {
for (let i = 0; i < COLUMNS; i++) {
const boardClone = JSON.parse(JSON.stringify(board));
const firstEmptyRow = this.getFirstEmptyRow(i, boardClone);
//Proceed only if row is ok
if (firstEmptyRow !== -1) {
boardClone[firstEmptyRow][i] = player;
// If this is winner, return the column
if (this.isWinner(player, boardClone)) {
return i;
}
}
}
return -1;
},
getColumnWithHighestScore(player, board) {
const returnObject = {
highestCount: -1,
columnIndex: -1,
};
for (let i = 0; i < COLUMNS; i++) {
const boardClone = JSON.parse(JSON.stringify(board));
const firstEmptyRow = this.getFirstEmptyRow(i, boardClone);
if (firstEmptyRow !== -1) {
boardClone[firstEmptyRow][i] = player;
const firstFilledRow = this.getFirstFilledRow(i, boardClone);
if (firstFilledRow !== -1) {
let count = 0;
count = this.countUp(i, firstFilledRow, player, boardClone);
if (count > returnObject.highestCount) {
returnObject.highestCount = count;
returnObject.columnIndex = i;
}
count = this.countRight(i, firstFilledRow, player, boardClone);
if (count > returnObject.highestCount) {
returnObject.highestCount = count;
returnObject.columnIndex = i;
}
count = this.countUpRight(i, firstFilledRow, player, boardClone);
if (count > returnObject.highestCount) {
returnObject.highestCount = count;
returnObject.columnIndex = i;
}
count = this.countDownRight(i, firstFilledRow, player, boardClone);
if (count > returnObject.highestCount) {
returnObject.highestCount = count;
returnObject.columnIndex = i;
}
}
}
}
return returnObject;
},
getRandomColumn(board) {
while (true) {
const boardClone = JSON.parse(JSON.stringify(board));
const randomColumnIndex = this.getRandomNumberBetween(0, COLUMNS - 1);
const firstEmptyRow = this.getFirstEmptyRow(randomColumnIndex, boardClone);
if (firstEmptyRow !== -1) {
return randomColumnIndex;
}
}
},
getCentralColumn(board) {
const boardClone = JSON.parse(JSON.stringify(board));
const centralColumn = parseInt((COLUMNS - 1) / 2);
if (this.getFirstEmptyRow(centralColumn, boardClone) !== -1) {
return centralColumn;
}
return -1;
},
async showWinner() {
if (this.currentPlayer === PLAYER_1) {
await Swal.fire('Winner is player 1');
} else {
await Swal.fire('Winner is player 2');
}
},
async showTie() {
await Swal.fire('Tie');
},
getFirstFilledRow(columnIndex, board) {
for (let i = ROWS - 1; i >= 0; i--) {
if (board[i][columnIndex] !== EMPTY_SPACE) {
return i;
}
}
return -1;
},
getFirstEmptyRow(columnIndex, board) {
for (let i = ROWS - 1; i >= 0; i--) {
if (board[i][columnIndex] === EMPTY_SPACE) {
return i;
}
}
return -1;
}
}
});
Like many of my projects, this software is free and open source. You can download the code from the GitHub repository.
Oh, and of course, you can see the demo right here.
I was quite excited to port this game to JavaScript with my favorite framework: Vue. Remember that previously I made this game in C language.
I’ve always liked how I made the CPU pick the best column and that, while it’s not perfect, can win sometimes.
In the last months I have been working on a ticket designer to print on…
In this post you will learn how to use the Origin Private File System with…
In this post you will learn how to download a file in the background using…
In this post I will show you how to use SQLite3 directly in the web…
In this tutorial, we'll explore how to effortlessly print receipts, invoices, and tickets on a…
When printing receipts on thermal printers (ESC POS) sometimes it is needed to print images…
Esta web usa cookies.