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

扫雷实现递归函数故障:uncovered计数异常问题排查

扫雷游戏胜利计数异常问题排查与修复

问题描述

我正在实现一款扫雷游戏,目前仅剩一个核心问题:追踪已揭开方块数量的uncovered变量有时无法正确递增,而这个变量是游戏胜利判定的关键。

核心函数职责

  • RevealAll(win, id):胜利/失败时揭开所有棋盘元素,也处理炸弹标记操作
  • generate(initX, initY):在棋盘放置炸弹,确保首次点击区域安全(必为0)
  • numberize():为非炸弹方块添加数字,标识相邻地雷数量
  • revealDeadSpaces(i, j):递归揭开0值方块及周边非炸弹区域
  • parseInput(i, j):核心交互处理函数,负责将sapper数据同步到棋盘、放置标记,疑似问题根源

问题定位

控制台日志显示,revealDeadSpaces会触发parseInput重复解析已处理单元格,导致计数异常。关键矛盾点在parseInput中的这行代码:

if(element.innerHTML != '' || element.classList.contains('td-flag')) return;

按设计逻辑,当元素已标记或已有内容时应终止函数,但保留这行代码会导致uncovered计数偏低;移除则计数偏高,游戏提前判定胜利。

已尝试的解决方案

  • 调整该if语句的判断条件
  • 测试标记相关逻辑
  • 咨询外部技术资源

但均未解决,推测问题出在判断条件的逻辑设置上。

控制台调试可查看parseInput中uncovered的实时变化及当前单元格的行列坐标。标记模式切换:点击右上角紫色方块启用标记模式(启用后变为绿色)。

完整代码

JavaScript

let uncovered = 0,
  max = 0,
  flagMode = false;
window.onload = () => {
  function revealAll(win, id = null) {
    let element;
    if (id)
      element = document.getElementById(id);
    if ((!flagMode && element !== undefined && !element.classList.contains("td-flag")) || win) {
      for (let i = 1; i < trueSize - 1; i++)
        for (let j = 1; j < trueSize - 1; j++) {
          const cellkey = document.getElementById("cell-" + i + "-" + j);
          cellkey.classList.remove("td-unclicked");
          if (!cellkey.classList.contains("td-flag")) {
            if (sapper[i][j] != 'b')
              cellkey.innerHTML = sapper[i][j];
            else
              cellkey.classList.add("td-bomb");
            flag.onclick = null;
          } else if (sapper[i][j] != 'b')
            setInterval(() => {
              cellkey.classList.toggle("td-flag");
              if (cellkey.innerHTML == '') cellkey.innerHTML = sapper[i][j];
              else cellkey.innerHTML = '';
            }, 1000);
          else
            setInterval(() => {
              cellkey.classList.toggle("td-flag");
              cellkey.classList.toggle("td-bomb");
            }, 1000);
        }
      if (win === false) setTimeout(() => {
        location.reload()
      }, 6000);
    }
    if (flagMode) element.classList.toggle("td-flag");
  }

  function revealDeadSpaces(i, j) {
    const cellkey = document.getElementById('cell-' + i + '-' + j);
    cellkey.innerHTML = sapper[i][j];
    if (i === 0 || i === trueSize - 1 || j === 0 || j === trueSize - 1 || sapper[i][j] != 0 || !cellkey.classList.contains("td-unclicked")) {
      cellkey.classList.remove("td-unclicked");
      return;
    }
    cellkey.classList.remove("td-unclicked");
    for (let x = i - 1; x < i + 2; x++)
      for (let y = j - 1; y < j + 2; y++) {
        if (sapper[x][y] == 0)
          revealDeadSpaces(x, y);
        else
          parseInput(x, y);
      }
  }

  function parseInput(i, j) {
    console.log(uncovered);
    console.log(i + " " + j);
    let element = document.getElementById("cell-" + i + "-" + j);
    if (element.innerHTML != '' || element.classList.contains('td-flag')) return;
    if (flagMode && element.innerHTML == '') element.classList.toggle("td-flag");
    if (!flagMode && sapper[i][j] != 'b' && !element.classList.contains("td-flag")) {
      element.onclick = null;
      element.innerHTML = sapper[i][j];
      uncovered++;
      if (uncovered == max) {
        for (let i = 1; i < trueSize - 1; i++)
          for (let j = 1; j < trueSize - 1; j++)
            document.getElementById("cell-" + i + "-" + j).classList.add("anim-rainbow");
        revealAll(true);
      } else if (sapper[i][j] == 0) revealDeadSpaces(i, j);
      else element.classList.remove("td-unclicked");
    }
  }

  function returnRand() {
    return Math.floor(Math.random() * (trueSize - 2) + 1);
  }
  let size = prompt("Size: ", 10);
  if (size == null) size = 10;
  let parsedPrompt = parseInt(size),
    trueSize = parsedPrompt + 2,
    maxBombs = Math.floor(parsedPrompt * parsedPrompt * 0.2);
  max = size * size - Math.floor(size * size * 0.2);
  let sapper = [];
  for (let i = 0; i < trueSize; i++) {
    sapper[i] = [];
    for (let j = 0; j < trueSize; j++)
      sapper[i][j] = 0;
  }
  console.log("PRE");
  console.log(sapper);
  for (let i = 0; i < trueSize; i++) {
    let row = document.createElement("tr"),
      rowkey = "row-" + i;
    row.classList.add("fx");
    row.id = rowkey;
    if (i == 0 || i == trueSize - 1) row.classList.add("dp-none");
    document.getElementById("sapper-core").appendChild(row)
    for (let j = 0; j < trueSize; j++) {
      let cell = document.createElement("td"),
        cellkey = "cell-" + i + "-" + j,
        cellClasses = ["td", "fx-center", "fx", "td-unclicked", "bg-full", "button"],
        size = 40 / parsedPrompt;
      cell.setAttribute("style", "width: " + size + "vw; height: " + size + "vw;")
      for (let k = 0; k < cellClasses.length; k++)
        cell.classList.add(cellClasses[k]);
      if (j == 0 || j == trueSize - 1) cell.classList.add("dp-none");
      cell.id = cellkey;
      document.getElementById(rowkey).appendChild(cell);
    }
  }

  function generate(initX = false, initY = false) {
    if (!initX)
      for (let j = 1; j < trueSize - 1; j++)
        for (let k = 1; k < trueSize - 1; k++)
          document.getElementById("cell-" + j + '-' + k).onclick = () => {
            generate(j, k)
          };
    else {
      for (let i = 0; i < maxBombs; i++) {
        let x = returnRand(),
          y = returnRand();
        while (sapper[x][y] == 'b' || (initX && ((x + 1 == initX && y + 1 == initY) || (x + 1 == initX && y == initY) || (x + 1 == initX && y - 1 == initY) || (x == initX && y + 1 == initY) || (x == initX && y == initY) || (x == initX && y - 1 == initY) || (x - 1 == initX && y + 1 == initY) || (x - 1 == initX && y == initY) || (x - 1 == initX && y - 1 == initY)))) {
          x = returnRand();
          y = returnRand();
        }
        const cellkey = document.getElementById("cell-" + x + "-" + y);
        sapper[x][y] = 'b';
        cellkey.onclick = () => {
          revealAll(false, cellkey.id)
        };
      }
      console.log("GEN");
      console.log(sapper);
      numberize();
      parseInput(initX, initY);
    }
  }
  generate();

  function numberize() {
    for (let i = 1; i < trueSize - 1; i++)
      for (let j = 1; j < trueSize - 1; j++) {
        const cellkey = document.getElementById("cell-" + i + "-" + j);
        if (sapper[i][j] != 'b') {
          let number = 0;
          for (let x = i - 1; x < i + 2; x++)
            for (let y = j - 1; y < j + 2; y++)
              if (sapper[x][y] == 'b')
                number++;
          sapper[i][j] = number;
          if (number > 0) cellkey.classList.add("td-" + number);
          cellkey.onclick = () => {
            parseInput(i, j)
          };
        }
      }
    console.log("NUM");
    console.log(sapper);
  }
  const flag = document.getElementById("flag")

  function toggleFlag() {
    if (flagMode) flagMode = false;
    else flagMode = true;
    for (let i = 1; i < trueSize - 1; i++)
      for (let j = 1; j < trueSize - 1; j++) {
        const cellkey = document.getElementById("cell-" + i + "-" + j);
        if (cellkey.classList.contains("td-flag"))
          cellkey.classList.toggle("td-unclicked");
      }
    flag.classList.toggle("td-flag");
    flag.classList.toggle("td-flag2");
  }
  flag.onclick = () => {
    toggleFlag()
  };
}

CSS

.body {
  min-width: 70vw;
  min-height: 100vh;
  margin: 0;
}

.table {
  border: 4px grey ridge;
}

.td {
  font: 1.5vw monospace;
  font-weight: bold;
}

.button {
  background-color: grey;
}

.flag {
  width: 6vw;
  height: 6vw;
  background-color: violet;
  position: fixed;
  top: 3vw;
  right: 3vw;
}

.bg-full {
  background-size: cover;
}

.td-unclicked {
  cursor: pointer;
}

.td-flag {
  background-color: violet;
}

.td-flag2 {
  background-color: green;
}

.td-1 {
  color: blue;
}

.td-2 {
  color: green;
}

.td-3 {
  color: red;
}

.td-4 {
  color: darkblue;
}

.td-5 {
  color: darkred;
}

.td-6 {
  color: #007e7d;
}

.td-7 {
  color: black;
}

.td-8 {
  color: slategray;
}

.td-bomb {
  background: red, grey;
  background-size: 60%, cover;
  background-repeat: no-repeat;
  background-position: center center, center center;
}

.fx {
  display: flex;
}

.fx-center {
  align-items: center;
  justify-content: center;
}

.anim-rainbow {
  animation: rainbow 3s ease-in-out infinite;
}

@keyframes rainbow {
  12.5% {
    color: red !important;
  }
  25% {
    color: orange;
  }
  37.5% {
    color: yellow;
  }
  50% {
    color: green;
  }
  62.5% {
    color: blue;
  }
  75% {
    color: indigo;
  }
  87.5% {
    color: violet;
  }
  100% {
    color: red !important;
  }
}

.dp-none {
  display: none;
}

HTML

<body class="body fx fx-center">
  <img class="dp-none" src="flag2.png" alt="flaga 2">
  <table id="sapper-core" class="table"></table>
  <div id="flag" class="table flag bg-full td-unclicked"></div>
</body>

修复思路

问题本质是重复计数/漏计数,核心原因是parseInput的判断条件没有准确区分“已被正常揭开的方块”和“未处理的方块”,同时revealDeadSpaces递归时会重复调用parseInput处理同一单元格。

具体修复步骤:

  1. 新增状态标记:为每个单元格添加一个uncovered类(或在sapper数组中新增状态字段),明确标记该方块是否已被计入uncovered计数,替代仅通过innerHTML判断的逻辑。
  2. 修改parseInput的判断条件:
    // 替换原判断行
    if (element.classList.contains('td-flag') || element.classList.contains('uncovered')) return;
    
  3. 计数时添加状态标记:在uncovered++后,给元素添加uncovered类:
    element.classList.add('uncovered');
    
  4. 修正revealDeadSpaces逻辑:递归处理0值方块时,直接标记状态并计数,避免重复调用parseInput:
    function revealDeadSpaces(i, j) {
      const cellkey = document.getElementById('cell-' + i + '-' + j);
      // 跳过边界、非0值、已标记或已处理的方块
      if (i === 0 || i === trueSize - 1 || j === 0 || j === trueSize - 1 || 
          sapper[i][j] != 0 || cellkey.classList.contains('td-flag') || 
          cellkey.classList.contains('uncovered')) {
        if (!cellkey.classList.contains('uncovered') && sapper[i][j] !== 'b') {
          // 处理周边数字方块,仅计数一次
          cellkey.innerHTML = sapper[i][j];
          cellkey.classList.remove("td-unclicked");
          cellkey.classList.add('uncovered');
          uncovered++;
        }
        return;
      }
      // 处理当前0值方块
      cellkey.innerHTML = sapper[i][j];
      cellkey.classList.remove("td-unclicked");
      cellkey.classList.add('uncovered');
      uncovered++;
      // 递归处理周围8格
      for (let x = i - 1; x < i + 2; x++)
        for (let y = j - 1; y < j + 2; y++) {
          revealDeadSpaces(x, y);
        }
    }
    
  5. 同步修改parseInput中调用revealDeadSpaces的逻辑:确保0值方块的处理完全由revealDeadSpaces负责,避免重复触发。

这样可以确保每个非炸弹方块仅被计数一次,彻底解决计数异常问题。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.04 13:47:03