We all have played this game in our life and it’s so much fun. I always wanted to build a Tic Tac Toe game ever since I saw and played.
Also, I wanted to explore React with Typescript. So, I realized that It will be a great idea to build Tic Tac Toe in React using Typescript. It will give me the exposure to Typescript that I need and at the same time It will be fun to build this game.
We will use React, Typescript, Hooks, and Ant Design for this. If you don’t have any idea of Typescript. I would highly recommend getting the basic idea.
So, Take a cup of tea and we are going to build a Tic Tac Toe in React using Typescript.
Create an App and set up a basic layout
We will create a React App using create-react-app
npx create-react-app tic-tac-toe --template typescript
cd tic-tac-toe
npm start
Install Ant Design
npm install antd
Now, you have created an app. Let’s fix the layout.
Remove all unnecessary files from the layout and make it as given below.
A couple of things:
Let’s start by creating a components folder.
Don’t worry about what will be inside the common and Utilities folder. You will get to know as we proceed.
Create files such
Start.tsx
Finish.tsx
HumanvsHuman.tsx
HumanvsComputer.tsx
.In
App.css
remove everything except.App
class, maybe we need that in the future but make that class also empty.
Let’s fix index.css
Just copy and paste the below content of index.css
into your file.
You can download the background image from here.
body {
margin: 0;
/* background-color: #44c4eb; */
background-image: url('./components/common/tic-tac-toe-seamless-background-on-dark-blue-illustration-vector.jpg');
background-repeat: no-repeat;
background-attachment: fixed;
background-size: cover;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}
Start Page
We will first build the Start Page. Firstly, create a file name Start.tsx
in components
folder. Now, the Start page will have a couple of options to select from.
Letters to choose i.e. X, O.
Number of Players to select i.e. Player 1 means with Computer and Player 2 means with another Player.
The start component will accept props named:
gameStatus
— It shows the status of the game i.e. “started”, “playing”, and “finished”. By Default: “started”.player
— It stores which opponent is chosen by the user i.e. “human” or “computer”.letter
— It stores the letter selected by the user i.e. “X” or “O”.
We are using the interface here to declare what props the Start component will accept. Once the user selects all the options and presses the Start Game!!
button then we are changing the status of the game to “playing”.
import React from "react";
import type { RadioChangeEvent } from "antd";
import { Radio, Typography, Button, Space } from "antd";
const { Title } = Typography;
interface Props {
gameStatus: string;
setGameStatus: (val: string) => void;
player: string;
setPlayer: (val: string) => void;
letter: string;
setLetter: (val: string) => void;
}
const styles = {
letterButton: {
fontWeight: "bold",
fontSize: "30px",
width: "150px",
color: "red",
},
playerButton: {
fontWeight: "bold",
fontSize: "25px",
width: "200px",
color: "red",
},
startButton: {
marginTop: "15px",
fontWeight: "bold",
width: "300px",
},
};
const Start: React.FC<Props> = ({
gameStatus,
setGameStatus,
player,
setPlayer,
letter,
setLetter,
}) => {
const handleLetterChange = (event: RadioChangeEvent): void => {
// console.log("l: ", event.target.value);
setLetter(event.target.value);
};
const handlePlayerChange = (event: RadioChangeEvent): void => {
// console.log("p: ", event.target.value);
setPlayer(event.target.value);
};
const handleStartGame = () => {
setGameStatus("playing");
};
return (
<div
style={{
display: "flex",
flexDirection: "column",
justifyContent: "center",
alignItems: "center",
textAlign: "center",
}}
>
<Title style={{ color: "#ebc334" }}>Choose your Letter</Title>
<Radio.Group
buttonStyle="solid"
size="large"
onChange={handleLetterChange}
>
<Space direction="vertical">
<Radio.Button style={styles.letterButton} value="X">
X
</Radio.Button>
<Radio.Button style={styles.letterButton} value="O">
O
</Radio.Button>
</Space>
</Radio.Group>
{letter && (
<Title style={{ color: "#ebc334" }}>
Whom do you want to play with?
</Title>
)}
{letter && (
<Radio.Group buttonStyle="solid" onChange={handlePlayerChange}>
<Space direction="vertical">
<Radio.Button style={styles.playerButton} value="computer">
1 Player
</Radio.Button>
<Radio.Button style={styles.playerButton} value="human">
2 Player
</Radio.Button>
</Space>
</Radio.Group>
)}
{letter && player && (
<Button
style={styles.startButton}
type="primary"
danger
size="large"
onClick={handleStartGame}
>
Start Game!!
</Button>
)}
</div>
);
};
export default Start;
Building the Utility
We all know that the Tic Tac Toe board is one of the most important parts of the game. And that board consists of 9 squares. Let’s build a utility called Square which is going to be used to design the board.
Create a file name Square.tsx
in components/Utilities
folder. Square.tsx
accepts props named value, index, handleClick function.
A specific square holds information such as index, value, and onClick function. This means I want to know, for any square whenever the onClick function triggers, I should have what index that square has and what value it contains.
import React from "react";
import { Button } from "antd";
interface Props {
value: string;
index: number;
handleClick: (index: number) => void;
}
const styles = {
square: {
height: "120px",
width: "120px",
backgroundColor: "#eb8034",
fontSize: "50px",
color: "red",
fontWeight: "bold",
},
};
const Square: React.FC<Props> = ({ value, index, handleClick }) => {
return (
<Button style={styles.square} onClick={() => handleClick(index)}>
{value}
</Button>
);
};
export default Square;
Building the game
This is the most interesting part because we will be working on the core of tic tac toe. There could be two scenarios I can think of.
Playing with Computer (1 Player)
Playing with Human (2 Player)
Before we start working on these two cases. Let’s build a common function name findWinner
. We need this because we will have to find the winner in both cases.
Create a file name findWinner.ts
in components/common
the folder.
Few things about findWinner
function:
There will be a total of 8 combinations to win the game.
You will have to keep on running the loop until you get a winner.
You will have to check for all 8 combinations to find the winner.
This function will return
getWinner
, a variable that has either “X” or “O” as a winner. If it contains “” i.e. empty string that means the game is “Tied”.
export const findWinner = (board: string[]): string => {
const winningPositions = [
[0, 1, 2],
[3, 4, 5],
[6, 7, 8],
[0, 3, 6],
[1, 4, 7],
[2, 5, 8],
[0, 4, 8],
[2, 4, 6],
];
let winningPositionIndex = 0;
let getWinner = "";
while (winningPositionIndex < winningPositions.length && getWinner === "") {
// Get the combination to check
const combinationToCheck = winningPositions[winningPositionIndex];
// Get the value to check. Also, you can't check empty string cuz it might get failed.
// You have to check with either X or O
const valuesToCheck = combinationToCheck.map((el) => board[el]);
// Check for every value
const isFinished = valuesToCheck.every(
(el) => el === valuesToCheck[0] && valuesToCheck[0]
);
getWinner = isFinished ? valuesToCheck[0] : "";
winningPositionIndex++;
}
return getWinner;
};
Building HumanvsHuman aka (2 Player)
Create a file name HumanvsHuman.tsx
in the components folder.
Some points about HumanvsHuman.tsx
component:
It accepts props such as
gameStatus
which just store the status of the game as we have discussed already.You might be thinking why it doesn’t take letter prop as we might need this. Well, in this case, it doesn’t matter.
The game will start with “X” by default in this case.
In
handleClick
function, we are setting the value on board as per the turn on the respective index.One thing to remember is, once the user has clicked on the square and it got value then the value shouldn’t be changed no matter how many times the user clicks on that specific square again.
Every time board gets updated, we need to find a winner. Thus we need to run the
useEffect
whenever the boards get updated and it will call thefindWinner
function.Once the game is finished, store the winner in the local storage and change the status of the game to
finished
.
import React, { useState, useEffect } from "react";
import { Typography } from "antd";
import Square from "./Utilities/Square";
import { findWinner } from "./common/findWinner";
interface Props {
gameStatus: string;
setGameStatus: (val: string) => void;
}
const { Title } = Typography;
const styles = {
board: {
display: "grid",
gridTemplateColumns: "repeat(3, 1fr)",
},
};
const HumanvsHuman: React.FC<Props> = ({ gameStatus, setGameStatus }) => {
const [board, setBoard] = useState<string[]>(Array(9).fill(""));
const [turn, setTurn] = useState<string>("X");
useEffect(() => {
const getWinner = findWinner(board);
if (getWinner) {
localStorage.setItem("winner", getWinner);
setGameStatus("finished");
return;
} else {
const data = board.every((v) => v !== "");
if (data) {
localStorage.setItem("winner", getWinner);
setGameStatus("finished");
}
return;
}
}, [board, setGameStatus]);
const handleClick = (index: number): void => {
// console.log("index", index);
if (index < 0 || index > 9 || board[index] !== "") return;
const newBoard = [...board];
newBoard[index] = turn;
setBoard(newBoard);
const newTurn = turn === "X" ? "O" : "X";
setTurn(newTurn);
};
return (
<div>
{turn.length > 0 && (
<Title
level={2}
style={{ textAlign: "center", marginTop: "20px", color: "#ebc334" }}
>
Player {turn} Turn
</Title>
)}
<div
style={{
display: "flex",
justifyContent: "center",
marginTop: "20px",
}}
>
{/* -----------------BUILDING THE BOARD-------------- */}
<div style={styles.board}>
{board.map((value, index) => {
return (
<Square
key={index}
value={value}
index={index}
handleClick={handleClick}
/>
);
})}
</div>
</div>
</div>
);
};
export default HumanvsHuman;
Building HumanvsComputer aka (1 Player)
Create a file name HumanvsComputer.tsx
in the components folder.
Some points about HumanvsComputer.tsx
component:
It accepts props such as
gameStatus
andletter
.Now, you might think why we are getting the letter selected by the player as a prop. We haven’t done the same in the previous component.
Well, it’s necessary because if you select a letter from “X” or “O” the opposite one should automatically for the computer. So, we need that information.
Apart from this,
handleClick
function is pretty much the same. The change will only occur on square through this when the player has a turn.By default, the player has to start the game in this case.
There is one more thing to notice, how do we decide which one has a turn now? Well, you can see in the code that we are having a state variable name
turn
. It has a letter value by default. So, the player will have to start the game in this case.When
turn
equals the letter selected by the player which means we have a player turn, and if theturn
doesn’t equals a letter then we have a computer turn.In this case, the computer will place only in empty positions. Thus, we will randomly select the empty position for the computer turn.
Every time the board gets updated, we have to run the
useEffect
. This will do two things. One, the computer will automatically play its turn whenever it gets, and second, we will have to call thefindWinner
function to get the winner.Once the game is finished, store the winner in the local storage and change the status of the game to
finished
.
import React, { useState, useEffect } from "react";
import Square from "./Utilities/Square";
import { findWinner } from "./common/findWinner";
interface Props {
gameStatus: string;
setGameStatus: (val: string) => void;
letter: string;
}
const styles = {
board: {
display: "grid",
gridTemplateColumns: "repeat(3, 1fr)",
},
};
const HumanvsComputer: React.FC<Props> = ({
gameStatus,
setGameStatus,
letter,
}) => {
const [board, setBoard] = useState<string[]>(Array(9).fill(""));
const [turn, setTurn] = useState<string>(letter);
useEffect(() => {
// Computer turn
if (letter !== turn) {
const emptyIndexes = board
.map((el, index) => (el === "" ? index : ""))
.filter((val) => val !== "");
const randomIndex =
emptyIndexes[Math.floor(Math.random() * emptyIndexes.length)];
const newBoard = board;
newBoard[
typeof randomIndex === "number"
? randomIndex
: parseInt(randomIndex, 10)
] = turn;
setBoard(newBoard);
const newTurn = turn === "X" ? "O" : "X";
setTurn(newTurn);
}
const getWinner = findWinner(board);
if (getWinner) {
localStorage.setItem("winner", getWinner);
setGameStatus("finished");
return;
} else {
const data = board.every((v) => v !== "");
if (data) {
localStorage.setItem("winner", getWinner);
setGameStatus("finished");
}
return;
}
}, [board, turn, setGameStatus, letter]);
const handleClick = (index: number): void => {
// console.log("index", index);
if (index < 0 || index > 9 || board[index] !== "") return;
if (letter === turn) {
const newBoard = [...board];
newBoard[index] = turn;
setBoard(newBoard);
const newTurn = turn === "X" ? "O" : "X";
setTurn(newTurn);
}
};
return (
<div>
{/* -----------------BUILDING THE BOARD-------------- */}
<div
style={{
display: "flex",
justifyContent: "center",
marginTop: "20px",
}}
>
<div style={styles.board}>
{board.map((value, index) => {
return (
<Square
key={index}
value={value}
index={index}
handleClick={handleClick}
/>
);
})}
</div>
</div>
</div>
);
};
export default HumanvsComputer;
Now, you might question whether I could’ve made HumanvsHuman and HumanvsComputer in a single file. You are correct, but I wanted to handle both scenarios in a different file. So, It will be easy for me to deal with bugs in the future.
Finish Page
Create a file name Finish.tsx
in the components folder.
This component does two things. One, declare the winner, and second, reset all the states.
A couple of things about the Finish component:
It accepts all the props such as
gameStatus
,letter
andplayer
.There is a restart button that resets the game to start over again.
We get the winner value from the local storage to show it on the page.
import React from "react";
import { Button, Typography } from "antd";
const { Title } = Typography;
interface Props {
gameStatus: string;
setGameStatus: (val: string) => void;
player: string;
setPlayer: (val: string) => void;
letter: string;
setLetter: (val: string) => void;
}
const styles = {
resetButton: {
fontWeight: "bold",
},
};
const Finish: React.FC<Props> = ({
gameStatus,
setGameStatus,
setPlayer,
player,
letter,
setLetter,
}) => {
const winner = localStorage.getItem("winner");
const handleRestart = () => {
localStorage.removeItem("winner");
setGameStatus("started");
setPlayer("");
setLetter("");
};
return (
<div
style={{
display: "flex",
flexDirection: "column",
justifyContent: "center",
alignItems: "center",
textAlign: "center",
}}
>
{winner === "" ? (
<>
<Title style={{ color: "#ebc334" }}>😅 Oops!</Title>
<Title style={{ color: "#ebc334" }}>Game is Tied 😟</Title>
</>
) : (
<>
<Title style={{ color: "#ebc334" }}>
🎉 🎊 YAY, We have a winner
</Title>
{player === "human" ? (
<Title style={{ color: "#ebc334" }} level={2}>
Player {winner} won the game
</Title>
) : letter === winner ? (
<Title style={{ color: "#ebc334" }} level={2}>
You won the game
</Title>
) : (
<Title style={{ color: "#ebc334" }} level={2}>
Computer won the game
</Title>
)}
</>
)}
<Button
style={styles.resetButton}
size="large"
type="primary"
danger
onClick={() => handleRestart()}
>
Restart the Game!!
</Button>
</div>
);
};
export default Finish;
Source Code: https://github.com/thisisnitish/tic-tac-toe
I hope you’ve enjoyed this tutorial. This was a lot of fun to make. If you feel stuck on something, please refer to the source code and If you have any questions, let me know in the comments.
Thanks!