Principal Engineer | Mentor
Turn: X
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"
);
});
});
});