Buscaminas: números, casilla más peligrosa y zonas despejadas
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
- 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. - 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. - 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). Minas: M de T casillas (P %), con el porcentaje con un decimal y coma decimal.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.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.
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.
Ejemplo
5 6 *..... ..*... ...... ...**. ......
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
Solución explicada
Ver la solución completa
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
Randomy una semilla para poder repetirlos. - Simula una partida: lee jugadas
fila columnay 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.