You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

《The Odin Project》井字棋项目游戏模式切换异常问题求助

提问:井字棋项目切换游戏模式不生效

这是我第一次在此提问,若有操作不当之处提前致歉!

我正在跟随The Odin Project学习编程,此前所有项目都没有遇到重大问题:偶有难点,经过思考后都能顺利解决。

目前我正在完成Tic Tac Toe(井字棋)JavaScript项目,已经实现了游戏的基础功能,包含两种游戏模式:双人对战(VS Player 2)和人机对战(VS CPU)。两种模式首次通过我自制的HTML欢迎窗口选择(避免使用prompt弹窗)时都能正常运行,但当我尝试通过界面内设置的按钮切换游戏模式时失败,模式始终保持原有状态。

举例来说,当我尝试从双人对战模式切换到人机对战模式时,分数会重置、玩家名称也会更新,但游戏实际仍运行在双人对战模式下。控制台没有任何报错,因此我判断问题和作用域或者逻辑有关!

我已经尝试过多种解决方案,包括通过JavaScript DOM操作生成HTML欢迎窗口,但始终无法解决问题,目前该问题已经卡了两周毫无进展!

以下是模式选择相关的代码片段:

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);
},    
}

此外还有若干为不同按钮绑定的click事件addEventListeners调用上述代码。


问题原因

核心问题是棋盘格子的点击事件监听器没有在切换模式时移除:首次加载模式时你给每个格子绑定了对应模式的点击事件,切换模式时又新增了新模式的点击事件,但旧事件仍然保留,点击格子时会先触发旧模式的逻辑,所以表现为模式没有切换。
另外你目前的modeSelector.gameMode("")分支是空的,没有做事件清理的逻辑,也是原因之一。

解决方案

  1. 把点击事件的处理函数改为命名函数,不要用匿名函数,这样才能正确移除事件。
    举个例子,原来movements.movement()里的双人模式绑定逻辑可能是:
// 原来的写法(匿名函数无法移除)
cells.forEach(cell => cell.addEventListener('click', () => {
  // 双人模式落子逻辑
}))

改成命名函数的写法:

// 全局或者movements对象内定义命名函数
function handleP2ModeClick(e) {
  // 原来的双人模式落子逻辑
}
function handleCpuModeClick(e) {
  // 原来的人机模式落子逻辑
}
// 绑定事件时用命名函数
cells.forEach(cell => cell.addEventListener('click', handleP2ModeClick))
  1. 在清理棋盘的逻辑里加移除旧事件的代码,修改gameBoard.cleanBoard()函数:
function cleanBoard() {
  const cells = document.querySelectorAll('.board-cell') // 替换成你实际的棋盘格子选择器
  cells.forEach(cell => {
    // 移除两种模式的所有旧事件
    cell.removeEventListener('click', handleP2ModeClick)
    cell.removeEventListener('click', handleCpuModeClick)
    // 保留你原来的清空格子内容、重置格子状态的逻辑
  })
}
  1. 如果你有全局变量存储当前游戏模式,记得切换模式时同步更新这个变量的值,避免其他逻辑读取旧的模式配置。

按照以上步骤修改后,切换模式时会先清空所有旧的点击事件,再绑定新模式的事件,就能正确切换模式了。


内容的提问来源于stack exchange,提问作者roznerx

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.04 14:24:03