ConnectFour

A beginner-friendly guide to building the game from scratch

๐ŸŽฏ What is this project?

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.

๐Ÿ“„ HTML โ€” The Skeleton

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
๐Ÿ’ก Notice that #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>

๐ŸŽจ CSS โ€” Just Enough to Know

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
โœ… The grid uses 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.

โš™๏ธ JavaScript โ€” Variables First

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;

๐Ÿ”„ How the Functions Connect

Before diving into each function, here is the big picture. The game has two main entry points โ€” page load and a column click.

On page load

Page loads
โ†’
initGrid()
โ†’
renderBoard()
โ†’
updateStatus()
โ†’
setArrows(true)

On column click โ–พ

drop(col)
โ†’
Find lowest empty row
โ†’
Update grid[][]
โ†’
renderBoard()
checkWin(row, col)
โ†’
Win โ†’ highlightWin() + update score
checkWin returns null
โ†’
Grid full? โ†’ Draw
โ†’
Else โ†’ switch player
๐Ÿ“Œ Key rule: initGrid() is the reset function. Calling it again resets everything โ€” that is exactly what resetGame() does.

๐Ÿงฉ Functions โ€” Step by Step

1 ยท 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.

function initGrid() {
  grid = Array.from({ length: ROWS }, () => Array(COLS).fill(0));
  gameOver = false;
  renderBoard();
  updateStatus();
  setArrows(true);
}

2 ยท renderBoard() โ€” Draw the Grid

Clears the #playground div and rebuilds every circle from the current grid[][] state. This is called after every move.

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);
    }
  }
}

3 ยท 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";
  }
}

4 ยท 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');
  }
}

5 ยท 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.

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();
  }
}

6 ยท 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;
}

7 ยท 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');
  });
}
๐Ÿ’ก Why 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.

8 ยท 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';
}

9 ยท Kick Everything Off

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();

โœ… Putting It All Together

Here is the complete call order summary to keep in mind:

โš ๏ธ Remember: 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.

๐Ÿ’ก Quick Tips