Search code examples
javascripthtmldom-eventsaddeventlistenertic-tac-toe

Changing game mode in Tic Tac Toe [The Odin Project]


This is my first question here, so I apologize beforehand if I'm doing this wrong!

I'm learning to code with The Odin Project. So far, I've been doing every project without any major problems: some difficulty here and there, but after some thinking I was able to solve them successfully.

Currently, I'm at the Tic Tac Toe JavaScript project. I've already made the game work, implementing two game modes: VS Player 2 and VS CPU. Both are first selected through an HTML welcome window I made in order to avoid using prompts, and both run without problems. However, whenever I try to change the game mode through buttons I placed in the interface, I fail and the mode keeps the same.

For example, if I want to change from VS Player 2 mode to VS CPU mode, the score resets and the player's names change, but the game keeps running in the VS Player 2 mode. There's no console error displayed, so I'm sure this is related either to scope or logic!

I've already tried many things, including generating the HTML welcome window with JavaScript DOM manipulation, but I'm not able to make it work. It's been two weeks since I faced this problem and I'm still hitting a brick wall!

Here's the part of the code that manages the mode selection:

const modeSelector = {
constants: [
    /* 0 */ modalOverlay = document.getElementById("modal-overlay"),
    /* 1 */ modalWindow = document.getElementById("modal-window"),
    /* 2 */ modalContent = document.getElementById("modal-content"),
    /* 3 */ gameButtons = document.getElementById("game-buttons"),
    /* 4 */ popUpVs = document.getElementById("popUpVs"),
    /* 5 */ popUpCpu = document.getElementById("popUpCpu"),     
    /* 6 */ vsPlayerTwo = document.getElementById("vsplayer2"),
    /* 7 */ vsCPU = document.getElementById("vsCPU"),
],
gameMode(mode) {
    if (mode === "") {
    } else if (mode === "vsPlayerTwoMode") {
        gameBoard.cleanBoard();
        p1 = playerCreator.createPlayer();
        p1.name = playerCreator.names[0].innerHTML;
        playerCreator.scores[0].innerHTML = "0";
        p2 = playerCreator.createPlayer();
        p2.name = playerCreator.names[1].innerHTML;
        playerCreator.scores[1].innerHTML = "0";
        movements.movement();
        rules.turnChanger();
    } else if (mode === "vsCPUMode") {
        gameBoard.cleanBoard();
        p1 = playerCreator.createPlayer();
        p1.name = playerCreator.names[0].innerHTML;
        playerCreator.scores[0].innerHTML = "0";
        p2 = playerCreator.createPlayer();
        p2.name = "CPU"
        playerCreator.names[1].innerHTML = p2.name;
        playerCreator.scores[1].innerHTML = "0";
        movements.vsCpuMovement();
        rules.turnChanger();
    }
},
reset() {
    gameBoard.cleanBoard();
    modeSelector.gameMode("");
    playerCreator.names[0].innerHTML = "0";
    playerCreator.names[1].innerHTML = "0";
    playerCreator.scores[0].innerHTML = "0";
    playerCreator.scores[1].innerHTML = "0";
},    
popUpMode(mode) {
    if (mode === "popUpVsMode") {
        modeSelector.constants[2].innerHTML = "Insert Player 1 name";
    } else if (mode === "popUpCPUMode") {
        modeSelector.constants[2].innerHTML = "Insert Player name";
    }
    modeSelector.constants[2].style.marginLeft = "130px";
    modeSelector.constants[4].remove();
    modeSelector.constants[5].remove();
    let p1NameBar = document.createElement("input");
    p1NameBar.id = "p1NameBar";
    p1NameBar.maxLength = "6";
    modeSelector.constants[3].appendChild(p1NameBar);
    let p1NameOk = document.createElement("h2");
    p1NameOk.class = "popUpButton";
    p1NameOk.type = "button";
    p1NameOk.id = "popUpP1NameOk";
    p1NameOk.innerHTML = "OK";
    modeSelector.constants[3].appendChild(p1NameOk);
    p1NameOk.addEventListener("click", () => {
        if (mode === "popUpVsMode") {
            playerCreator.names[0].innerHTML = p1NameBar.value;
            modeSelector.constants[2].innerHTML = "Insert Player 2 name";
            modeSelector.constants[2].style.marginLeft = "130px";
            p1NameBar.remove();
            p1NameOk.remove();
            let p2NameBar = document.createElement("input");
            p2NameBar.id = "p2NameBar";
            p2NameBar.maxLength = "6";
            modeSelector.constants[3].appendChild(p2NameBar);
            let p2NameOk = document.createElement("h2");
            p2NameOk.className = "popUpButton";
            p2NameOk.type = "button";
            p2NameOk.id = "popUpP2NameOk";
            p2NameOk.innerHTML = "OK";
            modeSelector.constants[3].appendChild(p2NameOk);
            p2NameOk.addEventListener("click", () => {
                playerCreator.names[1].innerHTML= p2NameBar.value;
                document.body.removeChild(modalOverlay);
                modeSelector.gameMode("vsPlayerTwoMode");                    
            });
        } else if (mode === "popUpCPUMode") {
            playerCreator.names[0].innerHTML = p1NameBar.value;
            document.body.removeChild(modalOverlay);
            modeSelector.gameMode("vsCPUMode");
        }
    });
},
inGameMode(mode) {
    modeSelector.reset()
    let inGameOverlay = document.createElement("div");
    inGameOverlay.id = "inGameOverlay";
    inGameOverlay.className = "modal-overlay";
    document.body.appendChild(inGameOverlay);
    let inGameWindow = document.createElement("div");
    inGameWindow.className = "modal-window";
    inGameWindow.id = "inGameWindow";
    inGameOverlay.appendChild(inGameWindow);
    let inGameTitleBar = document.createElement("div");
    inGameTitleBar.className = "modal-titlebar";
    inGameTitleBar.id = "inGameTitleBar";
    inGameWindow.appendChild(inGameTitleBar);
    let inGameTitle = document.createElement("span");
    inGameTitle.className = "modal-title";
    inGameTitle.id = "inGameTitle";
    if (mode === "VSP2") {
        inGameTitle.innerHTML = "VS Player 2 Mode";
        inGameTitle.style.marginLeft = "160px";
        inGameTitleBar.appendChild(inGameTitle);
        let inGameContent = document.createElement("div");
        inGameContent.className = "modal-content";
        inGameContent.id = "inGameContent";
        inGameContent.innerHTML = "Insert Player 1 name";
        inGameContent.style.marginLeft = "120px";
        inGameWindow.appendChild(inGameContent);
        let inGameButtons = document.createElement("div");
        inGameButtons.className = "modal-buttons";
        inGameButtons.id = "inGameButtons";
        inGameWindow.appendChild(inGameButtons);
        let inGameP1NameBar = document.createElement("input");
        inGameP1NameBar.id = "p1NameBar";
        inGameP1NameBar.maxLength = "6";
        inGameButtons.appendChild(inGameP1NameBar);
        let inGameP1NameOk = document.createElement("h2");
        inGameP1NameOk.className = "popUpButton";
        inGameP1NameOk.type = "button";
        inGameP1NameOk.id = "popUpP1NameOk";
        inGameP1NameOk.innerHTML = "OK";
        inGameButtons.appendChild(inGameP1NameOk);
        inGameP1NameOk.addEventListener("click", () => {
            playerCreator.names[0].innerHTML = inGameP1NameBar.value;
            inGameContent.innerHTML = "Insert Player 2 name";
            inGameContent.style.marginLeft = "130px";
            inGameP1NameBar.remove();
            inGameP1NameOk.remove();
            let inGameP2NameBar = document.createElement("input");
            inGameP2NameBar.id = "p2NameBar";
            inGameP2NameBar.maxLength = "6";
            inGameButtons.appendChild(inGameP2NameBar);
            let inGameP2NameOk = document.createElement("h2");
            inGameP2NameOk.className = "popUpButton";
            inGameP2NameOk.type = "button";
            inGameP2NameOk.id = "popUpP2NameOk";
            inGameP2NameOk.innerHTML = "OK";
            inGameButtons.appendChild(inGameP2NameOk);
            inGameP2NameOk.addEventListener("click", () => {
                playerCreator.names[1].innerHTML= inGameP2NameBar.value;
                document.body.removeChild(inGameOverlay);
                modeSelector.gameMode("vsPlayerTwoMode");
            })
        });
    } else if (mode === "VSCPU") {
        inGameTitle.innerHTML = "VS CPU Mode";
        inGameTitle.style.marginLeft = "210px";
        inGameTitleBar.appendChild(inGameTitle);
        let inGameContent = document.createElement("div");
        inGameContent.className = "modal-content";
        inGameContent.id = "inGameContent";
        inGameContent.innerHTML = "Insert Player 1 name";
        inGameContent.style.marginLeft = "120px";
        inGameWindow.appendChild(inGameContent);
        let inGameButtons = document.createElement("div");
        inGameButtons.className = "modal-buttons";
        inGameButtons.id = "inGameButtons";
        inGameWindow.appendChild(inGameButtons);
        let inGameP1NameBar = document.createElement("input");
        inGameP1NameBar.id = "p1NameBar";
        inGameP1NameBar.maxLength = "6";
        inGameButtons.appendChild(inGameP1NameBar);
        let inGameP1NameOk = document.createElement("h2");
        inGameP1NameOk.className = "popUpButton";
        inGameP1NameOk.type = "button";
        inGameP1NameOk.id = "popUpP1NameOk";
        inGameP1NameOk.innerHTML = "OK";
        inGameButtons.appendChild(inGameP1NameOk);
        inGameP1NameOk.addEventListener("click", () => {
            playerCreator.names[0].innerHTML = inGameP1NameBar.value;
            document.body.removeChild(inGameOverlay);
            modeSelector.gameMode("vsCPUMode");
        });    
    }
    let inGameInfo = document.createElement("div");
    inGameInfo.id = "modal-info";
    inGameInfo.innerHTML = "You can reset the game mode during the match by clicking on any of the buttons to the right";
    inGameWindow.appendChild(inGameInfo);
},    

};

There's also some addEventListeners for "click" that implement the above code in the different buttons, but I didn't want to add more code so I'm also sharing the full thing over at GitHub:

https://github.com/roznerx/tic_tac_toe

Thanks in advance and, again, sorry if I'm asking this the wrong way!


Solution

  • If we first start in vsPlayerTwoMode mode, then movements.movement() adds an event listener to each box on the board. When a box is clicked, it checks to see if its empty and depending on whose turn it is (based on the values of playerCreator.turns[0] and playerCreator.turns[1]), will set gameBoard.board[i].innerHTML to either X or O.

    Now let's switch to the vsCPUMode mode. Each box on the board now has the original event listener attached to it (from movements.movement()), and the new event listener from movements.vsCpuMovement(). When a box is clicked, it will first evaluate the logic in the above paragraph (as defined in the movements.movement() event listener), and then the logic in the movements.vsCpuMovement() function (notably, check to see if gameBoard.board[i].innerHTML == "").

    However, since the movements.movement() event is called before the movements.vsCpuMovement() event, then gameBoard.board[i].innerHTML is going to be set to either X or O depending on the previous values of playerCreator.turns[0] and playerCreator.turns[1]. Then, the movements.vsCpuMovement() checks to see if gameBoard.board[i].innerHTML is an empty string, and it won't be, so the function returns.

    Importantly, when the mode is vsCPUMode to start, the movements.movement() event listener is never registered, so it doesn't matter what the values of playerCreator.turns[0] or playerCreator.turns[1] are.

    How do we fix it? One option would be to reset the values of playerCreator.turns[...] so that when the movements.movement() event is called, it simply ignores the logic that depends on those values (which is somewhat equivalent to starting the game in mode vsCPUMode). In other words, add these two lines after line 215:

    playerCreator.turns[0] = ""
    playerCreator.turns[1] = ""
    

    Is there a better way to solve the problem? You bet! I would recommend posting your code over on Code Review and asking people for suggestions. If you are only interested in solving this immediate problem, I would keep track of all the event listeners you add to the gameboard boxes, and if you change the modes, remove any existing listeners before adding newer ones.