<div id="tictactoe"></div>
  <footer>
    coded with
    <span class="love">&#9829;</span> by
    <a
      href="https://twitter.com/yagoestevez/"
      target="_blank"
      alt="about Yago Estévez"
      title="about Yago Estévez">Yago Estévez
    </a>
  </footer>
body {
  height            : 100vh;
  font-family       : Merriweather, sans-serif;
  background        : #226293;
  background        : -webkit-radial-gradient(center, ellipse cover, #226293 0%,#1b4f79 100%);
  background        :    -moz-radial-gradient(center, ellipse cover, #226293 0%, #1b4f79 100%);
  background        :         radial-gradient(ellipse at center, #226293 0%,#1b4f79 100%);
  filter            : progid:DXImageTransform.Microsoft.gradient(
                              startColorstr='#226293',
                              endColorstr='#1b4f79',
                              GradientType=1 
                              );
  color             : #eee;
  display           : flex;
  justify-content   : center;
  align-items       : center;
}

footer {
  position          : fixed;
  bottom            : 0;
  background        : #000;
  color             : #eee;
  height            : 1.4rem;
  width             : 100%;
  padding           : .2rem;
  text-align        : center;
  font-size         : .8rem;
  z-index           : 999;
}

  footer a:link, footer a:visited {
    text-decoration : none;
    color           : #eee;
    transition      : color 0.5s ease;
  }

  footer a:hover {
    color           : #fc0;
  }

  footer .love {
    color           : #b10;
    font-size       : 1rem;
    vertical-align  : baseline;
  }

/* App.css */
.App {
  text-align          : center;
  display             : flex;
  flex-direction      : column;
  opacity             : 1;
  -webkit-transition  : opacity 500ms ease-in;
     -moz-transition  : opacity 500ms ease-in;
       -o-transition  : opacity 500ms ease-in;
          transition  : opacity 500ms ease-in;
}

.App-hidden {
  text-align          : center;
  display             : flex;
  flex-direction      : column;
  opacity             : 0;
}

.Game {
  display             : flex;
  justify-content     : center;
  align-items         : center;
}

  #Menu.display {
    display             : flex;
    flex-direction      : column;
    justify-content     : center;
    align-items         : center;
    background-color    : #1b4f79;
    background-color    : #1b4f79;
     -webkit-box-shadow : 0 0 2rem #00000080;
        -moz-box-shadow : 0 0 2rem #00000080;
             box-shadow : 0 0 2rem #00000080;
    padding             : 5rem;
    width               : 25vh;
    height              : 25vh;
    border-radius       : 50%;
    border              : 6px solid #fc0;
    position            : absolute;
    text-align          : center;
    top                 : 50%;
    left                : 50%;
    opacity             : 1;
    -webkit-transform   : translate(-50%, -50%);
        -ms-transform   : translate(-50%, -50%);
            transform   : translate(-50%, -50%);
    -webkit-transition  : opacity 300ms ease-in 1.5s;
       -moz-transition  : opacity 300ms ease-in 1.5s;
         -o-transition  : opacity 300ms ease-in 1.5s;
            transition  : opacity 300ms ease-in 1.5s;
  }

    #Menu.hidden {
      z-index             : -100;
      opacity             : 0;
      -webkit-transition  : opacity 300ms ease-in;
         -moz-transition  : opacity 300ms ease-in;
           -o-transition  : opacity 300ms ease-in;
              transition  : opacity 300ms ease-in;
    }

    #Menu .title {
      font-family         : 'Indie Flower';
      color               : #fc0;
      margin-bottom       : 0;
    }

    #Menu hr {
      color               : #eee;
      margin-bottom       : 1rem;
      width               : 100%;
    }

    #Menu .menu-text {
      font-size           : 1rem;
      font-weight         : bold;
      margin              : .2rem 0rem;
    }

    #Menu .token {
      color               : #fc0;
      margin              : 1rem;
      font-size           : 1rem;
      cursor              : pointer;
    }

    #Menu button {
      background-color    : white;
      color               : #1b4f79;
      border              : none;
      width               : 3rem;
      height              : 3rem;
      margin              : 1rem .3rem;
      border-radius       : 50%;
      font-size           : 1.5rem;
      cursor              : pointer;
    }

table {
  width               : 50vh;
  height              : 50vh;
  border-collapse     : collapse;
}
td {
  text-align          : center; 
  vertical-align      : middle;
  border              : transparent;
}

@media (max-height: 600px) {
  #Menu .title {
    font-size         : 1.5rem;
    margin-bottom     : 0;
  }

  #Menu .menu-text {
    font-size         : .8rem;
    font-weight       : bold;
    margin            : .2rem 0rem;
  }

  #Menu button {
    width             : 2rem;
    height            : 2rem;
    margin            : .3rem .3rem;
    font-size         : 1rem;
  }
}

/* Grid.css */
#Grid {
  position  : absolute;
  width     : 50vh;
  height    : 50vh;
  z-index   : -500;
}

  #Grid .line {
    stroke              : #fc0;
    stroke-width        : 3px;
    stroke-dasharray    : 180px;
    stroke-dashoffset   : -180px;
    fill                : transparent;
  }

  #Grid .animate1 {
    stroke-dashoffset   : 0px;
    -webkit-transition  : stroke-dashoffset 200ms ease-in;
       -moz-transition  : stroke-dashoffset 200ms ease-in;
         -o-transition  : stroke-dashoffset 200ms ease-in;
            transition  : stroke-dashoffset 200ms ease-in;
  }
  
    #Grid .line1-shown {
      stroke-dashoffset   : 0px;
      -webkit-transition  : stroke-dashoffset 200ms ease-in 200ms;
        -moz-transition   : stroke-dashoffset 200ms ease-in 200ms;
          -o-transition   : stroke-dashoffset 200ms ease-in 200ms;
              transition  : stroke-dashoffset 200ms ease-in 200ms;
    }

  #Grid .animate2 {
    stroke-dashoffset   : 0px;
    -webkit-transition  : stroke-dashoffset 200ms ease-in 200ms;
       -moz-transition  : stroke-dashoffset 200ms ease-in 200ms;
         -o-transition  : stroke-dashoffset 200ms ease-in 200ms;
            transition  : stroke-dashoffset 200ms ease-in 200ms;
  }

  #Grid .animate3 {
    stroke-dashoffset   : 0px;
    -webkit-transition  : stroke-dashoffset 200ms ease-in 400ms;
       -moz-transition  : stroke-dashoffset 200ms ease-in 400ms;
         -o-transition  : stroke-dashoffset 200ms ease-in 400ms;
            transition  : stroke-dashoffset 200ms ease-in 400ms;
  }

  #Grid .animate4 {
    stroke-dashoffset   : 0px;
    -webkit-transition  : stroke-dashoffset 200ms ease-in 600ms;
       -moz-transition  : stroke-dashoffset 200ms ease-in 600ms;
         -o-transition  : stroke-dashoffset 200ms ease-in 600ms;
            transition  : stroke-dashoffset 200ms ease-in 600ms;
  }


/* Square.css */
.cross {
  stroke              : #fc0;
  stroke-width        : 6px;
  stroke-dasharray    : 226.274169921875px;
  stroke-dashoffset   : -226.274169921875px;
  fill                : transparent;
  transition          : stroke-dashoffset 0.3 linear;
}

.cross1-shown {
  stroke              : #fc0;
  stroke-width        : 6px;
  stroke-dasharray    : 226.274169921875px;
  stroke-dashoffset   : 0px;
  fill                : transparent;
  -webkit-transition  : stroke-dashoffset 200ms ease-in;
     -moz-transition  : stroke-dashoffset 200ms ease-in;
       -o-transition  : stroke-dashoffset 200ms ease-in;
          transition  : stroke-dashoffset 200ms ease-in;
}

.cross2-shown {
  stroke              : #fc0;
  stroke-width        : 6px;
  stroke-dasharray    : 226.274169921875px;
  stroke-dashoffset   : 0px;
  fill                : transparent;
  -webkit-transition  : stroke-dashoffset 200ms ease-in 200ms;
     -moz-transition  : stroke-dashoffset 200ms ease-in 200ms;
       -o-transition  : stroke-dashoffset 200ms ease-in 200ms;
          transition  : stroke-dashoffset 200ms ease-in 200ms;
}

.xWon1 {
  stroke              : #b10;
  stroke-width        : 6px;
  stroke-dasharray    : 226.274169921875px;
  stroke-dashoffset   : 0px;
  fill                : transparent;
  -webkit-transition  : all 500ms ease-in 100ms, stroke 500ms ease-in 1.1s;
     -moz-transition  : all 500ms ease-in 100ms, stroke 500ms ease-in 1.1s;
       -o-transition  : all 500ms ease-in 100ms, stroke 500ms ease-in 1.1s;
          transition  : all 500ms ease-in 100ms, stroke 500ms ease-in 1.1s;
}

.xWon2 {
  stroke              : #b10;
  stroke-width        : 6px;
  stroke-dasharray    : 226.274169921875px;
  stroke-dashoffset   : 0px;
  fill                : transparent;
  -webkit-transition  : all 500ms ease-in 600ms, stroke 500ms ease-in 1.1s;
     -moz-transition  : all 500ms ease-in 600ms, stroke 500ms ease-in 1.1s;
       -o-transition  : all 500ms ease-in 600ms, stroke 500ms ease-in 1.1s;
          transition  : all 500ms ease-in 600ms, stroke 500ms ease-in 1.1s;
}

.circle {
  stroke              : #fc0;
  stroke-width        : 6px;
  stroke-dasharray    : 263.93072509765625px;
  stroke-dashoffset   : -263.93072509765625px;
  fill                : transparent;
  opacity             : 0;
}

.circle-shown {
  stroke              : #fc0;
  stroke-width        : 6px;
  stroke-dasharray    : 263.93072509765625px;
  stroke-dashoffset   : 0px;
  fill                : transparent;
  -webkit-transition  : stroke-dashoffset 300ms ease-in 100ms;
     -moz-transition  : stroke-dashoffset 300ms ease-in 100ms;
       -o-transition  : stroke-dashoffset 300ms ease-in 100ms;
          transition  : stroke-dashoffset 300ms ease-in 100ms;
}

.oWon {
  stroke              : #b10;
  stroke-width        : 6px;
  stroke-dasharray    : 263.93072509765625px;
  stroke-dashoffset   : 0px;
  fill                : transparent;
  -webkit-transition  : stroke-dashoffset 800ms ease-in 600ms, stroke 500ms ease-in 1.2s;
     -moz-transition  : stroke-dashoffset 800ms ease-in 600ms, stroke 500ms ease-in 1.2s;
       -o-transition  : stroke-dashoffset 800ms ease-in 600ms, stroke 500ms ease-in 1.2s;
          transition  : stroke-dashoffset 800ms ease-in 600ms, stroke 500ms ease-in 1.2s;
}

.clickable {
  cursor  : pointer;
}
class App extends React.Component {
  /***********************************************************
    Initiates the state properties for the App component.
  ************************************************************/
  constructor (props) {
    super(props);
    this.state = {
      human: 'X',
      computer: 'O',
      gameStart: false,
      winner: null,
      winningSequence: [],
      board: [0, 1, 2, 3, 4, 5, 6, 7, 8],
      possibleWins: [
        [0, 1, 2], // Horizontals
        [0, 3, 6],
        [3, 4, 5],
        [1, 4, 7], // Verticals
        [6, 7, 8],
        [2, 5, 8],
        [0, 4, 8], // Diagonals
        [2, 4, 6]
      ],
      showMenu: true
    };
  }

  startGame = human => {
    const computer = human === 'X' ? 'O' : 'X';
    this.setState(
      {
        board: [0, 1, 2, 3, 4, 5, 6, 7, 8],
        winner: null,
        gameStart: true,
        showMenu: false,
        human: human,
        computer: computer
      },
      () => {
        if (human === 'O') this.humanMove(-1, computer);
      }
    );
  };

  /***********************************************************
    Checks if there's a winner, it's a tie or still playing.
  ************************************************************/
  gameOver = board => {
    const possibleWins = this.state.possibleWins;
    for (let i = 0; i < possibleWins.length; i++) {
      const [a, b, c] = possibleWins[i];
      if (board[a] && board[a] === board[b] && board[a] === board[c]) {
        // Returns the winner: X or O.
        this.setState({ winningSequence: possibleWins[i] });
        return board[a];
      }
    }
    if (board.filter(sqr => sqr !== 'X' && sqr !== 'O').length) {
      // Returns true when the game is not finished.
      return false;
    } else {
      // Returns false when it's a tie.
      return 'TIE';
    }
  };

  /***********************************************************
    Handles the click events from the squares of the board.
  ************************************************************/
  play = index => {
    if (this.state.winner || !this.state.gameStart) return false;
    this.humanMove(index, this.state.computer);
  };

  /***********************************************************
    Handles the player's turn. Then, launches the AI.
  ***********************************************************/
  humanMove = (index, computerToken) => {
    const newBoard = [...this.state.board];
    if (index !== -1) {
      if (isNaN(newBoard[index])) return;
      newBoard[index] = this.state.human;
      const computerThink = this.computerThink(newBoard, computerToken);
      newBoard[computerThink.index] = computerToken;
    } else {
      // Faking the first computer's move as it slows the 'thinking' down
      // quite much.
      const randomFirstMove = Math.floor(Math.random() * 9);
      newBoard[randomFirstMove] = computerToken;
    }

    const gameState = this.gameOver(newBoard);
    if (gameState === 'X' || gameState === 'O') {
      this.setState({
        winner: gameState,
        board: newBoard,
        showMenu: true
      });
    } else if (gameState === 'TIE') {
      this.setState({
        winner: 'TIE',
        board: newBoard,
        showMenu: true
      });
    } else {
      this.setState({
        board: newBoard
      });
    }
  };

  /***********************************************************
    Implementation of the MINIMAX algorithm.
    [https://en.wikipedia.org/wiki/Minimax]

    Simulates all possible movements against its opponent
    and returns the score and index of the best moves.
  ************************************************************/
  computerThink = (board, player) => {
    // Has the available empty squares on the board.
    const emptySquares = board.filter(sqr => sqr !== 'X' && sqr !== 'O');

    // Checks if the game is over and returns its final state.
    // Known as the leaves of the tree generated by the algorithm.
    if (this.gameOver(board) === this.state.human) {
      return { score: -10 };
    } else if (this.gameOver(board) === this.state.computer) {
      return { score: 10 };
    } else if (emptySquares.length === 0) {
      return { score: 0 };
    }

    // Holds each move with index and score from the empty squares.
    // E.g.: { index: '' , score: '' }
    let moves = [];

    // Loops through the empty squares array
    for (let i = 0; i < emptySquares.length; i++) {
      let move = {}; // Holds each index/score.
      move.index = board[emptySquares[i]]; // Holds the board's index.
      board[emptySquares[i]] = player; // Simulates a player's move.

      // Changes the player to continue the simulation and makes a recursive
      // call to this method (the MiniMax Algorithm itself).
      if (player === this.state.computer) {
        let newMove = this.computerThink(board, this.state.human);
        move.score = newMove.score;
      } else if (player === this.state.human) {
        var newMove = this.computerThink(board, this.state.computer);
        move.score = newMove.score;
      }

      // Empties the board for the next iteration
      board[emptySquares[i]] = move.index;

      // Includes the simulated move into the moves array.
      moves.push(move);
    }

    // Holds the bestMove, the one which scores the highest for the computer and
    // the lowest for the human.
    let bestMove;

    // Returns the MiniMax scores: 'max' for the computer;'min' for the human.
    if (player === this.state.computer) {
      let bestScore = -5000; // Sets a small enough score to compare.
      for (let i = 0; i < moves.length; i++) {
        if (moves[i].score > bestScore) {
          bestScore = moves[i].score;
          bestMove = i;
        }
      }
    } else if (player === this.state.human) {
      let bestScore = 5000; // Sets a big enough score to compare.
      for (let i = 0; i < moves.length; i++) {
        if (moves[i].score < bestScore) {
          bestScore = moves[i].score;
          bestMove = i;
        }
      }
    }

    // Gives back the best possible moves as an array.
    return moves[bestMove];
  };

  /***********************************************************
    Renders the component and its children into the index.js
  ************************************************************/
  render() {
    return (
      <React.Fragment>
        <Menu
          showMenu={this.state.showMenu}
          startGame={this.startGame}
          winner={this.state.winner}
          players={[this.state.human,this.state.computer]}
        />
        <div className="App">
          {/* "App-hidden" */}
          <section className="Game">
            <Board
              play={this.play}
              gameBoard={this.state.board}
              winner={this.state.winner}
              winningSeq={this.state.winningSequence}
            />
            <Grid didGameStart={this.state.gameStart} />
          </section>
        </div>
      </React.Fragment>
    );
  }
}

const Board = props => {
  const { play, gameBoard, winner, winningSeq } = props;

  const buildSquares = i => {
    return (
      <td>
        <Square
          index={i}
          play={play}
          board={gameBoard}
          winner={winner}
          winningSeq={winningSeq}
        />
      </td>
    );
  };

  return (
    <table>
      <tbody>
        <tr>
          {buildSquares(0)}
          {buildSquares(1)}
          {buildSquares(2)}
        </tr>
        <tr>
          {buildSquares(3)}
          {buildSquares(4)}
          {buildSquares(5)}
        </tr>
        <tr>
          {buildSquares(6)}
          {buildSquares(7)}
          {buildSquares(8)}
        </tr>
      </tbody>
    </table>
  );
};

const Square = props => {
  const { index, play, board, winner, winningSeq } = props;

  const xClasses = i => {
    return [
      board[index] === 'X' ? `cross${i}-shown` : `cross`,
      winner === 'X' && winningSeq.indexOf(index) !== -1 ? `xWon${i}` : '',
      'clickable'
    ].join(' ');
  }
  const oClasses = () => {
    return [
      board[index] === 'O' ? 'circle-shown' : 'circle',
      winner === 'O' && winningSeq.indexOf(index) !== -1 ? 'oWon' : '',
      'clickable'
    ].join(' ');
  }

  return (
    <svg viewBox="0 0 120 120" onClick={() => play(index)}>
      {/* THE X TOKEN SPLIT INTO TWO PATHS */}
      <g>
        <path
          id={`cross1${index}`}
          className={xClasses(1)}
          d="M 20,100 L 100,20"
        />
        <path
          id={`cross2${index}`}
          className={xClasses(2)}
          d="M 100,100 L 20,20"
        />
      </g>
      {/* THE O TOKEN */}
      <circle
        id={`circle${index}`}
        className={oClasses()}
        r={42}
        cy={60}
        cx={60}
        fill="transparent"
        strokeWidth={6}
      />
    </svg>
  );
};

const Grid = props => {
  const animate= i => ( props.didGameStart ? `line animate${i}` : `line` );
  return (
    <svg id="Grid" viewBox="0 0 180 180">
      <g>
        <path className={animate(1)} d="M 0,60 H 180" />
        <path className={animate(2)} d="M 180,120 H 0" />
        <path className={animate(3)} d="M 60,180 V 0" />
        <path className={animate(4)} d="M 120,0 V 180" />
      </g>
    </svg>
  );
};

const Menu = props => {
  const { showMenu, startGame, winner, players } = props;
  const [ human, computer ] = players;

  const buildMenu = () => {
    if (winner === human) {
      return (
        <section id="Menu" className={showMenu ? 'display' : 'display hidden'}>
          <h1 className="title">Congrats! You made it!!</h1>
          <hr />
          <div>
            <p className="menu-text">Wanna try again?</p>
            <button onClick={token => startGame('X')}>X</button>
            <button onClick={token => startGame('O')}>O</button>
          </div>
        </section>
      );
    } else if (winner === computer) {
      return (
        <section id="Menu" className={showMenu ? 'display' : 'display hidden'}>
          <h1 className="title">Oops...Sorry!!</h1>
          <hr />
          <div>
            <p className="menu-text">Want another chance?</p>
            <button onClick={token => startGame('X')}>X</button>
            <button onClick={token => startGame('O')}>O</button>
          </div>
        </section>
      );
    } else if ( winner === 'TIE') {
      return (
        <section id="Menu" className={showMenu ? 'display' : 'display hidden'}>
          <h1 className="title">It was close!</h1>
          <hr />
          <div>
            <p className="menu-text">Wanna do better?</p>
            <button onClick={token => startGame('X')}>X</button>
            <button onClick={token => startGame('O')}>O</button>
          </div>
        </section>
      );
    } else {
      return (
        <section id="Menu" className={showMenu ? 'display' : 'display hidden'}>
          <h1 className="title">Let's play!</h1>
          <hr />
          <div>
            <button onClick={token => startGame('X')}>X</button>
            <button onClick={token => startGame('O')}>O</button>
          </div>
        </section>
      );
    }
  };

  return buildMenu();
};

ReactDOM.render(<App />, document.getElementById('tictactoe'));
View Compiled
Run Pen

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://cdnjs.cloudflare.com/ajax/libs/react/16.2.0/umd/react.development.js
  2. https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.2.0/umd/react-dom.development.js