Tic-Tac-Toe is a great project for beginners who want to learn how to build games. It’s simple to understand but gives you the chance to learn about game state, player turns, winning logic, and user input.

In this tutorial, you’ll learn how to build tic-tac-toe using Phaser.js, a fast, fun, and open source framework for making 2D games in the browser.

If you’re new to Phaser.js, don’t worry. We’ll walk through everything step-by-step. By the end, you’ll have a working game that you can play, share, or build upon.

You can play the game here to get a feel of what you are going to build.

Table of Contents

What is Phaser.js?

Phaser.js is a free and open-source JavaScript game framework. It helps developers create HTML5 games that work across web browsers. Phaser handles things like rendering graphics, detecting input, and running the game loop.

You can use Phaser to make simple games like Pong and Tic-Tac-Toe or advanced platformers and role playing games. It supports both Canvas and WebGL rendering, so your games will run smoothly on most devices.

Project Setup

Create a folder for your project and add two files: index.html and game.js. The HTML file loads Phaser and the JavaScript file contains the game logic. Here is the repository with the finished code.

Here’s what the index.html file should look like:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <title>Tic Tac Toe — Phaser 3</title>
  <meta name="viewport" content="width=device-width,initial-scale=1" />
  <style>
    html, body { height: 100%; margin: 0; background: #0f172a; display: grid; place-items: center; }
    #game { box-shadow: 0 10px 30px rgba(0,0,0,.35); border-radius: 12px; overflow: hidden; }
    .hint { color: #e2e8f0; margin-top: 10px; font-size: 14px; text-align: center; opacity: .85; }
  </style>
  <script src="https://cdn.jsdelivr.net/npm/phaser@3.60.0/dist/phaser.min.js"></script>
</head>
<body>
  <div id="game"></div>
  <div class="hint">Click a cell to play. Tap “Restart” to start over.</div>
  <script src="./game.js"></script>
</body>
</html>

This sets up a simple HTML page, loads Phaser from a CDN, and points to your game.js file. The #game container is where Phaser will insert the game canvas.

How to Set Up the Game Configuration

Phaser games are built from a configuration object that defines things like width, height, background color, and which functions to call for loading, creating, and updating the game.

(() => {
  const GRID = 3;
  const CELL = 120;
  const BOARD = GRID * CELL;
  const HUD = 72;
  const WIDTH = BOARD;
  const HEIGHT = BOARD + HUD;

  let scene;
  let board;
  let currentPlayer;
  let gameOver;

  let gridGfx;
  let overlayGfx;
  let marks = [];
  let statusText;
  let restartText;

  const config = {
    type: Phaser.AUTO,
    parent: "game",
    width: WIDTH,
    height: HEIGHT,
    backgroundColor: "#ffffff",
    scale: { mode: Phaser.Scale.FIT, autoCenter: Phaser.Scale.CENTER_BOTH },
    scene: { preload, create, update }
  };

  new Phaser.Game(config);

We start by defining constants for the grid size and cell size. The config object tells Phaser to create a game with these dimensions and use the preload, create, and update functions we will define.

How to Preload Assets

Since we are drawing everything with Phaser’s graphics and text tools, we do not need to load any external images or sounds.

  function preload() {
    // No assets to load
  }

This is a placeholder that lets Phaser call preload before the game starts.

How to Create the Game Scene

The create function runs once at the start of the scene. Here we draw the grid, set up the initial state, and add UI elements.

  function create() {
    scene = this;

    gridGfx = scene.add.graphics({ lineStyle: { width: 4, color: 0x000000 } });
    overlayGfx = scene.add.graphics();
    drawGrid();

    initGame();

    statusText = scene.add.text(WIDTH / 2, BOARD + 12, "Player X's turn", {
      fontSize: "20px",
      color: "#111",
      fontFamily: "Arial, Helvetica, sans-serif"
    }).setOrigin(0.5, 0);

    restartText = scene.add.text(WIDTH / 2, BOARD + 38, "Restart", {
      fontSize: "18px",
      color: "#2563eb",
      fontFamily: "Arial, Helvetica, sans-serif"
    }).setOrigin(0.5, 0).setInteractive({ useHandCursor: true });

    restartText.on("pointerup", hardReset);

    scene.input.on("pointerdown", onPointerDown, scene);
  }

We created two Graphics objects: one for the static grid and another for the win line. Then we called drawGrid() and initGame() to set up the game board. The status text and restart button are placed below the grid. We also listened for clicks on the board with pointerdown.

How to Draw the Grid

The grid is made up of two vertical and two horizontal lines.

  function drawGrid() {
    gridGfx.strokeLineShape(new Phaser.Geom.Line(CELL, 0, CELL, BOARD));
    gridGfx.strokeLineShape(new Phaser.Geom.Line(CELL * 2, 0, CELL * 2, BOARD));
    gridGfx.strokeLineShape(new Phaser.Geom.Line(0, CELL, BOARD, CELL));
    gridGfx.strokeLineShape(new Phaser.Geom.Line(0, CELL * 2, BOARD, CELL * 2));
  }

We use Phaser.Geom.Line to define the start and end points for each line and then draw them with strokeLineShape.

How to Initialize and Reset the Game

The initGame function sets up a new game, and hardReset is called when the restart button is clicked.

  function initGame() {
    board = Array.from({ length: GRID }, () => Array(GRID).fill(""));
    currentPlayer = "X";
    gameOver = false;
    overlayGfx.clear();
    for (const t of marks) t.destroy();
    marks = [];
    setStatus("Player X's turn");
  }

  function hardReset() {
    initGame();
  }

  function setStatus(msg) {
    statusText && statusText.setText(msg);
  }

The board is represented by a 2D array filled with empty strings. The current player starts as X, and the marks array keeps track of text objects so we can clear them on reset.

How to Handle Player Input

When the player clicks a cell, we determine its row and column and check if the move is valid.

  function onPointerDown(pointer) {
    if (gameOver) return;
    if (pointer.y > BOARD) return;

    const col = Math.floor(pointer.x / CELL);
    const row = Math.floor(pointer.y / CELL);
    if (!inBounds(row, col)) return;
    if (board[row][col] !== "") return;

    placeMark(row, col, currentPlayer);

    const win = checkWin(board);
    if (win) {
      gameOver = true;
      drawWinLine(win);
      setStatus(`Player ${currentPlayer} wins!`);
      return;
    }

    if (isFull(board)) {
      gameOver = true;
      setStatus("Draw! No more moves.");
      return;
    }

    currentPlayer = currentPlayer === "X" ? "O" : "X";
    setStatus(`Player ${currentPlayer}'s turn`);
  }

This ensures that we only act if the game is not over, the click is inside the board, and the chosen cell is empty. After placing a mark, we check for a win or draw before switching turns.

How to Place Marks on the Board

We display an X or O at the center of the clicked cell.

  function inBounds(r, c) {
    return r >= 0 && r < GRID && c >= 0 && c < GRID;
  }

  function placeMark(row, col, player) {
    board[row][col] = player;
    const cx = col * CELL + CELL / 2;
    const cy = row * CELL + CELL / 2;
    const t = scene.add.text(cx, cy, player, {
      fontSize: Math.floor(CELL * 0.66) + "px",
      color: "#111111",
      fontFamily: "Arial, Helvetica, sans-serif"
    }).setOrigin(0.5);
    marks.push(t);
  }

The coordinates are calculated so the text is centered in the cell. We store the text object in the marks array so it can be removed when resetting.

How to Check for a Winner

We check rows, columns, and diagonals to see if the current player has three in a row.

  function checkWin(b) {
    for (let r = 0; r < GRID; r++) {
      if (b[r][0] && b[r][0] === b[r][1] && b[r][1] === b[r][2]) {
        return { kind: "row", index: r };
      }
    }
    for (let c = 0; c < GRID; c++) {
      if (b[0][c] && b[0][c] === b[1][c] && b[1][c] === b[2][c]) {
        return { kind: "col", index: c };
      }
    }
    if (b[0][0] && b[0][0] === b[1][1] && b[1][1] === b[2][2]) {
      return { kind: "diag" };
    }
    if (b[0][2] && b[0][2] === b[1][1] && b[1][1] === b[2][0]) {
      return { kind: "anti" };
    }
    return null;
  }

If a win is found, we return an object describing the winning line so it can be drawn.

How to Detect a Draw

If every cell is filled and there is no winner, the game ends in a draw.

  function isFull(b) {
    for (let r = 0; r < GRID; r++) {
      for (let c = 0; c < GRID; c++) {
        if (b[r][c] === "") return false;
      }
    }
    return true;
  }

This loops over every cell and returns false if any are empty.

How to Draw the Winning Line

A red line is drawn over the winning cells.

  function drawWinLine(res) {
    overlayGfx.clear();
    overlayGfx.lineStyle(6, 0xef4444, 1);
    const pad = 14;
    const half = CELL / 2;

    if (res.kind === "row") {
      const y = res.index * CELL + half;
      overlayGfx.strokeLineShape(new Phaser.Geom.Line(pad, y, BOARD - pad, y));
    } else if (res.kind === "col") {
      const x = res.index * CELL + half;
      overlayGfx.strokeLineShape(new Phaser.Geom.Line(x, pad, x, BOARD - pad));
    } else if (res.kind === "diag") {
      overlayGfx.strokeLineShape(new Phaser.Geom.Line(pad, pad, BOARD - pad, BOARD - pad));
    } else if (res.kind === "anti") {
      overlayGfx.strokeLineShape(new Phaser.Geom.Line(BOARD - pad, pad, pad, BOARD - pad));
    }
  }
})();

The coordinates are calculated based on the type of win to ensure the line passes through the correct cells.

Great. Now open index.html and you can start playing the game!

Final Game

Final Thoughts

You have now built a complete Tic Tac Toe game in Phaser.js. This includes a 3x3 grid, alternating turns, win detection with a highlight line, draw detection, and a restart button. The code uses core game development concepts like input handling, game state management, and rendering, which you can use in larger projects.

If you enjoy online games, check out GameBoost, the ultimate marketplace for gamers. You can find Fortnite accounts with exclusive skins, along with options for other popular games like Grow a Garden, Clash of Clans, and more.