Как исправить Максимальный размер стека вызовов превышен в тральщик - PullRequest
0 голосов
/ 13 января 2020

Сегодня я делаю тральщик, используя чистые js и css. При нажатии одного блока другие блоки открываются с помощью рекурсии. Сначала я использовал его для доски 10x10. Работало совершенно нормально. Но теперь, когда я сделал доску 50x50. Выдает ошибку

Uncaught RangeError: Превышен максимальный размер стека вызовов.

Вот мой полный код. Это много, но вам нужно сосредоточиться только на функции openBlock, которая вызывается рекурсивно. На доске 50x50 всего 10 мин. Таким образом, все блоки должны открываться, кроме мин почти во всех случаях. Но некоторые блоки не открываются из-за ошибки.

// -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");
}


//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) {
      openBlock(this);
      openSingleBlock(this);
   }

   //For blocks touching mines.
   else {
      openSingleBlock(this);
   }
}

//A function which open the blocks recursively
function openBlock(td) {
   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 &&
            !td.classList.contains("opened")
         ) {
            openBlock(td);
         }

         //Touching a mine
         else {
            openSingleBlock(td);
         }
      }
   });
}


newGame(50, 50);
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>

Если у вас есть какие-либо советы по повышению производительности кода, добавьте его в свой ответ.

1 Ответ

2 голосов
/ 13 января 2020

Часто самый простой способ решить переполнение стека из-за рекурсии - не использовать рекурсию.

В этом случае вы можете использовать следующий алгоритм:

Когда пользователь щелкает пустой блок (здесь «пустой блок» означает блок без шахты и без смежных шахт):

  1. Pu sh блок в пустой стек
  2. Пока стек не пусто:
    1. Извлечь верхний элемент из стека
    2. Если элемент еще не открыт:
      1. Отметить элемент как открытый
      2. Проверить соседей элемента - 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>
...