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

单选按钮选择后无法执行嵌套函数?JavaScript游戏开发问题

问题:单选按钮触发对应游戏函数失败,嵌套函数未定义

问题概述

自学JavaScript,目标实现单选按钮选择游戏类型(PVP/PVC)并触发对应函数组的功能。当前代码逻辑混乱,遇到核心问题:

  • 选择PVP后运行pvp()时,提示内部函数未定义;
  • 移除pvp()外层包裹后,所有功能可正常运行。

已排查变量作用域、变量提升问题,调整过变量位置,控制台调试无进展,确认script标签在HTML末尾,无需window.onload。

预期操作流程

  • 用户通过单选按钮选择PVP或PVC游戏类型
  • 点击提交按钮
  • 根据选择为game变量赋值
  • 若game === "pvp"则运行pvp();若game === "pvc"则运行pvc()(PVC函数未编写,因PVP逻辑先失效)

原代码展示

JavaScript代码

'use-strict';

//declare variables here
let boxs = document.querySelectorAll('.box');
let statusTxt = document.querySelector('#status');
let btnRestart = document.querySelector('#restart');
let btnStart = document.querySelector('#start');
let btnNewGame = document.querySelector('#newGame');

//a timer for seconds when the game starts
//example in the text for the window.onload but I don't want to use that
//I need to start this timer when the first token is placed.....

//set up x and o stamps
//fix the height here because stamps are huge in the game
//I need to figure out how to get the players to be randomly started

/////////////////////////////////////////////////////////////

//which type of game is being played? test radio buttons...
//run functions based on what the value ofgame is...

btnStart.addEventListener("click", e => {
  console.log(e);
});

btnNewGame.addEventListener("click", e => {
  console.log(e);
});

var game;

//i have a hoisted variable problem
game = "no game radio button";
console.log("test game type at page load = " + game);

function checkGameType() {

  btnPVP = document.getElementById("pvp");
  btnPVC = document.getElementById("pvc");
  btnPVPchecked = document.getElementById("pvp").checked;
  btnPVCchecked = document.getElementById("pvc").checked;

  if (document.getElementById("pvp").checked) {
    document.getElementById("messageGame").innerHTML = "Player V Player Game!";
    console.log("player v player");
    clearCount();
    count = 0;
    game = "pvp";
    console.log("test game type at page load = " + game);

  } else if (document.getElementById("pvc").checked) {
    document.getElementById("messageGame").innerHTML = "Player V Computer Game!";
    console.log("player v computer");
    game = "pvc";
    console.log("test game type at page load = " + game);

  } else if (!btnPVPchecked && !btnPVCchecked) {
    document.getElementById("messageGame").innerHTML = "No Game Selected!";
    console.log("no game type selcted");
    game = "none";
    console.log("test game type at page load = " + game);
  }
} //end checkGameType

function clearGameType() {

  btnPVPunchecked = (document.getElementById("pvp").checked = false);
  btnPVCunchecked = !(document.getElementById("pvc").checked = false);
  console.log("game selection has been cleared");
  document.getElementById("messageGame").innerHTML = "Pick a game!";
  restartGame();
  game = "none";
  console.log(game + " ---- New Game var test");

} //end clearGameType

function restartGame() {
  options = ["", "", "", "", "", "", "", "", ""];

  setRandomPlayer();

  running = true;
  statusTxt.textContent = 'New Game! ' + player + ' turn!';

  boxs.forEach(box => {
    box.innerHTML = "";
    box.classList.remove('win');
  });
  counter = 0;
  stopCount();
} //end of restartgame

///////////////////////////////
//set up timer here so it can be accessed in funcs
//function for game type player v player
//game timer that looks ugly but works

//i tried making this a nested function and it was a disaster
var counter = 0;
var timeout;
var timer_on = 0;

function timedCount() {
  document.getElementById("timeClock").innerHTML = counter + " seconds";
  counter++;
  timeout = setTimeout(timedCount, 1000);
  //testing the timer in console
  //console.log(counter);
}

function startCount() {
  if (!timer_on) {
    timer_on = 1;
    timedCount();
  }
  //console.log(counter);
}

function stopCount() {
  clearTimeout(timeout);
  timer_on = 0;
  //counter = 0;
  document.getElementById("timeClock").innerHTML = counter + " seconds";
}

function clearCount() {
  clearTimeout(timeout);
  counter = 0;
}

//let currentPlayer=x; -- this is for if I wanted to start as x stamp every time

//initialize empty box options for the game board here

//when the game first loads in the browser

function init() {
  //the game waits for user interaction - a click in the box
  boxs.forEach(box => box.addEventListener('click', boxClick));
  btnRestart.addEventListener('click', restartGame);
  setRandomPlayer();
  statusTxt.textContent = 'Current Game - player ' + player + ' ----- turn!';
  running = true;
  console.log(game + " test in init");
  //here i see that game is undefined so this is why my pvp func is not working below....

} //end of init

init();

function setRandomPlayer() {
  //wow it actually works, sometimes o starts, sometimes x starts
  currentPlayer = Math.random() > 0.5 ? x : o; //images are being used not x and o
  //this needs to be fixed because it doesn't match the random player order
  //ok fixed the status message matches the current player
  player = currentPlayer;
} //end of random player func

function pvp() {

  var x = "x";
  var o = "o";
  var options = ["", "", "", "", "", "", "", "", ""];
  var running = false;

  //combo of wins possible that game checks for after each turn
  var win = [
    [0, 1, 2],
    [3, 4, 5],
    [6, 7, 8],
    [0, 3, 6],
    [1, 4, 7],
    [2, 5, 8],
    [0, 4, 8],
    [2, 4, 6]
  ];

  console.log('before game play func ---- ' + game);

  ///////////////////////////////
  //start game play here
  function boxClick() {
    const index = this.dataset.index;
    //if
    if (options[index] !== "" || !running) {
      return;
    }
    console.log("a click is recorded here");
    updateBox(this, index);
    isWinner();
  }

  //use a button to start the game play and attach timer to that     
  //not needed ^; timer works fine onclick, finally

  function updateBox(box, index) {
    options[index] = player;
    //dynamic html in the doc
    box.innerHTML = currentPlayer;
    console.log(currentPlayer + " plays their turn");
  }

  function nextPlayer() {
    player = (player === "x") ? "o" : "x";
    currentPlayer = (currentPlayer === x) ? o : x;
    statusTxt.textContent = player + ' turn';
    console.log("next player's turn " + currentPlayer);
  }

  //did anyone win
  function isWinner() {
    let isWon = false;

    for (let i = 0; i < win.length; i++) {
      const condition = win[i]; //[0,1,2]
      const box1 = options[condition[0]];
      const box2 = options[condition[1]];
      const box3 = options[condition[2]];
      if (box1 === "" || box2 === "" || box3 === "") {
        continue;
      }
      if (box1 === box2 && box2 === box3) {
        isWon = true;
        boxs[condition[0]].classList.add('win');
        boxs[condition[1]].classList.add('win');
        boxs[condition[2]].classList.add('win');
      }
    }

    if (isWon) {
      statusTxt.textContent = 'player ' + player + ' wins!!';
      running = false;
      //stop the timer here too
      stopCount();

    } else if (!options.includes("")) {
      statusTxt.textContent = 'The Game is Tied';
      running = false;
    } else {
      nextPlayer();
    }
  } //end of check winner
  //
} //end of pvp game


//functionality for game type player v computer

HTML代码

<!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>Title</title>
  <link rel="stylesheet" href="style.css">
  <link rel="shortcut icon" href="#">
</head>

<body>


  <br>
  <h3>What kind of game do you want to play ?</h3><br>

  <input type="radio" id="pvp" name="gameType" value="Player v Player">Player v Player
  <input type="radio" id="pvc" name="gameType" value="Player v Computer" />Player v Computer<br><br>
  <button id="start" onclick="checkGameType()">START!</button>
  <button id="newGame" onclick="clearGameType()">New Game</button>

  <div id="messageGame"></div><br>
  <div>----[[Game Clock]]----</div>
  <p id="timeClock"> </p>
  <div id="status">Place your stamp to start playing!</div>

  <div class="container" id="board">
    <div data-index="0" class="box" onclick="startCount()"></div>
    <div data-index="1" class="box" onclick="startCount()"></div>
    <div data-index="2" class="box" onclick="startCount()"></div>
    <div data-index="3" class="box" onclick="startCount()"></div>
    <div data-index="4" class="box" onclick="startCount()"></div>
    <div data-index="5" class="box" onclick="startCount()"></div>
    <div data-index="6" class="box" onclick="startCount()"></div>
    <div data-index="7" class="box" onclick="startCount()"></div>
    <div data-index="8" class="box" onclick="startCount()"></div>
  </div>
  <br>
  <button id="restart" onclick="restartGame()">Restart</button>

  <script src="pvpgame.js"></script>
</body>

</html>

问题根源与修复方案

核心问题分析

  1. 嵌套函数作用域冲突
    boxClick等核心游戏逻辑嵌套在pvp()内部,但全局的init()函数提前给格子绑定了boxClick事件——此时pvp()未执行,嵌套函数还未创建,导致报错“函数未定义”。移除pvp()外层后,函数变为全局作用域,init()能找到,所以功能正常。

  2. 未触发函数初始化
    checkGameType()仅给game变量赋值,未调用pvp()初始化游戏逻辑,也未重新绑定事件。

  3. 全局变量污染
    restartGame()中未声明options、running等变量,自动变为全局变量;而pvp()内部又声明同名局部变量,导致作用域混乱。

修复后的代码示例

'use-strict';

// 全局DOM元素与通用变量
let boxs = document.querySelectorAll('.box');
let statusTxt = document.querySelector('#status');
let btnRestart = document.querySelector('#restart');
let btnStart = document.querySelector('#start');
let btnNewGame = document.querySelector('#newGame');

var counter = 0;
var timeout;
var timer_on = 0;
let game = "no game radio button";

// 通用计时器函数
function timedCount() {
  document.getElementById("timeClock").textContent = counter + " seconds";
  counter++;
  timeout = setTimeout(timedCount, 1000);
}

function startCount() {
  if (!timer_on) {
    timer_on = 1;
    timedCount();
  }
}

function stopCount() {
  clearTimeout(timeout);
  timer_on = 0;
  document.getElementById("timeClock").textContent = counter + " seconds";
}

function clearCount() {
  clearTimeout(timeout);
  counter = 0;
}

// 移除格子事件绑定,避免冲突
function removeBoxEvents() {
  boxs.forEach(box => {
    box.removeEventListener('click', boxClick);
    box.removeEventListener('click', startCount);
  });
}

// PVP游戏初始化模块
function initPVP() {
  let x = "x";
  let o = "o";
  let options = ["", "", "", "", "", "", "", "", ""];
  let running = true;
  let currentPlayer;
  let player;

  // 随机选择起始玩家
  function setRandomPlayer() {
    currentPlayer = Math.random() > 0.5 ? x : o;
    player = currentPlayer;
    statusTxt.textContent = 'New Game! ' + player + ' turn!';
  }

  // 格子点击逻辑
  function boxClick() {
    const index = this.dataset.index;
    if (options[index] !== "" || !running) return;
    updateBox(this, index);
    isWinner();
  }

  function updateBox(box, index) {
    options[index] = player;
    box.textContent = currentPlayer;
  }

  function nextPlayer() {
    player = player === x ? o : x;
    currentPlayer = currentPlayer === x ? o : x;
    statusTxt.textContent = player + ' turn';
  }

  function isWinner() {
    let isWon = false;
    const win = [
      [0,1,2],[3,4,5],[6,7,8],
      [0,3,6],[1,4,7],[2,5,8],
      [0,4,8],[2,4,6]
    ];

    for (let i = 0; i < win.length; i++) {
      const condition = win[i];
      const box1 = options[condition[0]];
      const box2 = options[condition[1]];
      const box3 = options[condition[2]];
      if (box1 === "" || box2 === "" || box3 === "") continue;
      if (box1 === box2 && box2 === box3) {
        isWon = true;
        boxs[condition[0]].classList.add('win');
        boxs[condition[1]].classList.add('win');
        boxs[condition[2]].classList.add('win');
      }
    }

    if (isWon) {
      statusTxt.textContent = 'player ' + player + ' wins!!';
      running = false;
      stopCount();
    } else if (!options.includes("")) {
      statusTxt.textContent = 'The Game is Tied';
      running = false;
    } else {
      nextPlayer();
    }
  }

  // 初始化PVP游戏
  removeBoxEvents();
  setRandomPlayer();
  boxs.forEach(box => {
    box.addEventListener('click', boxClick);
    box.addEventListener('click', startCount);
  });
  clearCount();
  counter = 0;
}

// 检查游戏类型并初始化对应逻辑
function checkGameType() {
  const isPVP = document.getElementById("pvp").checked;
  const isPVC = document.getElementById("pvc").checked;

  if (isPVP) {
    document.getElementById("messageGame").textContent = "Player V Player Game!";
    game = "pvp";
    initPVP();
  } else if (isPVC) {
    document.getElementById("messageGame").textContent = "Player V Computer Game!";
    game = "pvc";
    // 后续添加PVC游戏初始化逻辑
  } else {
    document.getElementById("messageGame").textContent = "No Game Selected!";
    game = "none";
  }
}

// 重置游戏选择
function clearGameType() {
  document.getElementById("pvp").checked = false;
  document.getElementById("pvc").checked = false;
  document.getElementById("messageGame").textContent = "Pick a game!";
  removeBoxEvents();
  statusTxt.textContent = 'Place your stamp to start playing!';
相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.25 12:10:24