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
🎯 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.
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>
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:
Clique na seleção de dificuldade à direita para ver o número de células mudar.
Agora, vamos começar a inicializar o jogo, o que envolve principalmente três etapas:
Inicializar todas as células (representadas por um array no código) para 0.
Gerar um número aleatório de minas terrestres e colocá-las aleatoriamente no array, definindo o valor do item do array para 9.
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
};
},
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;
}
}
},
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:
Adicionar um ouvinte de evento de clique ao botão de início para redefinir os parâmetros do jogo.
Começar a contar o tempo quando o jogo começar.
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);
};
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.