DEV Community

raivikas
raivikas

Posted on • Originally published at nextjsdev.com on

Let's build Tic-Tac-Toe Game using React.js & Tailwind CSS.

Let's build Tic-Tac-Toe Game using React.js & Tailwind CSS.

Hello everyone, I hope you all are doing well. I am back with another exciting web dev project, which will help to learn some new web dev skills as a Front-end developer.

In this tutorial, I will show you how you can build a Tic-Tac-Toe Game, It's one of the Frontend Mentor challenge projects and our goal is to make it look like the design given by the Frontend Mentor.

Also, we will add some extra features to make it fully functional.

Here is the link to the FrontendMentorchallenge that we will build.

So without any further talk, Let's start building it 🚀.

🚀 Live Demo of the Project

Step-1 Initializing the Project

Create a new next-js app with Tailwind CSS bootstrapped in it.

You can use this one-line command to create a new nextjs app with TypeScript and Tailwind CSS.

npx create-next-app -e with-tailwindcss my-project-name

Enter fullscreen mode Exit fullscreen mode

You can name your project whatever you want, I will name it as tic-tac-toe .

💡

We are using TypeScript in this Project, so we have to explicitly define all the props, constants & function data types.

Now after creating the project open it in Vs Code or any IDE that you prefer.

Find the index.tsx file inside pages directory. and delete everything and paste the given code below.

import type { NextPage } from 'next'
import Head from 'next/head'

const Home: NextPage = () => {

return (
    <div className="flex min-h-screen flex-col items-center bg-[#1e1f29]">
      <Head>
        <title>Tic-Tac-Toe Game</title>
        <link rel="icon" href="/favicon.ico" />
      </Head>

)}

export default Home;

Enter fullscreen mode Exit fullscreen mode

I have created some custom classes as we are going to use the same utility classes, again and again, it's better to create a custom class inside the globals.css file which is inside the styles folder.

After that visit the globals.css file inside the styles folder add the custom classes as given below.

@tailwind base;
@tailwind components;
@tailwind utilities;

@layer utilities {

 .button{
     @apply flex items-center justify-center text-lg font-bold text-gray-900 transition duration-300 ease-in ;
 }
  .square {
    @apply flex h-[90px] w-[90px] md:h-[100px] md:w-[100px] items-center justify-center bg-[#1f3540] rounded-2xl shadow-md active:scale-125 transition duration-200 ease-in hover:bg-[#18272e] shadow-gray-400/30 ;
  }

  .board {
    @apply mt-24 flex h-[350px] w-[350px] md:mt-20 md:h-[400px] md:w-[400px] flex-col items-center justify-center space-y-4 rounded-xl;
  }

  .board-row {
    @apply flex items-center justify-center space-x-4;
  }
}

Enter fullscreen mode Exit fullscreen mode

Step-2 Creating the Components

If you see the design given by the Frontend Mentor Challenge, there are only many components that you can make or divide your app into.

But I am going to divide it into 5 components.

  1. XIcon.tsx
  2. OIcon.tsx
  3. ChoosePlayer.tsx
  4. Board.tsx
  5. WinnerModal.tsx

So, now create the components folder inside your project and create two files with the names XIcon.tsx , OIcon.tsx,ChoosePlayer.tsx, Board.tsx and WinnerModal.tsx.

We are using TypeScript in this Project, so we have to explicitly define all the props, constants & function data types.

First, we will create the XIcon and the OIcon component.

Inside XIcon.tsx

import React from 'react'

export const XIcon = () => {
  return (
    <div className="relative h-16 w-16 cursor-pointer ">
      <div className=" absolute origin-top-left rotate-[44deg] ml-2 -mt-[1px] bg-[#30c4bd] h-4 w-20 rounded-l-full rounded-r-full ">
      </div>
      <div className="absolute origin-top-right -rotate-[42deg] -ml-[23px] bg-[#30c4bd] h-4 w-20 rounded-l-full rounded-r-full ">
      </div>
    </div>
  )
}

Enter fullscreen mode Exit fullscreen mode

In the above code as you can see I have created a custom X-Icon using two divs placed inside another div.

I have positioned the inner two divs absolute and also added rotation (using transform rotate) to tilt the divs, to make the "X " icon.

You can also use an SVG Icon or a PNG image if it's available to you. I was not able to find any suitable SVG Icon for this, so that's why I created this Icon on my own using HTML and TailwindCSS.

Inside OIcon.tsx

import React from 'react'

export const OIcon = () => {
    return (
        <div className="flex items-center justify-center h-16 w-16 cursor-pointer ">
            <div className=" h-8 w-8 ring-[18px] ring-[#f3b236] rounded-full">
            </div>
        </div >
    )
}

Enter fullscreen mode Exit fullscreen mode

In the above code as you can see I have created a custom O-Icon using One div placed inside another div and using some CSS.

Inside ChoosePlayer.tsx

import React from 'react'
import { OIcon } from './OIcon'
import { XIcon } from './XIcon'

interface PlayerProp {
  handlePlayerX(): void,
  handlePlayerO(): void,
  handleNewGame(): void,
}

export const ChoosePlayer = ({handlePlayerX, handleNewGame, handlePlayerO }: PlayerProp) => {

  return (
    <div className="mt-20 md:mt-16 w-[500px] flex flex-col items-center justofy-center mx-auto">
      <div className="flex rounded-xl px-6 py-2 items-center justify-center space-x-4">
      <XIcon />
      <OIcon />
      </div>
      <div className="flex flex-col items-center py-8 w-[400px] md:w-[500px] h-64 md:h-72 rounded-2xl bg-[#1f3540] mt-6 space-y-6 md:space-y-8">
        <p className="text-md text-gray-300 uppercase font-semibold md:text-xl ">
          Please Select 
          {" "}
          <span className="text-[#30c4bd] text-xl font-bold ">X</span> 
          {" "}
          or 
          {" "}
          <span className="text-[#f3b236] text-xl font-bold">O</span>
        </p>
        <div className="w-3/4 bg-gray-800 flex items-center justify-evenly h-24 rounded-2xl p-6 ">
          <button onClick={handlePlayerX} className="focus:bg-gray-300 hover:bg-[#bcfefb] trasnsition duartion-300 ease-in flex items-center justify-center rounded-xl px-6 py-2 ">
            <XIcon />
          </button>
          <button onClick={handlePlayerO} className="focus:bg-gray-300 hover:bg-[#ffe1a9] trasnsition duartion-300 ease-in flex items-center justify-center rounded-xl px-6 py-2 " >
            <OIcon />
          </button>
        </div>
        <p className="text-md text-gray-500 uppercase font-semibold md:text-xl "> Remember: X goes first</p>
      </div>
      <button onClick={handleNewGame} className="button hover:ring-4 hover:ring-cyan-300 rounded-xl mt-8 px-6 py-3 bg-[#f3b236] hover:bg-[#30c4bd]">
        Start Game
      </button>
    </div>
  )
}

Enter fullscreen mode Exit fullscreen mode

In this component, what we will do here is that we have to create two buttons having Values "X" and "O".

So to provide the user that which icon the user wants to play with and then we have another button, whose function is to start the game based on the user's choice.

If the user chooses "X" , then the first player will use the "X" mark else it will use the "O" mark.

If the user doesn't choose any one of the icons, then the default icon will be the "X" mark, which means "X" goes first by default, until and unless the user selects "O" from the given choices.

As you can see in the above code, we are passing three props and all of them are functions.

  1. handlePlayerX()
  2. handlePlayerO()
  3. handleNewGame()

We will discuss the work of this function in step-3.

Inside Board.tsx

import React from 'react'
import { OIcon } from './OIcon'
import { XIcon } from './XIcon'

interface PlayerProp {
  winner:string,
  playerX: boolean,
  squares: Array<any>,
  handlePlayer(i: number): void,
  handleRestartGame(): void,
}

interface SquareProp {
  value: JSX.Element | string | null,
  onClick(): void,
}

export const Board = ({ winner, playerX, handlePlayer, handleRestartGame, squares }: PlayerProp) => {

  // Square Button and RenderSquare function
  function Square({ value, onClick }: SquareProp) {
    return (
      <button className="square" onClick={onClick} disabled={winner ? true :false} >
        {value}
      </button>
    )

  }

  function value(i:number){
     let value;
     if( squares[i] ==="X"){
       value=<XIcon />
     }else if( squares[i] === "O"){
        value=<OIcon />
     }else{
        value=null;
     }

     return value;

  }

  const renderSquare = (i: number) => {
    return <Square value={value(i)} onClick={() => handlePlayer(i)} />
  }

  return (
    <div>
      <div className="board">
        <div className=" w-[300px] md:[w-400px] rounded-lg flex items-center justify-center space-x-10">
          <div>
            {playerX
              ?
              <div className="text-white bg-gray-700 text-xl px-4 py-1 w-28 rounded-lg font-medium uppercase">
                <span className="text-[#30c4bd] text-2xl font-bold">
                  X
                </span>
                {" "}
                Turn
              </div>

              :
              <div className="text-white bg-gray-700 text-xl px-4 py-1 w-28 rounded-lg font-medium uppercase">
                <span className=" text-[#f3b236] text-2xl font-bold">
                  O
                </span>
                {" "}
                Turn
              </div>

            }

          </div>
          <button onClick={handleRestartGame} className="group button px-2 py-1 hover:ring-4 hover:ring-cyan-300 rounded-md bg-[#f3b236] hover:bg-[#30c4bd]" >
            <svg xmlns="http://www.w3.org/2000/svg" className="h-8 w-8 group-hover:rotate-180 transition duration-300 eas-in " fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="2">
              <path strokeLinecap="round" strokeLinejoin="round" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
            </svg>
          </button>
        </div>
        <div className="board-row">
          {renderSquare(0)}
          {renderSquare(1)}
          {renderSquare(2)}
        </div>

        <div className="board-row">
          {renderSquare(3)}
          {renderSquare(4)}
          {renderSquare(5)}
        </div>

        <div className="board-row">
          {renderSquare(6)}
          {renderSquare(7)}
          {renderSquare(8)}
        </div>
      </div>
    </div>
  )
}

Enter fullscreen mode Exit fullscreen mode

In this component, we will create the board for the game on which the user will play the game.

Here, the main aim is to style the board nicely using the Tailwind CSS, and along with that, we have two more things.

The first is an indicator div which tells the user whose turn is next to play (whether is "X" turns or "O" turns to play) and the other is also a button that can be used to clear/refresh the board to play the new game.

As you can see in the above code, we are passing five props.

  1. winner
  2. playerX
  3. squares
  4. handlePlayer()
  5. handleRestartGame()

We will discuss the work of the props and function in step-3.

Inside WinnerModal.tsx

In this component, we will create a modal, which will be displayed when the user won.

import React from 'react'
import { OIcon } from './OIcon';
import { XIcon } from './XIcon'

interface GameProps{
  winner:string
  handleQuitGame():void;
  handleNewGame():void;
}

export const WinnerModal = ({winner ,handleQuitGame , handleNewGame} :GameProps) => {
  return (
    <div className="bg-gray-900/90 z-10 min-h-screen w-full absolute top-0 left-0">
      <div className="w-[500px] h-[250px] rounded-xl bg-[#1f3540] space-y-10 px-6 py-4 mx-auto mt-52 flex items-center justify-center flex-col">
       <h2 className="flex flex-col items-center justify-center space-y-6 text-2xl md:text-4xl font-bold">
         {winner === "X" ? <XIcon /> : <OIcon />}
         <p className="uppercase text-[#30c4bd]">Takes the Round</p>
       </h2>

      <div className="flex items-center justify-center space-x-16">
        <button onClick={handleQuitGame} className="button px-4 rounded-md py-1 bg-[#a8bdc8] hover:bg-[#718087] hover:ring-4 hover:ring-gray-400">Quit</button>
        <button onClick={handleNewGame} className="button px-4 rounded-md py-1 bg-[#f3b236] hover:bg-[#30c4bd] hover:ring-4 hover:ring-cyan-300">Next Round</button>
      </div>
      </div>
    </div>
  )
}

Enter fullscreen mode Exit fullscreen mode

💡

Note: I have not added a modal for the Draw case. if you wish you can add it on your own as a challenge.

It's quite simple, you just have to create a DrawModal.tsx and display it if there is no winner, or else what you can do is create a prop as text which you will pass through the winner modal and the display the text inside the modal conditionally if there is a winner or not.

So these are the two ways you can do it. I want to give this as a challenge to you.

As you can see in the above code, we are passing three props.

  1. winner
  2. handleNewGame()
  3. handleQuitGame()

We will discuss the work of the props and function in step-3.

Step-3 Writing the Logic for the Game

After creating the components we will import them inside the index.tsx file in the Home Component.

But before that, we have to write the logic for our game.

Now, inside the Home component, we will first create three constants using the useState hook and a variable name winner, which will hold the value of the function calculate inner() which we will create later.

const [isX, setIsX] = useState<boolean>(true);
const [newGame, setNewGame] = useState<boolean>(false);
const [squares, setSqaures] = useState<Array<any>>(Array(9).fill(null));

let winner = calculateWinner(squares);

Now we will create the calculateWinner() function.

// Calculate the winner
  function calculateWinner(squares: Array<any>) {
    // Total 8 winning patterens
    const winningPatterns = [
      [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 < winningPatterns.length; i++) {
      const [a, b, c] = winningPatterns[i];

      if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
        return squares[a];
      }
    }
    return null;
  }

Enter fullscreen mode Exit fullscreen mode

In this function what we have done here is that first, we created an array name winningPatterns which contains all the possible winning combinations.

After that, we used for loop and then looped over the array winningPatterns and then inside that we checked for the condition that if the squares (containing the value either "X" or "O" ) at that given index is equal to the value that we clicked on the board.

If the condition is true, then we return the value that is inside the square at that given index.

If the condition is false then we return null.

Now, we will create six more functions, that we will have passed into the various component as props that we have created in step-2.

// handle Choose player
  function handlePlayerX() {
    setIsX(true);
  }

  function handlePlayerO() {
    setIsX(false);

  }

 //// It will Handle which Icon will appear on Board on clicking one of the Squares
  function handlePlayer(i: number) {

    if (calculateWinner(squares) || squares[i]) {
      return;
    }

    squares[i] = isX ? "X" : "O";
    setSqaures(squares);
    setIsX(!isX);
  }

  // It will handle the Restart of the Game 

  function handleRestartGame() {
    setIsX(true);
    setSqaures(Array(9).fill(null));
  }

  // It will handle the start Game when the player choose one of the Icon
  // with which they want to player
  function handleNewGame() {
    setIsX(true);
    setSqaures(Array(9).fill(null));
    setNewGame(true);
  };

Enter fullscreen mode Exit fullscreen mode

At last, we have finished writing the logic and creating the functions.

It's quite a long project, and writing it as a blog post was quite a time taking task.

All is left now to import all the components inside the index.tsx file and the project will be completed.

Step-4 Importing all the Components

Now, it is, time to import all the components inside the Home component, which is inside the index.tsx file.

Inside index.tsx

import type { NextPage } from 'next';
import Head from 'next/head';
import React, { useState } from 'react';
import { Board } from '../components/Board';
import { ChoosePlayer } from '../components/ChoosePlayer';
import { WinnerModal } from '../components/WinnerModal';

const Home: NextPage = () => {

  const [isX, setIsX] = useState<boolean>(true);
  const [newGame, setNewGame] = useState<boolean>(false);
  const [squares, setSqaures] = useState<Array<any>>(Array(9).fill(null));

  let winner = calculateWinner(squares);

  // handle Choose player
  function handlePlayerX() {
    setIsX(true);
  }

  function handlePlayerO() {
    setIsX(false);

  }

  //// It will Handle which Icon will apppear on Board on cliking one the Squares
  function handlePlayer(i: number) {

    if (calculateWinner(squares) || squares[i]) {
      return;
    }

    squares[i] = isX ? "X" : "O";
    setSqaures(squares);
    setIsX(!isX);
  }

  // It will handle the Restart of the Game 

  function handleRestartGame() {
    setIsX(true);
    setSqaures(Array(9).fill(null));
  }

  // It will handle the start Game when the player choose one of the Icon
  // with which they want to player
  function handleNewGame() {
    setIsX(true);
    setSqaures(Array(9).fill(null));
    setNewGame(true);
  };

  function handleQuitGame() {
    setIsX(true);
    setSqaures(Array(9).fill(null));
    setNewGame(false);
  }
  // Calculate the winner
  function calculateWinner(squares: Array<any>) {
    // Total 8 winning patterens
    const winningPatterns = [
      [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 < winningPatterns.length; i++) {
      const [a, b, c] = winningPatterns[i];

      if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
        return squares[a];
      }
    }
    return null;
  }

  return (
    <div className="flex min-h-screen bg-[#192a32] flex-col items-center py-2">
      <Head>
        <title>Tic-Tic-Toe Game</title>
        <link rel="icon" href="/favicon.ico" />
      </Head>

      <h1 className="text-4xl md:text-6xl font-extrabold mt-4 text-[#30c4bd] ">
        Tic
        {" "}
        <span className="text-[#f3b236]">Tac </span>
        {" "}
        Toe
      </h1>

      {!newGame
        ?
        <ChoosePlayer
          handleNewGame={handleNewGame}
          handlePlayerX={handlePlayerX}
          handlePlayerO={handlePlayerO}
        />
        :
        <Board
          winner={winner}
          playerX={isX}
          squares={squares}
          handlePlayer={handlePlayer}
          handleRestartGame={handleRestartGame}
        />
      }
      {winner && 
      <WinnerModal
          winner={winner}
          handleQuitGame={handleQuitGame}
          handleNewGame={handleNewGame}
        />
      }
    </div>
  )
}

export default Home

Enter fullscreen mode Exit fullscreen mode

The XIcon.tsx and the OIcon.tsx components will be imported inside the other three components, not inside the Home component.

The rest three components, ChoosePlayer.tsx , Board.tsx , and WinningModal.tsx will be imported and all the required props are needed to pass through them.

At last, we have finished the project 🎉🎉.

Now open the terminal inside the VsCode ad run the command npm run dev to start the development server and you will then see the application running on localhost:3000.

Conclusion

Hope you were able to build this amazing Tic-Tac-Toe Game. Feel free to follow me on Twitter and share this if you like this project 😉.

I hope you like this project and enjoyed building it, I would appreciate ✌️ it if you could share this blog post.

If you think that this was helpful and then please do consider visiting my blog website nextjsdev.com and do follow me on Twitter and connect with me on LinkedIn.

If you were stuck somewhere and not able to find the solution you can check out my completed Github Repo here.

Thanks for your time to read this project, if you like this please share it on Twitter and Facebook or any other social media and tag me there.

I will see you in my next blog ✌️. Till then take care and keep building projects.

Some Useful Link :

Next.js and Tailwind Installation Docs

Github link for the project

Connect with me:

Twitter Profile

LinkedIn Profile

GitHub Profile

Facebook Profile

Top comments (0)