Crea tu propio buscaminas


Desarrollo tech.lat dev

Crea tu propio buscaminas

Crea tu propio buscaminas

Jorge Rubira

Si hay algún juego favorito por profesores de universidad para enviar como prácticas, este es el buscaminas. Este juego fue creado por Robert Donner en 1989. Se hizo popular gracias a que se incluyó en el sistema operativo Windows 3.11. Desde entonces, ha permanecido en todas sus posteriores versiones.

A pesar de ser archiconocido este juego, es muy fácil desarrollar una versión aceptable o amateur. En este post vamos a explicar las funcionalidades más interesantes a tener en cuenta en el momento del desarrollo. Finalmente, dejaremos un código fuente en Java de ejemplo.

Reglas del juego

Este apartado lo explicaré resumido ya que en la actualidad es de conocimiento común las reglas de este juego. El juego se basa en un tablero de NxM casillas que pueden ser minas o terreno. El objetivo del juego es descubrir, haciendo clic sobre las casillas, las celdas que son terreno sin hacer clic a las minas.

Para ello, cuando se hace clic sobre terreno (sin mina) se visualiza un número con el total de minas que hay alrededor. Si se hace clic sobre terreno sin mina alrededor, se descubren las celdas continuas sucesivamente hasta que se encuentran terrenos con minas cerca. Si se hace clic sobre una mina se acaba la partida. Por último, si se tiene la sospecha de que existe una mina, se puede marcar la casilla pulsando el botón derecho.

En un buscaminas cualquiera es posible que el número de casillas y el número de minas pueda ser configurable por el usuario. En nuestro caso vamos a crear un tablero de 10×10 casillas con 10 minas en el tablero. Pintaremos de rojo las minas y de azul las celdas marcadas con una bandera.

Estructura del juego

La programación del juego se basa en dos algoritmos importantes de lógica y un algoritmo de visualización. Los algoritmos considerables a estudiar son: rellenar el tablero aleatoriamente y hacer que cuando se pulse una casilla sin mina cerca se despliegue el terreno hasta encontrar celdas cercanas a minas. En cuanto al algoritmo de visualización dependerá del modo de visualización que utilices: lienzo, botones, html, etc. Los dos algoritmos de lógica los veremos en próximos apartados.

Por otra parte, como elementos secundarios (pero esenciales), existirá una variable de estado que indicará si se está jugando (0), hemos perdido (1) o hemos ganado (2). Igualmente, cada celda tendrá también un estado que indicará si está tapada (0), está descubierta (1) o si se ha puesto una bandera (2).

También existe una variable casillasVistas que nos indicará cuantas casillas hemos descubierto para detectar si hemos conseguido el objetivo. Esta variable no es obligatoria ya que se puede calcular cada vez que destapamos una celda contando los estados de celdas descubiertas. Sin embargo, por motivos de eficiencia y evitar escribir más código, es bastante recomendable hacer esta variable.

Rellenar el tablero aleatorio

En muchas ocasiones al trasladar una imagen visual al problema tendemos a imitar lo que vemos. Por ello, lo más intuitivo es crear una matriz para almacenar el tablero. Sin embargo, esta no es la única solución ya que se podrían almacenar las minas en un vector dinámico y realizar las consultas únicamente recorriendo este vector de minas.

Para nuestro ejemplo … no vamos a ser tan malos y vamos a crear el tablero en forma de matriz para que sea más fácil de entender. En el momento de plantear un tablero en forma de matriz se pueden plantear dos posibles formas de trabajo. Poner únicamente las minas y calcular cuantas hay cerca al hacer clic o precalcular los números de las casillas con las minas cercanas al principio de la partida.

Para nuestro caso, programaremos la segunda opción y precalcularemos todo. Como consecuencia, como almacenaremos valores de 0 a 8 indicando cuantas minas hay alrededor, no podremos utilizarlos para representar una mina así que utilizaremos el valor 9 para indicar que hay una mina.

El primer problema que nos podemos encontrar es como poner 10 minas aleatoriamente. En el momento de poner una mina, al obtenerlo aleatoriamente hay que tener cuidado de no poner dos minas en la misma casilla. Por ello, deberemos repetir el resultado hasta que encuentre un sitio libre de mina. En todo caso es importante controlar que el número de minas es menor o igual al total de celdas ya que en caso contrario podría producirse un bucle infinito. Esta verificación puede realizarse en el momento de ejecutar el método o en el momento que el usuario cambia la configuración de minas, filas o columnas no permitiendo valores incoherentes al problema.

hacer{ obtener fila y columna al azar}mientras que (tablero[fila][columna]==mina);

Con esto ya tendríamos el algoritmo que pone las minas, pero faltaría obtener los valores 1-8 de las celdas cercanas a las minas. Esto se puede hacer de dos maneras: calcularlo al final de poner todas las minas o incrementar el alrededor de la celda cuando se una mina. Nosotros utilizaremos esta segunda.

En el momento de recorrer el alrededor de una mina hay que tener cuidado de no salirse del tablero en el caso de poner la mina en un lateral o esquina. La solución intuitiva es utilizar un if en el recorrido comprobando que no se sale del vector. Sin embargo, se puede sacar una solución más corta conociendo las funciones mínimo y máximo.

para fila2 = max(0,fila-1) hasta min(TAM-1, fila+1) hacer para columna2 = max(0,col-1) hasta min(TAM-1, col+1) hacer si (no es mina tablero[fila2, columna2]) entonces tablero[fila2,columna2]++ fin de si fin de parafin de para

Pulsar una celda sin minas alrededor

Otro reto que se encuentran los alumnos en este juego es descubrir más celdas en el caso de que no haya minas cerca. A simple vista, la solución intuitiva para un alumno novel es utilizar una iteración while, por no saber cuantas celdas (veces) se van a recorrer. Sin embargo, la siguiente duda que viene es como hacer que se recorra un área, bastante asimétrica, por cierto.

El while es una posible solución combinándola con un vector dinámico, sin embargo aprender a utilizar algoritmos recursivos para este caso es una buena idea. No hay que olvidar que una iteración while es una iteración lineal (de cada elemento solo puede ejecutar uno a continuación). Sin embargo, los algoritmos recursivos nos permiten que de cada elemento se ejecuten N elementos similares. En el caso del buscaminas de cada celda se pueden destapar 8 celdas más. El algoritmo de manera resumida sería algo así

si es celda a destapar entonces destapa celda si tiene valor 0 entonces para todas las celdas de alrededor hacer si no es mina entonces hacer clic recursivamente en las celdas cercanas fin de si fin de para fin de sifin de si

Código fuente del buscaminas

Finalmente aquí tenéis un código fuente de ejemplo de buscaminas que podeis tomar como ejemplo y, por supuesto, probarlo y modificarlo.

import java.awt.*;import java.awt.event.*;import javax.swing.JFrame;public class Buscaminas extends JFrame { public static int TAM=10; private int tablero[][]=new int[TAM][TAM]; //Representación del tablero private int visible[][]=new int[TAM][TAM]; //0 tapado, 1 descubierto, 2 bandera private int estado=0; //0 jugando, 1 game over, 2 victoria private int casillasVistas=0; //Contador de casillas vistas public Buscaminas(){ //Configuracion de la ventana setVisible(true); setSize(405, 440); setTitle( "Buscaminas casero. By Jorge Rubira" ); setResizable(false); //Crea el tablero crearTablero(); //Eventos al pulsar el raton addMouseListener(new MouseListener() { public void mouseReleased(MouseEvent arg) { //Si estamos jugando if (estado==0){ //Obtiene fila y columna pulsada int f=(arg.getY()-40)/40; int c=arg.getX()/40; if (arg.getButton()==MouseEvent.BUTTON1){ if (visible[f][c]==0){ if (tablero[f][c]==9){ //Si pulsa una mina acaba la partida gameOver(); }else{ //Si pulsa un terreno lo visualiza ejecutando una funcion recursiva clicCasilla(f,c); } } }else if (arg.getButton()==MouseEvent.BUTTON3){ if (visible[f][c]==0){ visible[f][c]=2; }else if (visible[f][c]==2){ visible[f][c]=0; } } }else{ crearTablero(); } repaint(); } public void mousePressed(MouseEvent arg0) {} public void mouseClicked(MouseEvent e) {} public void mouseExited(MouseEvent arg0) {} public void mouseEntered(MouseEvent arg0) {} }); addWindowListener(new WindowListener() { public void windowClosing(WindowEvent arg0) { System.exit(0); } public void windowOpened(WindowEvent arg0) {} public void windowIconified(WindowEvent arg0) {} public void windowDeiconified(WindowEvent arg0) {} public void windowDeactivated(WindowEvent arg0) {} public void windowClosed(WindowEvent arg0) {} public void windowActivated(WindowEvent arg0) {} }); } public void gameOver(){ estado=1; } public void victoria(){ estado=2; } public void clicCasilla(int f, int c){ //Si la casilla esta tapada if (visible[f][c]==0){ //Descubre la casilla visible[f][c]=1; casillasVistas++; if (casillasVistas==90){ //Si llega a las 90 casillas descubiertas gana victoria(); }else{ //Si no hay minas cercanas if (tablero[f][c]==0){ //Recorre las casillas cercanas y tambien las ejecuta for (int f2=max(0, f-1);f2 < min(TAM,f+2);f2++){ for (int c2=max(0,c-1);c2 < min(TAM,c+2);c2++){ clicCasilla(f2, c2); } } } } } } public void crearTablero(){ //Inicializa el tablero for (int f=0;f < TAM;f++){ for (int c=0;c < TAM;c++){ tablero[f][c]=0; visible[f][c]=0; } } estado=0; casillasVistas=0; //Pone diez minas for (int mina=0;mina < 10;mina++){ //Busca una posición aleatoria donde no haya otra bomba int f,c; do{ f=(int)(Math.random()*10); c=(int)(Math.random()*10); }while(tablero[f][c]==9); //Pone la bomba tablero[f][c]=9; //Recorre el contorno de la bomba e incrementa los contadores for (int f2=max(0, f-1);f2 < min(TAM,f+2);f2++){ for (int c2=max(0,c-1);c2 < min(TAM,c+2);c2++){ if (tablero[f2][c2]!=9){ //Si no es bomba tablero[f2][c2]++; //Incrementa el contador } } } } } public void update(Graphics g){ paint(g); } public void paint(Graphics g){ g.setFont(new Font( "ARIAL" , Font.BOLD, 14)); g.clearRect(0, 0, getWidth(), 40); //Pinta las casillas for (int f=0;f < TAM;f++){ for (int c=0;c < TAM;c++){ int x=c*40; int y=f*40+40; if (visible[f][c]==0 && estado==0){ g.setColor(Color.gray); g.fillRect(x, y, 40, 40); }else if (visible[f][c]==2 && estado==0){ g.setColor(Color.blue); g.fillRect(x, y, 40, 40); }else if (tablero[f][c] < 9){ g.setColor(Color.white); g.fillRect(x, y, 40, 40); if (tablero[f][c]>0){ g.setColor(Color.black); g.drawString( ""+tablero[f][c], x+15, y+25); } }else{ g.setColor(Color.red); g.fillRect(x, y, 40, 40); } g.setColor(Color.DARK_GRAY); g.drawRect(x, y, 40, 40); } } //Texto de estados (no jugando) if (estado==1){ g.drawString( "Game over", 10, 40); } if (estado==2){ g.drawString( "Conseguido!!!", 10, 40); } } //Funciones para abreviar letras. public int max(int a, int b){ return Math.max(a,b); } public int min(int a, int b){ return Math.min(a,b); } public static void main(String arg[]){ new Buscaminas(); }}