Day 1: Building a Customizable Tic Tac Toe with Socket.io - Set Your Own Board Size and Play Online!

Tanmay BhansaliTanmay Bhansali
6 min read

Welcome to Day 1 of building customizable Tic Tac Toe game! Today, I’m focusing on setting up the basics with an Pass & Play mode, where players can select their own board size and play on the same device. My goal for Day 1 is to create a smooth, flexible gaming experience that lets users play with different grid sizes, moving beyond the classic 3x3 setup to something even more challenging, like a 5x5 board. By the end of this post, you’ll see how I built a fully functional, Pass & Play version of the game using JavaScript and React. In Day 2, I’ll take things to the next level by adding Connect & Play with Socket.io!

This is the folder structure of the src directory in this Tic Tac Toe React app, designed to keep the project modular, organized, and scalable.

  • Components: This folder contains the building blocks of the UI, like reusable buttons, grid cells, and other elements that appear throughout the game. By organizing components here, I can easily reuse them across different parts of the app, keeping the code DRY (Don't Repeat Yourself).

  • hooks: This folder stores custom React hooks, which help handle specific logic or state management separately from the main components. By managing complex logic with hooks, I can keep the main components focused on rendering and UI, while hooks take care of specific behavior or side effects, like managing the game state.

  • layout: The layout folder is where layout components reside, such as RootLayout and any structural components that give shape to the app. These elements ensure that each page has a consistent look and feel.

  • Page: Each main screen or view of the app, such as the home screen or game board, can be found in the Page folder. By storing larger, page-specific components here, it keeps them separate from smaller, reusable components, making it easier to work on individual pages without interference.

  • store: This folder is dedicated to global state management, using tools like Redux. The centralized store allows data and state to be easily shared across the app, helping manage the game’s state efficiently.

  • utils: Utility functions or helper methods are organized in utils. These includes mathematical functions, constants, and other logic needed across the app. This folder makes it easy to access shared functions without duplicating code.

Each folder in src serves a specific purpose, keeping the app’s structure clear and organized as the project grows. This setup makes development faster and maintenance easier, as each part of the app is logically separated and easy to navigate.

import { useMemo, useState } from "react"
import intializeBoard from "../utils/initializeBoard"
import generateWinningPatterns from "../utils/generateWinningPatterns"

function useTicTacToe(boardSize){
    const [board,setBoard]=useState(intializeBoard(boardSize))
    const [isX,setIsX]=useState(true)

    const winningPatterns=useMemo(()=>generateWinningPatterns(boardSize),[])

    function calculateWinner(){
        for(let i=0;i<winningPatterns.length;i++){
            let winnerFlag=true
            for(let j=0;j<winningPatterns[i].length-1;j++){
                if(!board[winningPatterns[i][j]]){
                    winnerFlag=false
                    break
                }
                if(board[winningPatterns[i][j]]!==board[winningPatterns[i][j+1]]){
                    winnerFlag=false
                    break
                }
            }
            if(winnerFlag)return board[winningPatterns[i][0]]
        }
        return null
    }

    function statusMessage(){
        const winner=calculateWinner()
        if(winner)return `Player ${winner} is winner`
        if(!board.includes(null))return "Game is draw"
        return `Player ${isX ? "X" : "O"} turn`
    }

    function handleClick(index){
        if(calculateWinner())return
        if(board[index])return
        const newBoard=[...board]
        newBoard[index]=isX ? "X" : "O"
        setBoard(newBoard)
        setIsX(!isX)
    }

    return {board,statusMessage,handleClick}
}

export default useTicTacToe

The useTicTacToe hook is designed to manage the state and logic of Tic Tac Toe game. Here's a breakdown of its functionality:

  1. State Initialization:

    • board holds the current state of the game (array of X, O, or null).

    • isX indicates whose turn it is (true for X, false for O).

  2. Winning Patterns:

    • winningPatterns is generated using generateWinningPatterns, which defines the possible ways to win based on the board size.
  3. calculateWinner Function:

    • This checks if there’s a winner by iterating through the winningPatterns. If all positions in a pattern are filled with the same player's mark (X or O), it returns the winner. If not, it returns null.
  4. statusMessage Function:

    • This function generates a message based on the game's state. It announces the winner, indicates a draw if the board is full, or prompts the current player’s turn.
  5. handleClick Function:

    • This function handles player moves. If the game is over or the clicked cell is already filled, it returns early. Otherwise, it updates the board and toggles the turn between "X" and "O".

Overall, this hook encapsulates the game logic and state management for Tic Tac Toe game.

function generateWinningPatterns(boardSize){
    const winningPattern=[]
    let count=0
    let tempSize=boardSize

    for(let i=0;i<boardSize;i++){

        let j=tempSize-boardSize
        const pattern1Arr=[]
        const pattern2Arr=[]

        while(j<tempSize){           
            pattern1Arr.push(j)
            j++
        }

        let k=count

        while(k<=(boardSize*boardSize+count)-boardSize){
            pattern2Arr.push(k)
            k+=boardSize
        }

        winningPattern.push(pattern1Arr)
        winningPattern.push(pattern2Arr)
        tempSize+=boardSize

        if(count>=2){
            count++
            continue
        }

        const pattern3Arr=[]
        let val=count ? 0 : boardSize-1
        let valToInc=count ? boardSize+1 : boardSize-1 
        let m=0

        while(m<boardSize){
            pattern3Arr.push(val)
            val+=valToInc
            m++
        }

        winningPattern.push(pattern3Arr)
        count++
    }
    return winningPattern   
}

export default generateWinningPatterns

This function, generateWinningPatterns, creates winning patterns for a tic-tac-toe board of variable size. Here’s how it works:

  1. Initialize Variables: winningPattern holds all winning combinations, count tracks the columns and diagonal entries, and tempSize increments to determine row boundaries.

  2. Horizontal Pattern: pattern1Arr stores indexes for each row. Starts at j = tempSize - boardSize, increments j to collect indexes across a row.

  3. Vertical Pattern: pattern2Arr stores column indexes. Starts at k = count, increments by boardSize for column cells. Both pattern1Arr and pattern2Arr are saved to winningPattern.

  4. Diagonal Patterns (pattern3Arr): Diagonal patterns are handled by count. Only two diagonals exist, so pattern3Arr is populated for two cases:

    • From top-left to bottom-right with increment by boardSize + 1.

    • From top-right to bottom-left with increment by boardSize - 1.

  5. Count Management: The count variable increments columns in vertical patterns (e.g., [0,3,6], [1,4,7]) and limits diagonal generation to two patterns.

This approach ensures all horizontal, vertical, and diagonal patterns are generated dynamically based on boardSize.

import { useSelector } from "react-redux"
import useTicTacToe from "../hooks/useTicTacToe"
import "./gameBoardPage.css"

function GameBoardPage(){
    const {boardSize}=useSelector(store=>store.gameOptionReducer)
    const {statusMessage,handleClick,board}=useTicTacToe(boardSize)

    return(
        <div>
            <p>{statusMessage()}</p>
            <div style={{gridTemplateColumns:`repeat(${boardSize},50px)`}} className="grid-container">
            {board.map((content,index)=><button key={index} onClick={()=>handleClick(index)}>{content}</button>)}
            </div>
        </div>
    )
}

export default GameBoardPage

The GameBoardPage functional component renders a dynamic tic-tac-toe board based on the selected boardSize. Here’s a breakdown:

  1. State and Actions: It uses useSelector to get boardSize from the Redux store (gameOptionReducer). It also uses useTicTacToe, a custom hook, to manage game state, providing statusMessage, handleClick, and board.

  2. Status Message: statusMessage() displays game updates like the current player's turn, win status or draw.

  3. Dynamic Grid: The gridTemplateColumns property is set to a repeat pattern based on boardSize, defining a flexible grid layout for the board, with each cell set to 50x50 pixels.

  4. Board Mapping: The board array, containing the state of each cell (e.g., 'X' or 'O'), maps to individual buttons. Each button triggers handleClick with its index, handling cell clicks and updates.

Overall, GameBoardPage dynamically renders the board and handles user interactions, making it scalable for any board size.

CONCLUSION:

By the end of Day 1, you’ll have a customizable, Pass & Play tic-tac-toe game with the freedom to choose board sizes that best suit your playstyle—from classic 3x3 games to more challenging grids like 5x5. This foundation will set me up perfectly for Day 2, where I’ll enhance the experience with real-time online play using Socket.io, allowing players to challenge friends remotely. Let’s dive into creating an engaging, flexible tic-tac-toe game!

10
Subscribe to my newsletter

Read articles from Tanmay Bhansali directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Tanmay Bhansali
Tanmay Bhansali