Develop an Unbeatable Tic-Tac-Toe AI Using React
Tags: javascript, webdev, react, tutorial
Published: 2022-04-06 16:45:41

## Introduction
Do you love Tic-Tac-Toe? Want to show off your React skills and swindle fools who think they can beat your AI? If you answered yes to any of these questions, you've come to the right place! Today we are building an unbeatable tic-tac-toe game.

**Check out the finished demo below!**

{% codesandbox https://codesandbox.io/embed/tic-tac-toe-design-euumb?fontsize=14&hidenavigation=1&theme=dark %}

## Prerequisites
- Basic CSS, HTML, and JavaScript knowledge
- Knowledge of React and hooks.

## Dependencies
- <a href="https://reactjs.org/" target="blank" rel="noreferrer">React</a> - JavaScript framework for building the UI.
- <a href="https://tailwindcss.com/" target="blank" rel="noreferrer">Tailwind CSS</a> - A utility-first css library for styling components.
- <a href="https://fonts.google.com/specimen/Open+Sans" target="blank" rel="noreferrer">Open Sans</a> - UI font

## Building the UI
Below is the boilerplate for the board and UI:

![First tic-tac-toe UI iterration](https://media.graphcms.com/xDtUw3X5QPSunI0G7x9z)

```javascript 
import React from "react";
import "./styles.css";

export default function App() {

  const Square = (props) => {
    return (
      <div
        className="shadow-md h-24 w-24 rounded-lg bg-white text-7xl text-center cursor-default font-light flex items center justify-center x-player"
      >
        X
      </div>
    );
  };

  return (
    <>
      <div className="text-center py-2 shadow-sm text-gray-400 z-50 sticky">
        Your Turn
      </div>
      <section className="game-board py-10">
        <div className="max-w-md mx-auto">
          <div className="max-w-lg flex flex-col gap-5 mx-auto">
            <div className="flex gap-5 mx-auto">
              <Square squareIndex={0} />
              <Square squareIndex={1} />
              <Square squareIndex={2} />
            </div>
            <div className="flex gap-5 mx-auto">
              <Square squareIndex={3} />
              <Square squareIndex={4} />
              <Square squareIndex={5} />
            </div>
            <div className="flex gap-5 mx-auto">
              <Square squareIndex={6} />
              <Square squareIndex={7} />
              <Square squareIndex={8} />
            </div>
          </div>

          <div className="text-center">
            <button className="bg-blue-500 text-white w-full py-2 font-semibold mt-10 rounded-md shadow-lg">
              Reset
            </button>
          </div>
        </div>
      </section>
    </>
  );
}
```

```css
html,
body {
  font-family: "Open Sans", sans-serif;
  height: 100%;
  background-color: #f9fafb;
}

.game-board {
  font-family: "Open Sans", sans-serif;
}

.shadow-md {
  box-shadow: rgba(7, 65, 210, 0.1) 0px 9px 30px !important;
}

.o-player {
  background: #cb6893;
  background: -webkit-linear-gradient(to right, #cb6893 0%, #f6d9d7 100%);
  background: -moz-linear-gradient(to right, #cb6893 0%, #f6d9d7 100%);
  background: linear-gradient(to right, #cb6893 0%, #f6d9d7 100%);
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
}

.x-player {
  background: #746dd0;
  background: -webkit-linear-gradient(to right, #746dd0 0%, #c4e1eb 100%);
  background: -moz-linear-gradient(to right, #746dd0 0%, #c4e1eb 100%);
  background: linear-gradient(to right, #746dd0 0%, #c4e1eb 100%);
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
}

.x-winner {
  text-shadow: 0 0 10px #746dd0, 0 0 0px #746dd0, 0 0 40px #746dd0,
    0 0 2px #746dd0;
}

.o-winner {
  text-shadow: 0 0 10px #ff9bc6, 0 0 0px #ff9bc6, 0 0 40px #ff9bc6,
    0 0 2px #ff9bc6;
}
```

## Build Game Logic
Let's start writing game logic; a board that does nothing isn't much fun! 

The game flows as follows:

<ol>
  <li>Player clicks a `Square`. If space is empty fill with X, else go to step 1.</li>
  <li>Check if game won or draw.</li>
  <li>AI fills empty space with O.</li>
 <li>Check if game won or draw.</li>
 <li>Go to step 1.</li>
</ol>

### Types for representing State
Imagine having a state called `gameWon` represented with a `boolean` for `true` or `false`. Soon after, you add a game draw condition and another `boolean` and logic. A week later, you're adding a `gameOvertime` condition and writing more logic. See how this can become a problem? 

Using primitive data types like `integers` or `booleans` to represent state is flaky, limited, and riddles code with `if/else` statements! Using enums or objects/types is a far better alternative.

Below is the above scenario, but represented with an object:

```javascript
const GAME_WON = {
  YES: 'game_won_yes',
  NO: 'game_won_no',
  DRAW: 'game_draw',
  OVERTIME: 'game_overtime',
}
```

As a result, we can easily add new states into the `GAME_WON` type and cut down on redundant logic.

### Game State
Defining a game state type and hook based on the game flow is easy.

```javascript
const GAME_STATE = {
  PLAYER_TURN: "player_turn",
  AI_TURN: "ai_turn",
  PLAYER_WON: "player_won",
  AI_WON: "player_o_won",
  DRAW: "game_draw",
  ERROR: "game_error"
};

// Current game state
const [gameState, setGameState] = useState(GAME_STATE.PLAYER_TURN);
```

### Game Board
The board represents an array with a length of nine that corresponds to each `Square`. Each `Square` can either be empty or filled by the player or AI. To easily represent the state of a `Square`, we will create a type to represent who owns it. The `createEmptyGrid` function returns an array filled with `SPACE_STATE.EMPTY`.

```javascript
export const GRID_LENGTH = 9;

export const SPACE_STATE = {
  PLAYER: "player_filled",
  AI: "ai_filled",
  EMPTY: "empty_space"
};

const createEmptyGrid = () => {
  return Array(GRID_LENGTH).fill(SPACE_STATE.EMPTY);
};

const [grid, setGrid] = useState(createEmptyGrid());
```

### Move Count
Tracking the number of moves taken is vital for determining a draw. AI logic also depends on the move count to formulate the best strategy.

```javascript
  // Count of moves made
  const [moveCount, setMoveCount] = useState(0);
```

### Handling Player Clicks
In the JSX, each `Square` has an index passed as a prop that corresponds to a grid index.

```javascript
...
<Square squareIndex={0} />
<Square squareIndex={1} />
<Square squareIndex={2} />
...
```

Inside the `Square` function, an `onClick` handler pulls the `squareIndex` from its props to call `handlePlayerClick` to fill in the corresponding `grid` index with `SPACE_STATE.PLAYER`. After filling the player's `Square`, the function fills the correct symbol with `getSquareSymbol` then updates the `gameState` to `GAME_STATE.AI_TURN`.

Because the AI and player's symbols have different colors, we introduce the `getSpaceStateClass` function to get the correct CSS class names.
 
```javascript
// Get the correct space class names
const getSpaceStateClass = (spaceState) => {
let space = "";

if (spaceState === SPACE_STATE.AI) {
  return "o-player";
}

if (spaceState === SPACE_STATE.PLAYER) {
  return "x-player";
}

return "";
};

const getSquareSymbol = (spaceStatus) => {
  switch (spaceStatus) {
    case SPACE_STATE.PLAYER: {
      return "X";
    }
    case SPACE_STATE.AI: {
      return "O";
    }
    case SPACE_STATE.EMPTY: {
      return "";
    }
    default: {
      return "";
    }
  }
};

// Fill in a grid square with status
const fillGridSpace = (gridIndex, spaceStatus) => {
    setGrid((oldGrid) => {
    oldGrid[gridIndex] = spaceStatus;
    return [...oldGrid];
  });
};

// Fill in the grid array with the player space state.
const handlePlayerClick = (gridIndex) => {
  // If not the player turn, then exit.
  if (gameState !== GAME_STATE.PLAYER_TURN) {
    return;
  }
   
  // If the current square is empty, then fill in space.
  if (grid[gridIndex] === SPACE_STATE.EMPTY) {
     // Fill grid space
      fillGridSpace(gridIndex, SPACE_STATE.PLAYER);
      // Update game state to AI's turn.
      setGameState(GAME_STATE.AI_TURN);
      // Update move count
      setMoveCount((oldMoves) => {
        return oldMoves + 1;
      });
  }
};

 const Square = (props) => {
    return (
      <div
        className="shadow-md h-24 w-24 rounded-lg bg-white text-7xl text-center cursor-default font-light flex items-center justify-center "
        // Connect click listener
        onClick={() => {
           handlePlayerClick(props.squareIndex);
        }}
      >
        // Get square symbol
        {getSquareSymbol(grid[props.squareIndex])}
      </div>
    );
  };
```

## Writing the AI Logic
For the AI, the <a href="https://en.wikipedia.org/wiki/Tic-tac-toe" target="_blank" rel="noreferrer">Tic-tac-toe Wikipedia</a> details a strategy to get a perfect game meaning each game is a draw or a win.

<ol>
    <li><b>Win</b>: If the player has two in a row, they can place a third to get three in a row.</li>
    <li><b>Block</b>: If the opponent has two in a row, the player must play the third themselves to block the opponent.
    </li>
    <li><b>Fork</b>: Cause a scenario where the player has two ways to win (two non-blocked lines of 2).</li>
    <li><b>Blocking an opponent's fork</b>: If there is only one possible fork for the opponent, the player should block
        it. 
Otherwise, the player should block all forks in any way that simultaneously allows them to make two in a
        row. Otherwise, the player should make a two in a row to force the opponent into defending, as long as it does
        not result in them producing a fork. For example, if "X" has two opposite corners and "O" has the center, "O"
        must not play a corner move to win. (Playing a corner move in this scenario produces a fork for "X" to win.)
    </li>
    <li><b>Center</b>: A player marks the center. (If it is the first move of the game, playing a corner move gives the
        second player more opportunities to make a mistake and may therefore be the better choice; however, it makes no
        difference between perfect players.)
    </li>
    <li><b>Opposite corner</b>: If the opponent is in the corner, the player plays the opposite corner.</li>
    <li><b>Empty corner</b>: The player plays in a corner square.</li>
    <li><b>Empty side</b>: The player plays in a middle square on any of the four sides.</li>
</ol>

The `calculateAITurn` function uses the strategy above to determine the best `Square` to fill to achieve a perfect game.

```javascript
import { SPACE_STATE } from "./App";

// Calculate the best space for the AI to fill to get a perfect game.
export const calculateAITurn = (grid, moveCount) => {
  let aiSpace = aiCanWin(grid);

  if (Number.isInteger(aiSpace)) {
    console.log("Ai winning");
    return aiSpace;
  }

  aiSpace = aiCanBlock(grid);

  if (Number.isInteger(aiSpace)) {
    console.log("Ai blocking");
    return aiSpace;
  }

  aiSpace = aiCanBlockFork(grid, moveCount);

  if (Number.isInteger(aiSpace)) {
    console.log("AI forking");
    return aiSpace;
  }

  aiSpace = aiCanCenter(grid);

  if (Number.isInteger(aiSpace)) {
    console.log("AI centering");
    return aiSpace;
  }

  aiSpace = aiCanFillOppositeCorner(grid);

  if (Number.isInteger(aiSpace)) {
    console.log("AI filling opposite corner");
    return aiSpace;
  }

  aiSpace = aiCanFillEmptyCorner(grid);

  if (Number.isInteger(aiSpace)) {
    console.log("AI filling empty corner");
    return aiSpace;
  }

  aiSpace = aiCanFillEmptySide(grid);

  if (Number.isInteger(aiSpace)) {
    console.log("AI filling empty side");
    return aiSpace;
  }

  // console.log("AI can't move");
  return null;
};

// Convert row, col to grid index.
const convertCordToIndex = (row, col) => {
  return row * 3 + col;
};
/**
 * Check if AI can win
 * @returns Space for AI to win
 */
const aiCanWin = (grid) => {
  let count = 0;
  let row, col;

  // Check Rows
  for (let i = 0; i < 3; ++i) {
    count = 0;

    for (let j = 0; j < 3; ++j) {
      if (grid[convertCordToIndex(i, j)] === SPACE_STATE.AI) {
        count++;
      } else if (grid[convertCordToIndex(i, j)] === SPACE_STATE.PLAYER) {
        count--;
      } else if (grid[convertCordToIndex(i, j)] === SPACE_STATE.EMPTY) {
        row = i;
        col = j;
      }
    }

    // Has two consecutive spaces, return third to win.
    if (count === 2) {
      return convertCordToIndex(row, col);
    }
  }

  // Check Cols
  for (let i = 0; i < 3; ++i) {
    count = 0;

    for (let j = 0; j < 3; ++j) {
      if (grid[convertCordToIndex(j, i)] === SPACE_STATE.AI) {
        count++;
      } else if (grid[convertCordToIndex(j, i)] === SPACE_STATE.PLAYER) {
        count--;
      } else if (grid[convertCordToIndex(j, i)] === SPACE_STATE.EMPTY) {
        row = j;
        col = i;
      }
    }

    // Has two consecutive spaces, return third to win.
    if (count === 2) {
      return convertCordToIndex(row, col);
    }
  }

  count = 0;

  // Check Diag
  for (let i = 0; i < 3; ++i) {
    if (grid[convertCordToIndex(i, i)] === SPACE_STATE.AI) {
      count++;
    } else if (grid[convertCordToIndex(i, i)] === SPACE_STATE.PLAYER) {
      count--;
    } else if (grid[convertCordToIndex(i, i)] === SPACE_STATE.EMPTY) {
      row = i;
      col = i;
    }
  }

  // Has two consecutive spaces, return third to win.
  if (count === 2) {
    return convertCordToIndex(row, col);
  }

  count = 0;

  // Check Anti-Diag
  for (var i = 0; i < 3; ++i) {
    if (grid[convertCordToIndex(i, 3 - 1 - i)] === SPACE_STATE.AI) {
      count++;
    } else if (grid[convertCordToIndex(i, 3 - 1 - i)] === SPACE_STATE.PLAYER) {
      count--;
    } else if (grid[convertCordToIndex(i, 3 - 1 - i)] === SPACE_STATE.EMPTY) {
      row = i;
      col = 3 - 1 - i;
    }
  }

  // Has two consecutive spaces, return third to win.
  if (count === 2) {
    return convertCordToIndex(row, col);
  }

  return null;
};

/**
 * Ai checks if it can block opponents win
 * @returns Can ai block opponent
 */
function aiCanBlock(grid) {
  var count = 0;
  var row, col;

  // Check Rows
  for (let i = 0; i < 3; ++i) {
    count = 0;

    for (let j = 0; j < 3; ++j) {
      if (grid[convertCordToIndex(i, j)] === SPACE_STATE.PLAYER) {
        count++;
      } else if (grid[convertCordToIndex(i, j)] === SPACE_STATE.AI) {
        count--;
      } else if (grid[convertCordToIndex(i, j)] === SPACE_STATE.EMPTY) {
        row = i;
        col = j;
      }
    }

    // Opponent two consecutive spaces, return third to block.
    if (count === 2) {
      return convertCordToIndex(row, col);
    }
  }

  // Check Cols
  for (let i = 0; i < 3; ++i) {
    count = 0;

    for (let j = 0; j < 3; ++j) {
      if (grid[convertCordToIndex(j, i)] === SPACE_STATE.PLAYER) {
        count++;
      } else if (grid[convertCordToIndex(j, i)] === SPACE_STATE.AI) {
        count--;
      } else if (grid[convertCordToIndex(j, i)] === SPACE_STATE.EMPTY) {
        row = j;
        col = i;
      }
    }

    // Opponent two consecutive spaces, return third to block.
    if (count === 2) {
      return convertCordToIndex(row, col);
    }
  }

  count = 0;

  // Check Diag
  for (let i = 0; i < 3; ++i) {
    if (grid[convertCordToIndex(i, i)] === SPACE_STATE.PLAYER) {
      count++;
    } else if (grid[convertCordToIndex(i, i)] === SPACE_STATE.AI) {
      count--;
    } else if (grid[convertCordToIndex(i, i)] === SPACE_STATE.EMPTY) {
      row = i;
      col = i;
    }
  }

  // Opponent two consecutive spaces, return third to block.
  if (count === 2) {
    return convertCordToIndex(row, col);
  }

  count = 0;

  // Check Anti-Diag
  for (let i = 0; i < 3; ++i) {
    if (grid[convertCordToIndex(i, 3 - 1 - i)] === SPACE_STATE.PLAYER) {
      count++;
    } else if (grid[convertCordToIndex(i, 3 - 1 - i)] === SPACE_STATE.AI) {
      count--;
    } else if (grid[convertCordToIndex(i, 3 - 1 - i)] === SPACE_STATE.EMPTY) {
      row = i;
      col = 3 - 1 - i;
    }
  }

  // Opponent two consecutive spaces, return third to block.
  if (count === 2) {
    return convertCordToIndex(row, col);
  }

  return null;
}

/**
 * Ai checks if it can block a fork
 * @returns Can ai block opponent
 */
function aiCanBlockFork(grid, moveCount) {
  if (moveCount === 3) {
    if (
      grid[convertCordToIndex(0, 0)] === SPACE_STATE.PLAYER &&
      grid[convertCordToIndex(1, 1)] === SPACE_STATE.AI &&
      grid[convertCordToIndex(2, 2)] === SPACE_STATE.PLAYER
    ) {
      aiCanFillEmptySide(grid);
      return true;
    }
    if (
      grid[convertCordToIndex(2, 0)] === SPACE_STATE.PLAYER &&
      grid[convertCordToIndex(1, 1)] === SPACE_STATE.AI &&
      grid[convertCordToIndex(0, 2)] === SPACE_STATE.PLAYER
    ) {
      aiCanFillEmptySide(grid);
      return true;
    }
    if (
      grid[convertCordToIndex(2, 1)] === SPACE_STATE.PLAYER &&
      grid[convertCordToIndex(1, 2)] === SPACE_STATE.PLAYER
    ) {
      return convertCordToIndex(2, 2);
    }
  }

  return null;
}

/**
 * Ai checks if it can fill center square
 * @returns Can ai fill center square
 */
function aiCanCenter(grid) {
  if (grid[convertCordToIndex(1, 1)] === SPACE_STATE.EMPTY) {
    return convertCordToIndex(1, 1);
  }
  return false;
}

/**
 * Ai checks if it can fill opposite corner
 * @returns Can ai fill opposite corner
 */
function aiCanFillOppositeCorner(grid) {
  if (
    grid[convertCordToIndex(0, 0)] === SPACE_STATE.PLAYER &&
    grid[convertCordToIndex(2, 2)] === SPACE_STATE.EMPTY
  ) {
    return convertCordToIndex(2, 2);
  }

  if (
    grid[convertCordToIndex(2, 2)] === SPACE_STATE.PLAYER &&
    grid[convertCordToIndex(0, 0)] === SPACE_STATE.EMPTY
  ) {
    return convertCordToIndex(0, 0);
  }

  if (
    grid[convertCordToIndex(0, 2)] === SPACE_STATE.PLAYER &&
    grid[convertCordToIndex(2, 0)] === SPACE_STATE.EMPTY
  ) {
    return convertCordToIndex(2, 0);
  }

  if (
    grid[convertCordToIndex(2, 0)] === SPACE_STATE.PLAYER &&
    grid[convertCordToIndex(0, 2)] === SPACE_STATE.EMPTY
  ) {
    return convertCordToIndex(0, 2);
  }

  return null;
}

/**
 * Ai checks if it can fill empty corner
 * @returns Can ai fill empty corner
 */
function aiCanFillEmptyCorner(grid) {
  if (grid[convertCordToIndex(0, 0)] === SPACE_STATE.EMPTY) {
    return convertCordToIndex(0, 0);
  }

  if (grid[convertCordToIndex(0, 2)] === SPACE_STATE.EMPTY) {
    return convertCordToIndex(0, 2);
  }

  if (grid[convertCordToIndex(2, 0)] === SPACE_STATE.EMPTY) {
    return convertCordToIndex(2, 0);
  }

  if (grid[convertCordToIndex(2, 2)] === SPACE_STATE.EMPTY) {
    return convertCordToIndex(2, 2);
  }

  return null;
}

/**
 * Ai checks if it can fill empty side
 * @returns Can ai fill empty side
 */
function aiCanFillEmptySide(grid) {
  if (grid[convertCordToIndex(0, 1)] === SPACE_STATE.EMPTY) {
    return convertCordToIndex(0, 1);
  }

  if (grid[convertCordToIndex(1, 0)] === SPACE_STATE.EMPTY) {
    return convertCordToIndex(1, 0);
  }

  if (grid[convertCordToIndex(1, 2)] === SPACE_STATE.EMPTY) {
    return convertCordToIndex(1, 2);
  }

  if (grid[convertCordToIndex(2, 1)] === SPACE_STATE.EMPTY) {
    return convertCordToIndex(2, 1);
  }

  return null;
}
```

### Checking for a Winner
A draw or winner is checked after every turn. Counting the move count against the maximum moves determines if the game is drawn. 

For a winner, a check is made for three consecutive filled horizontal, vertical, or diagonal squares by either the player or AI. The 3-indexes required for a win are defined as a 2d-array then compared against the `grid`.

```javascript
const MAX_MOVES = 10;

const isDraw = (moveCount) => {
  return moveCount === MAX_MOVES;
};

const checkWinner = (grid, moveCount) => {
  const winnerSpaces = [
    [0, 1, 2],
    [3, 4, 5],
    [6, 7, 8],
    [0, 3, 6],
    [1, 4, 7],
    [2, 5, 8],
    [0, 4, 8],
    [2, 4, 6]
  ];

  if (isDraw(moveCount)) {
    return {
      winner: GAME_STATE.DRAW,
      winSpaces: []
    };
  }

  for (let i = 0; i < winnerSpaces.length; i++) {
    const [a, b, c] = winnerSpaces[i];

    if (
      grid[a] === SPACE_STATE.EMPTY &&
      grid[b] === SPACE_STATE.EMPTY &&
      grid[c] === SPACE_STATE.EMPTY
    ) {
      continue;
    }

    if (grid[a] && grid[a] === grid[b] && grid[a] === grid[c]) {
      let winner = null;

      if (grid[a] === SPACE_STATE.PLAYER) {
        winner = GAME_STATE.PLAYER_WON;
      } else {
        winner = GAME_STATE.AI_WON;
      }

      return {
        winner: winner,
        winSpaces: [a, b, c]
      };
    }
  }

  return null;
};
```

### Game Loop
The `useEffect` hook is responsible for game flow. You control when this hook runs by providing a dependency that tells it to re-run each time the dependency changes. The `gameState` variable is the perfect dependency, as each game action updates it, allowing the game to flow smoothly.

```javascript
useEffect(() => {
  ...
  // I need to re-run on gameState change.
  }, [gameState]);
```

After each turn, `useEffect` checks for a winner, calculates the AI's turn, checks for a winner again, then changes the `gameState` to `GAME_STATE.PLAYER_TURN` and waits to repeat the loop.

```javascript
// Spaces used to get a win
const [winSpaces, setWinSpaces] = useState([]);

useEffect(() => {
  // Player took turn and changed game state,
  // check for a winner.
  let winner = checkWinner(grid, moveCount);

  // If the someone won, update state to reflect and set winner spaces.
  if (winner) {
    setGameState(winner.winner);
    setWinSpaces(winner.winSpaces);
  }

  // Run AI turn
  if (gameState === GAME_STATE.AI_TURN && moveCount < 10) {
    const aiSpace = calculateAITurn(grid, moveCount);
    setMoveCount((oldMoves) => {
      return oldMoves + 1;
  });

    fillGridSpace(aiSpace, SPACE_STATE.AI);
    winner = checkWinner(grid, moveCount);
  }

  // If AI won, update state to reflect, else
  // go back to player turn.
  if (winner) {
    setGameState(winner.winner);
    setWinSpaces(winner.winSpaces);
  } else {
    setGameState(GAME_STATE.PLAYER_TURN);
  }

  // I need to re-run on gameState change.
}, [gameState]);
```

## Highlighting Winner Spaces
We track winner spaces, modifying the `getSpaceStateClass` function to account for the `gameState` and `winSpaces` when determining the CSS class names is an easy change.

```javascript
const getSpaceStateClass = (spaceState, gameState, winSpaces, spaceIndex) => {
  let space = "";

  if (spaceState === SPACE_STATE.AI) {
    space += "o-player";

    if (gameState === GAME_STATE.AI_WON && winSpaces.includes(spaceIndex)) {
      space += " o-winner";
    }
  }

  if (spaceState === SPACE_STATE.PLAYER) {
    space += "x-player";

    if (gameState === GAME_STATE.PLAYER_WON && winSpaces.includes(spaceIndex)) {
      space += " x-winner";
    }
  }

  return space;
};
```

## Resetting
Having to refresh the browser every time you want to restart the game is irritating. So we create a `reset` function that resets all state variables to their default values.

```javascript
// Reset state to default values
const reset = () => {
  setGrid(createEmptyGrid());
  setGameState(GAME_STATE.PLAYER_TURN);
  setMoveCount(0);
  setWinSpaces([]);
};

<button
  className="bg-blue-500 text-white w-full py-2 font-semibold mt-10 rounded-md shadow-lg"
  onClick={() => {
    reset();
  }}
>
  Reset
</button>
```

## Conclusion
This unbeatable playable tic-tac-toe game was super fun to implement and made me think about:
- Using types to represent state.
- Creating an AI using a strategy.
- Utilizing `useEffect` for game flow.

I hope you learned as much as I did! Now swindle money from bets you know you'll win (I take a 15% cut naturally 😉). If you're successful, let me know in the comments below.

Consider signing up for my <a href="https://gregorygaines.substack.com/subscribe">newsletter</a> or <a href="https://www.gregorygaines.com/support/">supporting me</a> if this was helpful. Thanks for reading!