Maheer Ali
Maheer Ali

Reputation: 36574

How to fix Maximum call stack size exceeded in minesweeper

I make a minesweeper today using pure js and css. When one block is clicked other blocks are opened using recursion. First I was using it for 10x10 board. It was working completely fine. But now when I made a 50x50 board. It gives error

Uncaught RangeError: Maximum call stack size exceeded.

Here is my complete code. Its much but you have to only concentrate on openBlock function which is called recursively. There are only 10 mines in 50x50 board. So all the blocks should open up except mines in almost all the cases. But some of the blocks are not opened due to the error.

// -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>

If you have any tips to increase performance of code please add it to your answer.

Upvotes: 0

Views: 349

Answers (1)

JLRishe
JLRishe

Reputation: 101690

Often, the simplest way to solve an overflowing stack due to recursion is to not use recursion.

In this case you can use the following algorithm:

When user clicks an empty block (here, "empty block" means a block with no mine and no adjacent mines):

  1. Push the block to an empty stack
  2. While the stack is non-empty:
    1. Pop the top item from the stack
    2. If the item is not yet open:
      1. Mark the item as open
      2. Check the item's neighbors - push any empty, non-opened neighbors to the stack and mark any non-mine neighbors that have adjacent mines as open

Here is the central portion of that algorithm:

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];
            }
        }
    }
}

See below for the full solution.

FYI, I think this would run considerably faster if you used plain objects to represent the game data and only touch the DOM when you need to change part of it (reveal a block, etc.). The DOM is notoriously slow for various reasons.

// -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>

Upvotes: 2

Related Questions