Часто самый простой способ решить переполнение стека из-за рекурсии - не использовать рекурсию.
В этом случае вы можете использовать следующий алгоритм:
Когда пользователь щелкает пустой блок (здесь «пустой блок» означает блок без шахты и без смежных шахт):
- Pu sh блок в пустой стек
- Пока стек не пусто:
- Извлечь верхний элемент из стека
- Если элемент еще не открыт:
- Отметить элемент как открытый
- Проверить соседей элемента - pu sh любых пустых, неоткрытых соседей по стеку и пометить все не принадлежащие мне соседи, у которых есть смежные мины, как открытые
Здесь центральная часть этого алгоритма:
function openBlocks(startingBlock) {
let blocksToOpen = [startingBlock];
while (blocksToOpen.length) {
let nextBlock = blocksToOpen.pop();
if (!nextBlock.classList.contains("opened")) {
// openBlock returns an array of empty neighbors that are not
// yet open
let additionalBlocksToOpen = openBlock(nextBlock);
if (additionalBlocksToOpen.length) {
blocksToOpen = [...blocksToOpen, ...additionalBlocksToOpen];
}
}
}
}
Полное решение см. ниже.
К вашему сведению, я думаю, что это будет работать значительно быстрее, если вы будете использовать простые объекты для представления игровых данных и Прикасайтесь к DOM только тогда, когда вам нужно изменить его часть (показать блок и т. д. c. ). DOM известен своей медлительностью по разным причинам.
// -1 => mine
// 0 => a block without touching any mine
// 1 => block touching 1 mine
// etc
//Helper Methods
//Short for querySelector and querySelectorAll
const qs = str => document.querySelector(str);
const qsa = str => document.querySelectorAll(str);
//To create an element
const ce = ({ tag, style, ...rest }) => {
const element = document.createElement(tag);
if (rest) {
for (let k in rest) {
element[k] = rest[k];
}
}
return element;
};
const main = qs("#main");
//Main variables
let len, wid, mines, blocks;
let isPlaying = true;
//Helping Data
let touchingBlockArr = [
[1, 0],
[0, 1],
[1, 1],
[1, -1]
];
touchingBlockArr = touchingBlockArr.concat(
touchingBlockArr.map(x => x.map(a => -a))
);
//Object to assign colors for different numbers.
const colorObj = {
"-1": "red",
0: "gray",
1: "blue",
2: "orange",
3: "green",
4: "purple",
5: "maroon"
};
//Function to create new game.
function newGame(l, w, m = 10) {
len = l;
wid = w;
mines = m;
main.innerHTML = "";
game = [];
blocks = [];
createBoard();
}
//Create board
function createBoard() {
for (let i = 0; i < len; i++) {
let tr = ce({ tag: "tr" });
blocks.push([]);
for (let j = 0; j < len; j++) {
let td = ce({
className: "block",
tag: "td",
onclick: onBlockClick,
id: `${i},${j}`
});
tr.appendChild(td);
td.id = `${i},${j}`;
blocks[blocks.length - 1].push(td);
}
main.appendChild(tr);
}
addMines();
setValues();
}
//Adding Mines
function addMines() {
let addedMines = [];
for (let i = 0; i < mines; i++) {
let str, randX, randY;
//To avoid repition of mines on same block.
do {
randX = Math.floor(Math.random() * wid);
randY = Math.floor(Math.random() * len);
str = `${randX},${randY}`;
} while (addedMines.includes(str));
addedMines.push(str);
blocks[randX][randY].classList.add("mine");
blocks[randX][randY].setAttribute("data-value", -1);
}
}
//Set Numbers for each block
function setValues() {
for (let i = 0; i < len; i++) {
for (let j = 0; j < len; j++) {
let val;
let tile = +blocks[i][j].dataset.value;
if (tile !== -1) {
val = touchingBlockArr.filter(([y, x]) => {
if (blocks[i + y] && blocks[i + y][j + x]) {
return +blocks[i + y][j + x].dataset.value === -1;
}
}).length;
}
val = val === undefined ? -1 : val;
blocks[i][j].setAttribute("data-value", val);
blocks[i][j].style.color = colorObj[val];
}
}
}
function openSingleBlock(td) {
let val = +td.dataset.value;
if (val === -1) {
} else {
td.innerHTML = val || "";
}
td.classList.add("opened");
}
function openBlocks(startingBlock) {
let blocksToOpen = [startingBlock];
while (blocksToOpen.length) {
let nextBlock = blocksToOpen.pop();
if (!nextBlock.classList.contains("opened")) {
// openBlock returns an array of empty neighbors that are not
// yet open
let additionalBlocksToOpen = openBlock(nextBlock);
if (additionalBlocksToOpen.length) {
blocksToOpen = [...blocksToOpen, ...additionalBlocksToOpen];
}
}
}
}
//When a left mouse button is clicked
function onBlockClick() {
if (this.classList.contains("flagged")) return false;
let val = +this.dataset.value;
//If mine is clicked.
if (val === -1) {
openSingleBlock(this);
}
//Empty block
else if (val === 0) {
openBlocks(this);
}
//For blocks touching mines.
else {
openSingleBlock(this);
}
}
function alreadyOpened(td) {
return td.classList.contains('opened');
}
//A function which open the blocks recursively
function openBlock(td) {
let blocksToOpen = [];
const [x, y] = td.id.split(",").map(Number);
//If the block is not empty then don't proceed further.
if (+td.dataset.value !== 0) return false;
let touchingBlocks = touchingBlockArr.map(([dx, dy]) => [x + dx, dy + y]);
openSingleBlock(td);
touchingBlocks.forEach(([x, y]) => {
//To checks if blocks are not out of range
if (blocks[x] === undefined) return false;
if (blocks[x][y] === undefined) return false;
let val = +blocks[x][y].dataset.value;
let td = blocks[x][y];
//Not a mine
if (val !== -1) {
//Not touching mine and not opened and not flagged.
if (
val === 0 &&
!alreadyOpened(td)
) {
blocksToOpen.push(td);
}
//Touching a mine
else {
openSingleBlock(td);
}
}
});
return blocksToOpen;
}
newGame(50, 50, 20);
body {
font-family: cursive;
}
.block {
height: 10px;
width: 10px;
text-align: center;
border: 1px solid black;
background-color: lightgray;
filter: brightness(0.8);
cursor: pointer;
font-size: 0.25rem;
box-shadow: 1px 1px c10px black;
background-size: contain;
}
.block:hover {
filter: brightness(1);
}
.opened {
background-color: rgb(255, 255, 255);
filter: brightness(1);
}
#main {
border-collapse: collapse;
}
.opened.mine {
background-image: url(mine.jpg);
background-size: contain;
}
.flagged {
background-image: url(flag.png);
background-size: contain;
}
<table id="main"></table>