《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("")分支是空的,没有做事件清理的逻辑,也是原因之一。
解决方案
- 把点击事件的处理函数改为命名函数,不要用匿名函数,这样才能正确移除事件。
举个例子,原来movements.movement()里的双人模式绑定逻辑可能是:
// 原来的写法(匿名函数无法移除) cells.forEach(cell => cell.addEventListener('click', () => { // 双人模式落子逻辑 }))
改成命名函数的写法:
// 全局或者movements对象内定义命名函数 function handleP2ModeClick(e) { // 原来的双人模式落子逻辑 } function handleCpuModeClick(e) { // 原来的人机模式落子逻辑 } // 绑定事件时用命名函数 cells.forEach(cell => cell.addEventListener('click', handleP2ModeClick))
- 在清理棋盘的逻辑里加移除旧事件的代码,修改
gameBoard.cleanBoard()函数:
function cleanBoard() { const cells = document.querySelectorAll('.board-cell') // 替换成你实际的棋盘格子选择器 cells.forEach(cell => { // 移除两种模式的所有旧事件 cell.removeEventListener('click', handleP2ModeClick) cell.removeEventListener('click', handleCpuModeClick) // 保留你原来的清空格子内容、重置格子状态的逻辑 }) }
- 如果你有全局变量存储当前游戏模式,记得切换模式时同步更新这个变量的值,避免其他逻辑读取旧的模式配置。
按照以上步骤修改后,切换模式时会先清空所有旧的点击事件,再绑定新模式的事件,就能正确切换模式了。
内容的提问来源于stack exchange,提问作者roznerx

