3
\$\begingroup\$

I've implemented a simple class for managing a Tic-Tac-Toe game's logic and state in Typescript. This code is intended to be used by a larger program; it doesn't implement any aspects of a UI. I've designed the class to present an immutable API, although it's mutable internally; I'm not using ImmutableJS or other, similar libraries. I'd appreciate review on any aspect of the code, though one thing I'd like to do is figure out a way to condense the setup code in the tests.

TicTacToeGame.ts

export type Player = "PlayerX" | "PlayerO";
export type MoveResult = "Victory" | "WaitingForMove" | "SquareFilled" | "GameOver";
export type CellNumber = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8;
type CellState = "X" | "O" | "EMPTY";

export class TicTacToeGame {
    private _currentPlayer: Player;
    private _winningPlayer?: Player;
    private board: CellState[];

    constructor () {
        this.board = new Array<CellState>(9);
        this.board.fill("EMPTY");
        this._currentPlayer = "PlayerX";
        this._winningPlayer = undefined;
    }

    public get currentPlayer(): Player {
        return this._currentPlayer;
    }

    public get winningPlayer(): Player | undefined {
        return this._winningPlayer;
    }

    public makeMove(cellNum: CellNumber): [MoveResult, TicTacToeGame] {
        const updatedGame = new TicTacToeGame();
        updatedGame._currentPlayer = this.currentPlayer;
        updatedGame._winningPlayer = this.winningPlayer;
        updatedGame.board = this.board.map(element => element);

        if (updatedGame.board[cellNum] !== "EMPTY") {
            return ["SquareFilled", updatedGame];
        }

        if (updatedGame.winningPlayer) {
            return ["GameOver", updatedGame];
        }

        if (updatedGame.currentPlayer === "PlayerX") {
            updatedGame.board[cellNum] = "X";
        } else {
            updatedGame.board[cellNum] = "O";
        }

        updatedGame.checkForVictory();
        if(updatedGame.winningPlayer) {
            return ["Victory", updatedGame];
        }

        if (updatedGame.currentPlayer === "PlayerX") {
            updatedGame._currentPlayer = "PlayerO";
        } else {
            updatedGame._currentPlayer = "PlayerX";
        }
        return ["WaitingForMove", updatedGame];
    }

    private checkForVictory (): void {
        const lines = {
            "row1": [this.board[0], this.board[1], this.board[2]],
            "row2": [this.board[3], this.board[4], this.board[5]],
            "row3": [this.board[6], this.board[7], this.board[8]],

            "col1": [this.board[0], this.board[3], this.board[6]],
            "col2": [this.board[1], this.board[4], this.board[7]],
            "col3": [this.board[2], this.board[5], this.board[8]],

            "diag1": [this.board[0], this.board[5], this.board[8]],
            "diag2": [this.board[3], this.board[5], this.board[7]]
        }

        for (const line in lines) {
            if ((lines[line] as CellState[]).every(cell => cell === "X")) {
                this._winningPlayer = "PlayerX";
                return;
            } else if ((lines[line] as CellState[]).every(cell => cell === "O")) {
                this._winningPlayer = "PlayerO";
                return;
            }
        }
        this._winningPlayer = undefined; // no winning player detected
        return;
    }
}

TicTacToeGame.test.ts

import { TicTacToeGame } from "./TicTacToeGame";

describe ("Tic Tac Toe game class", () => {
    it("Recognizes player X's victory correctly", () => {
        const game0 = new TicTacToeGame();
        // [1] needed to get just updated game; ignore result
        const game1 = game0.makeMove(0)[1]; // X moves
        const game2 = game1.makeMove(6)[1]; // O moves
        const game3 = game2.makeMove(1)[1]; // X moves
        const game4 = game3.makeMove(7)[1]; // O moves
        const [result, game5] = game4.makeMove(2); // X moves, completing top row
        expect(result).toBe("Victory");
        expect(game5.winningPlayer).toBe("PlayerX");
    });

    it("Recognizes player O's victory correctly", () => {
        const game0 = new TicTacToeGame();
        const game1 = game0.makeMove(0)[1]; // X moves
        const game2 = game1.makeMove(6)[1]; // O moves
        const game3 = game2.makeMove(1)[1]; // X moves
        const game4 = game3.makeMove(7)[1]; // O moves
        const game5 = game4.makeMove(5)[1]; // X moves
        const [result, game6] = game5.makeMove(8); // O moves, completing bottom row
        expect(result).toBe("Victory");
        expect(game6.winningPlayer).toBe("PlayerO");
    });

    it ("Returns an error when attempting to fill an already-filled square", () => {
        const game0 = new TicTacToeGame();
        const game1 = game0.makeMove(0)[1]; // get updated game, ignore result
        const result = game1.makeMove(0)[0]; // get result, ignore updated game
        expect(result).toBe("SquareFilled");
    });

    it ("Returns an error when making a move in a completed game", () => {
        const game0 = new TicTacToeGame();
        const game1 = game0.makeMove(0)[1]; // X moves
        const game2 = game1.makeMove(6)[1]; // O moves
        const game3 = game2.makeMove(1)[1]; // X moves
        const game4 = game3.makeMove(7)[1]; // O moves
        const game5 = game4.makeMove(2)[1]; // X moves, completing top row
        const result = game5.makeMove(3)[0];
        expect(result).toBe("GameOver");
    });

    it("Recognizes when it's X's turn", () => {
        const game0 = new TicTacToeGame();
        const game1 = game0.makeMove(0)[1]; // X moves
        const [result, game2] = game1.makeMove(1); // O moves
        expect(result).toBe("WaitingForMove");
        expect(game2.currentPlayer).toBe("PlayerX");
    });

    it("Recognizes when it's O's turn", () => {
        const game0 = new TicTacToeGame();
        const [result, game1] = game0.makeMove(0); // X moves
        expect(result).toBe("WaitingForMove");
        expect(game1.currentPlayer).toBe("PlayerO");
    });
});
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.