Crear un juego de Minesweeper con JavaScript

JavaScriptBeginner
Practicar Ahora

Introducción

Creo que todos han jugado alguna vez al clásico juego del Minesweeper. Tiene reglas simples pero es altamente adictivo. ¿Alguna vez has pensado en desarrollarlo tú mismo? Hoy, crearemos una versión web del Minesweeper. Primero, echemos un vistazo a una captura de pantalla de la interfaz.

👀 Vista previa

Vista previa de la interfaz del juego del Minesweeper

🎯 Tareas

En este proyecto, aprenderás:

  • Cómo diseñar el algoritmo del juego para el Minesweeper
  • Cómo crear la estructura de archivos para el proyecto
  • Cómo implementar el diseño de la página utilizando HTML y CSS
  • Cómo dibujar la cuadrícula utilizando JavaScript
  • Cómo agregar eventos de clic a las celdas para manejar la jugabilidad
  • Cómo implementar funciones de control del juego como el inicio y el final del juego

🏆 Logros

Después de completar este proyecto, podrás:

  • Diseñar e implementar algoritmos de juego
  • Crear la estructura de archivos para un proyecto web
  • Utilizar HTML y CSS para crear diseños de página
  • Utilizar JavaScript para dibujar cuadrículas y manejar eventos
  • Implementar funciones de control del juego

Preparación para el desarrollo

Antes de comenzar el desarrollo, primero diseñemos el algoritmo del juego.

Las reglas del juego del Minesweeper son simples:

  • Hay varios cuadrados en el tablero de juego. Cada cuadrado contiene un número (un espacio en blanco indica que el número es 0) o una bomba. El número en el cuadrado representa la cantidad de bombas en los cuadrados adyacentes. La tarea del jugador es encontrar los cuadrados con números en el menor tiempo posible.
  • Excepto los cuadrados en los bordes, cada cuadrado tiene 8 cuadrados vecinos: arriba, abajo, izquierda, derecha y 4 cuadrados diagonales. Por lo tanto, el rango de números es de 0 a 8.

Entonces, nuestro algoritmo es el siguiente:

  • Basado en el nivel de dificultad seleccionado por el usuario (hay tres niveles: principiante, intermedio y avanzado, y a medida que aumenta el nivel, hay más bombas y cuadrados), generar aleatoriamente una cierta cantidad de bombas y colocarlas aleatoriamente en los cuadrados. Luego, recorrer los cuadrados, calcular el número en cada cuadrado y marcarlo en el cuadrado. Cuando el jugador hace clic izquierdo en un cuadrado, se muestra el contenido del cuadrado (si el cuadrado contiene una bomba, el desafío falla y el juego termina), y cuando el jugador hace clic derecho en un cuadrado, el cuadrado se marca como una bomba. El desafío es exitoso solo cuando todas las bombas están correctamente marcadas y todos los cuadrados sin bombas están abiertos, y el juego termina.

Consejo útil: Dado que el rango de números en los cuadrados es de 0 a 8, podemos considerar marcar los números en los cuadrados donde hay bombas como 9 para facilitar el cálculo.

✨ Revisar Solución y Practicar

Estructura de archivos del proyecto

Primero, debemos crear la siguiente estructura de archivos en la ruta ~/proyecto:

~/proyecto
 |__ index.html
 |__ index.css
 |__ index.js
 |__ jms.js
 |__ mine.png
 |__ flag.png

Las imágenes mine.png y flag.png en el directorio del proyecto son las imágenes de la mina y la bandera respectivamente.

✨ Revisar Solución y Practicar

Diseño de página

Primero, necesitamos un panel para mostrar la información del juego, incluyendo el número de minas restantes, el tiempo transcurrido, el nivel de dificultad, etc. Dado que el número de cuadrados no es fijo, no dibujaremos los cuadrados por ahora y los dibujaremos en el código JS en su lugar.

Crea un archivo index.html y agrega el siguiente código:

<!-- index.html -->
<!doctype html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>Minesweeper con JavaScript</title>
    <link
      href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css"
      rel="stylesheet"
    />
    <link rel="stylesheet" type="text/css" href="index.css" />
  </head>
  <body class="bg-gray-100">
    <div
      id="JMS_main"
      class="container mx-auto p-4 bg-white shadow-lg rounded-lg"
    >
      <table id="landmine" class="mx-auto"></table>
      <div id="operation" class="text-center">
        <div class="text-lg text-red-600">
          Número de minas restantes:
          <span class="font-semibold" id="landMineCount">0</span>
        </div>
        <div class="text-lg text-orange-500">
          Tiempo transcurrido:
          <span class="font-semibold" id="costTime">0</span> s
        </div>
        <fieldset class="my-4">
          <legend class="text-lg font-semibold">
            Selección de dificultad:
          </legend>
          <input
            type="radio"
            name="level"
            id="llevel"
            checked="checked"
            value="10"
            class="mr-2"
          />
          <label for="llevel">Principiante (10*10) </label><br />
          <input
            type="radio"
            name="level"
            id="mlevel"
            value="15"
            class="mr-2"
          />
          <label for="mlevel">Intermedio (15*15) </label><br />
          <input
            type="radio"
            name="level"
            id="hlevel"
            value="20"
            class="mr-2"
          />
          <label for="hlevel">Avanzado (20*20) </label><br />
        </fieldset>
        <button
          id="begin"
          class="px-4 py-2 bg-blue-500 text-white font-semibold rounded-lg hover:bg-blue-700"
        >
          Iniciar juego</button
        ><br />
        <div class="text-left mt-4">
          <div class="font-semibold">Sugerencia:</div>
          <ul class="list-disc ml-6">
            <li>
              1. Haz clic en "Iniciar juego" para iniciar el temporizador del
              juego.
            </li>
            <li>
              2. Haciendo clic en "Iniciar juego" durante el juego se comenzará
              un nuevo juego.
            </li>
          </ul>
        </div>
      </div>
    </div>
    <script src="jms.js"></script>
    <script src="index.js"></script>
  </body>
</html>
✨ Revisar Solución y Practicar

Estilos de diseño de página

A continuación, necesitamos ajustar la posición de la información del juego en el panel y agregar algunos estilos. Agrega el siguiente código a index.css y guarda:

@import "tailwindcss/base";
@import "tailwindcss/components";
@import "tailwindcss/utilities";

.container {
  max-width: 600px;
}
table {
  background: #ccc;
  margin: 0 auto;
}
td {
  border: 2px outset #eee;
  font-size: 20px;
  width: 32px;
  height: 32px;
  text-align: center;
  cursor: pointer;
}
button {
  padding: 2px 10px;
  margin: 5px;
  font-size: 20px;
  cursor: pointer;
}

.clicked-cell {
  background-color: #a0aec0;
  color: #fff;
  font-weight: bold;
}

.landMine {
  background-image: url(mine.png);
  background-position: center;
  background-repeat: no-repeat;
}

.flag {
  background-image: url(flag.png);
  background-position: center;
  background-repeat: no-repeat;
}

Después de completar este paso, abre index.html en tu navegador.

Interfaz del juego del Minesweeper
Interfaz del juego del Minesweeper
✨ Revisar Solución y Practicar

Cuadrícula de dibujo

Después de completar los pasos anteriores, necesitamos dibujar la cuadrícula. Para que el código sea más claro, separamos la parte de implementación del juego y la parte de llamada. La parte de implementación del juego se coloca en jms.js, que está en el mismo directorio que index.html, y la parte de llamada del juego se coloca en index.js, también en el mismo directorio.

Para dibujar la cuadrícula, necesitamos pasar algunos parámetros, como el id de la tabla donde se colocará la cuadrícula y el número de celdas (representado por el número de filas y columnas). Además, se deben inicializar otros datos relacionados con el juego.

Parte de jms.js

Agrega el siguiente código a jms.js y guárdalo:

(function () {
  // Inicializar el objeto Minesweeper e inicializar los datos
  var JMS = function (
    id,
    rowCount,
    colCount,
    minLandMineCount,
    maxLandMineCount
  ) {
    if (!(this instanceof JMS))
      return new JMS(
        id,
        rowCount,
        colCount,
        minLandMineCount,
        maxLandMineCount
      );
    this.doc = document;
    this.table = this.doc.getElementById(id); // Tabla para dibujar la cuadrícula
    this.cells = this.table.getElementsByTagName("td"); // Celdas
    this.rowCount = rowCount || 10; // Número de filas en la cuadrícula
    this.colCount = colCount || 10; // Número de columnas en la cuadrícula
    this.landMineCount = 0; // Número de minas
    this.markLandMineCount = 0; // Número de minas marcadas
    this.minLandMineCount = minLandMineCount || 10; // Número mínimo de minas
    this.maxLandMineCount = maxLandMineCount || 20; // Número máximo de minas
    this.arrs = []; // Arreglo correspondiente a las celdas
    this.beginTime = null; // Tiempo de inicio del juego
    this.endTime = null; // Tiempo de fin del juego
    this.currentSetpCount = 0; // Número de pasos dados
    this.endCallBack = null; // Función de devolución de llamada cuando el juego termina
    this.landMineCallBack = null; // Función de devolución de llamada para actualizar el número restante de minas cuando se marca una mina
    this.doc.oncontextmenu = function () {
      // Deshabilitar el menú contextual
      return false;
    };
    this.drawMap();
  };

  // Crear celdas en el prototipo de JMS
  JMS.prototype = {
    // Dibujar la cuadrícula
    drawMap: function () {
      var tds = [];
      // Para la compatibilidad con navegadores
      if (
        window.ActiveXObject &&
        parseInt(navigator.userAgent.match(/msie ([\d.]+)/i)[1]) < 8
      ) {
        // Crear un nuevo archivo de estilo CSS
        var css = "#JMS_main table td{background-color:#888;}",
          // Obtener la etiqueta head
          head = this.doc.getElementsByTagName("head")[0],
          // Crear una etiqueta de estilo
          style = this.doc.createElement("style");
        style.type = "text/css";
        if (style.styleSheet) {
          // Asignar el estilo CSS a la etiqueta de estilo
          style.styleSheet.cssText = css;
        } else {
          // Crear un nodo en la etiqueta de estilo
          style.appendChild(this.doc.createTextNode(css));
        }
        // Adjuntar la etiqueta de estilo como etiqueta hija de la etiqueta head
        head.appendChild(style);
      }
      // Bucle para crear la tabla
      for (var i = 0; i < this.rowCount; i++) {
        tds.push("<tr>");
        for (var j = 0; j < this.colCount; j++) {
          tds.push("<td id='m_" + i + "_" + j + "'></td>");
        }
        tds.push("</tr>");
      }
      this.setTableInnerHTML(this.table, tds.join(""));
    },
    // Agregar HTML a la tabla
    setTableInnerHTML: function (table, html) {
      if (navigator && navigator.userAgent.match(/msie/i)) {
        // Crear un div dentro del documento propietario de la tabla
        var temp = table.ownerDocument.createElement("div");
        // Crear el contenido del tbody de la tabla
        temp.innerHTML = "<table><tbody>" + html + "</tbody></table>";
        if (table.tBodies.length == 0) {
          var tbody = document.createElement("tbody");
          table.appendChild(tbody);
        }
        table.replaceChild(temp.firstChild.firstChild, table.tBodies[0]);
      } else {
        table.innerHTML = html;
      }
    }
  };

  window.JMS = JMS;
})();

El código anterior incluye algunos códigos para hacer que sea compatible con navegadores IE, que se pueden ignorar.

Parte de index.js

En el código de llamada en index.js, necesitamos enlazar el evento de los botones de selección de dificultad y luego llamar a la JMS que definimos anteriormente para comenzar a dibujar la cuadrícula.

Agrega el siguiente código a index.js y guárdalo:

var jms = null,
  timeHandle = null;
window.onload = function () {
  var radios = document.getElementsByName("level");
  for (var i = 0, j = radios.length; i < j; i++) {
    radios[i].onclick = function () {
      if (jms != null)
        if (jms.landMineCount > 0)
          if (!confirm("¿Está seguro de que desea terminar el juego actual?"))
            return false;
      var value = this.value;
      init(value, value, (value * value) / 5 - value, (value * value) / 5);
      document.getElementById("JMS_main").style.width =
        value * 40 + 180 + 60 + "px";
    };
  }
  init(10, 10);
};

function init(rowCount, colCount, minLandMineCount, maxLandMineCount) {
  var doc = document,
    landMineCountElement = doc.getElementById("landMineCount"),
    timeShow = doc.getElementById("costTime"),
    beginButton = doc.getElementById("begin");
  if (jms != null) {
    clearInterval(timeHandle);
    timeShow.innerHTML = 0;
    landMineCountElement.innerHTML = 0;
  }
  jms = JMS("landmine", rowCount, colCount, minLandMineCount, maxLandMineCount);
}

Luego abre index.html en el navegador y se debería mostrar la cuadrícula. El efecto es el siguiente:

Visualización de la cuadrícula del juego del Minesweeper

Haga clic en la selección de dificultad en la derecha para ver cómo cambia el número de celdas.

✨ Revisar Solución y Practicar

Inicialización del juego

Ahora, comencemos con la inicialización del juego, lo que principalmente implica tres pasos:

  1. Inicializar todas las celdas (representadas por un arreglo en el código) a 0.
  2. Generar un número aleatorio de minas y colocarlas aleatoriamente en el arreglo, estableciendo el valor del elemento del arreglo en 9.
  3. Calcular los números en las otras celdas y almacenar los valores en el arreglo.

Agrega el siguiente código dentro de JMS.prototype en jms.js:

// Inicialización: establecer el valor predeterminado del arreglo en 0 y determinar el número de minas
init: function () {
    for (var i = 0; i < this.rowCount; i++) {
        this.arrs[i] = [];
        for (var j = 0; j < this.colCount; j++) {
            this.arrs[i][j] = 0;
        }
    }
    this.landMineCount = this.selectFrom(this.minLandMineCount, this.maxLandMineCount);
    this.markLandMineCount = 0;
    this.beginTime = null;
    this.endTime = null;
    this.currentSetpCount = 0;
},
// Establecer el valor de los elementos del arreglo que contienen minas en 9
landMine: function () {
    var allCount = this.rowCount * this.colCount - 1,
        tempArr = {};
    for (var i = 0; i < this.landMineCount; i++) {
        var randomNum = this.selectFrom(0, allCount),
            rowCol = this.getRowCol(randomNum);
        if (randomNum in tempArr) {
            i--;
            continue;
        }
        this.arrs[rowCol.row][rowCol.col] = 9;
        tempArr[randomNum] = randomNum;
    }
},
// Calcular los números en las otras celdas
calculateNoLandMineCount: function () {
    for (var i = 0; i < this.rowCount; i++) {
        for (var j = 0; j < this.colCount; j++) {
            if (this.arrs[i][j] == 9)
                continue;
            if (i > 0 && j > 0) {
                if (this.arrs[i - 1][j - 1] == 9)
                    this.arrs[i][j]++;
            }
            if (i > 0) {
                if (this.arrs[i - 1][j] == 9)
                    this.arrs[i][j]++;
            }
            if (i > 0 && j < this.colCount - 1) {
                if (this.arrs[i - 1][j + 1] == 9)
                    this.arrs[i][j]++;
            }
            if (j > 0) {
                if (this.arrs[i][j - 1] == 9)
                    this.arrs[i][j]++;
            }
            if (j < this.colCount - 1) {
                if (this.arrs[i][j + 1] == 9)
                    this.arrs[i][j]++;
            }
            if (i < this.rowCount - 1 && j > 0) {
                if (this.arrs[i + 1][j - 1] == 9)
                    this.arrs[i][j]++;
            }
            if (i < this.rowCount - 1) {
                if (this.arrs[i + 1][j] == 9)
                    this.arrs[i][j]++;
            }
            if (i < this.rowCount - 1 && j < this.colCount - 1) {
                if (this.arrs[i + 1][j + 1] == 9)
                    this.arrs[i][j]++;
            }
        }
    }
},
// Obtener un número aleatorio
selectFrom: function (iFirstValue, iLastValue) {
    var iChoices = iLastValue - iFirstValue + 1;
    return Math.floor(Math.random() * iChoices + iFirstValue);
},
// Encontrar los números de fila y columna basados en el valor
getRowCol: function (val) {
    return {
        row: parseInt(val / this.colCount),
        col: val % this.colCount
    };
},
✨ Revisar Solución y Practicar

Agregar eventos de clic a las celdas

Ahora, es necesario agregar eventos de clic a las celdas. Al hacer clic con el botón izquierdo, mostrar el número en la celda (si es una mina, el juego terminará). Al hacer clic con el botón derecho, marcarla como mina.

Además, cuando se hace clic por primera vez en la celda (por lo general, implica suerte), si hay un área en blanco alrededor de ella, debe expandirse directamente.

Agrega el siguiente código al JMS.prototype en jms.js:

// Obtener elemento
$: function (id) {
    return this.doc.getElementById(id);
},
// Vincular eventos de clic (izquierdo y derecho) a cada celda
bindCells: function () {
    var self = this;
    for (var i = 0; i < this.rowCount; i++) {
        for (var j = 0; j < this.colCount; j++) {
            (function (row, col) {
                self.$("m_" + i + "_" + j).onmousedown = function (e) {
                    e = e || window.event;
                    var mouseNum = e.button;
                    var className = this.className;
                    if (mouseNum == 2) {
                        if (className == "flag") {
                            this.className = "";
                            self.markLandMineCount--;
                        } else {
                            this.className = "flag";
                            self.markLandMineCount++;
                        }
                        if (self.landMineCallBack) {
                            self.landMineCallBack(self.landMineCount - self.markLandMineCount);
                        }
                    } else if (className!= "flag") {
                        self.openBlock.call(self, this, row, col);
                    }
                };
            })(i,j);
        }
    }
},
// Expandir el área sin minas
showNoLandMine: function (x, y) {
    for (var i = x - 1; i < x + 2; i++)
        for (var j = y - 1; j < y + 2; j++) {
            if (!(i == x && j == y)) {
                var ele = this.$("m_" + i + "_" + j);
                if (ele && ele.className == "") {
                    this.openBlock.call(this, ele, i, j);
                }
            }
        }
},
// Mostrar
openBlock: function (obj, x, y) {
    if (this.arrs[x][y]!= 9) {
        this.currentSetpCount++;
        if (this.arrs[x][y]!= 0) {
            obj.innerHTML = this.arrs[x][y];
        }
        obj.className = "clicked-cell";
        if (this.currentSetpCount + this.landMineCount == this.rowCount * this.colCount) {
            this.success();
        }
        obj.onmousedown = null;
        if (this.arrs[x][y] == 0) {
            this.showNoLandMine.call(this, x, y);
        }
    } else {
        this.failed();
    }
},
// Mostrar minas
showLandMine: function () {
    for (var i = 0; i < this.rowCount; i++) {
        for (var j = 0; j < this.colCount; j++) {
            if (this.arrs[i][j] == 9) {
                this.$("m_" + i + "_" + j).className = "landMine";
            }
        }
    }
},
// Mostrar información de todas las celdas
showAll: function () {
    for (var i = 0; i < this.rowCount; i++) {
        for (var j = 0; j < this.colCount; j++) {
            if (this.arrs[i][j] == 9) {
                this.$("m_" + i + "_" + j).className = "landMine";
            } else {
                var ele=this.$("m_" + i + "_" + j);
                if (this.arrs[i][j]!= 0)
                    ele.innerHTML = this.arrs[i][j];
                ele.className = "normal";
            }
        }
    }
},
// Ocultar la información de las celdas mostradas
hideAll: function () {
    for (var i = 0; i < this.rowCount; i++) {
        for (var j = 0; j < this.colCount; j++) {
            var tdCell = this.$("m_" + i + "_" + j);
            tdCell.className = "";
            tdCell.innerHTML = "";
        }
    }
},
// Quitar los eventos vinculados a las celdas
disableAll: function () {
    for (var i = 0; i < this.rowCount; i++) {
        for (var j = 0; j < this.colCount; j++) {
            var tdCell = this.$("m_" + i + "_" + j);
            tdCell.onmousedown = null;
        }
    }
},
✨ Revisar Solución y Practicar

Agregar función de control del juego

Hasta ahora, se ha completado la parte principal del juego. El siguiente paso es agregar funciones de control del juego para que el juego funcione sin problemas. Los pasos principales son los siguientes:

  1. Agregar un controlador de eventos de clic al botón de inicio para restablecer los parámetros del juego.
  2. Comenzar a contar el tiempo cuando el juego comienza.
  3. Detener el conteo del tiempo y mostrar un mensaje cuando el juego termina.

Sección de jms.js

Agrega la función de entrada y inicio del juego al JMS.prototype en jms.js:

// Inicio del juego
begin: function() {
    this.currentSetpCount = 0; // Restablece el contador de pasos a cero
    this.markLandMineCount = 0;
    this.beginTime = new Date(); // Tiempo de inicio del juego
    this.hideAll();
    this.bindCells();
},
// Fin del juego
end: function() {
    this.endTime = new Date(); // Tiempo de fin del juego
    if (this.endCallBack) { // Llama a la función de devolución de llamada si existe
        this.endCallBack();
    }
},
// Éxito del juego
success: function() {
    this.end();
    this.showAll();
    this.disableAll();
    alert("¡Felicitaciones!");
},
// Fallo del juego
failed: function() {
    this.end();
    this.showAll();
    this.disableAll();
    alert("GAME OVER!");
},
// Punto de entrada
play: function() {
    this.init();
    this.landMine();
    this.calculateNoLandMineCount();
},

Sección de index.js

En index.js, agrega un controlador de eventos al botón de inicio para comenzar el juego y mostrar el tiempo del juego y el número restante de minas (después de jms = JMS("landmine", rowCount, colCount, minLandMineCount, maxLandMineCount); en la función init de index.js):

jms.endCallBack = function () {
  clearInterval(timeHandle);
};
jms.landMineCallBack = function (count) {
  landMineCountElement.innerHTML = count;
};

// Vincular evento al botón "Iniciar Juego"
beginButton.onclick = function () {
  jms.play(); // Inicializar el juego

  // Mostrar el número de minas
  landMineCountElement.innerHTML = jms.landMineCount;

  // Comenzar el juego
  jms.begin();

  // Actualizar el tiempo transcurrido
  timeHandle = setInterval(function () {
    timeShow.innerHTML = parseInt((new Date() - jms.beginTime) / 1000);
  }, 1000);
};
✨ Revisar Solución y Practicar

Ejecución y pruebas

En este momento, nuestra versión web del Minesweeper está completa. Abra el archivo index.html en su navegador para comenzar a jugar.

Haga clic en el botón Go Live en la esquina inferior derecha de WebIDE y cambie a la pestaña Web 8080.

Demostración del juego del Minesweeper
✨ Revisar Solución y Practicar

Resumen

Este experimento utiliza principalmente JavaScript para implementar una versión web del clásico juego Minesweeper. Se cree que a través de este experimento, los usuarios pueden mejorar su comprensión y habilidades de aplicación de JavaScript. Además, los usuarios también pueden aprender cómo utilizar el lenguaje JavaScript para abstraer y encapsular objetos en el juego.