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\$

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Browse other questions tagged or ask your own question.