Reputation: 36574
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
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):
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