A beginner-friendly guide to building the game from scratch
Connect Four is a two-player strategy game played on a 7-column ร 6-row grid. Players take turns dropping colored discs into columns. The first player to line up four discs in a row โ horizontally, vertically, or diagonally โ wins.
This tutorial walks you through building the entire game using plain HTML, CSS, and JavaScript โ no frameworks, no libraries. The main focus is the JavaScript logic. HTML and CSS are kept short and to the point.
โ Example board โ the white-outlined cells show a winning horizontal four.
The HTML is minimal. Its job is to provide the containers that JavaScript will fill in. Here are the key parts:
| Element / Class | Purpose |
|---|---|
.container |
Centers everything on the page |
.scoreboard |
Shows Player 1 score, VS, Player 2 score |
#statusText |
Displays whose turn it is (or win/draw message) |
.drop-arrows |
Seven โพ buttons โ one above each column |
#playground |
The 7ร6 grid of circles โ built by JS |
.controls |
"New Game" and "Reset Score" buttons |
#playground starts empty. JavaScript creates every circle
(cell) dynamically using document.createElement each time the board renders.
<!-- drop arrows (one per column) -->
<div class="drop-arrows" id="dropArrows">
<button onclick="drop(0)">โพ</button>
<button onclick="drop(1)">โพ</button>
...
</div>
<!-- board grid (JS fills this) -->
<section class="playground" id="playground"></section>
The CSS uses custom properties (variables) so colors and sizes can be changed in one place. You don't need to memorize this โ just know where to look.
:root {
--ground-color: #062dca; /* board blue */
--player-one: #c0392b; /* red */
--player-two: #f0c030; /* yellow */
--cell-size: 62px;
--cell-gap: 8px;
}
Three CSS classes control the circles' color:
| Class | Meaning |
|---|---|
.cell |
Empty circle (dark background) |
.cell.p1 |
Player 1 disc โ red |
.cell.p2 |
Player 2 disc โ yellow |
.cell.win |
Winning four cells โ white outline + pulse animation |
display: grid with repeat(7, var(--cell-size)) for columns and
repeat(6, ...) for rows โ that's how the 7ร6 layout is made automatically.
Before writing any function, set up these variables at the top of your index.js:
| Variable | Type | Initial Value | What it stores |
|---|---|---|---|
ROWS |
number | 6 | Number of rows in the grid |
COLS |
number | 7 | Number of columns |
grid |
2D array | all zeros | Game state โ 0 = empty, 1 = P1, 2 = P2 |
currentPlayer |
number | 1 | Whose turn it is (1 or 2) |
gameOver |
boolean | false | Whether the game has ended |
scores |
array | [0, 0] | Score for P1 and P2 |
round |
number | 1 | Current round number |
const ROWS = 6;
const COLS = 7;
let grid = [];
let currentPlayer = 1;
let gameOver = false;
let scores = [0, 0];
let round = 1;
Before diving into each function, here is the big picture. The game has two main entry points โ page load and a column click.
initGrid() is the reset function. Calling it again
resets everything โ that is exactly what resetGame() does.
initGrid() โ Reset & Initialize
This is the master reset. Call it on page load and whenever a new game starts.
It rebuilds the 2D grid array filled with zeros, resets gameOver,
then calls the three helper functions to get the UI ready.
ROWS ร COLS) filled with
0.
gameOver = false.
renderBoard() to draw empty circles.
updateStatus() with no argument โ shows "Player
1's Turn".
setArrows(true) to enable the drop buttons.
function initGrid() {
grid = Array.from({ length: ROWS }, () => Array(COLS).fill(0));
gameOver = false;
renderBoard();
updateStatus();
setArrows(true);
}
renderBoard() โ Draw the Grid
Clears the #playground div and rebuilds every circle from the current
grid[][] state. This is called after every move.
#playground, set its innerHTML = ''
to wipe old circles.
<div>, add class
cell.
grid[r][c] === 1 โ also add class p1
(red).
grid[r][c] === 2 โ also add class p2
(yellow).
#playground.
function renderBoard() {
const pg = document.getElementById('playground');
pg.innerHTML = '';
for (let r = 0; r < ROWS; r++) {
for (let c = 0; c < COLS; c++) {
const cell = document.createElement('div');
cell.classList.add('cell');
if (grid[r][c] === 1) cell.classList.add('p1');
if (grid[r][c] === 2) cell.classList.add('p2');
pg.appendChild(cell);
}
}
}
updateStatus(message) โ Turn / Win / Draw Text
Updates the small dot and text line that tells players what is happening.
The optional message parameter is only passed when the game ends.
| Situation | Dot color | Text shown |
|---|---|---|
| No message, Player 1's turn | Red (default) | "Player 1's Turn" |
| No message, Player 2's turn | Yellow (.p2) |
"Player 2's Turn" |
| Message passed (draw) | Grey (.neutral) |
The message string |
function updateStatus(message) {
const dot = document.getElementById('statusDot');
const text = document.getElementById('statusText');
dot.className = 'status-dot'; // reset classes
if (message) {
dot.classList.add('neutral');
text.textContent = message;
} else if (currentPlayer === 2) {
dot.classList.add('p2');
text.textContent = "Player 2's Turn";
} else {
text.textContent = "Player 1's Turn";
}
}
setArrows(enabled) โ Enable / Disable Drop Buttons
Prevents players from clicking after the game ends. Pass true to enable,
false to disable. It simply toggles the .disabled CSS class
on the arrows container (CSS handles the fading and blocking of clicks).
function setArrows(enabled) {
const arrows = document.getElementById('dropArrows');
if (enabled) {
arrows.classList.remove('disabled');
} else {
arrows.classList.add('disabled');
}
}
drop(col) โ The Main Move Function
Called when a player clicks a โพ button. col is the column index (0โ6).
This is the heart of the game.
gameOver is true โ return immediately
(ignore click).
row (start at -1).
row is still -1 after the loop โ column is full โ
return.
grid[row][col] = currentPlayer.
renderBoard() to show the new disc.
checkWin(row, col). If it returns a winning
sequence โ handle win. Otherwise check for draw. Otherwise switch player.
function drop(col) {
if (gameOver) return;
let row = -1;
for (let r = ROWS - 1; r >= 0; r--) {
if (grid[r][col] === 0) { row = r; break; }
}
if (row === -1) return; // column full
grid[row][col] = currentPlayer;
renderBoard();
const win = checkWin(row, col);
if (win) {
highlightWin(win);
scores[currentPlayer - 1]++;
document.getElementById(`score${currentPlayer}`).textContent =
scores[currentPlayer - 1];
round++;
document.getElementById('roundNum').textContent = round;
updateStatus(`Player ${currentPlayer} Wins! ๐`);
gameOver = true;
setArrows(false);
} else if (grid[0].every(cell => cell !== 0)) {
updateStatus("It's a Draw!");
gameOver = true;
setArrows(false);
} else {
currentPlayer = currentPlayer === 1 ? 2 : 1;
updateStatus();
}
}
checkWin(row, col) โ Detect Four in a Row
Checks whether the disc just placed at (row, col) completes a sequence of four.
Returns the winning cells array if yes, or null if no.
It checks four directions. For each direction it looks both ways from the placed cell and counts how many matching discs are in a line.
| Direction | [dr, dc] |
Checks |
|---|---|---|
| Horizontal โ | [0, 1] | left โ right |
| Vertical โ | [1, 0] | up โ down |
| Diagonal โ | [1, 1] | top-left โ bottom-right |
| Diagonal โ | [1, -1] | top-right โ bottom-left |
function checkWin(row, col) {
const directions = [[0,1],[1,0],[1,1],[1,-1]];
const p = currentPlayer;
for (const [dr, dc] of directions) {
let seq = [[row, col]];
// look in positive direction
for (let d = 1; d < 4; d++) {
const r = row + dr * d, c = col + dc * d;
if (r>=0 && r<ROWS && c>=0 && c<COLS && grid[r][c]===p)
seq.push([r, c]);
else break;
}
// look in negative direction
for (let d = 1; d < 4; d++) {
const r = row - dr * d, c = col - dc * d;
if (r>=0 && r<ROWS && c>=0 && c<COLS && grid[r][c]===p)
seq.push([r, c]);
else break;
}
if (seq.length >= 4) return seq.slice(0, 4);
}
return null;
}
highlightWin(cells) โ Highlight Winning Cells
Receives the array of 4 winning [row, col] pairs. Finds each cell
in the DOM (using the formula r * COLS + c for grid index) and adds
the win class, triggering the pulse animation.
function highlightWin(cells) {
const pg = document.getElementById('playground');
cells.forEach(([r, c]) => {
pg.children[r * COLS + c].classList.add('win');
});
}
r * COLS + c? The playground's children are a flat list.
Row 0 occupies indices 0โ6, row 1 occupies 7โ13, etc. Multiplying the row by the number
of columns gives the starting index of that row.
resetGame() and resetScores()These are simple wrappers that use the functions you already built:
function resetGame() {
currentPlayer = 1;
initGrid(); // resets grid, re-renders, re-enables arrows
}
function resetScores() {
scores = [0, 0];
round = 1;
document.getElementById('score1').textContent = '0';
document.getElementById('score2').textContent = '0';
document.getElementById('roundNum').textContent = '1';
}
At the very bottom of your script โ outside all functions โ call
initGrid() once so the board is ready when the page loads.
// at the bottom of index.js
initGrid();
Here is the complete call order summary to keep in mind:
initGrid() โ
renderBoard() + updateStatus() + setArrows(true)
drop(col) โ
renderBoard() โ checkWin()
highlightWin() + update
scores + setArrows(false)
setArrows(false)
updateStatus()
resetGame() โ
initGrid()
resetScores()
renderBoard() clears and rebuilds the entire grid from
grid[][] every time. The array is the single source of truth โ the DOM is just
a reflection of it.
console.log(grid) inside
drop() to watch the 2D array update in real time.
disabled class on .drop-arrows uses
pointer-events: none โ no extra JS needed to block clicks when the game ends.
ROWS and COLS โ the grid CSS already
uses repeat(7, ...) variables, so update those too.checkWin logic works for any four-in-a-row variant โ you can swap it into
other grid games.