Apuntes DAM
Volver al inicio

Buscaminas: números, casilla más peligrosa y zonas despejadas

Ejercicio de JavaDifícilUnos 70 minutos

A partir de un tablero de Buscaminas con sus minas, calcula el número de cada casilla, el porcentaje de minas, la casilla con más minas alrededor y cuántas zonas se abren de golpe al pulsar un cero. Arrays bidimensionales, vecinos de una casilla y recorrido de zonas con una pila.

  • Arrays bidimensionales
  • Recorrer los 8 vecinos
  • Validación de la entrada
  • Recorrido de zonas (flood fill)
  • ArrayDeque como pila
  • String.format

Enunciado

En el Buscaminas, cada casilla sin mina muestra cuántas minas hay en las ocho casillas que la rodean (también en diagonal). Con esos números el jugador deduce dónde están las minas.

Cuando se pulsa una casilla con un 0, el juego no la abre sola: abre de golpe todos los ceros conectados con ella (también en diagonal), como una mancha. Cada una de esas manchas es una «zona despejada», y contar cuántas hay da una idea de lo fácil que es un tablero.

Vas a escribir la parte del juego que prepara un tablero nuevo: recibe dónde están las minas y calcula todo lo demás. Los tableros vienen de un fichero de niveles que a veces está mal escrito, así que hay que validarlo.

Qué tiene que hacer el programa

  1. La primera línea tiene el número de filas y de columnas separados por un espacio (de 1 a 30 cada uno). Después vienen tantas líneas como filas, con exactamente tantos caracteres como columnas: * es una mina y . una casilla libre.
  2. Si la cabecera no es válida, escribe Tablero no válido: línea 1. Si una fila no tiene la longitud correcta o tiene otro carácter, Tablero no válido: línea N (N es su número de línea en la entrada, contando la cabecera). Si faltan filas, Tablero no válido: faltan filas. En todos los casos no se escribe nada más.
  3. Escribe Tablero: y el tablero con las minas como * y cada casilla libre sustituida por el número de minas vecinas (de 0 a 8).
  4. Minas: M de T casillas (P %), con el porcentaje con un decimal y coma decimal.
  5. Casilla más peligrosa: fila F, columna C (K minas alrededor): la casilla libre con más minas vecinas (filas y columnas se numeran desde 1; si hay empate, la primera leyendo por filas; «mina» en singular si K es 1). Si ninguna casilla libre tiene minas alrededor (o no hay casillas libres), Casilla más peligrosa: ninguna.
  6. Zonas despejadas: Z: cuántos grupos de casillas con 0 hay, considerando que dos ceros están conectados si son vecinos, también en diagonal.

Entrada

Línea 1: filas columnas. Después, una línea por fila con * (mina) y . (libre).

Ejemplos de ejecución

Tu programa debe escribir exactamente esta salida para estas entradas. Las pruebas del editor incluyen estos ejemplos y otros casos ocultos.

Un tablero de 5 × 6

Entrada

5 6
*.....
..*...
......
...**.
......

Salida por consola

Tablero:
*21100
12*100
012321
001**1
001221
Minas: 4 de 30 casillas (13,3 %)
Casilla más peligrosa: fila 3, columna 4 (3 minas alrededor)
Zonas despejadas: 2

Un tablero mal escrito

Entrada

3 4
..*.
.x..
....

Salida por consola

Tablero no válido: línea 3

Guía paso a paso

Intenta resolverlo por tu cuenta y abre un paso solo cuando te atasques: cada uno te acerca a la solución sin dártela entera.

1. Leer y validar

Lee la cabecera y comprueba su formato con matches("\\d{1,2} \\d{1,2}") y su rango. Después lee cada fila con un bucle: si falta, o su longitud no es la de las columnas, o tiene algo distinto de * y . (matches("[*.]+")), escribe el error y termina con return.

2. Los ocho vecinos

Recorre los desplazamientos de -1 a 1 en filas y columnas, saltándote el (0, 0), y comprueba que la casilla vecina está dentro del tablero antes de mirarla. Este patrón lo vas a usar dos veces.

java
for (int df = -1; df <= 1; df++)
    for (int dc = -1; dc <= 1; dc++) {
        int ff = f + df, cc = c + dc;
        if ((df != 0 || dc != 0) && ff >= 0 && ff < filas && cc >= 0 && cc < columnas && tablero[ff][cc] == '*') n++;
    }
3. Un segundo array con los números

Guarda los números en un int[][] cuenta (con -1 en las minas). Así la casilla más peligrosa y las zonas se calculan sobre números ya hechos, sin volver a contar minas.

4. Contar zonas: recorrer cada mancha una vez

Recorre el tablero; cada vez que encuentres un 0 que no hayas visitado, es una zona nueva: súmala y marca como visitados todos los ceros conectados. Para marcarlos usa una pila (ArrayDeque): mete la casilla inicial y, mientras queden casillas en la pila, saca una y mete sus vecinos con 0 aún no visitados.

5. Formato del porcentaje

String.format(Locale.ROOT, "%.1f", x) da siempre punto decimal, sea cual sea el idioma del sistema; después cambia el punto por una coma.

Resuélvelo aquí

El editor trae el esqueleto del programa. Pulsa «Ejecutar» para comprobarlo con los ejemplos y con 4 casos ocultos que buscan los errores típicos.

☕JavaBuscaminas: números, casilla más peligrosa y zonas despejadasDifícil

Ejemplo

Entrada (lo que se escribe por teclado)
5 6
*.....
..*...
......
...**.
......
Salida esperada
Tablero:
*21100
12*100
012321
001**1
001221
Minas: 4 de 30 casillas (13,3 %)
Casilla más peligrosa: fila 3, columna 4 (3 minas alrededor)
Zonas despejadas: 2
⏳
Test oculto #3
⏳
Test oculto #4
⏳
Test oculto #5
⏳
Test oculto #6
0/6 tests pasados · pulsa un test para ver su entrada y su salida esperada

Solución explicada

Ver la solución completa
java
1import java.util.ArrayDeque;
2import java.util.Deque;
3import java.util.Locale;
4import java.util.Scanner;
5
6public class Main {
7    static int filas, columnas;
8    static char[][] tablero;
9    static int[][] cuenta;
10
11    /** Minas en las 8 casillas vecinas de (f, c). */
12    static int minasAlrededor(int f, int c) {
13        int n = 0;
14        for (int df = -1; df <= 1; df++) {
15            for (int dc = -1; dc <= 1; dc++) {
16                int ff = f + df, cc = c + dc;
17                if ((df != 0 || dc != 0) && ff >= 0 && ff < filas && cc >= 0 && cc < columnas && tablero[ff][cc] == '*') n++;
18            }
19        }
20        return n;
21    }
22
23    /** Marca como visitada toda la zona de ceros conectada (también en diagonal) con (f, c). */
24    static void despejar(int f, int c, boolean[][] visto) {
25        Deque<int[]> pendientes = new ArrayDeque<>();
26        pendientes.push(new int[]{f, c});
27        visto[f][c] = true;
28        while (!pendientes.isEmpty()) {
29            int[] actual = pendientes.pop();
30            for (int df = -1; df <= 1; df++) {
31                for (int dc = -1; dc <= 1; dc++) {
32                    int ff = actual[0] + df, cc = actual[1] + dc;
33                    if (ff >= 0 && ff < filas && cc >= 0 && cc < columnas && !visto[ff][cc] && cuenta[ff][cc] == 0) {
34                        visto[ff][cc] = true;
35                        pendientes.push(new int[]{ff, cc});
36                    }
37                }
38            }
39        }
40    }
41
42    public static void main(String[] args) {
43        Scanner sc = new Scanner(System.in);
44        String cabecera = sc.hasNextLine() ? sc.nextLine().trim() : "";
45        if (!cabecera.matches("\\d{1,2} \\d{1,2}")) {
46            System.out.println("Tablero no válido: línea 1");
47            return;
48        }
49        filas = Integer.parseInt(cabecera.split(" ")[0]);
50        columnas = Integer.parseInt(cabecera.split(" ")[1]);
51        if (filas < 1 || filas > 30 || columnas < 1 || columnas > 30) {
52            System.out.println("Tablero no válido: línea 1");
53            return;
54        }
55        tablero = new char[filas][];
56        for (int f = 0; f < filas; f++) {
57            if (!sc.hasNextLine()) {
58                System.out.println("Tablero no válido: faltan filas");
59                return;
60            }
61            String linea = sc.nextLine().trim();
62            if (linea.length() != columnas || !linea.matches("[*.]+")) {
63                System.out.println("Tablero no válido: línea " + (f + 2));
64                return;
65            }
66            tablero[f] = linea.toCharArray();
67        }
68
69        // Números de cada casilla (-1 en las minas)
70        cuenta = new int[filas][columnas];
71        int minas = 0;
72        System.out.println("Tablero:");
73        for (int f = 0; f < filas; f++) {
74            StringBuilder sb = new StringBuilder();
75            for (int c = 0; c < columnas; c++) {
76                if (tablero[f][c] == '*') {
77                    cuenta[f][c] = -1;
78                    minas++;
79                    sb.append('*');
80                } else {
81                    cuenta[f][c] = minasAlrededor(f, c);
82                    sb.append(cuenta[f][c]);
83                }
84            }
85            System.out.println(sb);
86        }
87
88        int total = filas * columnas;
89        String porcentaje = String.format(Locale.ROOT, "%.1f", minas * 100.0 / total).replace('.', ',');
90        System.out.println("Minas: " + minas + " de " + total + " casillas (" + porcentaje + " %)");
91
92        // La primera casilla (por filas) con más minas alrededor
93        int mejorF = -1, mejorC = -1, max = 0;
94        for (int f = 0; f < filas; f++) {
95            for (int c = 0; c < columnas; c++) {
96                if (cuenta[f][c] > max) {
97                    max = cuenta[f][c];
98                    mejorF = f;
99                    mejorC = c;
100                }
101            }
102        }
103        if (max == 0) {
104            System.out.println("Casilla más peligrosa: ninguna");
105        } else {
106            System.out.println("Casilla más peligrosa: fila " + (mejorF + 1) + ", columna " + (mejorC + 1) + " (" + max + (max == 1 ? " mina" : " minas") + " alrededor)");
107        }
108
109        // Cada recorrido que empieza en un cero sin visitar es una zona nueva
110        boolean[][] visto = new boolean[filas][columnas];
111        int zonas = 0;
112        for (int f = 0; f < filas; f++) {
113            for (int c = 0; c < columnas; c++) {
114                if (cuenta[f][c] == 0 && !visto[f][c]) {
115                    zonas++;
116                    despejar(f, c, visto);
117                }
118            }
119        }
120        System.out.println("Zonas despejadas: " + zonas);
121    }
122}

La validación va primero y termina el programa al primer error, así el resto del código puede suponer que el tablero es correcto.

El cálculo de los números es el patrón clásico de los vecinos en una matriz: dos bucles de -1 a 1 y una comprobación de límites. Guardarlos en cuenta evita repetir el trabajo y deja preparadas las dos preguntas siguientes.

Contar zonas es un problema de componentes conexas, el mismo que el cubo de pintura de un programa de dibujo (flood fill). Cada recorrido que empieza en un cero no visitado marca su mancha entera, así que el número de recorridos es el número de zonas.

El recorrido usa una pila explícita en lugar de recursión: en un tablero grande lleno de ceros, una función recursiva podría desbordar la pila de llamadas de Java.

Para ir más allá

  • Genera tableros aleatorios con un número de minas dado, con Random y una semilla para poder repetirlos.
  • Simula una partida: lee jugadas fila columna y muestra el tablero con las casillas descubiertas, abriendo la zona completa al pulsar un cero.
  • Calcula cuántas casillas abre cada zona y cuál es la mayor.

Dónde se explica