3
\$\begingroup\$

I've made a Tic Tac Toe game with minimax in Javascript. It works but I have to use location.reload(true); to automatically reload the page every time user starts a new game (by pressing 'Play again!' in the modal), otherwise the game becomes slower with every round until it stops responding. I guess the problem is in the way I've written runGame() function which does not free the memory and keeps all the context through closure throughout all the rounds. My question is therefore how can I rewrite it so I don't have to reload page on each new round and don't have those memory leaks.

The relevant code:

Board class

  //Game's constants
var DRAW = 0, PLAYERX = 1, PLAYERO = 2,
    SCORES = {
        1: 1,
        0: 0,
        2: -1
    };

var dims = 3; //dimensions for the game's board

//Class representing the game's board
function Board(dims, board) {
    this.dims = dims;

    this.grid = [];
    for (var square = 0; square < dims * dims; square++) {
        if (board) {
            this.grid.push(board[square]);
        } else {
            this.grid[square] = 0;
        }
    }
}

//Show visual representation of the grid for debugging purposes
Board.prototype.showGrid = function () {
    var grid = [];
    for (var i = 0; i < dims * dims; i++) {
        grid.push(this.grid[i]);
    }
    return grid;
};

/*Returns one of the three constants for EMPTY, PLAYERX, or PLAYERO
 that correspond to the contents of the board at position (square).*/
Board.prototype.square = function (sqr) {

    return this.grid[sqr];
};

//Return an array of all empty squares in form [squares]
Board.prototype.getEmptySquares = function () {
    var empty = [];
    for (var i = 0; i < this.dims * this.dims; i++) {
        if (this.square(i) === 0) empty.push(i);
    }
    return empty;
};

/*Place player on the board at position (square).
 player should be either the constant PLAYERX or PLAYERO.
 Does nothing if board square is not empty.*/
Board.prototype.move = function (square, player) {
    if (this.square(square) === 0) {
        this.grid[square] = player;
    }
};

/*Returns a constant associated with the state of the game
 If PLAYERX wins, returns 1.
 If PLAYERO wins, returns 2.
 If game is drawn, returns 0.
 If game is in progress, returns 'None'.*/
Board.prototype.checkWin = function () {
    var winning = [[0, 1, 2], [3, 4, 5], [6, 7, 8], [0, 3, 6], [1, 4, 7], [2, 5, 8], [0, 4, 8], [2, 4, 6]];
    var self = this;
    var res = 'None';
    winning.forEach(function (el) {
        if (self.square(el[0]) !== 0 && self.square(el[0]) === self.square(el[1]) && self.square(el[0]) === self.square(el[2])) {
            res = self.square(el[0]);
        } else if (res == 'None' && self.getEmptySquares().length === 0) {
            res = DRAW;
        }
    });
    return res;
};
//return a copy of the board
Board.prototype.clone = function () {

    return new Board(dims, this.grid);
};

app.js

$(document).ready(function () {

    var board;

    //helper function
    function switchPlayer(player) {
        return player === PLAYERX ? PLAYERO : PLAYERX;
    }

    function minimax(board, player) {
        var mult = SCORES[String(player)], thisScore,
            empty = board.getEmptySquares(), l = empty.length,
            maxScore = -1, bestMove = null;

        if (board.checkWin() != 'None') {
            return [SCORES[board.checkWin()], 0];
        } else {
            for (var i = 0; i < l; i++) {
                var copy = board.clone();
                copy.move(empty[i], player);
                thisScore = mult * minimax(copy, switchPlayer(player))[0];

                if (thisScore >= maxScore) {
                    maxScore = thisScore;
                    bestMove = empty[i];
                }
            }

            return [mult * maxScore, bestMove];
        }
    }

    //Function that runs the game
    function runGame() {
        //Create a new board for the game
        board = new Board(dims);

        //clear previous board
        for (var i = 0; i < dims * dims; i++) {
            $('#' + i).text('');
        }
        playerMove();
    }

    function playerMove() {
        var sq = $('.square');
        sq.on('click', function () {

            var id = $(this).attr('id');
            if (board.square(id) === 0) {
                $(this).text('X');
                board.move(id, PLAYERX);
                if (board.checkWin() === 'None') {
                    AImove(board);
                } else {
                    declareWinner(board.checkWin());
                }
            } else {
                playerMove(board);
            }
        });
    }

    function AImove() {
        var move = minimax(board, PLAYERO)[1];
        board.move(move, PLAYERO);
        $('#' + move).text('O');
        if (board.checkWin() === 'None') {
            playerMove(board);
        } else {
            declareWinner(board.checkWin());
        }
    }

    function declareWinner(winner) {
        winner = winner === 1 ? 'Player X' : winner === 2 ? 'Player O' : 'Draw';
        var text = winner == 'Draw' ? "It's a draw!" : winner + ' wins!';
        $('.modal-body').html('<h3>' + text + '</h3>');
        $('.winner').modal('show');
    }

    runGame();

    $('#replay').on('click', function () {
        $('.winner').modal('hide');
        location.reload(true);
    });
});

Here's also the body of my html file just in case

<body>
<div class="modal fade winner" tabindex="-1" role="dialog" aria-labelledby="mySmallModalLabel">
    <div class="modal-dialog modal-sm">
        <div class="modal-content">
            <div class="modal-header">
                <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span
                        aria-hidden="true">&times;</span></button>
                <h4 class="modal-title" id="gridSystemModalLabel">Game over</h4>
            </div>
            <div class="modal-body">

            </div>
            <div class="modal-footer">
                <button type="button" class="btn btn-default" id="replay">Play again!</button>
            </div>
        </div>
    </div>
</div>
<h1>Tic Tac Toe</h1>

<p id="rules"> Player = X <br> Computer = O </p>

<div class="container">
    <div id="board" align="center">
        <table>
            <tr id="row1">
                <td class="square" id="0"></td>
                <td class="square v" id="1"></td>
                <td class="square" id="2"></td>
            </tr>
            <tr id="row2">
                <td class="square h" id="3"></td>
                <td class="square v h" id="4"></td>
                <td class="square h" id="5"></td>
            </tr>
            <tr id="row3">
                <td class="square" id="6"></td>
                <td class="square v" id="7"></td>
                <td class="square" id="8"></td>
            </tr>
        </table>
    </div>
</div>
</body>
\$\endgroup\$
1
\$\begingroup\$

Your code is leaking indeed. jQuery .on is always adding events to the element. Not replacing them. Since you call this each time it's the player's turn. After 5 games, the event is already called 25 times for each click at the same time.

Fixed App.js

$(document).ready(function () {
    var board;
    //helper function
    function switchPlayer(player) {
        return player === PLAYERX ? PLAYERO : PLAYERX;
    }

    function minimax(board, player) {
        var mult = SCORES[String(player)], thisScore,
            empty = board.getEmptySquares(), l = empty.length,
            maxScore = -1, bestMove = null;

        if (board.checkWin() != 'None') {
            return [SCORES[board.checkWin()], 0];
        } else {
            for (var i = 0; i < l; i++) {
                var copy = board.clone();
                copy.move(empty[i], player);
                thisScore = mult * minimax(copy, switchPlayer(player))[0];

                if (thisScore >= maxScore) {
                    maxScore = thisScore;
                    bestMove = empty[i];
                }
            }

            return [mult * maxScore, bestMove];
        }
    }

    //Function that runs the game
    function runGame() {
        //Create a new board for the game
        board = new Board(dims);

        //clear previous board
        for (var i = 0; i < dims * dims; i++) {
            $('#' + i).text('');
        }
    }
    $('.square').on('click', function () {
        var id = $(this).attr('id');
        if (board.square(id) === 0) {
            $(this).text('X');
            board.move(id, PLAYERX);
            if (board.checkWin() === 'None') {
                AImove(board);
            } else {
                declareWinner(board.checkWin());
            }
        }else{
            // still player's move
        }
    });

    function AImove() {
        var move = minimax(board, PLAYERO)[1];
        board.move(move, PLAYERO);
        $('#' + move).text('O');
        if (board.checkWin() === 'None') {
            // player move
        } else {
            declareWinner(board.checkWin());
        }
    }

    function declareWinner(winner) {
        winner = winner === 1 ? 'Player X' : winner === 2 ? 'Player O' : 'Draw';
        var text = winner == 'Draw' ? "It's a draw!" : winner + ' wins!';
        $('.modal-body').html('<h3>' + text + '</h3>');
        $('.winner').modal('show');
    }

    runGame();

    $('#replay').on('click', function () {
        $('.winner').modal('hide');
        //location.reload(true);
        runGame();
    });
});

(ps: shouldn't this be moved to stackoverflow)

\$\endgroup\$
  • \$\begingroup\$ Thanks for pointing out the jQuery issue! I was actually able to fix the leaks by adding sq.off() at the beginning of runGame() function to remove all event listeners from element before every new round. \$\endgroup\$ – Clarity Nov 29 '15 at 20:42
  • \$\begingroup\$ You will still add multiple eventlisteners in one game then. You should add it before adding it i suppose. \$\endgroup\$ – Sam Segers Nov 29 '15 at 20:46

Your Answer

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

Not the answer you're looking for? Browse other questions tagged or ask your own question.