1
\$\begingroup\$

I made a Minesweeper game using HTML, CSS and JavaScript and would like to ask for advice and feedback specifically on the code.
Here are some questions to review:

  • Is the use of HTML semantics correct? I would like to know if I am using HTML tags properly and if there is any way to improve the semantic structure of the code.
  • Is there any way to improve the organization and reusability of the CSS code, any tips to make it more modular and easy to maintain?
  • Is there any way to optimize JavaScript code to make it more efficient and elegant? Any advice on best practices in terms of architecture or code writing?

Code:

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>Minesweeper</title>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" href="assets/css/normalize.css">
    <link rel="stylesheet" href="assets/css/style.css">
    <link rel="icon" href="assets/img/favicon.png">
    <script src="assets/js/index.js" defer></script>
  </head>
  <body>
    <section class="minesweeper">
      <h1>Minesweeper</h1>

      <div class="level">
        <button class="btn active" id="beginner">Beginner</button>
        <button class="btn" id="intermediate">Intermediate</button>
        <button class="btn" id="advanced">Advanced</button>
        <button class="btn" id="new-game">New game</button>
      </div>

      <p class="info">🚩 <span id="remaining-flags">10</span></p>

      <table id="board"></table>
    </section>
  </body>
</html>
body {
  background-color: #55ddff;
  font-family: Arial, sans-serif;
}

h1 {
  text-align: center;
  color: #263be8;
  margin-bottom: 0;
}

.minesweeper {
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  gap: 10px;
}

.btn {
  background-color: #0000ff;
  border: 0;
  color: #fff;
  cursor: pointer;
  font-weight: bold;
  line-height: normal;
  border-radius: 5px;
  padding: 5px;
  margin-left: 8px;
}

.active {
  background-color: red;
}

.info {
  color: red;
  font-weight: bold;
  font-size: 20px;
}

table {
  border-spacing: 0px;
}

td {
  padding: 0;
  width: 25px;
  height: 25px;
  background-color: #fff;
  border: 1px solid #a1a1a1;
  text-align: center;
  line-height: 20px;
  font-weight: bold;
  font-size: 18px;
}

.mine {
  background: #eeeeee url(../img/mine.png) no-repeat center;
  background-size: cover;
}

.flag {
  background: #eeeeee url(../img/flag.png) no-repeat center;
  background-size: cover;
}

.zero {
  background-color: #eeeeee;
}

.one {
  background-color: #eeeeee;
  color: #0332fe;
}

.two {
  background-color: #eeeeee;
  color: #019f02;
}

.three {
  background-color: #eeeeee;
  color: #ff2600;
}

.four {
  background-color: #eeeeee;
  color: #93208f;
}

.five {
  background-color: #eeeeee;
  color: #ff7f29;
}

.six {
  background-color: #eeeeee;
  color: #ff3fff;
}

.seven {
  background-color: #eeeeee;
  color: #53b8b4;
}

.eight {
  background-color: #eeeeee;
  color: #22ee0f;
}
const BOARD = document.getElementById("board");
const REMAINING_FLAGS_ELEMENT = document.getElementById("remaining-flags");
const NEW_GAME_BUTTON = document.getElementById("new-game");
const LEVEL_BUTTONS = {
  beginner: document.getElementById("beginner"),
  intermediate: document.getElementById("intermediate"),
  advanced: document.getElementById("advanced"),
};
const LEVEL_SETTINGS = {
  beginner: { rows: 9, cols: 9, mines: 10 },
  intermediate: { rows: 16, cols: 16, mines: 40 },
  advanced: { rows: 16, cols: 30, mines: 99 },
};

let currentLevel = "beginner";
let currentLevelConfig = LEVEL_SETTINGS[currentLevel];
let rows = currentLevelConfig.rows;
let columns = currentLevelConfig.cols;
let remainingMines = LEVEL_SETTINGS[currentLevel].mines;
let remainingFlags = remainingMines;
let totalCellsRevealed = 0;
let correctFlagsCount = 0;
let boardArray = [];
let gameFinish;

/**
 * Creates the game board by generating the HTML table structure.
 * Initializes the game board array.
 * Updates the remaining flags count displayed on the webpage.
 * Places the mines randomly on the board.
 * Counts the number of adjacent mines for each cell.
 */
function createBoard() {
  const BOARD_FRAGMENT = document.createDocumentFragment();

  BOARD.textContent = "";

  for (let i = 0; i < rows; i++) {
    const ROW = document.createElement("tr");
    boardArray[i] = [];

    for (let j = 0; j < columns; j++) {
      const CELL = document.createElement("td");
      boardArray[i][j] = 0;
      ROW.appendChild(CELL);
    }

    BOARD_FRAGMENT.appendChild(ROW);
  }

  BOARD.appendChild(BOARD_FRAGMENT);

  REMAINING_FLAGS_ELEMENT.textContent = remainingFlags;

  placeMines();
  countAdjacentMines();
}

/**
 * Randomly places the mines on the game board.
 * Updates the "boardArray" with the "mine" value for each mine location.
 */
function placeMines() {
  let minesToPlace = remainingMines;

  while (minesToPlace > 0) {
    const RANDOM_ROW = Math.floor(Math.random() * rows);
    const RANDOM_COL = Math.floor(Math.random() * columns);

    if (boardArray[RANDOM_ROW][RANDOM_COL] !== "mine") {
      boardArray[RANDOM_ROW][RANDOM_COL] = "mine";
      minesToPlace--;
    }
  }
}

/**
 * Counts the number of adjacent mines for each non-mine cell on the game board.
 * Updates the "boardArray" with the corresponding mine count for each cell.
 */
function countAdjacentMines() {
  for (let row = 0; row < rows; row++) {
    for (let col = 0; col < columns; col++) {
      if (boardArray[row][col] !== "mine") {
        let minesCount = 0;

        for (let i = row - 1; i <= row + 1; i++) {
          for (let j = col - 1; j <= col + 1; j++) {
            const VALID_ROW = i >= 0 && i < rows;
            const VALID_COL = j >= 0 && j < columns;

            if (VALID_ROW && VALID_COL && boardArray[i][j] === "mine") {
              minesCount++;
            }
          }
        }

        boardArray[row][col] = minesCount;
      }
    }
  }
}

/**
 * Reveals the content of a cell and handles game logic.
 *
 * @param {number} row - The row index of the cell.
 * @param {number} col - The column index of the cell.
 */
function revealCell(row, col) {
  const CELL = BOARD.rows[row].cells[col];

  if (CELL.classList.contains("flag") || CELL.textContent || gameFinish) return;

  if (boardArray[row][col] === "mine") {
    gameFinish = true;
    revealMines();
    alert("Game over! You hit a mine.");
  } else if (boardArray[row][col] === 0) {
    revealAdjacentsCells(row, col);
  } else {
    const NUMBER_CLASS = getNumberClass(boardArray[row][col]);
    CELL.textContent = boardArray[row][col];
    CELL.classList.add(NUMBER_CLASS);
  }

  totalCellsRevealed++;

  if (checkWin()) {
    gameFinish = true;
    alert("You win!");
    return;
  }
}

/**
 * Reveals adjacents cells surrounding the specified cell.
 *
 * @param {number} row - The row index of the cell.
 * @param {number} col - The column index of the cell.
 */
function revealAdjacentsCells(row, col) {
  const CELL = BOARD.rows[row].cells[col];

  if (CELL.textContent) return;

  CELL.classList.add("zero");

  for (let i = row - 1; i <= row + 1; i++) {
    for (let j = col - 1; j <= col + 1; j++) {
      const VALID_ROW = i >= 0 && i < rows;
      const VALID_COL = j >= 0 && j < columns;

      if (VALID_ROW && VALID_COL && !(i === row && j === col)) {
        const CELL = BOARD.rows[i].cells[j];
        if (!CELL.classList.value) revealCell(i, j);
      }
    }
  }
}

/**
 * Reveals all the mines on the game board.
 * Adds the "mine" class to the HTML elements representing mine cells.
 */
function revealMines() {
  for (let i = 0; i < rows; i++) {
    for (let j = 0; j < columns; j++) {
      if (boardArray[i][j] === "mine") {
        const MINE_CELL = BOARD.rows[i].cells[j];
        MINE_CELL.classList.add("mine");
      }
    }
  }
}

/**
 * Returns the CSS class name for a given number.
 *
 * @param {number} number - The number of adjacent mines.
 * @returns {string} The CSS class name for the number.
 */
function getNumberClass(number) {
  switch (number) {
    case 1:
      return "one";
    case 2:
      return "two";
    case 3:
      return "three";
    case 4:
      return "four";
    case 5:
      return "five";
    case 6:
      return "six";
    case 7:
      return "seven";
    case 8:
      return "eight";
    default:
      return "";
  }
}

/**
 * Changes the game level to the specified level.
 *
 * @param {string} level - The level to change to.
 */
function changeLevel(level) {
  if (currentLevel === level) return;

  gameFinish = false;
  LEVEL_BUTTONS[currentLevel].classList.remove("active");
  currentLevel = level;
  LEVEL_BUTTONS[currentLevel].classList.add("active");

  currentLevelConfig = LEVEL_SETTINGS[currentLevel];
  rows = currentLevelConfig.rows;
  columns = currentLevelConfig.cols;
  remainingMines = currentLevelConfig.mines;
  remainingFlags = remainingMines;
  REMAINING_FLAGS_ELEMENT.textContent = remainingFlags;

  createBoard();
}

/**
 * Toggles the flag on a cell when the player right-clicks on it.
 *
 * @param {HTMLElement} cell - The HTML element representing the cell.
 */
function addFlagToCell(cell) {
  if (cell.classList.contains("zero") || cell.textContent || gameFinish) return;

  const HAS_FLAG = cell.classList.contains("flag");
  const ROW = cell.parentNode.rowIndex;
  const COL = cell.cellIndex;

  cell.classList.toggle("flag", !HAS_FLAG);
  remainingFlags += HAS_FLAG ? 1 : -1;
  REMAINING_FLAGS_ELEMENT.textContent = remainingFlags;

  if (!HAS_FLAG && boardArray[ROW][COL] === "mine") correctFlagsCount++;

  if (checkWin()) {
    gameFinish = true;
    alert("You win!");
    return;
  }
}

/**
 * Checks if the player has won the game.
 * Returns true if all non-mine cells have been revealed and all flags are correctly placed on mine cells.
 *
 * @returns {boolean} True if the player has won, false otherwise.
 */
function checkWin() {
  return (
    totalCellsRevealed === rows * columns - remainingMines &&
    correctFlagsCount === remainingMines
  );
}

/**
 * Resets the game by resetting the game variables and creating a new board.
 */
function newGame() {
  gameFinish = false;
  correctFlagsCount = 0;
  totalCellsRevealed = 0;
  remainingMines = currentLevelConfig.mines;
  remainingFlags = remainingMines;
  REMAINING_FLAGS_ELEMENT.textContent = remainingFlags;

  createBoard();
}

document.addEventListener("click", (event) => {
  const TARGET = event.target;

  if (TARGET.tagName === "TD") {
    const ROW = TARGET.parentNode.rowIndex;
    const COL = TARGET.cellIndex;
    revealCell(ROW, COL);
  } else if (TARGET === LEVEL_BUTTONS["beginner"]) {
    changeLevel("beginner");
  } else if (TARGET === LEVEL_BUTTONS["intermediate"]) {
    changeLevel("intermediate");
  } else if (TARGET === LEVEL_BUTTONS["advanced"]) {
    changeLevel("advanced");
  } else if (TARGET === NEW_GAME_BUTTON) {
    newGame();
  }
});

document.addEventListener("contextmenu", (event) => {
  const TARGET = event.target;

  if (TARGET.tagName === "TD") {
    event.preventDefault();
    addFlagToCell(TARGET);
  }
});

createBoard();

Github link
Live host

\$\endgroup\$

1 Answer 1

1
\$\begingroup\$

bugs

  • Clicking on revealed zero cells would increase totalCellsRevealed unexpected.

semantics

  • It is a good idea to add type="button" to your buttons.
  • Each clickable cell should be a <button type="button"> not <td>. I would expect a <button> is inserted into the <td>.
  • You can add role="grid" to the table and role="row" to tr, role="gridcell" to td.
  • You can add aria-pressed to buttons for selecting levels.

css

  • Your css works. just personal preference, I would avoid using zero to eight as class names like this.

javascript

  • Getting classname for number could be simplified into ["", "one", /* ... */, "eight"][number] || "".
  • You could add data- attribute to the three level buttons, so JavaScript can only read the dataset to get the difficulty level.
\$\endgroup\$
7
  • \$\begingroup\$ Thanks for the tips! What do you propose to do instead of using 8 classes for the 8 number styles? Can you think of anything to fix the bug in the revealAdjacentCells() function? I didn't find the solution and that's why I patched it with the CELL.textContent condition in revealCell(). \$\endgroup\$ Commented Jul 17, 2023 at 21:50
  • \$\begingroup\$ @LucioMazzini Instead of maintain the reveal status in DOM, you may also maintain it in JavaScript so it could be easier to detect if some cell is already revealed. Or, you may also want to add an extra classname to maintain the status so you do not need to tracking many different attributes. The third way could be tricky, you can write 0 as textContent for empty cells as well and make the text transparent or have same color as background. \$\endgroup\$
    – tsh
    Commented Jul 18, 2023 at 2:12
  • \$\begingroup\$ @LucioMazzini I would prefer cell-0, cell-1 classname instead. So i just write "cell-" + number instead of a dictionary lookup. But this is really not some matters. \$\endgroup\$
    – tsh
    Commented Jul 18, 2023 at 2:13
  • 1
    \$\begingroup\$ You write "I would expect a <button> is inserted into the <td>" This is very poor advice. 480 additional elements (buttons) represent a significant memory and processing overhead, for a somewhat dubious sematic improvement (cells act like checkboxes not buttons). Rather use ARIA roles property for each cell <td>. In this case role="checkbox" would be most appropriate. \$\endgroup\$
    – Blindman67
    Commented Jul 18, 2023 at 4:54
  • 1
    \$\begingroup\$ It's not about "performance", rather power use, battery life and device resources (RAM / Cache). Always aim to keep the client's resource use as low as possible. Checkbox is the correct role as it has two visual/selectable states. \$\endgroup\$
    – Blindman67
    Commented Jul 18, 2023 at 9:19

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.