Tic-Tac-Toe
May 6, 2021ā¢721 words
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>
)
}