How to create a fun tic tac toe game in 10 mins with react

How to create a fun tic tac toe game in 10 mins with react

Welcome once again to another tutorial!

Today it's going to be everyone's favorite tic tac toe game and we will build it in just 5 mins. This requires you to be familiar with react and react hooks. You don't have to be an expert but you should be able to follow and implement this in another language or framework of your choice.

Tic tac toe is a game where two players take turns and in a race to fill 3 out of 9 spots in a grid. For one to be a winner, they would have to fill the 3 spots on a row, a column or diagonally.

Take some time and picture this grid and the 9 spots, 3 of which the user has to fill in the sequence mentioned above. Here is an image to help you

screenshot-localhost_3000-2021.06.23-15_56_34.png

And these are the various ways a user can win

By row, column, and diagonally.

screenshot-localhost_3000-2021.06.23-16_01_50.png

I guess you get the picture now. You don't have to be a guru in data structures but given a grid 3 rows and 3 columns, we can easily represent this with an array. One array that has 3 arrays, each of length 3.

let grid  = [
         [0,1,2],
         [3,4,5],
         [6,7,8]
]

Now that we have represented our grid with an array, we can pick out the combinations that make a user win, using the picture above. We can have the various winning spots also in an array below:

let winningSpots  = [
                  [0,1,2], //the rows the user can win by filling, from top to bottom
                  [3,4,5],
                  [6,7,8],
                  [0,3,6], //the columns the user can win by filling, from left to right
                  [1,4,7],
                  [2,5,8],
                  [0,4,8], //the diagonal spots from top left to bottom right
                  [2,4,6] //the diagonal spots from top right to bottom left
]

Another thing we need to take care of will be the way users will take turns to play. In this simple tutorial, we will automate the other player but in the next tutorial, we will connect users by websockets and have them play against each other.

For now, we make a user play solo. Now that we know the logic behind the game and how a user will win, lets go ahead write some code!! My favorite part :-)

You can use react's useReducer to manage the state and you should get the same result. But for simplicity, i will just use useState. First we create the different states of our main board component;

const [whoToPlay, setWhoToPlay] = React.useState<"X" | "O">("X");
const [winner, setWinner] = React.useState(null);
const [aDraw, setDraw] = React.useState(false);
const [board, setBoard] = React.useState(Array(9).fill(null));

We have the two players being represented by either X or O and we initialize with X, so X will be the first to play. The next state will be to set who wins, which will be either X or O or a draw which is set on the next line.

The last state will be the board itself of 9 spots, so we initialize this with an array of length 9 with null at each index. This represents the 9 empty spots in our grid as explained above.

You can use a grid view to draw your grid of rows and three columns, using this css

.container {
  display: grid; 
  grid-template-columns: 1fr 1fr 1fr; 
  grid-template-rows: 1fr 1fr 1fr; 
  gap: 0px 0px; 
  grid-template-areas: 
    ". . ."
    ". . ."
    ". . ."; 
}

Make sure each child has a border to make the grid come out as seen above. Since we are using react, we can create a component to represent each spot and have it generated 9 times in our container. A simple component will look like below;

interface Props extends React.HTMLAttributes<HTMLDivElement>{
    value?: number;
    selected: ()=>void;
}

export const index = ({ value, selected, ...rest }:Props) => {
    const[showValue, setShowValue] = React.useState(false);
    const clicked = ()=>{
        if(showValue)return;
        setShowValue(true);
        selected()
    }
    return (
        <div  onClick={clicked} {...rest}>
            {showValue?value:null}
        </div>

    )
}

This component will receive props from it's parent where

  • value is the text that will be shown in that spot when the function clicked is called. This text will either be X or O, indicating that it was user X or O that clicked that spot.
  • After we show the value in the clicked function, we call the selected function that was passed as a prop from the parent component. This will be implemented in the parent component to let it know that this particular spot has been clicked by the user.

We return from the clicked function if value is already showing, meaning, that spot has been selected already so nothing should happen.

Now in the parent component where we have the container of our grid, we implement several functions as seen below

const userSelect = (select: number) => {
        const currentBoard = board;
        if (currentBoard[select]) return;
        currentBoard[select] = whoToPlay;
        const userWon = whoWon(currentBoard);
        if (!userWon) {
            return setWhoToPlay(whoToPlay === "X" ? "O" : "X");
        } else {
            setWinner(userWon)
        }
    }

 const whoWon = (currentBoard: any[]) => {
        const winningSpots = [
            [0, 1, 2],
            [3, 4, 5],
            [6, 7, 8],
            [0, 3, 6],
            [1, 4, 7],
            [2, 5, 8],
            [0, 4, 8],
            [2, 4, 6]
        ];

        for (let i = 0; i < winningSpots.length; i++) {
            const [spot1, spot2, spot3] = winningSpots[i];
            if (currentBoard[spot1] && currentBoard[spot1] === currentBoard[spot2] 
                && currentBoard[spot2] === currentBoard[spot3]) {
                return currentBoard[spot1];
            }
        }
        return null;
    }


render(){
......
       {!winner && <div className=".container">
          {board.map((element, index: number) => ( 
<Grid key={index} value={element} selected={() =>userSelect(index)} />
          ))}
......
}

I know this is a lot, but we will go through each function so you understand.

In the render method, we only want to show the grid for play when we don't have a winner, that means as soon as we have a winner, we will rather show who the winner is and not the grid, since the game is over. So we render the grid conditionally - Only when we don't have a winner.

Since we want 9 spots in the container and having created the component above already, it is exported as Grid, so we import it here, and given the board state we created earlier on, we go over each element and render the grid.

We pass the element as the value prop to the component and a function that will be called when the user selects that spot. This will pass the index or position of that spot in our board so we know exactly where the user clicked.

In the userSelect function, an index is passed to it which shows where the user clicked. In this function, we first create a reference to our board state, then check if where the user clicked has already clicked, thus that particular position in the board will be filled, so we return the function since you can't select a spot multiple times.

If that spot hasn't been selected, we fill that spot with who clicked that spot, thus whoToPlay. After which we call the function whoWon to get the user who won assuming that was the last move for the user in play.

In the whoWon function, we first have all our winning combinations, so we go over each combination then compare with our board to see if the current user has filled spots that match with any of the winning combinations.

If the user has filled positions that is in line with any of the winning combinations, we return that user, if not, we return null. If a user wasn't returned, we give the other user a turn to play. If a user was returned, then we have a winner, so we set the winner.

How about you make this a challenge and based on the knowledge you have now regarding creating game logic, go ahead and build one like this where your users play with a bot like this.