See all posts

Tic Tac Toe EP 1: UI/UX

Tic Tac Toe EP 1: UI/UX

This is a 3 parts serious we will cover from smooth UI/UX, undo step, different ways to build AI, from easy to unbeatable. Complete source code at GitHub, if it help you place a stars.

Introduction

In part 1, we will focuse on creating a smooth and intutive UI/UX with React & Styled Componets, Game Logic with Redux, to create a maintainable and highly reuseable Player vs Player Tic Tac Toe.


Final Product in Part 1

Tic Tac Toe Gameplay

UI/UX

To simplfy the problem, we will need:

  1. Gameboard

    • 3x3 grid for with pawn border
  2. Cell

    • while player is onClick to the cell, X or O will shown
  3. Game Logic

    • Use redux to handle all the game logic

Coding time

Game board

We will use table for create 3x3 grid, and use styled components to add css style.

GameBoard
import styled from "styled-components"
 
const Board = styled.table`
  animation: ${clipMask} 1s linear;
  border-collapse: collapse;
  position: relative;
`
const Row = styled.tr`
  &:first-child td {
    border-top: 0;
  }
  &:last-child td {
    border-bottom: 0;
  }
  & td:first-child {
    border-left: 0;
  }
  & td:last-child {
    border-right: 0;
  }
`
 
function GameBoard() {
  return (
    <Board>
      <tbody>
        {[...Array(3)].map((_, rowIndex) => (
          <Row key={rowIndex}>
            {[...Array(3)].map((_, colIndex) => {
              const cellIndex = rowIndex * 3 + colIndex
              return <div>{/* Cell */}</div>
            })}
          </Row>
        ))}
      </tbody>
    </Board>
  )
}
 
export default GameBoard

Cell

Cell while player is onClick to the cell, X or O will shown

import styled from "styled-components"
 
import Circle from "./Circle"
import Cross from "./Cross"
 
type CellProps = {
  cellIndex: number
  value: Marks | null
}
 
const StyledCell = styled.td`
  border: 6px solid var(--background-color-darker);
  height: 125px;
  width: 125px;
  text-align: center;
  vertical-align: middle;
  padding: 15px;
  cursor: pointer;
  padding: 0.3em 0.3em;
`
function Cell({ cellIndex, value }: CellProps) {
  const onClickHandler = (index: number) => {
    if (value !== null) {
      // only empty cell allow to place new mark
      return
    }
    console.log(`placed a mark at ${index}`)
  }
  return (
    <StyledCell onClick={() => onClickHandler(cellIndex)}>
      {value === null ? " " : value === "X" ? <Cross /> : <Circle />}
    </StyledCell>
  )
}
 
export default Cell

Marks

We also need to prepare the X and the O.

Cross
// Cross.tsx
import styled, { keyframes } from "styled-components"
 
type Props = { width?: number; height?: number }
 
const crossAnimation = keyframes`
  to {
    stroke-dashoffset: 0;
  }
`
const StyledCross = styled.svg`
  fill: none;
  stroke: var(--cross-color);
  stroke-width: 8px;
  stroke-linecap: round;
  stroke-dasharray: 1000;
  stroke-dashoffset: 1000;
  transform: rotate(90deg);
  & g line:nth-child(1) {
    animation: ${crossAnimation} 0.3s ease-in forwards;
  }
  & g line:nth-child(2) {
    animation: ${crossAnimation} 400ms ease-out 200ms forwards;
  }
`
 
function Cross({ width = 88, height = 88 }: Props) {
  return (
    <StyledCross
      version="1.1"
      xmlns="http://www.w3.org/2000/svg"
      x="0px"
      y="0px"
      width={`${width}px`}
      height={`${height}px`}
      viewBox="0 0 88 88"
    >
      <g>
        <line className="cross" x1="3.18" y1="3.18" x2="83.18" y2="83.18" />
        <line className="cross" x1="3.18" y1="83.18" x2="83.18" y2="3.18" />
      </g>
    </StyledCross>
  )
}
 
export default Cross
Circle Mark
// Circle.tsx
import styled, { keyframes } from "styled-components"
 
type CircleProps = { width?: number; height?: number }
 
const circleAnimation = keyframes`
  to {
    stroke-dashoffset: 0;
  }
`
const StyledCircleSVG = styled.svg`
  fill: none;
  stroke: var(--circle-color);
  stroke-width: 7px;
  stroke-linecap: round;
  stroke-dasharray: 1000;
  stroke-dashoffset: 1000;
  transform: scaleX(-1) rotate(-90deg);
 
  animation-iteration-count: 1;
  animation: ${circleAnimation} 0.5s ease-in forwards;
`
 
function Circle({ width = 88, height = 88 }: CircleProps) {
  return (
    <StyledCircleSVG
      version="1.1"
      xmlns="http://www.w3.org/2000/svg"
      x="0px"
      y="0px"
      width={`${width}px`}
      height={`${height}px`}
      viewBox="0 0 88 88"
    >
      <circle cx="44" cy="44" r="40" />
    </StyledCircleSVG>
  )
}
 
export default Circle

Game Flowchart

react game flowchart

we are going to preare the reducers based on the game flowchart.

  1. editBoard(mark: "X"|"O", cellIndex: number)
  2. check winner
  3. nextPlayer
  4. restartNewGame

First we create a slice ticTacToeSlice

import { createSlice, type PayloadAction } from "@reduxjs/toolkit"
import _ from "lodash"
 
import type { AppThunk, RootState } from "../../app/store"
 
export type LineType = "horizontal" | "vertical" | "diagonal"
 
export type Cell = Marks | null
 
export type Marks = "X" | "O"
 
export type GameResult =
  | { winner: Marks; lineType: LineType; linePosition: number }
  | "TIE"
  | null
 
export interface GameState {
  board: Cell[]
  currentRound: Marks
  result: GameResult
}
 
// Define the initial state using that type
const initialState: GameState = {
  board: Array(9).fill(null),
  currentRound: "X",
  result: null,
}
 
export const ticTacToeSlice = createSlice({
  name: "ticTacToe",
  initialState,
  reducers: {
    // 1. editBoard(mark: "X"|"O", cellIndex: number)
    // 2. check winner
    // 3. nextPlayer
    // 4. restartNewGame
  },
})
 
export const { editBoard, nextPlayer, checkWinner, newGame } =
  ticTacToeSlice.actions
export const ticTacToeSlice = createSlice({
  name: "ticTacToe",
  initialState,
  reducers: {
    initialGame: (state) => {
      state.board = initialState.board
      state.currentRound = initialState.currentRound
      state.result = initialState.result
    },
    newGame: (state) => {
      state.board = initialState.board
      state.currentRound = initialState.currentRound
      state.result = initialState.result
    },
    editBoard: (
      state,
      { payload }: PayloadAction<{ player: Marks; cellIndex: number }>
    ) => {
      if (state.board[payload.cellIndex] !== null) return
 
      state.board = state.board.map((cell, index) =>
        index === payload.cellIndex ? payload.player : cell
      )
    },
    nextPlayer: (state) => {
      state.currentRound = state.currentRound === "X" ? "O" : "X"
    },
    checkWinner: (state) => {
      state.result = checkForWinner(state.board)
    },
  },
})

CheckWinner

check winner for the tic tac toe is pretty easy, since there is only 8 Form of win combination. So we will first define those combination as winCombination. We will iterate all the combination, to check is there any match from the gameboard. if true: we find the winner, if no empty cell: "Tie", otherwise, no winner is found.

// ticTacToe Reducers ...
 
const winCombination = [
  [0, 1, 2],
  [3, 4, 5],
  [6, 7, 8],
  [0, 3, 6],
  [1, 4, 7],
  [2, 5, 8],
  [0, 4, 8],
  [2, 4, 6],
]
 
function checkForWinner(board: Cell[]): GameResult {
  for (let i = 0; i < winCombination.length; i++) {
    const [a, b, c] = winCombination[i]
    if (board[a] !== null && board[a] === board[b] && board[a] === board[c]) {
      return {
        winner: board[a] as Marks,
        ...caclDrawingPosition(i, winCombination[i]),
      }
    }
  }
  if (!board.includes(null)) return "TIE" // 0 indicates a draw
  return null
}

Draw the winning line

react game flowchart

Once we found that there is a winner, we daaw the line. The First 3 combination in winCombination [0, 1, 2], [3, 4, 5], [6, 7, 8]are horizontal.

The 4-6 combination in winCombination [0, 3, 6],[1, 4, 7],[2, 5, 8] are vertical.

The last 2 combination in winCombination [0, 4, 8],[2, 4, 6] are diagonal

So we First define a type of line horizontal | vertical | diagonal.

const caclDrawingPosition = (
  i: number,
  combine: number[]
): { lineType: LineType; linePosition: number } => {
  const [a] = combine
 
  if (i < 3) {
    return {
      lineType: "horizontal",
      linePosition: Math.floor(a / 3) + 1,
    }
  } else if (i < 6) {
    return {
      lineType: "vertical",
      linePosition: (a % 3) + 1,
    }
  } else {
    return {
      lineType: "diagonal",
      linePosition: i === 6 ? 1 : 2,
    }
  }
}

Final Product

Tic tac toe Gameplay

Project Files

For more information about how to use the plugin and the features it includes, GitHub Repository for Part 1.


Ready to Lesson 2

In Lesson 2, we are going to create a Tic tac toe A.I. from easy -> medium -> Unbeatable. And we also will talk about the algorithm behine it. Tic-Tac-Toe-Lesson 2