1
\$\begingroup\$

I made a tic tac toe game using JavaScript. I tried to focus on a logical and well structured program. Could someone give me some feedback on the structuring of the JavaScript code?

//create and manage gameboard
const GameBoard = (function () {

    //represents board on website
    let board = ["0", "1", "2", "3", "4", "5", "6", "7", "8"];
    /*         col     col     col
      row  0   0,0     0,1     0,2       -> row, col
      row  1   1,0     1,1     1,2       -> row, col
      row  2   2,0     2,1     2,2       -> row, col
    */

    function getboard() {
        return board;
    }

    function setboard(tile, value) {
        board[tile] = value;
    }

    function resetboard() {
        board = ["0", "1", "2", "3", "4", "5", "6", "7", "8"];
    }

    return { getboard, setboard, resetboard };

})();

//create and manage players
const players = (function () {

    let player1 = {
        symbol: "X",
    }

    let player2 = {
        symbol: "O",
    }
    return { player1, player2 };

})();

//manage gameflow
const displayControl = (function () {
    const winDrawOutput = document.querySelector(".win");//select field for output.
    let counter = 0;

    //get all 9 tiles:
    const tile0 = document.querySelector(".tile.zero");
    const tile1 = document.querySelector(".tile.one");
    const tile2 = document.querySelector(".tile.two");
    const tile3 = document.querySelector(".tile.three");
    const tile4 = document.querySelector(".tile.four");
    const tile5 = document.querySelector(".tile.five");
    const tile6 = document.querySelector(".tile.six");
    const tile7 = document.querySelector(".tile.seven");
    const tile8 = document.querySelector(".tile.eight");

    //add event listeners:
    tile0.addEventListener("click", () => {
        if(tile0.textContent === "") {//if empty, add symbol, otherwise dont
            tile0.textContent = currentPlayerSymbol();//placeholder for now
            GameBoard.setboard(0, currentPlayerSymbol());
            checkForWin();

            counter++;
        }
    });
    
    tile1.addEventListener("click", () => {
        if(tile1.textContent === "") {//if empty, add symbol, otherwise dont
            tile1.textContent = currentPlayerSymbol();
            GameBoard.setboard(1, currentPlayerSymbol());//set symbol to board array.
            checkForWin();

            counter++;
        }
    });

    tile2.addEventListener("click", () => {
        if(tile2.textContent === "") {//if empty, add symbol, otherwise dont
            tile2.textContent = currentPlayerSymbol();
            GameBoard.setboard(2, currentPlayerSymbol());//set symbol to board array.
            checkForWin();

            counter++;
        }
    });

    tile3.addEventListener("click", () => {
        if(tile3.textContent === "") {//if empty, add symbol, otherwise dont
            tile3.textContent = currentPlayerSymbol();
            GameBoard.setboard(3, currentPlayerSymbol());//set symbol to board array.
            checkForWin();

            counter++;
        }
    });

    tile4.addEventListener("click", () => {
        if(tile4.textContent === "") {//if empty, add symbol, otherwise dont
            tile4.textContent = currentPlayerSymbol();
            GameBoard.setboard(4, currentPlayerSymbol());//set symbol to board array.
            checkForWin();

            counter++;
        }
    });

    tile5.addEventListener("click", () => {
        if(tile5.textContent === "") {//if empty, add symbol, otherwise dont
            tile5.textContent = currentPlayerSymbol();
            GameBoard.setboard(5, currentPlayerSymbol());//set symbol to board array.
            checkForWin();

            counter++;
        }
    });

    tile6.addEventListener("click", () => {
        if(tile6.textContent === "") {//if empty, add symbol, otherwise dont
            tile6.textContent = currentPlayerSymbol();
            GameBoard.setboard(6, currentPlayerSymbol());//set symbol to board array.
            checkForWin();

            counter++;
        }
    });

    tile7.addEventListener("click", () => {
        if(tile7.textContent === "") {//if empty, add symbol, otherwise dont
            tile7.textContent = currentPlayerSymbol();
            GameBoard.setboard(7, currentPlayerSymbol());//set symbol to board array.
            checkForWin();

            counter++;
        }
    });

    tile8.addEventListener("click", () => {
        if(tile8.textContent === "") {//if empty, add symbol, otherwise dont
            tile8.textContent = currentPlayerSymbol();
            GameBoard.setboard(8, currentPlayerSymbol());//set symbol to board array.
            checkForWin();

            counter++;
        }
    });

    function checkForWin() {
        /*
            what is possible?
            -> horizontal win -> xxx/ooo: dont just check for occupied, check that symbols match
                                    -> 0,0 and 0,1 and 0,2 -> first row horiz. win
                                    -> 1,0 and 1,1 and 1,2 -> sec. row horiz. win
                                    -> 2,0 and 2,1 and 2,2 -> third row horiz. win
            -> vertical win -> 
                                    -> 0,0 and 1,0 and 2,0 -> first col
                                    -> 0,1 and 1,1 and 2,1 -> second col
                                    -> 0,2 and 1,2 and 2,2 -> third col
            -> diagonal win ->
                                    -> 0,0 and 1,1 and 2,2 -> top left to bottom right or vice versa
                                    -> 2,0 and 1,1 and 0,2 -> bottom left to top right or vice versa

            -> draw -> everyting occupied but no win registered.
        */
        const board = GameBoard.getboard();//get board.
        //x win horizontal:
        if((board[0] === "X" && board[1] === "X" && board[2] === "X") ||
           (board[3] === "X" && board[4] === "X" && board[5] === "X") ||
           (board[6] === "X" && board[7] === "X" && board[8] === "X") ||
           (board[0] === "X" && board[3] === "X" && board[6] === "X") || //x win vertical
           (board[1] === "X" && board[4] === "X" && board[7] === "X") ||
           (board[2] === "X" && board[5] === "X" && board[8] === "X") ||
           (board[0] === "X" && board[4] === "X" && board[8] === "X") || //x win diagonal
           (board[6] === "X" && board[4] === "X" && board[2] === "X")) {

                winDrawOutput.textContent = `X wins!`; //X wins
        } //same for O:
        else if ((board[0] === "X" && board[1] === "X" && board[2] === "X") ||
            (board[3] === "X" && board[4] === "X" && board[5] === "X") ||
            (board[6] === "X" && board[7] === "X" && board[8] === "X") ||
            (board[0] === "X" && board[3] === "X" && board[6] === "X") ||
            (board[1] === "X" && board[4] === "X" && board[7] === "X") ||
            (board[2] === "X" && board[5] === "X" && board[8] === "X") ||
            (board[0] === "X" && board[4] === "X" && board[8] === "X") ||
            (board[6] === "X" && board[4] === "X" && board[2] === "X")) {

             winDrawOutput.textContent = `O wins!`; //O wins

        }//else if everything is occupied but no win has been registered -> draw
        else if(board[0] !== "0" && board[1] !== "1" && board[2] !== "2" &&
                board[3] !== "3" && board[4] !== "4" && board[5] !== "5" &&
                board[6] !== "6" && board[7] !== "7" && board[8] !== "8") {

                    winDrawOutput.textContent = "It's a draw!";
                }
    }

    function currentPlayerSymbol() {
        let symbol = "";
        if(counter === 0 || counter % 2 === 0) {
            symbol = players.player1.symbol;
        }
        else if(counter % 2 !== 0) {
            symbol = players.player2.symbol;
        }
        return symbol;
    }

    //reset functionality:
    function resetView() {
        tile0.textContent = "";
        tile1.textContent = "";
        tile2.textContent = "";
        tile3.textContent = "";
        tile4.textContent = "";
        tile5.textContent = "";
        tile6.textContent = "";
        tile7.textContent = "";
        tile8.textContent = "";
    }

    function reset() {
        //add reset button, on click, reset board and array for board.
        GameBoard.resetboard();
        resetView();//reset content of all tiles.
        winDrawOutput.textContent = "";//reset output field
        counter = 0;
    }
    //on click, reset everything.
    const resetButton = document.querySelector(".reset");
    resetButton.addEventListener("click", () => {
        reset();
        resetView();
    });
})();
* {
    margin: none;
    padding: none;
    border-style: border-box;
    font-family:'Courier New', Courier, monospace;
}

.wrapper {
    display: flex;
    height: 100vh;
    width: 100vw;
    flex-direction: column;
    align-items: center;
    gap: 30px;
}

.title {
    font-size: large;
    color: darkblue;
}

.title:hover {
    color: lightseagreen;
    transform: scale(1.1);
}

.board {
    display: grid;
    height: 24.5rem;
    width: 24.5rem;
    grid-template-columns: repeat(3,8rem);
    grid-template-rows: repeat(3, 8rem);
}

.tile {
    width: 8rem;
    height: 8rem;
    border: 5px solid grey;

    display:flex;
    justify-content: center;
    align-items: center;
    font-size: 50px;
}

.tile.zero {
    border-top: none;
    border-left: none;
}

.tile.one {
    border-top: none;
}

.tile.two {
    border-top: none;
    border-right: none;
    border-left: none;
}

.tile.three {
    border-left: none;
    border-bottom: none;
}

.tile.four {
    border-bottom: none;
}

.tile.five {
    border-right: none;
    border-left: none;
    border-bottom: none;
}

.tile.six {
    border-left: none;
    border-bottom: none;
}

.tile.seven {
    border-bottom: none;
}

.tile.eight {
    border-left: none;
    border-right: none;
    border-bottom: none;
}

.win {
    height: 100px;
    width: 250px;
    text-align: center;
}

button {
    width: 8rem;
    height: 4rem;
    font-size: 1.5rem;
    border-radius: 1rem;
    background: white;
}

button:hover {
    transform: scale(1.1);
}
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Tic Tac Toe</title>
    <link rel="stylesheet" href="./css/style.css">
    <script defer src="./javaScript/doStuff.js"></script>
</head>
<body>
    <div class="wrapper">
        <div class="title">
            <h1>Tic-Tac-Toe</h1>
        </div>

        <div class="gridContainer">
            <div class="board">
                <div class="tile zero"></div>
                <div class="tile one"></div>
                <div class="tile two"></div>
                <div class="tile three"></div>
                <div class="tile four"></div>
                <div class="tile five"></div>
                <div class="tile six"></div>
                <div class="tile seven"></div>
                <div class="tile eight"></div>
            </div>
        </div>

        <div class="win"></div>
        <button type="button "class="reset">Reset?</button>
    </div>
</body>
</html>

Edit: just realized that I forgot to change the Symbols that the program checks for in the win function to "O" instead of "X" for the if condition that checks whether "O" won. Ignore that please.

\$\endgroup\$

1 Answer 1

2
\$\begingroup\$

The biggest issue here is the repetition. Whenever there is repeated code that only varies by variable value, we can use loops or iteration to remove the repetition.

In the case of the DOM elements that represents the tiles, we know that DOM querySelectorAll will return the elements in the order in which they appear in the DOM, which is also their natural sequence. Instead of querySelector for each tile in turn, we can just get all of the tiles, and then iterator over them to apply the same event handler.

checkForWin simplification

There are specific known sequences that define a win (8 of them) and so again, instead of repeatedly querying the DOM elements and comparing them, we can iterate over the combinations and reduce is down to one of three values, either of the player symbols or undefined to indicate no winner.

This also allows us to simplify how we update the winDrawOutput element.

IIFE information hiding

The choice to use an IIFE to hide the private data associated with the board is interesting, bit it's not necessary for the players as there is nothing to be hidden. This could just as easily be written as:

const players = {
    player1 = {
        symbol: 'X'
    },
    player2 = {
        symbol: 'O'
    }
}

It might also be good to bring all of the game state together into a single closure (board and count). I actually don't think you need count, it might be better to just store the active player and toggle back and forth after each turn. I didn't use count to calculate gameOver for example, but just counted the number of played tiles in the board.

Game Over

Once there is a winner or all of the tiles have been played (a draw) we should block the ability to continue to play tiles. I've not dealt with this.

Board initialisation

It's not necessary to populate the board with values, we can just use undefined as the starting point, and use Array.fill to simplify the statement.

Game State and UI content

At present, there is an overlap between the board state (the value assigned to board) and the UI state (the textContent value of each tile). This raises the possibility that they could get out of sync. It would be better to have a single source of truth and use the board state to populate the UI elements. When playing the tiles, the board state should be checked and updated, and then a render method could be used to update the DOM content according to the board state. I moved partially in this direction in checkForWin, using the board state rather than the DOM content. The click handlers could be updated to refer to board state and then update DOM content accordingly.

CSS

I've not touched the CSS, but its not strictly necessary to have a class for each tile element (.zero, .one, .two, ...) because we can use CSS's nth-child to similar effect.

//create and manage gameboard
const GameBoard = (function () {

    //represents board on website
    let board

    function getboard() {
        return board;
    }

    function setboard(tile, value) {
        board[tile] = value;
    }

    function resetboard() {
        board = Array(9).fill(undefined);
    }

    resetboard();
    return { getboard, setboard, resetboard };

})();

//create and manage players
const players = (function () {

    let player1 = {
        symbol: "X",
    }

    let player2 = {
        symbol: "O",
    }
    return { player1, player2 };

})();

//manage gameflow
const displayControl = (function () {
    const winDrawOutput = document.querySelector(".win");//select field for output.
    let counter = 0;

    //get all 9 tiles:
    const tiles = document.querySelectorAll('.tile');

    tiles.forEach((tile, index) => {
        tile.addEventListener('click', () => {
            if (tile.textContent === '') {
                const player = currentPlayerSymbol();
                tile.textContent = player;
                GameBoard.setboard(index, player);
                checkForWin();
                counter++;
            }
        });
    });
    
    function checkForWin() {
        
        const board = GameBoard.getboard();//get board.

        const lines = [
            [ 0, 1, 2 ],
            [ 3, 4, 5 ],
            [ 6, 7, 8 ],
            [ 0, 3, 6 ],
            [ 1, 4, 7 ],
            [ 2, 5, 8 ],
            [ 0, 4, 8 ],
            [ 2, 4, 6 ]
        ];
        
        const winner = lines.reduce((acc, line) => {
            return acc || line.reduce((acc, tile) => acc === board[tile] ? acc : undefined, board[line[0]]);
        }, undefined);

        const gameOver = !!winner || board.reduce((acc, tile) => acc + (tile !== undefined), 0) === 9;
        
        winDrawOutput.textContent = gameOver ? (winner ? `${winner} wins!` : `It's a draw!`) : '';
        
    }

    function currentPlayerSymbol() {
        let symbol = "";
        if(counter === 0 || counter % 2 === 0) {
            symbol = players.player1.symbol;
        }
        else if(counter % 2 !== 0) {
            symbol = players.player2.symbol;
        }
        return symbol;
    }

    //reset functionality:
    function resetView() {
        document.querySelectorAll('.tile').forEach(tile => tile.textContent = '');
    }

    function reset() {
        //add reset button, on click, reset board and array for board.
        GameBoard.resetboard();
        resetView();//reset content of all tiles.
        winDrawOutput.textContent = "";//reset output field
        counter = 0;
    }
    //on click, reset everything.
    const resetButton = document.querySelector(".reset");
    resetButton.addEventListener("click", () => {
        reset();
        resetView();
    });
})();
* {
    margin: none;
    padding: none;
    border-style: border-box;
    font-family:'Courier New', Courier, monospace;
}

.wrapper {
    display: flex;
    height: 100vh;
    width: 100vw;
    flex-direction: column;
    align-items: center;
    gap: 30px;
}

.title {
    font-size: large;
    color: darkblue;
}

.title:hover {
    color: lightseagreen;
    transform: scale(1.1);
}

.board {
    display: grid;
    height: 24.5rem;
    width: 24.5rem;
    grid-template-columns: repeat(3,8rem);
    grid-template-rows: repeat(3, 8rem);
}

.tile {
    width: 8rem;
    height: 8rem;
    border: 5px solid grey;

    display:flex;
    justify-content: center;
    align-items: center;
    font-size: 50px;
}

.tile.zero {
    border-top: none;
    border-left: none;
}

.tile.one {
    border-top: none;
}

.tile.two {
    border-top: none;
    border-right: none;
    border-left: none;
}

.tile.three {
    border-left: none;
    border-bottom: none;
}

.tile.four {
    border-bottom: none;
}

.tile.five {
    border-right: none;
    border-left: none;
    border-bottom: none;
}

.tile.six {
    border-left: none;
    border-bottom: none;
}

.tile.seven {
    border-bottom: none;
}

.tile.eight {
    border-left: none;
    border-right: none;
    border-bottom: none;
}

.win {
    height: 100px;
    width: 250px;
    text-align: center;
}

button {
    width: 8rem;
    height: 4rem;
    font-size: 1.5rem;
    border-radius: 1rem;
    background: white;
}

button:hover {
    transform: scale(1.1);
}
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Tic Tac Toe</title>
</head>
<body>
    <div class="wrapper">
        <div class="title">
            <h1>Tic-Tac-Toe</h1>
        </div>

        <div class="gridContainer">
            <div class="board">
                <div class="tile zero"></div>
                <div class="tile one"></div>
                <div class="tile two"></div>
                <div class="tile three"></div>
                <div class="tile four"></div>
                <div class="tile five"></div>
                <div class="tile six"></div>
                <div class="tile seven"></div>
                <div class="tile eight"></div>
            </div>
        </div>

        <div class="win"></div>
        <button type="button "class="reset">Reset?</button>
    </div>
</body>
</html>

\$\endgroup\$

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