GAZAR

Principal Engineer | Mentor

Turn: X

X/O or Tic Tac Toe Game

X/O or Tic Tac Toe Game

Tic-Tac-Toe is a classic game that has been enjoyed by people of all ages for decades. The game is played on a 3x3 grid, where two players, X and O, take turns marking a square on the grid. The first player to get three in a row (horizontally, vertically, or diagonally) wins the game.

To implement the game logic in TypeScript, we'll create a TicTacToe class that will manage the game state and provide methods for making moves.

Here is the Cell Code:

export type MARKED = "X" | "O" | null;
class Cell {
  private x: number;
  private y: number;
  private mark: MARKED = null;

  constructor(x: number, y: number) {
    this.x = x;
    this.y = y;
  }

  getX() {
    return this.x;
  }
  getY() {
    return this.y;
  }

  getMark() {
    return this.mark;
  }

  fill(turn: MARKED) {
    this.mark = turn;
  }

  isMarked() {
    return this.mark !== null;
  }
}

export default Cell;

And here is the Board Code:

import type { MARKED } from "./Cell";
import Cell from "./Cell";

class Board {
  public size: number;
  private cells: Cell[][];

  constructor(size: number) {
    this.size = size;
    this.cells = [];
    for (let i = 0; i < size; i++) {
      this.cells[i] = [];
      for (let j = 0; j < size; j++) {
        this.cells[i][j] = new Cell(i, j);
      }
    }
  }

  getCells() {
    return this.cells;
  }

  getCell(x: number, y: number) {
    return this.cells[x][y];
  }

  setCell(x: number, y: number, turn: MARKED) {
    this.cells[x][y].fill(turn);
  }

  isFull() {
    return this.cells.every((row) => row.every((cell) => cell.isMarked()));
  }

  isWinnerLine(items: Cell[]) {
    if (!items[0].isMarked()) return false;
    const mark = items[0].getMark();
    for (const item of items) {
      if (item.isMarked() && item.getMark() === mark) {
        continue;
      } else {
        return false;
      }
    }
    return true;
  }

  isWinning() {
    // check columns
    for (let index = 0; index < this.size; index++) {
      if (this.isWinnerLine(this.cells[index])) {
        return true;
      }
    }

    // check rows
    for (let indexI = 0; indexI < this.size; indexI++) {
      const column = [];
      for (let indexJ = 0; indexJ < this.size; indexJ++) {
        column.push(this.cells[indexJ][indexI]);
      }
      if (this.isWinnerLine(column)) {
        return true;
      }
    }

    // check diagonal
    const diagonal1 = [];
    const diagonal2 = [];
    for (let index = 0; index < this.size; index++) {
      diagonal1.push(this.cells[index][index]);
      diagonal2.push(this.cells[index][this.size - 1 - index]);
    }
    if (this.isWinnerLine(diagonal1) || this.isWinnerLine(diagonal2)) {
      return true;
    }

    return false;
  }
}

export default Board;

And for testing in RemixJS

import { createRemixStub } from "@remix-run/testing";
import XO from "../routes/games.x-o._index";
import { describe, expect, test } from "vitest";
import { json } from "@remix-run/node";
import {
  act,
  fireEvent,
  render,
  screen,
  waitFor,
} from "@testing-library/react";

const postData = {
  posts: {
    data: [
      {
        attributes: {
          title: "test",
          content: [],
          image: {
            data: {
              attributes: {
                url: "none",
              },
            },
          },
        },
      },
    ],
  },
};

const RemixStub = createRemixStub([
  {
    path: "/",
    Component: XO,
    loader() {
      return json(postData);
    },
  },
]);

describe("XO", () => {
  test("should be defined", () => {
    expect(XO).toBeDefined();
  });
  test("should click on button and change the text", async () => {
    render(<RemixStub />);
    const button = await screen.findByTestId("cell-0-0");
    await act(async () => {
      fireEvent.click(button);
    });
    await waitFor(async () => {
      expect((await screen.findByTestId("cell-0-0")).textContent).toBe("X");
    });
  });

  test("should end up winning x", async () => {
    render(<RemixStub />);
    const buttonX1 = await screen.findByTestId("cell-0-0");
    const buttonO1 = await screen.findByTestId("cell-1-1");
    const buttonX2 = await screen.findByTestId("cell-0-1");
    const buttonO2 = await screen.findByTestId("cell-2-2");
    const buttonX3 = await screen.findByTestId("cell-0-2");
    await act(async () => {
      fireEvent.click(buttonX1);
      fireEvent.click(buttonO1);
      fireEvent.click(buttonX2);
      fireEvent.click(buttonO2);
      fireEvent.click(buttonX3);
    });

    await waitFor(async () => {
      expect((await screen.findByTestId("message")).textContent).toContain(
        "X wins"
      );
    });
  });
});