Voltar para a página inicial

Campo Minado parece simples à primeira vista — é um grid com números e minas. Mas implementá-lo do zero revela várias decisões técnicas interessantes: como distribuir minas de forma verdadeiramente aleatória? Como garantir que o primeiro clique nunca seja uma mina? Como implementar o reveal em cascata de células vazias sem travar a UI? Neste artigo mostro como resolvi cada uma dessas questões no Campo Minado Evolution.

A estrutura de dados: modelando uma célula

Cada posição do tabuleiro é representada por um objeto Celula com todo o estado necessário para renderização e lógica do jogo:

class Celula {
  final int linha;
  final int coluna;
  bool temMina;
  bool revelada;
  bool marcada;      // bandeirinha do jogador
  int minasAdjacentes;

  Celula({required this.linha, required this.coluna})
      : temMina = false,
        revelada = false,
        marcada = false,
        minasAdjacentes = 0;
}

O tabuleiro inteiro é uma lista bidimensional inicializada com células limpas. Nenhuma mina é colocada na criação — isso é intencional, como veremos em seguida.

List<List<Celula>> tabuleiro = List.generate(
  linhas,
  (l) => List.generate(colunas, (c) => Celula(linha: l, coluna: c)),
);

Distribuindo as minas: o problema do primeiro clique

Uma regra clássica do Campo Minado: o primeiro clique nunca pode explodir uma mina. Isso significa que as minas precisam ser distribuídas após o primeiro clique, excluindo a célula clicada e todos os seus vizinhos do sorteio.

void distribuirMinas(int linhaSegura, int colunaSegura) {
  final rand = Random();
  int minasColocadas = 0;

  while (minasColocadas < totalMinas) {
    int l = rand.nextInt(linhas);
    int c = rand.nextInt(colunas);

    // Exclui a célula clicada e os 8 vizinhos ao redor dela
    bool ehZonaSegura =
        (l - linhaSegura).abs() <= 1 && (c - colunaSegura).abs() <= 1;

    if (!ehZonaSegura && !tabuleiro[l][c].temMina) {
      tabuleiro[l][c].temMina = true;
      minasColocadas++;
    }
  }
}

Esse loop continua sorteando posições até preencher todas as minas. A zona de exclusão de 3×3 ao redor do primeiro clique garante que o jogador sempre começa com algum espaço para raciocinar — sem explosão imediata na primeira jogada.

Calculando os números adjacentes

Após distribuir as minas, percorremos todo o tabuleiro e calculamos quantas minas existem nos 8 vizinhos de cada célula que não é mina:

void calcularAdjacentes() {
  for (int l = 0; l < linhas; l++) {
    for (int c = 0; c < colunas; c++) {
      if (!tabuleiro[l][c].temMina) {
        tabuleiro[l][c].minasAdjacentes = _contarVizinhos(l, c);
      }
    }
  }
}

int _contarVizinhos(int l, int c) {
  int count = 0;
  for (int dl = -1; dl <= 1; dl++) {
    for (int dc = -1; dc <= 1; dc++) {
      if (dl == 0 && dc == 0) continue; // ignora a própria célula
      int nl = l + dl;
      int nc = c + dc;
      if (nl >= 0 && nl < linhas && nc >= 0 && nc < colunas) {
        if (tabuleiro[nl][nc].temMina) count++;
      }
    }
  }
  return count;
}

O reveal em cascata: flood fill recursivo

Quando o jogador clica em uma célula com minasAdjacentes == 0, todas as células vazias conectadas devem ser reveladas automaticamente — e os números nas bordas dessa região também. Isso é um algoritmo de flood fill (preenchimento por inundação), o mesmo usado em ferramentas de pintura tipo balde.

void revelar(int l, int c) {
  // Limites do tabuleiro
  if (l < 0 || l >= linhas || c < 0 || c >= colunas) return;

  final celula = tabuleiro[l][c];

  // Não revela células já reveladas ou marcadas com bandeira
  if (celula.revelada || celula.marcada) return;

  celula.revelada = true;

  // Se a célula não tem minas ao redor, revela os 8 vizinhos recursivamente
  if (!celula.temMina && celula.minasAdjacentes == 0) {
    for (int dl = -1; dl <= 1; dl++) {
      for (int dc = -1; dc <= 1; dc++) {
        if (dl == 0 && dc == 0) continue;
        revelar(l + dl, c + dc);
      }
    }
  }
}

A recursão para naturalmente quando encontra células já reveladas, marcadas com bandeira, ou células com número maior que zero (bordas da região vazia). Em tabuleiros grandes, a pilha de chamadas pode crescer. Para tabuleiros muito grandes, vale converter para uma abordagem iterativa com uma fila (Queue).

Renderizando o tabuleiro com Flutter

O tabuleiro é renderizado com GridView.builder com um delegate de contagem fixa de colunas. Cada célula é um GestureDetector que responde ao toque simples (revelar) e ao toque longo (colocar bandeira):

GridView.builder(
  gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
    crossAxisCount: colunas,
    childAspectRatio: 1.0,
  ),
  itemCount: linhas * colunas,
  itemBuilder: (context, index) {
    final l = index ~/ colunas;
    final c = index % colunas;
    final celula = tabuleiro[l][c];

    return GestureDetector(
      onTap: () => controller.revelar(l, c),
      onLongPress: () => controller.alternarBandeira(l, c),
      child: CelulaWidget(celula: celula),
    );
  },
)

O CelulaWidget recebe a célula e decide o que exibir: célula coberta, bandeira, número colorido ou explosão. As cores dos números seguem a convenção clássica do Campo Minado: 1 azul, 2 verde, 3 vermelho, etc.

Condições de vitória e derrota

As duas condições são verificadas após cada jogada:

// Derrota: o jogador revelou uma mina
bool verificarDerrota(int l, int c) => tabuleiro[l][c].temMina;

// Vitória: todas as células SEM mina foram reveladas
bool verificarVitoria() {
  return tabuleiro
      .expand((row) => row)
      .where((c) => !c.temMina)
      .every((c) => c.revelada);
}

Na vitória, todas as minas restantes são automaticamente marcadas com bandeira e o cronômetro para. Na derrota, todas as minas são reveladas com animação e a sessão termina.


Implementar Campo Minado em Flutter é um exercício excelente porque combina algoritmos clássicos (flood fill, geração aleatória com restrição) com UI reativa. O resultado é um jogo que roda suavemente a 60fps porque toda a lógica vive fora da árvore de widgets — o estado do tabuleiro é gerenciado no controller e a UI só renderiza o que mudou.

Experimente o Campo Minado Evolution gratuitamente no Google Play:

Baixar Campo Minado Evolution