Tic-Tac-Toe

The game Tic-Tac-Toe, as a React, TypeScript and Styled-Components app
I built it during a coding interview
For a live demo, see tic-tac-toe.as93.net

index.tsx

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById('root')
);

global.css

html {
  height: 100%;
}

body {
  background: #e2e1e0;
  height: 90%;
  display: flex;
  flex-flow: column;
  align-items: center;
  justify-content: center;
  font-family: 'Hack', 'Courier New', monospace;
}

App.tsx

import React from 'react';
import styled from 'styled-components';

import Grid from './components/Grid';
import './styles/global.css';

const PageWrapper = styled.main`
  background: #fff;
  box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24);
  width: fit-content;
  margin: 0 auto;
  padding: 1rem 2rem;
  border-radius: 4px;
  text-align: center;
`;

const Title = styled.h1`
  font-family: 'Hack', 'Courier New', monospace;
  font-size: 2rem;
  margin: 0 0 0.5rem;
  font-weight: 100;
`;

function App() {
  return (
    <PageWrapper className="App">
      <Title>The Game</Title>
      <Grid gridSize={3} />
    </PageWrapper>
  );
}

export default App;

GameModels.ts


export type iGrid = iCell[][];

export enum iCell { null, X, O };

export enum Player {
  Player1 = iCell.X,
  Player2 = iCell.O,
};

export enum GameState {
  Playing,
  Draw,
  Won,
};

Grid.tsx

import React, { FC, useState } from "react";
import styled from 'styled-components';

import Cell from './Cell';
import { iGrid, iCell, GameState, Player } from '../models/GameModels';

export type GridProps = {
  gridSize: number;
};

const Button = styled.button`
  background: #00CCB4;
  color: white;
  border-radius: 4px;
  padding: 0.25rem 0.5rem;
  cursor: pointer;
  font-size: 1rem;
  border: 1px solid #03ae9a;
  &:hover {
    background: #03ae9a;
  }
`;

/* Board Grid Component */
const Grid: FC<GridProps> = (props: GridProps) => {

  const { gridSize } = props;

  const GridWrapper = styled.div`
    display: grid;
    grid-template: repeat(${gridSize}, 1fr) / repeat(${gridSize}, 1fr);
    border: 1px solid #00CCB4;
    border-radius: 4px;
  `;

  /* Returns an empty Grid array */
  const generateCellData = (rows: number = 3, cols: number = 3) =>
    [...Array(rows)].map(x => [...Array(cols)].map(x => iCell.null));

  /* Define game state */
  const [gameState, setGameState] = useState<GameState>(GameState.Playing);
  const [currentPlayer, setCurrentPlayer] = useState<Player>(1);
  const [gameData, setGameData] = useState<iGrid>(generateCellData(gridSize, gridSize));

  const togglePlayer = () => {
    setCurrentPlayer(currentPlayer === 1 ? 2 : 1);
  }

  const checkIfDraw = () =>
    gameData.every((row) => row.every((cell) => cell > 0));

  const restartGame = () => {
    setGameState(GameState.Playing);
    setGameData(generateCellData(gridSize, gridSize));
    setCurrentPlayer(1);
  }

  const checkIfGameWon = (player: Player) => {
    const checkRow = (player: Player) => {
      let counter = 0;
      gameData.forEach((row) => {
        if (counter < gridSize) {
          row.forEach((cell) => counter = (cell as number === player as number) ? counter += 1 : 0);
        }
      });
      return counter >= gridSize;
    }
    const checkCol = (player: Player) => {
      let counter = 0;
      for (let i = 0; i < gridSize; i++) {
        if (counter < gridSize) {
          gameData.forEach((row) => {
            counter = (row[i] as number === player as number) ? counter += 1 : 0;
          });
        }
      }
      return counter >= gridSize;
    }
    return (checkRow(player) || checkCol(player));
  }


  const cellClicked = (x: number, y: number) => {
    if (gameData[x][y] !== iCell.null) return;
    let gameDataCopy = [...gameData];
    gameDataCopy[x][y] = currentPlayer === Player.Player1 ? iCell.X : iCell.O;
    setGameData(gameDataCopy);
    if (checkIfGameWon(currentPlayer)) {
      setGameState(GameState.Won);
    } else if (checkIfDraw()) {
      setGameState(GameState.Draw);
    } else {
      togglePlayer()
    }
  }

  return (
    <>
      <GridWrapper>
        {gameData.map((rows, rowIndex) => (
          rows.map((cell: iCell, colIndex: number) => (
            <Cell
              key={`${rowIndex}-${colIndex}`}
              cellValue={cell}
              clickFunction={cellClicked}
              coordinates={{ x: rowIndex, y: colIndex }}
              gameOver={gameState !== GameState.Playing}
            />
          ))
        ))}
      </GridWrapper>
      <p>
        {gameState === GameState.Playing && `Player ${currentPlayer}'s Turn`}
        {gameState === GameState.Won && `Player ${currentPlayer} Won the Game!`}
        {gameState === GameState.Draw && `It's a Draw!`}
      </p>
      <Button onClick={restartGame}>Restart Game</Button>
    </>
  );
};

export default Grid;

Cell.tsx

import React from 'react';
import styled from 'styled-components';

import { iCell } from '../models/GameModels';

const CellWrapper = styled.button`
  width: 5rem;
  height: 5rem;
  font-size: 3rem;
  border: 1px solid #00CCB4;
  background: #fff;
  cursor: pointer;
  &:hover {
    background: #00ccb41f;
  }
  &:disabled {
    pointer-events: none;
  }
`;

type CellProps = {
  cellValue: iCell,
  coordinates: { x: number, y: number },
  clickFunction: (x: number, y: number) => void,
  gameOver: boolean,
};

const getCellValue = (cellValue: iCell) => {
  switch (cellValue) {
    case iCell.X: return 'āŒ';
    case iCell.O: return 'ā­•';
    default: return '';
  }
}

export default function Cell(props: CellProps) {
  const { cellValue, clickFunction, coordinates, gameOver } = props;
  return (
    <CellWrapper
      disabled={gameOver}
      onClick={() => clickFunction(coordinates.x, coordinates.y)}>
      {getCellValue(cellValue)}
    </CellWrapper>
  )
}