DEV Community

Cover image for Tic Tac then Toe
Luc van Kerkvoort
Luc van Kerkvoort

Posted on • Updated on

Tic Tac then Toe

Hi everyone,

continuing on from the last article, read it here
I'll be continuing with a second coding exercise I got during one of my interviews.

TicTacToe

I was asked by one of my interviewers if I could build Tic Tac Toe. The challenging part of this exercise was that you only had 20 min to finish the coding exercise.

Just in case if someone doesn't know the rules of TicTacToe, it's a roster or matrix of 3*3 where players take turn writing either an X or O in a square. The winner is determined by there indicator (X or O) being in 3 consecutive squares, either horizontal, vertical or diagonal.

For practice purposes I have written the logic in both React as well as Vanilla JS since some interviewers will only allow Vanilla.

React Approach

The beauty of React is that in an interview you can sprinkle a lot of your knowledge in your approach. By making decisions on design patterns (e.g. using hooks or HOC's) you can showcase how you would interact with a bigger application and how you would solve issues for performance, scalability and readability.

These are things I often take into consideration and I verbalize during my interviews so the interviewer knows my intentions, understands my logic and decisions and sees that I'm thinking ahead by making performant and modular components.

Logic

I tend to use helper functions or hooks to create modular components in React. This also helps with separating concerns between logic an the UI.

My approach with TicTacToe is that the logic of the game is build into a hook that gives access to certain parts of the logic by using encapsulation. So the UI component doesn't get polluted with code, improving the readability.

import { useState } from "react";

const Logic = () => {
  const [board, setBoard] = useState(new Array(9).fill(null));
  const [isPlayerX, setPlayer] = useState("X");
  const [winner, setWinner] = useState(null);

  const move = (i) => {
    const copyBoard = [...board];

    if (copyBoard[i] || winner) return;

    copyBoard[i] = isPlayerX ? "X" : "O";
    setPlayer(!isPlayerX);
    hasAWinner(copyBoard);
    setBoard(copyBoard);
  };

  const hasAWinner = (square) => {
    const lines = [
      [0, 1, 2],
      [3, 4, 5],
      [6, 7, 8],
      [0, 4, 8],
      [2, 4, 6],
      [0, 3, 6],
      [1, 4, 7],
      [2, 5, 8],
    ];

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

      if (square[a] && square[a] === square[b] && square[a] === square[c]) {
        return setWinner(square[a]);
      }
    }
  };

  return {
    board,
    move,
    winner,
  };
};

export default Logic;

Enter fullscreen mode Exit fullscreen mode

This logic component has 4 parts:

  • The Board
  • A function that makes a move on the board
  • A function to check whether there is a winner
  • The winner that is being returned.

The Board

The board is a visual representation of the actual TicTacToe board. I've created an array with a length of 9 representing the 3*3 matrix. The initial value is set to null as to keep the squares empty. We return this state and use it to create the UI.

The Move

The move is a function that deals with the actual actions that are being played out on the field.

const move = (i) => {
    const copyBoard = [...board];

    if (copyBoard[i] || winner) return;

    copyBoard[i] = isPlayerX ? "X" : "O";
    setPlayer(!isPlayerX);
    hasAWinner(copyBoard);
    setBoard(copyBoard);
  };

Enter fullscreen mode Exit fullscreen mode

we start of by creating a copy of the board and use that to do all the actions we need to before setting the state. The process of using a copy of the state and not the actual state is called immutable.

    const copyBoard = [...board];
Enter fullscreen mode Exit fullscreen mode

Then we check the copy if the square we clicked already has a value or there already is a winner. if so we stop the function and move on.

if (copyBoard[i] || winner) return;
Enter fullscreen mode Exit fullscreen mode

if the square is empty we fill it with the current player, this is decided by a ternary operator.

then we set the board to be the copy of the board we just adjusted and check if there is a winner

    copyBoard[i] = isPlayerX ? "X" : "O";
    setPlayer(!isPlayerX);
    hasAWinner(copyBoard);
Enter fullscreen mode Exit fullscreen mode

HasAWinner

the has a winner is the part of the application with the most intricate logic for this application.

const hasAWinner = (square) => {
    const lines = [
      [0, 1, 2],
      [3, 4, 5],
      [6, 7, 8],
      [0, 4, 8],
      [2, 4, 6],
      [0, 3, 6],
      [1, 4, 7],
      [2, 5, 8],
    ];

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

      if (square[a] && square[a] === square[b] && square[a] === square[c]) {
        return setWinner(square[a]);
      }
    }
  };
Enter fullscreen mode Exit fullscreen mode

In this a representation of all the combinations for winning are stored inside a matrix (array of arrays). the numbers inside the matrix represent the indices of the squares in the board array.

const lines = [
      [0, 1, 2],
      [3, 4, 5],
      [6, 7, 8],
      [0, 4, 8],
      [2, 4, 6],
      [0, 3, 6],
      [1, 4, 7],
      [2, 5, 8],
    ];
Enter fullscreen mode Exit fullscreen mode

Then we use a for loop to loop over all the possible combinations and check the board if any of those equate to a single value.

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

      if (square[a] && square[a] === square[b] && square[a] === square[c]) {
        return setWinner(square[a]);
      }
    }
Enter fullscreen mode Exit fullscreen mode

the logic makes sense in my head when I start filling it in.
we de-structure lines[i] lets say i is 0 in this case, meaning lines[0] = [0,1,2]. we can then say a = 0 b = 1 c = 2

then we move to the if statement. if on the board (square) 0 has a value, square[0] has the same value as square[1] and square[0] has the same value as square[2] meaning the top row of the TicTacToe board, then we return setting the winner to the value of square[0].

We loop through all the combinations: rows, columns and diagonals. If none of them are good we continue to game.

The Winner

the winner starts of as a null value and will only receive a value if hasAWinner returns one.

UI

The UI for this application is very straightforward. Once again I used styled components to create some CSS. In this case it wasn't necessary but since I was making use of the library I might as well use it.

import React from "react";
import Logic from "./utils";
import { Tile, Board } from "./styles";

const TicTacToe = () => {
  const { board, move, winner } = Logic();

  return (
    <div>
      <h1
        style={{
          textAlign: "center",
        }}
      >
        {winner && `The winner is: ${winner}`}
      </h1>
      <Board>
        {board.map((item, i) => (
          <Tile onClick={() => move(i)}>{item}</Tile>
        ))}
      </Board>
    </div>
  );
};

export default TicTacToe;

Enter fullscreen mode Exit fullscreen mode

There is a simple breakdown for the UI here. It is a parent element (the div) that wraps both the Winner component (h1) and the Board component.

we use the hook we just created to setup the board by mapping over it and returning a Tile component with an onClick function that uses the move we created and i as in the index of the element clicking it (which is represented in the board). and we set the inner html to the value of the index, which starts off as null.

by clicking any of the squares we set the value to either X or O (starting with X) in the copyBoard and set the new board which then gets mapped over, completing the cycle until we have a winner.

CSS

The best implementation for the CSS on this application is the use of a grid. that way we can set the grid to represent our 3*3 and we don't have to worry about responsive behavior or creating difficult patterns of div's with child components to create the matrix.

import styled from "styled-components";

export const Tile = styled.div`
  border: 1px solid black;
  display: flex;
  justify-content: center;
  align-items: center;
`;

export const Board = styled.div`
  margin: 50px auto;
  display: grid;
  height: 500px;
  width: 500px;
  grid-template: repeat(3, 1fr) / repeat(3, 1fr);
`;
Enter fullscreen mode Exit fullscreen mode

JavaScript Approach

The Vanilla approach is in a lot of ways very similar to the React approach. The biggest difference lies in the way we have to represent the data using vanilla js. We can't use a single board and map over it that will be reactive on every click and will automatically reload.

We would have to find the correct element and set its inner html and make changes that way.

Here is a solution to the Vanilla approach. Yours might be different and I encourage you to share your approach as well as it may help others.

<!DOCTYPE html>
<html lang="en">
  <head>
    <link rel="stylesheet" href="./styles.css" />
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Tic Tac Toe</title>
  </head>
  <body>
    <div id="root"></div>

    <script>
      const root = document.getElementById("root");
      let winner = null;
      let board = new Array(9).fill("");
      let isX = true;

      const hasAWinner = (square) => {
        const lines = [
          [0, 1, 2],
          [3, 4, 5],
          [6, 7, 8],
          [0, 4, 8],
          [2, 4, 6],
          [0, 3, 6],
          [1, 4, 7],
          [2, 5, 8],
        ];

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

          if (square[a] && square[a] === square[b] && square[a] === square[c]) {
            const winnerParagraph = document.createElement("p");
            winnerParagraph.innerHTML = `The winner is ${square[a]}`;
            root.append(winnerParagraph);

            return (winner = square[a]);
          }
        }
      };

      const move = (index) => {
        const square = document.getElementById(index);

        if (!!square.innerHTML || winner) return;

        board[index] = isX ? "X" : "O";
        square.innerHTML = isX ? "X" : "O";
        isX = !isX;
        hasAWinner(board);
      };

      board.map((item, i) => {
        const square = document.createElement("div");
        square.classList.add("square");
        square.id = i;
        square.innerHTML = item;
        square.addEventListener("click", () => move(i));

        root.append(square);
      });
    </script>
  </body>
</html>

Enter fullscreen mode Exit fullscreen mode

From this solution there are a couple of things standing out:

  • The move function has altered
  • we map through the board and create our elements on the fly after page loads.
  • the hasAWinner function has altered

but for the main parts they are very similar.
lets go over the changes:

Mapping over the board

to initialize our board we have to loop over the board and create it on the fly, and append it to the root div. That way we start of the page with a board

board.map((item, i) => {
        const square = document.createElement("div");
        square.classList.add("square");
        square.id = i;
        square.innerHTML = item;
        square.addEventListener("click", () => move(i));

        root.append(square);
      });
Enter fullscreen mode Exit fullscreen mode

The Move

after the board has initialized we know the ID for is the same as the index. Therefore we can look up the element by index.

const move = (index) => {
        const square = document.getElementById(index);

        if (!!square.innerHTML || winner) return;

        board[index] = isX ? "X" : "O";
        square.innerHTML = isX ? "X" : "O";
        isX = !isX;
        hasAWinner(board);
      };

Enter fullscreen mode Exit fullscreen mode

after the lookup we check the values like before only now we check !!square.innerHTML

Now this is where it gets a little tricky, we still have our data structure representing our board, but if we change the values of the board it will not be represented on the page. so we also have to set the innerHTML to the same value.

The reason we want our board to still represent it is because we need it to check the winner. You could loop over all the squares and check every squares value but that adds complexity that I deemed unnecessary in this case.

hasAWinner

This function has just one thing added to it. We create a winner paragraph on the fly instead of setting the variable which is represented in the React application

if (square[a] && square[a] === square[b] && square[a] === square[c]) {
            const winnerParagraph = document.createElement("p");
            winnerParagraph.innerHTML = `The winner is ${square[a]}`;
            root.append(winnerParagraph);

            return (winner = square[a]);
          }
        }
Enter fullscreen mode Exit fullscreen mode

Thank You

Thank you again for joining and reading my article. I really appreciate any feedback that you guys can give me. Once again I hope this helps you in your journey as it did for me.

I'll be dropping the next part to this series tomorrow which will be going over my implementation of Rock Paper Scissors

Top comments (0)