Como criar um tabuleiro de Campo Minado com JavaScript

Como criar um tabuleiro de Campo Minado com JavaScript

O desenvolvimento de jogos é um ótimo modo de aplicar diversos conceitos de programação, neste tutorial, vamos entender qual foi a lógica utilizada para criar o tabuleiro do jogo de campo minado publicado aqui no blog.

Entendendo o problema

Vamos comparar um tabuleiro de campo minado com um tabuleiro de xadrez, em ambos, cada casa pode ser acessada através da combinação de dois valores:

  1. índice da linha;
  2. índice da coluna.

A diferença é que, por convenção, as colunas no xadrez são representadas por letras, mas em nosso tabuleiro de campo minado, vamos usar números para identificar tanto as linhas quanto as colunas, além disso, vamos começar a nossa contagem no 0.

Tabuleiro de xadrez e de campo minado lado a lada com os índices das casas

Qualquer combinação entre estes dois índices existentes representa uma posição no tabuleiro. Por exemplo, a posição “1, G” no tabuleiro de xadrez é equivalente à posição “0, 1” no tabuleiro de campo minado. É a mesma lógica usada também no jogo Batalha Naval.

Para que seja possível desenhar o tabuleiro utilizando Javascript é necessário encontrar uma estrutura de dados nesta linguagem de programação que possua o mesmo modo de acesso de seus valores, ou seja, através da combinação de suas linhas e colunas. Para isso, a estrutura ideal é o array multidimensional.

Array e Array multidimensional

Array é um tipo de dado presente em diversas linguagens de programação, em Javascript ele pode ser entendido como um dado capaz de armazenar uma lista. Essa lista pode conter itens de diversos tipos, como números, strings, objetos, etc.

const simpleArr = [ 1, '2', {c: 3} ];

Cada item do array pode ser acessado pelo seu índice. Como arrays em JavaScript começam com o índice 0, para acessarmos o primeiro item do array executamos:

const firstItem = simpleArr[0];
console.log(firstItem); // 1

Um array pode também conter outros arrays, a essa estrutura se dá o nome de Array multidimensional:

const multidimensionalArr = [
    [ 1, 2, 3 ],
    [ 4, 5, 6 ],
    [ 7, 8, 9 ]
];

Do mesmo modo que no exemplo anterior, utilizamos o índice para acessarmos um item do Array:

const firstArr = multidimensionalArr[0];
console.log(firstArr); // [ 1, 2, 3, ];

A constante firstArr armazena o primeiro array da lista. Sendo assim, para acessarmos o primeiro item dentro do primeiro array, devemos continuar buscando pelo seu índice:

const firstItem = multidimensionalArr[0][0];
console.log(firstItem); // 1

Portanto, assim como em um tabuleiro de campo minado, um Array multidimensional também tem os seus valores acessíveis através da combinação de dois índices, onde o primeiro se refere à linha e o segundo à coluna.

Criando o tabuleiro

Agora que temos a estrutura que representará o nosso tabuleiro, são necessárias algumas etapas para a sua criação:

  1. Definir a quantidade de linhas, colunas e minas;
  2. Criar um Array multidimensional com todos os itens zerados;
  3. Definir aleatoriamente a posição das minas;
  4. Inserir as minas no Array multidimensional;
  5. Para cada mina, incrementar em 1 o valor das casas ao seu redor;
  6. Imprimir o tabuleiro no DOM.

1. Definir a quantidade de linhas, colunas e minas

Para simplificar nosso exemplo, vamos criar um tabuleiro do nível mais fácil do jogo, com 9 linhas, 9 colunas e 10 minas:

class Minesweeper {
    constructor() {
        this.boardRowsLength = 9;
        this.boardColumnsLength = 9;
        this.minesLength = 10;
    }
}

2. Criar um Array multidimensional com todos os itens zerados

Agora é necessário criar um Array que represente uma linha no tabuleiro, esse Array irá conter nove zeros, referente ao número de colunas, e devemos inserir esse Array no nosso array multidimensional. Como nosso tabuleiro terá 9 linhas, devemos repetir essa ação por nove vezes.

generateEmptyBoard() {
    for (let y = 0; y < this.boardRowsLength; y++) {
        this.board.push([]);
        for (let x = 0; x < this.boardColumnsLength; x++) {
            this.board[y][x] = 0;
        }
    }
}

Este é o array atribuído à propriedade this.board após a execução do método generateEmptyBoard():

this.board = [
    [0, 0, 0, 0, 0, 0, 0, 0, 0,],
    [0, 0, 0, 0, 0, 0, 0, 0, 0,],
    [0, 0, 0, 0, 0, 0, 0, 0, 0,],
    [0, 0, 0, 0, 0, 0, 0, 0, 0,],
    [0, 0, 0, 0, 0, 0, 0, 0, 0,],
    [0, 0, 0, 0, 0, 0, 0, 0, 0,],
    [0, 0, 0, 0, 0, 0, 0, 0, 0,],
    [0, 0, 0, 0, 0, 0, 0, 0, 0,],
    [0, 0, 0, 0, 0, 0, 0, 0, 0,],
];

3. Definir aleatoriamente a posição das minas

Vamos armazenar em uma outra lista todos os pares de coordenadas que representarão a posição das minas e que serão gerados aleatoriamente a partir dos índices possíveis de linhas e colunas. Mais uma vez, utilizamos um array multidimensional para isso:

generateMinesPositions() {
    this.minesPositions = [];

    while (this.minesPositions.length < this.minesLength) {
        const y = this.getRandomInt(0, this.boardRowsLength);
        const x = this.getRandomInt(0, this.boardColumnsLength);

        if (!this.isAlreadyAMine([y, x])) {
            this.minesPositions.push([y, x]);
        }
    }
}

// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/random
getRandomInt(min, max) {
    return Math.floor(Math.random() * (max - min)) + min;
}

isAlreadyAMine(minePosition) {
    return this.minesPositions.join(" ").includes(minePosition.toString());
}

A propriedade this.minesPositions ficará mais ou menos assim (porém com outros índices, pois as posições são geradas aleatoriamente cada vez que o método é chamado):

this.minesPositions = [
    [0, 4], [2, 5], [1, 2], [3, 3], [7, 5],
    [6, 4], [2, 7], [8, 7], [4, 5], [5, 1],
];

4. Inserir as minas no Array multidimensional

O array this.minesPositions possui as dez posições onde as minas devem ser colocadas no tabuleiro. Para isso, acessamos essas posições no array this.board e trocamos o seu valor de 0 para “M” (“M” de mina)

insertMines() {
    for (let i = 0; i < this.minesPositions.length; i++) {
        const y = this.minesPositions[i][0];
        const x = this.minesPositions[i][1];
        this.board[y][x] = "M";
    }
}

this.board ficará assim após a execução do método insertMines():

this.board = [
    [0, 0, 0, 0, M, 0, 0, 0, 0,],
    [0, 0, M, 0, 0, 0, 0, 0, 0,],
    [0, 0, 0, 0, 0, M, 0, M, 0,],
    [0, 0, 0, M, 0, M, 0, 0, 0,],
    [0, 0, 0, 0, 0, 0, 0, 0, 0,],
    [0, M, 0, 0, 0, 0, 0, 0, 0,],
    [0, 0, 0, 0, M, 0, 0, 0, 0,],
    [0, 0, 0, 0, 0, M, 0, 0, 0,],
    [0, 0, 0, 0, 0, 0, 0, M, 0,],
];

ATENÇÃO: A letra M presente nos arrays é na realidade uma string (“M”). Removi as aspas apenas para facilitar a visualização das posições.

5. Para cada mina, incrementar em 1 o valor das casas ao seu redor

A partir da posição de uma mina devemos acessar todas as outra casas ao seu redor. Mais uma vez, utilizaremos o array minesPositions para nos guiar.
Se temos uma mina na posição [5][3] e queremos acessar a casa logo acima, então devemos apenas subtrair em 1 o índice da linha da mina, logo, a posição da casa será [4][3].
Ou se quisermos acessar a casa ao lado direto na mina, basta adicionar 1 ao índice de sua coluna: [5][4].
Para facilitar o processo, podemos então criar um novo array multidimensional fora de nossa classe Minesweeper que contenha todas as operações que devem ser realizadas para acessar as casas ao redor de uma mina:

const AROUND_CELL_OPERATORS = [
    [-1, -1], [-1, 0], [-1, 1],
    [0, -1],           [0, 1],
    [1, -1],  [1, 0],  [1, 1],
];

Agora para cada uma das minas podemos utilizar este array para acessar as casas ao seu redor e incrementar o seu valor

updateBoardNumbers() {
    for (let i = 0; i < this.minesPositions.length; i++) {
        for (let j = 0; j < AROUND_CELL_OPERATORS.length; j++) {
            const minePosition = this.minesPositions[i];
            const around = AROUND_CELL_OPERATORS[j];
            const boardY = minePosition[0] + around[0];
            const boardX = minePosition[1] + around[1];

            if (boardY >= 0 && boardY < this.boardRowsLength &&
                boardX >= 0 && boardX < this.boardColumnsLength &&
                typeof this.board[boardY][boardX] === 'number') {
                this.board[boardY][boardX]++;
            }
        }
    }
}

this.board ficará assim:

this.board = [
    [0, 1, 1, 2, M, 1, 0, 0, 0,],
    [0, 1, M, 2, 2, 2, 2, 1, 1,],
    [0, 1, 2, 2, 3, M, 3, M, 1,],
    [0, 0, 1, M, 3, M, 3, 1, 1,],
    [1, 1, 2, 1, 2, 1, 1, 0, 0,],
    [1, M, 1, 1, 1, 1, 0, 0, 0,],
    [1, 1, 1, 1, M, 2, 1, 0, 0,],
    [0, 0, 0, 1, 2, M, 2, 1, 1,],
    [0, 0, 0, 0, 1, 1, 2, M, 1,],
];

6. Imprimir o tabuleiro no DOM

O método printBoard($board) colocar o tabuleiro no DOM a partir da propriedade this.board.

printBoard($board) {
    for (let y = 0; y < this.board.length; y++) {
        const $row = document.createElement('DIV');
        $row.classList.add('row');
        
        for (let x = 0; x < this.board[y].length; x++) {
            const $cell = document.createElement('SPAN');
            
            $cell.innerHTML = this.board[y][x];
            $row.appendChild($cell);
        }

        $board.appendChild($row);
    }
}

Para deixar nossa classe mais genérica, vamos definir três parâmetros no seu constructor, desse modo, será possível criar tabuleiros com qualquer número de linhas, colunas e quantidade de minas. Além disso, vamos chamar o método init() também dentro do constructor:

class Minesweeper {
    constructor(boardRowsLength, boardColumnsLength, minesLength) {
        this.boardRowsLength = boardRowsLength;
        this.boardColumnsLength = boardColumnsLength;
        this.minesLength = minesLength;
        this.board = [];
        this.minesPositions = [];
        
        this.init();
    }

    init() {
        this.generateEmptyBoard();
        this.generateMinesPositions();
        this.insertMines();
        this.updateBoardNumbers();
    }
    // ...
}

Para concluir, criamos uma instância da classe Minesweeper, e chamamos o método printBoard($board), passando como parâmetro o elemento HTML no qual o tabuleiro deve ser renderizado:

const minesweeper = new Minesweeper(9, 9, 10);
const $board = document.getElementById('board');

minesweeper.printBoard($board);

Veja o resultado no CodePen:

Este é o código final do tabuleiro de campo minado feito com JavaScript no Github.

Embora este seja só o começo do desenvolvimento do jogo, esta é uma etapa importante do processo. O código completo do Jogo de Campo Minado está também disponível no GitHub.

Abraço!

Categoria: TutoriaisTags:
Autor

Gabriel Manussakis

Designer e desenvolvedor front-end. Entusiasta por JavaScript. Atualmente desenvolvendo em Angular e RxJS.