javascript – Minesweeper checkerboard effect

One way to achieve alternating shades of green or brown for your Minesweeper is with the following CSS technologies:

We can start by setting the color of your tiles based on some dynamic custom properties:

#board div {
  background-color: hsl(var(--hue) var(--sat) var(--lightness));
}

Let’s declare the default values ​​for our tiles (green):

#board div {
  --hue: 80.7deg;
  --sat: 65.1%;
  --lightness: 82%;
}

Now this is the fun part, we can form a selector that targets alternating tiles on the board:

#board div:nth-child(odd) {}

And because we split our colors up dynamically with custom properties, we only need to change the lightness:

#board div:nth-child(odd) {
  --lightness: 70%;
}

And as for our revealed tiles, we can set the hue and saturation, but keep the previous lightness code so they can alternate as well. We don’t even have to declare background-color again, as we’re just changing the properties that are already used for the colour:

#board .tile-clicked {
  --hue: 30deg;
  --sat: 57.4%;
  border: 1px solid #E5C29F;
}

Note that the selector is now prefixed by #board so that the existing hue and saturation declared on #board div does not override these colours.

Let’s take a look at our board:

var board = [];
var rows = 10;
var columns = 10;

var minesCount = 10;
var minesLocation = [];

var tilesClicked = 0;
var flagEnabled = false;

var gameOver = false;

window.onload = function() {
  startGame();
}

function setMines() {
  let minesLeft = minesCount;
  while (minesLeft > 0) {
    let r = Math.floor(Math.random() * rows);
    let c = Math.floor(Math.random() * columns);
    let id = r.toString() + "-" + c.toString();

    if (!minesLocation.includes(id)) {
      minesLocation.push(id);
      minesLeft -= 1;
    }
  }
}


function startGame() {
  document.getElementById("mines-count").innerText = minesCount;
  document.getElementById("flag-button").addEventListener("click", setFlag);
  setMines();

  for (let r = 0; r < rows; r++) {
    let row = [];
    for (let c = 0; c < columns; c++) {
      let tile = document.createElement("div");
      tile.id = r.toString() + "-" + c.toString();
      tile.addEventListener("click", clickTile);
      document.getElementById("board").append(tile);
      row.push(tile);
    }
    board.push(row);
  }

  //console.log(board);
}

function setFlag() {
  if (flagEnabled) {
    flagEnabled = false;
    document.getElementById("flag-button").style.backgroundColor = "lightgray";
  } else {
    flagEnabled = true;
    document.getElementById("flag-button").style.backgroundColor = "darkgray";
  }
}

function clickTile() {
  if (gameOver || this.classList.contains("tile-clicked")) {
    return;
  }

  let tile = this;
  if (flagEnabled) {
    if (tile.innerText == "") {
      tile.innerText = "🚩";
    } else if (tile.innerText == "🚩") {
      tile.innerText = "";
    }
    return;
  }

  if (minesLocation.includes(tile.id)) {
    gameOver = true;
    revealMines();
    return;
  }


  let coords = tile.id.split("-");
  let r = parseInt(coords[0]);
  let c = parseInt(coords[1]);
  checkMine(r, c);

}

function revealMines() {
  for (let r = 0; r < rows; r++) {
    for (let c = 0; c < columns; c++) {
      let tile = board[r][c];
      if (minesLocation.includes(tile.id)) {
        tile.innerText = "πŸ’£";
        tile.style.backgroundColor = "red";
      }
    }
  }
}

function checkMine(r, c) {
  if (r < 0 || r >= rows || c < 0 || c >= columns) {
    return;
  }
  if (board[r][c].classList.contains("tile-clicked")) {
    return;
  }

  board[r][c].classList.add("tile-clicked");
  tilesClicked += 1;

  let minesFound = 0;

  minesFound += checkTile(r - 1, c - 1); //TOP LEFT
  minesFound += checkTile(r - 1, c); //TOP
  minesFound += checkTile(r - 1, c + 1); //TOP RIGHT

  minesFound += checkTile(r, c - 1); //LEFT
  minesFound += checkTile(r, c + 1); //RIGHT

  minesFound += checkTile(r + 1, c - 1); //BOTTOM LEFT
  minesFound += checkTile(r + 1, c); //BOTTOM 
  minesFound += checkTile(r + 1, c + 1); //BOTTOM RIGHT

  if (minesFound > 0) {
    board[r][c].innerText = minesFound;
    board[r][c].classList.add("x" + minesFound.toString());
  } else {
    checkMine(r - 1, c - 1); //TOP LEFT
    checkMine(r - 1, c); //TOP
    checkMine(r - 1, c + 1); //TOP RIGHT

    checkMine(r, c - 1); //LEFT
    checkMine(r, c + 1); //RIGHT

    checkMine(r + 1, c - 1); //BOTTOM LEFT
    checkMine(r + 1, c); //BOTTOM
    checkMine(r + 1, c + 1); //BOTTOM RIGHT
  }

  if (tilesClicked == rows * columns - minesCount) {
    document.getElementById("mines-count").innerText = "Cleared";
    gameOver = true;
  }

}


function checkTile(r, c) {
  if (r < 0 || r >= rows || c < 0 || c >= columns) {
    return 0;
  }
  if (minesLocation.includes(r.toString() + "-" + c.toString())) {
    return 1;
  }
  return 0;
}
body {
  font-family: Arial, Helvetica, sans-serif;
  font-weight: bold;
  text-align: center;
}

#board {
  width: 500px;
  height: 500px;
  border: 10px solid darkgray;
  margin: 0 auto;
  display: flex;
  flex-wrap: wrap;
}

#board div {
  --hue: 80.7deg;
  --sat: 65.1%;
  --lightness: 82%;
  background-color: hsl(var(--hue) var(--sat) var(--lightness));
  width: 48px;
  height: 48px;
  border: 1px solid black;
  /* text */
  font-size: 30px;
  display: flex;
  justify-content: center;
  align-items: center;
}

#board div:nth-child(odd) {
  --lightness: 70%;
}

#board .tile-clicked {
  --hue: 30deg;
  --sat: 57.4%;
  border: 1px solid #E5C29F;
}

.x1 {
  color: blue;
}

.x2 {
  color: green;
}

.x3 {
  color: red;
}

.x4 {
  color: navy;
}

.x5 {
  color: brown;
}

.x6 {
  color: teal;
}

.x7 {
  color: black;
}

.x8 {
  color: gray;
}

#flag-button {
  width: 100px;
  height: 50px;
  font-size: 30px;
  background-color: lightgray;
  border: none;
}
<h1>Mines: <span id="mines-count">0</span></h1>
<div id="board"></div>
<br>
<button id="flag-button">🚩</button>

This is not what we want though… We don’t want each row to line up with the previous, we want the tiles to alternate on each row as well as in each columnso our simple nth-child(odd) selector won’t be enough.

One method is to hardcode in the amount of tiles that will be on each row. We can keep the existing nth-child selector and build on top of it:

#board div:nth-child(odd) {
  --lightness: 70%;
}

#board div:is(:nth-child(20n+2), :nth-child(20n+4), :nth-child(20n+6), :nth-child(20n+8), :nth-child(20n+10)) {
  --lightness: 70%;
}

#board div:is(:nth-child(20n+1), :nth-child(20n+3), :nth-child(20n+5), :nth-child(20n+7), :nth-child(20n+9)) {
  --lightness: 82%;
}

These two super long selectors select even tiles in multiples of 20, and odd tiles in multiples of 20 respectively. We are basically overriding our even numbered rows with alternate alternates.

These lightness values ​​could be more clear so let’s put them into some nice custom properties. This is the CSS we have to add to get everything working:

#board div {
  --primary-lightness: 82%;
  --secondary-lightness: 70%;
  --hue: 80.7deg;
  --sat: 65.1%;
  --lightness: var(--primary-lightness);
  background-color: hsl(var(--hue) var(--sat) var(--lightness));
}

#board div:nth-child(odd) {
  --lightness: var(--secondary-lightness);
}

#board div:is(:nth-child(20n+2), :nth-child(20n+4), :nth-child(20n+6), :nth-child(20n+8), :nth-child(20n+10)) {
  --lightness: var(--secondary-lightness);
}

#board div:is(:nth-child(20n+1), :nth-child(20n+3), :nth-child(20n+5), :nth-child(20n+7), :nth-child(20n+9)) {
  --lightness: var(--primary-lightness);
}

#board .tile-clicked {
  --hue: 30deg;
  --sat: 57.4%;
}

Finally, here is a working snippet:

var board = [];
var rows = 10;
var columns = 10;

var minesCount = 10;
var minesLocation = [];

var tilesClicked = 0;
var flagEnabled = false;

var gameOver = false;

window.onload = function() {
  startGame();
}

function setMines() {
  let minesLeft = minesCount;
  while (minesLeft > 0) {
    let r = Math.floor(Math.random() * rows);
    let c = Math.floor(Math.random() * columns);
    let id = r.toString() + "-" + c.toString();

    if (!minesLocation.includes(id)) {
      minesLocation.push(id);
      minesLeft -= 1;
    }
  }
}


function startGame() {
  document.getElementById("mines-count").innerText = minesCount;
  document.getElementById("flag-button").addEventListener("click", setFlag);
  setMines();

  for (let r = 0; r < rows; r++) {
    let row = [];
    for (let c = 0; c < columns; c++) {
      let tile = document.createElement("div");
      tile.id = r.toString() + "-" + c.toString();
      tile.addEventListener("click", clickTile);
      document.getElementById("board").append(tile);
      row.push(tile);
    }
    board.push(row);
  }

  //console.log(board);
}

function setFlag() {
  if (flagEnabled) {
    flagEnabled = false;
    document.getElementById("flag-button").style.backgroundColor = "lightgray";
  } else {
    flagEnabled = true;
    document.getElementById("flag-button").style.backgroundColor = "darkgray";
  }
}

function clickTile() {
  if (gameOver || this.classList.contains("tile-clicked")) {
    return;
  }

  let tile = this;
  if (flagEnabled) {
    if (tile.innerText == "") {
      tile.innerText = "🚩";
    } else if (tile.innerText == "🚩") {
      tile.innerText = "";
    }
    return;
  }

  if (minesLocation.includes(tile.id)) {
    gameOver = true;
    revealMines();
    return;
  }


  let coords = tile.id.split("-");
  let r = parseInt(coords[0]);
  let c = parseInt(coords[1]);
  checkMine(r, c);

}

function revealMines() {
  for (let r = 0; r < rows; r++) {
    for (let c = 0; c < columns; c++) {
      let tile = board[r][c];
      if (minesLocation.includes(tile.id)) {
        tile.innerText = "πŸ’£";
        tile.style.backgroundColor = "red";
      }
    }
  }
}

function checkMine(r, c) {
  if (r < 0 || r >= rows || c < 0 || c >= columns) {
    return;
  }
  if (board[r][c].classList.contains("tile-clicked")) {
    return;
  }

  board[r][c].classList.add("tile-clicked");
  tilesClicked += 1;

  let minesFound = 0;

  minesFound += checkTile(r - 1, c - 1); //TOP LEFT
  minesFound += checkTile(r - 1, c); //TOP
  minesFound += checkTile(r - 1, c + 1); //TOP RIGHT

  minesFound += checkTile(r, c - 1); //LEFT
  minesFound += checkTile(r, c + 1); //RIGHT

  minesFound += checkTile(r + 1, c - 1); //BOTTOM LEFT
  minesFound += checkTile(r + 1, c); //BOTTOM 
  minesFound += checkTile(r + 1, c + 1); //BOTTOM RIGHT

  if (minesFound > 0) {
    board[r][c].innerText = minesFound;
    board[r][c].classList.add("x" + minesFound.toString());
  } else {
    checkMine(r - 1, c - 1); //TOP LEFT
    checkMine(r - 1, c); //TOP
    checkMine(r - 1, c + 1); //TOP RIGHT

    checkMine(r, c - 1); //LEFT
    checkMine(r, c + 1); //RIGHT

    checkMine(r + 1, c - 1); //BOTTOM LEFT
    checkMine(r + 1, c); //BOTTOM
    checkMine(r + 1, c + 1); //BOTTOM RIGHT
  }

  if (tilesClicked == rows * columns - minesCount) {
    document.getElementById("mines-count").innerText = "Cleared";
    gameOver = true;
  }

}


function checkTile(r, c) {
  if (r < 0 || r >= rows || c < 0 || c >= columns) {
    return 0;
  }
  if (minesLocation.includes(r.toString() + "-" + c.toString())) {
    return 1;
  }
  return 0;
}
body {
  font-family: Arial, Helvetica, sans-serif;
  font-weight: bold;
  text-align: center;
}

#board {
  width: 500px;
  height: 500px;
  border: 10px solid darkgray;
  margin: 0 auto;
  display: flex;
  flex-wrap: wrap;
}

#board div {
  --primary-lightness: 82%;
  --secondary-lightness: 70%;
  --hue: 80.7deg;
  --sat: 65.1%;
  --lightness: var(--primary-lightness);
  background-color: hsl(var(--hue) var(--sat) var(--lightness));
  width: 48px;
  height: 48px;
  border: 1px solid black;
  /* text */
  font-size: 30px;
  display: flex;
  justify-content: center;
  align-items: center;
}

#board div:nth-child(odd) {
  --lightness: var(--secondary-lightness);
}

#board div:is(:nth-child(20n+2), :nth-child(20n+4), :nth-child(20n+6), :nth-child(20n+8), :nth-child(20n+10)) {
  --lightness: var(--secondary-lightness);
}

#board div:is(:nth-child(20n+1), :nth-child(20n+3), :nth-child(20n+5), :nth-child(20n+7), :nth-child(20n+9)) {
  --lightness: var(--primary-lightness);
}

#board .tile-clicked {
  --hue: 30deg;
  --sat: 57.4%;
  border: 1px solid #E5C29F;
}

.x1 {
  color: blue;
}

.x2 {
  color: green;
}

.x3 {
  color: red;
}

.x4 {
  color: navy;
}

.x5 {
  color: brown;
}

.x6 {
  color: teal;
}

.x7 {
  color: black;
}

.x8 {
  color: gray;
}

#flag-button {
  width: 100px;
  height: 50px;
  font-size: 30px;
  background-color: lightgray;
  border: none;
}
<h1>Mines: <span id="mines-count">0</span></h1>
<div id="board"></div>
<br>
<button id="flag-button">🚩</button>

Leave a Comment