Criando um Jogo Campo Minado com JavaScript

JavaScriptBeginner
Pratique Agora

Introdução

Acredito que todos já jogaram o clássico jogo Campo Minado (Minesweeper). Ele tem regras simples, mas é altamente viciante. Você já pensou em desenvolver um você mesmo? Hoje, criaremos uma versão web do Campo Minado. Primeiro, vamos dar uma olhada em uma captura de tela da interface.

👀 Pré-visualização

Minesweeper game interface preview

🎯 Tarefas

Neste projeto, você aprenderá:

  • Como projetar o algoritmo do jogo para o jogo Campo Minado
  • Como criar a estrutura de arquivos para o projeto
  • Como implementar o layout da página usando HTML e CSS
  • Como desenhar a grade usando JavaScript
  • Como adicionar eventos de clique às células para lidar com o jogo
  • Como implementar funções de controle do jogo, como início e fim do jogo

🏆 Conquistas

Após concluir este projeto, você será capaz de:

  • Projetar e implementar algoritmos de jogos
  • Criar a estrutura de arquivos para um projeto web
  • Usar HTML e CSS para criar layouts de página
  • Usar JavaScript para desenhar grades e lidar com eventos
  • Implementar funções de controle do jogo

Preparação para o Desenvolvimento

Antes de começar o desenvolvimento, vamos primeiro projetar o algoritmo do jogo.

As regras do jogo Campo Minado são simples:

  • Existem vários quadrados no tabuleiro do jogo, cada quadrado contém um número (em branco indica que o número é 0) ou uma bomba. O número no quadrado representa o número de bombas nos quadrados vizinhos. A tarefa do jogador é encontrar os quadrados numéricos no menor tempo possível.
  • Exceto pelos quadrados nas bordas, cada quadrado tem 8 quadrados vizinhos: acima, abaixo, esquerda, direita e 4 quadrados diagonais. Portanto, a faixa de números é de 0 a 8.

Então, nosso algoritmo é o seguinte:

  • Com base no nível de dificuldade selecionado pelo usuário (existem três níveis: iniciante, intermediário e avançado, com mais bombas e quadrados à medida que o nível aumenta), gere aleatoriamente um certo número de bombas e coloque-as aleatoriamente nos quadrados. Em seguida, percorra os quadrados, calcule o número em cada quadrado e marque-o no quadrado. Quando o jogador clica com o botão esquerdo em um quadrado, o conteúdo do quadrado é exibido (se o quadrado contiver uma bomba, o desafio falha e o jogo termina), e quando o jogador clica com o botão direito em um quadrado, o quadrado é marcado como uma bomba. O desafio é bem-sucedido somente quando todas as bombas são marcadas corretamente e todos os quadrados sem bombas são abertos, e o jogo termina.

Dica útil: Como a faixa de números nos quadrados é de 0 a 8, podemos considerar marcar os números nos quadrados onde as bombas estão localizadas como 9 para facilitar o cálculo.

✨ Verificar Solução e Praticar

Estrutura de Arquivos do Projeto

Primeiro, precisamos criar a seguinte estrutura de arquivos no caminho ~/project:

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

Os arquivos mine.png e flag.png no diretório do projeto são imagens para a mina e a bandeira, respectivamente.

✨ Verificar Solução e Praticar

Layout da Página

Primeiramente, precisamos ter um painel para exibir informações do jogo, incluindo o número de minas restantes, o tempo decorrido, o nível de dificuldade, etc. Como o número de quadrados não é fixo, não desenharemos os quadrados por enquanto e, em vez disso, os desenharemos no código JS.

Crie um arquivo index.html e adicione o seguinte código:

<!-- index.html -->
<!doctype html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>Campo Minado com 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">
          Tempo de duração: <span class="font-semibold" id="costTime">0</span> s
        </div>
        <fieldset class="my-4">
          <legend class="text-lg font-semibold">Seleção de dificuldade:</legend>
          <input
            type="radio"
            name="level"
            id="llevel"
            checked="checked"
            value="10"
            class="mr-2"
          />
          <label for="llevel">Primário (10*10) </label><br />
          <input
            type="radio"
            name="level"
            id="mlevel"
            value="15"
            class="mr-2"
          />
          <label for="mlevel">Intermediário (15*15) </label><br />
          <input
            type="radio"
            name="level"
            id="hlevel"
            value="20"
            class="mr-2"
          />
          <label for="hlevel">Avançado (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 Jogo</button
        ><br />
        <div class="text-left mt-4">
          <div class="font-semibold">Dica:</div>
          <ul class="list-disc ml-6">
            <li>
              1. Clique em "Iniciar Jogo" para iniciar o cronômetro do jogo.
            </li>
            <li>
              2. Clicar em "Iniciar Jogo" durante o jogo iniciará um novo jogo.
            </li>
          </ul>
        </div>
      </div>
    </div>
    <script src="jms.js"></script>
    <script src="index.js"></script>
  </body>
</html>
✨ Verificar Solução e Praticar

Estilos do Layout da Página

Em seguida, precisamos ajustar a posição das informações do jogo no painel e adicionar alguns estilos. Adicione o seguinte código a index.css e salve:

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

Após concluir esta etapa, abra index.html em seu navegador.

Interface do jogo Campo Minado
Interface do jogo Campo Minado
✨ Verificar Solução e Praticar

Desenho da Grelha

Após concluir as etapas anteriores, precisamos desenhar a grelha. Para tornar o código mais claro, separamos a parte de implementação do jogo e a parte de chamada. A parte de implementação do jogo é colocada em jms.js, que está no mesmo diretório de index.html, e a parte de chamada do jogo é colocada em index.js, também no mesmo diretório.

Para desenhar a grelha, precisamos passar alguns parâmetros, como o id da tabela onde a grelha será colocada e o número de células (representado pelo número de linhas e colunas). Além disso, outros dados relacionados ao jogo precisam ser inicializados.

Parte jms.js

Adicione o seguinte código a jms.js e salve-o:

(function () {
  // Inicializa o objeto Campo Minado e inicializa os dados
  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); // Tabela para desenhar a grelha
    this.cells = this.table.getElementsByTagName("td"); // Células
    this.rowCount = rowCount || 10; // Número de linhas na grelha
    this.colCount = colCount || 10; // Número de colunas na grelha
    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 = []; // Array correspondente às células
    this.beginTime = null; // Tempo de início do jogo
    this.endTime = null; // Tempo de término do jogo
    this.currentSetpCount = 0; // Número de passos dados
    this.endCallBack = null; // Função de callback quando o jogo termina
    this.landMineCallBack = null; // Função de callback para atualizar o número restante de minas quando uma mina é marcada
    this.doc.oncontextmenu = function () {
      // Desabilita o menu de clique com o botão direito
      return false;
    };
    this.drawMap();
  };

  // Cria células no prototype de JMS
  JMS.prototype = {
    // Desenha a grelha
    drawMap: function () {
      var tds = [];
      // Para compatibilidade com navegadores
      if (
        window.ActiveXObject &&
        parseInt(navigator.userAgent.match(/msie ([\d.]+)/i)[1]) < 8
      ) {
        // Cria um novo arquivo de estilo CSS
        var css = "#JMS_main table td{background-color:#888;}",
          // Obtém a tag head
          head = this.doc.getElementsByTagName("head")[0],
          // Cria uma tag style
          style = this.doc.createElement("style");
        style.type = "text/css";
        if (style.styleSheet) {
          // Atribui o estilo CSS à tag style
          style.styleSheet.cssText = css;
        } else {
          // Cria um nó na tag style
          style.appendChild(this.doc.createTextNode(css));
        }
        // Anexa a tag style como uma tag filha da tag head
        head.appendChild(style);
      }
      // Loop para criar a tabela
      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(""));
    },
    // Adiciona HTML à tabela
    setTableInnerHTML: function (table, html) {
      if (navigator && navigator.userAgent.match(/msie/i)) {
        // Cria uma div dentro do documento proprietário da tabela
        var temp = table.ownerDocument.createElement("div");
        // Cria o conteúdo do tbody da tabela
        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;
})();

O código acima inclui algum código para torná-lo compatível com navegadores IE, que pode ser ignorado.

Parte index.js

No código de chamada em index.js, precisamos vincular o evento dos botões de seleção de dificuldade e, em seguida, chamar o JMS que definimos acima para começar a desenhar a grelha.

Adicione o seguinte código a index.js e salve-o:

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("Are you sure you want to end the current game?"))
            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);
}

Em seguida, abra index.html no navegador, e a grelha deve ser exibida. O efeito é o seguinte:

Exibição da grelha do jogo Campo Minado

Clique na seleção de dificuldade à direita para ver o número de células mudar.

✨ Verificar Solução e Praticar

Inicialização do Jogo

Agora, vamos começar a inicializar o jogo, o que envolve principalmente três etapas:

  1. Inicializar todas as células (representadas por um array no código) para 0.
  2. Gerar um número aleatório de minas terrestres e colocá-las aleatoriamente no array, definindo o valor do item do array para 9.
  3. Calcular os números em outras células e armazenar os valores no array.

Adicione o seguinte código dentro de JMS.prototype em jms.js:

// Inicialização: define o valor padrão do array para 0 e determina o número de minas terrestres
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;
},
// Define o valor dos itens do array que contêm minas terrestres para 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;
    }
},
// Calcula os números em outras células
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]++;
            }
        }
    }
},
// Obtém um número aleatório
selectFrom: function (iFirstValue, iLastValue) {
    var iChoices = iLastValue - iFirstValue + 1;
    return Math.floor(Math.random() * iChoices + iFirstValue);
},
// Encontra os números da linha e da coluna com base no valor
getRowCol: function (val) {
    return {
        row: parseInt(val / this.colCount),
        col: val % this.colCount
    };
},
✨ Verificar Solução e Praticar

Adicionar Eventos de Clique às Células

Agora, é necessário adicionar eventos de clique às células. Quando clicado com o botão esquerdo, exibe o número na célula (se for uma mina, o jogo terminará). Quando clicado com o botão direito, marque-a como uma mina.

Além disso, quando a célula é clicada pela primeira vez (geralmente envolvendo sorte), se houver uma área em branco ao redor, ela deve ser expandida diretamente.

Adicione o seguinte código ao JMS.prototype em jms.js:

// Obter elemento
$: function (id) {
    return this.doc.getElementById(id);
},
// Vincular eventos de clique (esquerdo e direito) a cada célula
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 a área sem 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);
                }
            }
        }
},
// Exibir
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();
    }
},
// Exibir 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 informações de todas as células
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";
            }
        }
    }
},
// Limpar informações da célula exibida
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 = "";
        }
    }
},
// Remover os eventos vinculados às células
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;
        }
    }
},
✨ Verificar Solução e Praticar

Adicionando a Função de Controle do Jogo

Até agora, a parte principal do jogo foi concluída. A próxima etapa é adicionar funções de controle do jogo para fazer o jogo rodar sem problemas. As principais etapas são as seguintes:

  1. Adicionar um ouvinte de evento de clique ao botão de início para redefinir os parâmetros do jogo.
  2. Começar a contar o tempo quando o jogo começar.
  3. Parar de contar o tempo e exibir uma mensagem quando o jogo terminar.

Seção jms.js

Adicione a entrada do jogo e a função de início ao JMS.prototype em jms.js:

// Início do jogo
begin: function() {
    this.currentSetpCount = 0; // Redefinir a contagem de passos para zero
    this.markLandMineCount = 0;
    this.beginTime = new Date(); // Tempo de início do jogo
    this.hideAll();
    this.bindCells();
},
// Fim do jogo
end: function() {
    this.endTime = new Date(); // Tempo de término do jogo
    if (this.endCallBack) { // Chamar a função de callback se existir
        this.endCallBack();
    }
},
// Jogo bem-sucedido
success: function() {
    this.end();
    this.showAll();
    this.disableAll();
    alert("Parabéns!");
},
// Jogo fracassado
failed: function() {
    this.end();
    this.showAll();
    this.disableAll();
    alert("FIM DE JOGO!");
},
// Ponto de entrada
play: function() {
    this.init();
    this.landMine();
    this.calculateNoLandMineCount();
},

Seção index.js

Em index.js, adicione um ouvinte de evento ao botão de início para iniciar o jogo e exibir o tempo de jogo e o número restante de minas terrestres (após jms = JMS("landmine", rowCount, colCount, minLandMineCount, maxLandMineCount); na função init de index.js):

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

// Vincular evento ao botão "Iniciar Jogo"
beginButton.onclick = function () {
  jms.play(); // Inicializar o jogo

  // Exibir o número de minas terrestres
  landMineCountElement.innerHTML = jms.landMineCount;

  // Iniciar o jogo
  jms.begin();

  // Atualizar o tempo decorrido
  timeHandle = setInterval(function () {
    timeShow.innerHTML = parseInt((new Date() - jms.beginTime) / 1000);
  }, 1000);
};
✨ Verificar Solução e Praticar

Executando e Testando

Neste ponto, nossa versão web do Campo Minado está completa. Abra o arquivo index.html em seu navegador para começar a jogar.

Clique no botão Go Live no canto inferior direito do WebIDE e mude para a aba Web 8080.

Demonstração da jogabilidade do Campo Minado
✨ Verificar Solução e Praticar

Resumo

Este experimento utiliza principalmente JavaScript para implementar uma versão web do clássico jogo Campo Minado. Acredita-se que, através deste experimento, os usuários podem aprimorar sua compreensão e habilidades de aplicação do JavaScript. Além disso, os usuários também podem aprender como usar a linguagem JavaScript para abstrair e encapsular objetos no jogo.