JavaScript Game Tutorial
How to Make Connect Four in JavaScript: Gravity, Win Checks and AI
Connect Four adds one twist to Tic-Tac-Toe: gravity. Discs fall to the lowest free cell, the board is bigger and a win can appear in four directions. The code stays short because every check starts from the last disc played.
Production context
This guide studies Connect Four, a published Supagames game. Repository source: games/lvl01/14-connect-four.html.
Play Connect Four on Supagames with a friend: click a column to drop a disc, and the first player with four in a row wins. The snippets below come from that game, including the winning-line highlight we finished while writing this guide.
The board is a CSS grid of round div cells. All game state lives in a two-dimensional array.
1. Store the Board as a 6x7 Array
The board has 6 rows and 7 columns. Each cell holds 0 for empty, 1 for the first player or 2 for the second. Row 0 is the top of the board, which matters when discs fall.
currentPlayer and gameActive are the only other pieces of state the game needs.
const ROWS = 6, COLS = 7;
let board = Array.from({ length: ROWS }, () => Array(COLS).fill(0));
let currentPlayer = 1;
let gameActive = true;
2. Render the Grid From the Array
Rendering creates one div per cell and adds a player class for occupied cells. CSS turns those classes into red and yellow discs with radial gradients, so the JavaScript never deals with colours.
Every cell stores its column and calls dropPiece with it. The player clicks anywhere in a column, not on an exact cell.
function render() {
grid.innerHTML = "";
for (let r = 0; r < ROWS; r++) {
for (let c = 0; c < COLS; c++) {
const cell = document.createElement("div");
cell.className = "c4-cell" + (board[r][c] === 1 ? " player1" : board[r][c] === 2 ? " player2" : "");
cell.addEventListener("click", () => dropPiece(c));
grid.appendChild(cell);
}
}
}
3. Drop Discs With Gravity
To drop a disc, scan the chosen column from the bottom row upwards and take the first empty cell. If the loop finishes without finding one, the column is full and nothing happens.
After placing the disc, check for a win, then for a draw, and only then hand the turn to the other player.
function dropPiece(col) {
if (!gameActive) return;
for (let r = ROWS - 1; r >= 0; r--) {
if (board[r][col] !== 0) continue;
board[r][col] = currentPlayer;
render();
if (checkWin(r, col, currentPlayer)) {
gameActive = false;
highlightWinning(r, col, currentPlayer);
return;
}
if (board.every((row) => row.every((cell) => cell !== 0))) { gameActive = false; return; }
currentPlayer = currentPlayer === 1 ? 2 : 1;
return;
}
}
4. Check Four in a Row Around the Last Disc
A new win must include the disc that was just played, so there is no need to scan the whole board. For each of the four directions (horizontal, vertical and both diagonals), count matching discs walking both ways from the last move.
Starting the count at 1 includes the new disc itself. Four or more in any direction is a win.
function checkWin(row, col, player) {
const dirs = [[0, 1], [1, 0], [1, 1], [1, -1]]; // horizontal, vertical, two diagonals
for (const [dr, dc] of dirs) {
let count = 1;
for (const dir of [-1, 1]) {
let r = row + dr * dir, c = col + dc * dir;
while (r >= 0 && r < ROWS && c >= 0 && c < COLS && board[r][c] === player) {
count++;
r += dr * dir;
c += dc * dir;
}
}
if (count >= 4) return true;
}
return false;
}
5. Highlight the Winning Line
Players should see why the game ended. The highlight walks the same directions as checkWin, but collects the cell coordinates instead of counting them, then adds a winning class that pulses with a CSS animation.
Cells in the rendered grid are in row-major order, so the element for row r and column c is grid.children[r * COLS + c].
function highlightWinning(row, col, player) {
for (const [dr, dc] of [[0, 1], [1, 0], [1, 1], [1, -1]]) {
const cells = [[row, col]];
for (const dir of [-1, 1]) {
let r = row + dr * dir, c = col + dc * dir;
while (r >= 0 && r < ROWS && c >= 0 && c < COLS && board[r][c] === player) {
cells.push([r, c]);
r += dr * dir;
c += dc * dir;
}
}
if (cells.length >= 4) {
cells.forEach(([r, c]) => grid.children[r * COLS + c].classList.add("winning"));
return;
}
}
}
6. Find Legal Moves for an AI
Any AI first needs the list of playable columns and the row each disc would land in. A column is playable when its top cell is empty.
Keep this helper separate from dropPiece so the AI can test moves on the board without rendering or switching players.
function landingRow(col) {
for (let r = ROWS - 1; r >= 0; r--) if (board[r][col] === 0) return r;
return -1; // full column
}
function legalColumns() {
return [...Array(COLS).keys()].filter((c) => board[0][c] === 0);
}
7. Add a Simple AI: Win, Block, Then Prefer the Centre
A surprisingly decent opponent follows three rules. Play a winning move if one exists. Otherwise block the opponent's winning move. Otherwise prefer columns near the centre, because the middle column takes part in more possible lines than any other.
The AI tries each move on the real board and undoes it immediately. For a stronger opponent, replace the third rule with a depth-limited minimax search with alpha-beta pruning.
function aiColumn(ai, human) {
const cols = legalColumns();
for (const player of [ai, human]) { // 1. win, 2. block
for (const c of cols) {
const r = landingRow(c);
board[r][c] = player;
const wins = checkWin(r, c, player);
board[r][c] = 0;
if (wins) return c;
}
}
const order = [3, 2, 4, 1, 5, 0, 6]; // 3. centre first
return order.find((c) => cols.includes(c));
}
8. Build checklist
- Store the board as a 6x7 array with 0, 1 and 2.
- Drop discs by scanning the column from the bottom up.
- Check wins only around the last disc, in four directions.
- Highlight the winning cells so the result is obvious.
- Detect a full board as a draw before switching players.
- Give the AI win, block and centre rules before trying minimax.
9. FAQ
Is Connect Four a solved game?
Yes. In 1988 James D. Allen and Victor Allis independently showed that the first player can always win with perfect play by starting in the middle column.
What is the best first move in Connect Four?
The centre column. It belongs to more possible lines of four than any other column, and it is the opening that leads to a forced win with perfect play.
How many positions does Connect Four have?
There are about 4.5 trillion legal positions on the standard 6x7 board, which is why strong programs need alpha-beta search and clever position storage rather than brute force.
Why check only around the last disc?
Any new line of four must contain the disc just played. Checking from that disc is much faster than scanning all 69 possible lines after every move, and it is easier to get right.