如何基于单选按钮选择动态执行不同游戏逻辑

12次阅读

如何基于单选按钮选择动态执行不同游戏逻辑

本文详解 javascript 中单选按钮(radio)值驱动函数执行的核心机制,重点解决因作用域、函数声明时机与调用时序不当导致的“函数未定义”问题,并提供可立即落地的模块化架构方案。

在开发如井字棋(Tic-Tac-Toe)这类支持多种对战模式(玩家 vs 玩家 / 玩家 vs 电脑)的游戏时,一个常见误区是:将核心游戏逻辑(如 pvp())封装为 内部定义的嵌套函数 ,再试图在事件回调中直接调用它——这会导致 ReferenceError: pvp is not defined。根本原因在于: 嵌套函数仅在其父作用域内可见,且不会被提升(hoisted);若未显式调用其外层函数,内部函数根本不会被声明到当前作用域中。

回顾原代码中的关键问题:

  • pvp() 函数体被完整包裹在顶层作用域中,但它内部定义的 boxClick、updateBox、isWinner 等函数 全部是局部变量,仅在 pvp() 执行时才创建;
  • init() 在页面加载时立即执行,此时 game 变量仍为 “no game radio button”,而 pvp() 从未被调用,因此其内部函数根本不存在;
  • 后续点击棋盘时尝试触发 boxClick,但该函数尚未声明 → 报错 undefined。

✅ 正确解法:分离「配置」与「执行」,采用工厂函数 + 事件驱动初始化模式

以下是一个结构清晰、可维护性强的重构方案:

✅ 推荐架构:模块化游戏控制器

// 1. 定义通用游戏状态(全局或模块级)const gameState = {gameMode: 'none', // 'pvp' | 'pvc' | 'none'   board: ['','', '','', '','', '','', ''],   currentPlayer:'x',   running: false,   timerCounter: 0 };  // 2. 工厂函数:返回特定模式的游戏逻辑对象 const GameModes = {pvp: () => ({onBoxClick: (index) => {if (!gameState.running || gameState.board[index] !=='') return;       gameState.board[index] = gameState.currentPlayer;       updateBoardUI();       if (checkWin()) {endGame(`Player ${gameState.currentPlayer} wins!`);       } else if (gameState.board.every(cell => cell !== '')) {endGame('Game tied!');       } else {gameState.currentPlayer = gameState.currentPlayer ==='x'?'o':'x';         updateStatus(`${gameState.currentPlayer}'s turn`);       }     },     onStart: () => {console.log('PvP mode activated');       startTimer(); // 可复用的计时器       gameState.running = true;       updateStatus(`${gameState.currentPlayer}'s turn`);     }   }),    pvc: () => ({onBoxClick: (index) => {// 此处添加 AI 决策逻辑(如 minimax)if (!gameState.running || gameState.board[index] !=='') return;       gameState.board[index] = 'x';       updateBoardUI();       if (checkWin()) return endGame('You win!');       if (gameState.board.every(cell => cell !== '')) return endGame('Tied!');        // 模拟 AI 落子(简化版)setTimeout(() => {const emptyIndices = gameState.board           .map((cell, i) => cell ==='' ? i : -1)           .filter(i => i !== -1);         if (emptyIndices.length> 0) {const aiMove = emptyIndices[Math.floor(Math.random() * emptyIndices.length)];           gameState.board[aiMove] = 'o';           updateBoardUI();           if (checkWin()) endGame('Computer wins!');         }       }, 500);     },     onStart: () => {console.log('PvC mode activated');       startTimer();       gameState.running = true;       updateStatus("Your turn (X)");     }   }) };  // 3. 统一事件绑定与初始化入口 document.getElementById('start').addEventListener('click', () => {const pvpRadio = document.getElementById('pvp');   const pvcRadio = document.getElementById('pvc');    if (pvpRadio.checked) {gameState.gameMode = 'pvp';     document.getElementById('messageGame').textContent = 'Player vs Player Game!';   } else if (pvcRadio.checked) {gameState.gameMode = 'pvc';     document.getElementById('messageGame').textContent = 'Player vs Computer Game!';   } else {alert('Please select a game mode first!');     return;   }    // ✅ 关键:获取当前模式的逻辑实例,并绑定事件   const modeLogic = GameModes[gameState.gameMode]();    // 清空并重新绑定棋盘点击事件   document.querySelectorAll('.box').forEach((box, index) => {box.onclick = () => modeLogic.onBoxClick(index);   });    // 启动该模式专属初始化流程   modeLogic.onStart();});  // 4. 复用型辅助函数(脱离具体模式)function updateBoardUI() {   document.querySelectorAll('.box').forEach((box, i) => {box.innerHTML = gameState.board[i] || '';   }); }  function updateStatus(text) {document.getElementById('status').textContent = text; }  function endGame(message) {gameState.running = false;   stopTimer();   updateStatus(message); }  function startTimer() {   gameState.timerCounter = 0;   document.getElementById('timeClock').textContent ='0 seconds';   const tick = () => {gameState.timerCounter++;     document.getElementById('timeClock').textContent = `${gameState.timerCounter} seconds`;     if (gameState.running) setTimeout(tick, 1000);   };   tick();}  function stopTimer() {   // 无须 clearTimeout —— 我们通过 running 标志控制递归}

⚠️ 关键注意事项

  • 禁止嵌套函数作为逻辑入口:pvp() 不应是“容器函数”,而应是返回逻辑对象的工厂函数;
  • 事件监听必须在用户确认模式后动态绑定:避免提前绑定未定义的处理函数;
  • 状态统一管理:使用 gameState 对象集中维护跨模式共享数据(如 board、running),避免闭包污染;
  • HTML 结构优化建议:将 onclick=”startCount()” 从 HTML 移至 JS 中绑定,实现关注点分离;
  • 调试技巧:在 start 点击事件中加入 console.log({gameState, modeLogic}),快速验证模式是否正确加载。

✅ 总结

单选按钮驱动函数执行的本质,不是“条件调用某个嵌套函数”,而是 根据用户输入动态装配一套行为契约(即事件处理器)。通过工厂函数生成模式专属逻辑、统一状态管理、延迟绑定事件,即可彻底规避作用域与提升陷阱,构建出可扩展、易测试、符合现代前端工程规范的游戏架构。

text=ZqhQzanResources