Task

Completing the GHW Games challenge - Connect Four Game

What it does

A user can play Connect Four on a browser with their friends.

Node Dependencies

  • react
  • react-dom
  • react-scripts

How we built it

I've created it on codesandbox.io, using the react.js template. Firstly, import the fontawesome kit script in the /public/index.html as,

<script src="https://kit.fontawesome.com/6f42fc440c.js" crossorigin="anonymous" ></script>

A connect four board contains 42 cells, so let's create a Cell component. Only the cells in top row should be hoverable and clickable to select a column the player wants to drop their coin in. Depending on player 1 or 2 the color of coin should be red or blue.

import React, { useState } from "react";

const Cell = ({ value, onClick, hoverable, player }) => {
  const [isHovered, setIsHovered] = useState(false);

  return (
    <div
      className={`cell ${hoverable ? "hoverable" : ""}`}
      onClick={onClick}
      onMouseEnter={() => setIsHovered(true)}
      onMouseLeave={() => setIsHovered(false)}
    >
      {isHovered && hoverable ? (
        player == 1 ? (
          <i className="fa-solid fa-circle red"></i>
        ) : (
          <i className="fa-solid fa-circle blue"></i>
        )
      ) : null}
      {value === 1 ? (
        <i className="fa-solid fa-circle red"></i>
      ) : value === 2 ? (
        <i className="fa-solid fa-circle blue"></i>
      ) : null}
    </div>
  );
};

export default Cell;
.row {
  display: flex;
}

.cell {
  width: 50px;
  height: 50px;
  border: 1px solid rgb(204, 204, 204);
  display: flex;
  justify-content: center;
  align-items: center;
  font-size: 24px;
}

.hoverable {
  cursor: pointer;
}

.red {
  color: red;
}

.blue {
  color: blue;
}

Now lets create a Board and place the 42 cells in it, in 6x7 grid format. Send parameters to only allow the first row of cells as hoverable and clickable, along with the function to add a player coin in the column.

import React from "react";
import Cell from "./Cell";

const Board = ({ board, insertCoin, player }) => {
  return (
    <div className="board">
      {board.map((row, rowIndex) => (
        <div key={rowIndex} className="row">
          {row.map((cell, colIndex) => (
            <Cell
              key={colIndex}
              value={cell}
              onClick={rowIndex === 0 ? () => insertCoin(colIndex) : null}
              hoverable={rowIndex === 0}
              player={player}
            />
          ))}
        </div>
      ))}
    </div>
  );
};

export default Board;

.board {
  display: inline-block;
  border: 1px solid rgb(51, 51, 51);
}

.row {
  display: flex;
}

Create a component Game, with state variables such as board to manage which cells in the board are empty and which contain a player coin, player to determine which player's turn is it, and winner to determine who won. And display the required components such as board, status, and a reset button.

import React, { useState, useEffect } from "react";
import Board from "./Board";

const Game = () => {
  const [board, setBoard] = useState(
    Array.from({ length: 6 }, () => Array(7).fill(null))
  );
  const [player, setPlayer] = useState(1);
  const [winner, setWinner] = useState(null);

  return (
    <div className="game">
      <h1>Connect Four</h1>
      <Board board={board} insertCoin={insertCoin} player={player} />
      <div className="status">
        {winner && winner !== "tie" && <p>Player {winner} wins!</p>}
        {winner && winner === "tie" && <p>It's a tie!</p>}
        {!winner && <p>Player {player}'s turn</p>}
      </div>
      <button className="reset-btn" onClick={resetGame}>
        Reset Game
      </button>
    </div>
  );
};

export default Game;
.game {
  text-align: center;
}

.status {
  font-style: italic;
  font-weight: bold;
  margin: 20px 0;
}

.reset-btn {
  width: 120px;
  height: 50px;
  border: none;
  background-color: white;
  border-radius: 5px;
  font-size: 16px;
}

.reset-btn:hover {
  background-color: transparent;
  color: white;
}

Create a function to insert a coin at the bottom-most empty cell of the selected row. No coin should be inserted if column is full or winner is declared.

  const insertCoin = (col) => {
    if (winner || board[0][col] !== null) return;

    const newBoard = [...board];
    for (let row = board.length - 1; row >= 0; row--) {
      if (newBoard[row][col] === null) {
        newBoard[row][col] = player;
        setBoard(newBoard);
        setPlayer(player === 1 ? 2 : 1);
        break;
      }
    }
  };

Everytime board updates keep checking if there are four coins of same color consecutively in a row or column or diagonally. If such case is found then the player of that color coin wins. If the board is full, but no one has won then it's should be a tie.

  useEffect(() => {
    const checkWinner = () => {

      for (let row = 0; row < 6; row++) {
        for (let col = 0; col < 4; col++) {
          if (
            board[row][col] &&
            board[row][col] === board[row][col + 1] &&
            board[row][col] === board[row][col + 2] &&
            board[row][col] === board[row][col + 3]
          ) {
            setWinner(board[row][col]);
            return;
          }
        }
      }

      for (let col = 0; col < 7; col++) {
        for (let row = 0; row < 3; row++) {
          if (
            board[row][col] &&
            board[row][col] === board[row + 1][col] &&
            board[row][col] === board[row + 2][col] &&
            board[row][col] === board[row + 3][col]
          ) {
            setWinner(board[row][col]);
            return;
          }
        }
      }

      for (let row = 0; row < 3; row++) {
        for (let col = 0; col < 4; col++) {
          if (
            board[row][col] &&
            board[row][col] === board[row + 1][col + 1] &&
            board[row][col] === board[row + 2][col + 2] &&
            board[row][col] === board[row + 3][col + 3]
          ) {
            setWinner(board[row][col]);
            return;
          }
        }
      }

      for (let row = 0; row < 3; row++) {
        for (let col = 3; col < 7; col++) {
          if (
            board[row][col] &&
            board[row][col] === board[row + 1][col - 1] &&
            board[row][col] === board[row + 2][col - 2] &&
            board[row][col] === board[row + 3][col - 3]
          ) {
            setWinner(board[row][col]);
            return;
          }
        }
      }

      if (board.every((row) => row.every((cell) => cell !== null))) {
        setWinner("tie");
      }
    };

    checkWinner();
  }, [board]);

Create a function to reset the game.

  const resetGame = () => {
    setBoard(Array.from({ length: 6 }, () => Array(7).fill(null)));
    setPlayer(1);
    setWinner(null);
  };

Wrap the Game component in App.js

// App.js
import React from "react";
import Game from "./Game";
import "./styles.css";

function App() {
  return (
    <div className="app">
      <Game />
    </div>
  );
}

export default App;

Full Code:

// App.js
import React from "react";
import Game from "./Game";
import "./styles.css";

function App() {
  return (
    <div className="app">
      <Game />
    </div>
  );
}

export default App;
// Game.js
import React, { useState, useEffect } from "react";
import Board from "./Board";

const Game = () => {
  const [board, setBoard] = useState(
    Array.from({ length: 6 }, () => Array(7).fill(null))
  );
  const [player, setPlayer] = useState(1);
  const [winner, setWinner] = useState(null);

  useEffect(() => {
    const checkWinner = () => {
      for (let row = 0; row < 6; row++) {
        for (let col = 0; col < 4; col++) {
          if (
            board[row][col] &&
            board[row][col] === board[row][col + 1] &&
            board[row][col] === board[row][col + 2] &&
            board[row][col] === board[row][col + 3]
          ) {
            setWinner(board[row][col]);
            return;
          }
        }
      }

      for (let col = 0; col < 7; col++) {
        for (let row = 0; row < 3; row++) {
          if (
            board[row][col] &&
            board[row][col] === board[row + 1][col] &&
            board[row][col] === board[row + 2][col] &&
            board[row][col] === board[row + 3][col]
          ) {
            setWinner(board[row][col]);
            return;
          }
        }
      }

      for (let row = 0; row < 3; row++) {
        for (let col = 0; col < 4; col++) {
          if (
            board[row][col] &&
            board[row][col] === board[row + 1][col + 1] &&
            board[row][col] === board[row + 2][col + 2] &&
            board[row][col] === board[row + 3][col + 3]
          ) {
            setWinner(board[row][col]);
            return;
          }
        }
      }

      for (let row = 0; row < 3; row++) {
        for (let col = 3; col < 7; col++) {
          if (
            board[row][col] &&
            board[row][col] === board[row + 1][col - 1] &&
            board[row][col] === board[row + 2][col - 2] &&
            board[row][col] === board[row + 3][col - 3]
          ) {
            setWinner(board[row][col]);
            return;
          }
        }
      }

      if (board.every((row) => row.every((cell) => cell !== null))) {
        setWinner("tie");
      }
    };

    checkWinner();
  }, [board]);

  const insertCoin = (col) => {
    if (winner || board[0][col] !== null) return;

    const newBoard = [...board];
    for (let row = board.length - 1; row >= 0; row--) {
      if (newBoard[row][col] === null) {
        newBoard[row][col] = player;
        setBoard(newBoard);
        setPlayer(player === 1 ? 2 : 1);
        break;
      }
    }
  };

  const resetGame = () => {
    setBoard(Array.from({ length: 6 }, () => Array(7).fill(null)));
    setPlayer(1);
    setWinner(null);
  };

  return (
    <div className="game">
      <h1>Connect Four</h1>
      <Board board={board} insertCoin={insertCoin} player={player} />
      <div className="status">
        {winner && winner !== "tie" && <p>Player {winner} wins!</p>}
        {winner && winner === "tie" && <p>It's a tie!</p>}
        {!winner && <p>Player {player}'s turn</p>}
      </div>
      <button className="reset-btn" onClick={resetGame}>
        Reset Game
      </button>
    </div>
  );
};

export default Game;
// Board.js
import React from "react";
import Cell from "./Cell";

const Board = ({ board, insertCoin, player }) => {
  return (
    <div className="board">
      {board.map((row, rowIndex) => (
        <div key={rowIndex} className="row">
          {row.map((cell, colIndex) => (
            <Cell
              key={colIndex}
              value={cell}
              onClick={rowIndex === 0 ? () => insertCoin(colIndex) : null}
              hoverable={rowIndex === 0}
              player={player}
            />
          ))}
        </div>
      ))}
    </div>
  );
};

export default Board;
// Cell.js
import React, { useState } from "react";

const Cell = ({ value, onClick, hoverable, player }) => {
  const [isHovered, setIsHovered] = useState(false);

  return (
    <div
      className={`cell ${hoverable ? "hoverable" : ""}`}
      onClick={onClick}
      onMouseEnter={() => setIsHovered(true)}
      onMouseLeave={() => setIsHovered(false)}
    >
      {isHovered && hoverable ? (
        player == 1 ? (
          <i className="fa-solid fa-circle red"></i>
        ) : (
          <i className="fa-solid fa-circle blue"></i>
        )
      ) : null}
      {value === 1 ? (
        <i className="fa-solid fa-circle red"></i>
      ) : value === 2 ? (
        <i className="fa-solid fa-circle blue"></i>
      ) : null}
    </div>
  );
};

export default Cell;
/* styles.css */
body {
  background-color: rgb(29, 29, 29);
  color: white;
}

.app {
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100vh;
}

.game {
  text-align: center;
}

.board {
  display: inline-block;
  border: 1px solid rgb(51, 51, 51);
}

.row {
  display: flex;
}

.cell {
  width: 50px;
  height: 50px;
  border: 1px solid rgb(204, 204, 204);
  display: flex;
  justify-content: center;
  align-items: center;
  font-size: 24px;
}

.hoverable {
  cursor: pointer;
}

.status {
  font-style: italic;
  font-weight: bold;
  margin: 20px 0;
}

.reset-btn {
  width: 120px;
  height: 50px;
  border: none;
  background-color: white;
  border-radius: 5px;
  font-size: 16px;
}

.reset-btn:hover {
  background-color: transparent;
  color: white;
}

.red {
  color: red;
}

.blue {
  color: blue;
}

including the remaining react files such as /public/index.html, /src/index.js, and package.json.

Share this project:

Updates