Skip to main content

Overview

The Tic-Tac-Toe game showcases:
  • Game state management
  • Win condition checking
  • Reducer pattern for state updates
  • Conditional rendering
  • Modular code organization

Live Demo

View the complete source code on GitHub

What You’ll Learn

Game Logic

Implementing win detection algorithms

Reducer Pattern

Using reducers for state transitions

Modular Code

Separating logic from UI

Grid Rendering

Rendering 2D arrays efficiently

Complete Code

Game Logic (game.js)

game.js
export function makeInitialState() {
  return {
    board: [
      [null, null, null],
      [null, null, null],
      [null, null, null],
    ],
    player: "X",
    draw: false,
    winner: null,
  };
}

export function markReducer(state, { row, col }) {
  if (row > 3 || row < 0 || col > 3 || col < 0) {
    throw new Error("Invalid move");
  }

  if (state.board[row][col]) {
    throw new Error("Invalid move");
  }

  const newBoard = [
    [...state.board[0]],
    [...state.board[1]],
    [...state.board[2]],
  ];
  newBoard[row][col] = state.player;

  const newPlayer = state.player === "X" ? "O" : "X";
  const winner = checkWinner(newBoard, state.player);
  const draw = !winner && newBoard.every((row) => row.every((cell) => cell));

  return {
    board: newBoard,
    player: newPlayer,
    draw,
    winner,
  };
}

const checkWinner = (board, player) => {
  // Check rows
  for (let i = 0; i < 3; i++) {
    if (checkRow(board, i, player)) {
      return player;
    }
  }
  
  // Check columns
  for (let i = 0; i < 3; i++) {
    if (checkColumn(board, i, player)) {
      return player;
    }
  }
  
  // Check diagonals
  if (checkMainDiagonal(board, player)) {
    return player;
  }
  
  if (checkSecondaryDiagonal(board, player)) {
    return player;
  }

  return null;
};

const checkRow = (board, idx, player) => {
  const row = board[idx];
  return row.every((cell) => cell === player);
};

const checkColumn = (board, idx, player) => {
  const column = [board[0][idx], board[1][idx], board[2][idx]];
  return column.every((cell) => cell === player);
};

const checkMainDiagonal = (board, player) => {
  const diagonal = [board[0][0], board[1][1], board[2][2]];
  return diagonal.every((cell) => cell === player);
};

const checkSecondaryDiagonal = (board, player) => {
  const diagonal = [board[0][2], board[1][1], board[2][0]];
  return diagonal.every((cell) => cell === player);
};

Component (app.js)

app.js
import {
  Component,
  h,
  hFragment,
} from "@glyphui/runtime";
import { makeInitialState, markReducer } from "./game.js";

class TicTacToeApp extends Component {
  constructor() {
    super({}, {
      initialState: makeInitialState()
    });
  }
  
  mark(position) {
    const { row, col } = position;
    if (row > 3 || row < 0 || col > 3 || col < 0) {
      return; // Invalid move
    }
    
    if (this.state.board[row][col]) {
      return; // Cell already marked
    }
    
    this.setState(markReducer(this.state, position));
  }
  
  render(props, state) {
    return hFragment([
      this.renderHeader(state),
      this.renderBoard(state)
    ]);
  }
  
  renderHeader(state) {
    if (state.winner) {
      return h("h3", { class: "win-title" }, [
        `Player ${state.winner} wins!`,
      ]);
    }

    if (state.draw) {
      return h("h3", { class: "draw-title" }, [`It's a draw!`]);
    }

    return h("h3", {}, [`It's ${state.player}'s turn!`]);
  }
  
  renderBoard(state) {
    const freezeBoard = state.winner || state.draw;

    return h("table", { class: freezeBoard ? "frozen" : "" }, [
      h(
        "tbody",
        {},
        state.board.map((row, i) => this.renderRow(row, i))
      ),
    ]);
  }
  
  renderRow(row, i) {
    return h(
      "tr",
      {},
      row.map((cell, j) => this.renderCell(cell, i, j))
    );
  }
  
  renderCell(cell, i, j) {
    const mark = cell
      ? h("span", { class: "cell-text" }, [cell])
      : h(
          "div",
          {
            class: "cell",
            on: { click: () => this.mark({ row: i, col: j }) },
          },
          []
        );

    return h("td", {}, [mark]);
  }
}

const game = new TicTacToeApp();
game.mount(document.body);

Key Concepts

1. Reducer Pattern

A reducer is a pure function that takes state and an action, returning new state:
export function markReducer(state, { row, col }) {
  // Create new board (immutability)
  const newBoard = [
    [...state.board[0]],
    [...state.board[1]],
    [...state.board[2]],
  ];
  newBoard[row][col] = state.player;
  
  // Calculate derived state
  const winner = checkWinner(newBoard, state.player);
  const draw = !winner && newBoard.every(row => row.every(cell => cell));
  
  // Return new state object
  return {
    board: newBoard,
    player: newPlayer,
    draw,
    winner,
  };
}
Reducers should be pure functions with no side effects, always returning new state objects.

2. Win Detection

The game checks rows, columns, and diagonals:
const checkWinner = (board, player) => {
  // Check all rows
  for (let i = 0; i < 3; i++) {
    if (checkRow(board, i, player)) return player;
  }
  
  // Check all columns
  for (let i = 0; i < 3; i++) {
    if (checkColumn(board, i, player)) return player;
  }
  
  // Check both diagonals
  if (checkMainDiagonal(board, player)) return player;
  if (checkSecondaryDiagonal(board, player)) return player;
  
  return null;
};

3. Immutable Updates

Always create new arrays/objects instead of mutating:
// ❌ Wrong - mutates original
state.board[row][col] = player;

// ✅ Correct - creates new board
const newBoard = [
  [...state.board[0]],
  [...state.board[1]],
  [...state.board[2]],
];
newBoard[row][col] = player;

4. Grid Rendering

Use nested maps to render 2D arrays:
renderBoard(state) {
  return h("table", {}, [
    h("tbody", {},
      state.board.map((row, i) => 
        h("tr", {},
          row.map((cell, j) => 
            h("td", {}, [this.renderCell(cell, i, j)])
          )
        )
      )
    )
  ]);
}

Game Flow

1

Initial State

Game starts with empty 3x3 board, player X goes first
2

Player Move

Player clicks a cell, triggering mark() method
3

State Update

Reducer validates move, updates board, checks for winner
4

Re-render

Component re-renders with new state
5

Game End

If winner or draw detected, board is frozen

Features Demonstrated

  • Reducer pattern for complex state transitions
  • Immutable state updates
  • Derived state (winner, draw)
  • Win detection algorithms
  • Draw detection
  • Move validation
  • Player switching
  • Conditional rendering based on game state
  • Grid rendering with nested maps
  • Disabling board when game ends
  • Dynamic styling based on state
  • Separation of concerns (logic vs UI)
  • Reusable helper functions
  • Clear function naming

Running the Example

1

Clone the repository

git clone https://github.com/x0bd/glyphui.git
cd glyphui/examples/tictactoe
2

Open in browser

Open index.html in your browser:
npx serve .
3

Play the game

  • Click cells to make moves
  • Try to get three in a row
  • Test edge cases (invalid moves, draw)

Enhancements to Try

Reset Button

Add a button to reset the game

Score Tracking

Track wins for X and O

AI Opponent

Implement computer player

Animations

Add transition animations

Next Steps

Memory Game

See another game implementation

State Management

Learn advanced state patterns

Reducers

Deep dive into reducer pattern

Events

Master event handling

Build docs developers (and LLMs) love