扫雷实现递归函数故障: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处理同一单元格。
具体修复步骤:
- 新增状态标记:为每个单元格添加一个
uncovered类(或在sapper数组中新增状态字段),明确标记该方块是否已被计入uncovered计数,替代仅通过innerHTML判断的逻辑。 - 修改
parseInput的判断条件:// 替换原判断行 if (element.classList.contains('td-flag') || element.classList.contains('uncovered')) return; - 计数时添加状态标记:在
uncovered++后,给元素添加uncovered类:element.classList.add('uncovered'); - 修正
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); } } - 同步修改
parseInput中调用revealDeadSpaces的逻辑:确保0值方块的处理完全由revealDeadSpaces负责,避免重复触发。
这样可以确保每个非炸弹方块仅被计数一次,彻底解决计数异常问题。
内容的提问来源于stack exchange,提问作者whitehat
相关产品推荐
相关产品推荐

