En este post vamos a programar el juego de Buscaminas (Minesweeper) en C++ o CPP usando el paradigma de la programación orientada a objetos o POO.
Anteriormente ya había mostrado este mismo juego pero en C, y aunque el código de C es compatible con C++ en este caso lo hice de nuevo y orientado a objetos en C++.
Clase celda
El juego de buscaminas se compone de un tablero y celdas. La celda puede tener o no tener una mina dentro, y puede estar descubierta o no.
En caso de que la celda esté descubierta se debe mostrar la cantidad de celdas contiguas que pueden tener una mina.
class Celda
{
private:
int x, y;
bool mina, descubierta;
public:
Celda(int x, int y, bool tieneMina)
{
this->x = x;
this->y = y;
this->mina = tieneMina;
this->descubierta = false;
}
void imprimir()
{
cout << "Celda en " << this->x << ", " << this->y << " con mina? " << this->mina << "\n";
}
bool establecerMina(bool tieneMina)
{
if (this->tieneMina())
{
return false;
}
else
{
this->mina = tieneMina;
return true;
}
}
bool tieneMina()
{
return this->mina;
}
bool estaDescubierta()
{
return this->descubierta;
}
void setDescubierta(bool descubierta)
{
this->descubierta = descubierta;
}
};
También tiene la opción de imprimir el estado de la celda, el constructor y algunos getters y setters.
Esos métodos nos permitirán saber si la celda está descubierta, tiene mina y para colocar una mina o establecerla como descubierta.
Los getters, setters y constructores son algo esencial de la programación orientada a objetos.
Tablero de juego de Buscaminas en C++
Ahora veamos la clase Tablero
que tendrá las celdas. En este caso estoy ocupando un vector de vectores compuesto de celdas (de la clase Celda).
El tablero de buscaminas tiene varios métodos, vamos a ir explicándolos. Tenemos la función de obtener la representación de una mina, que devuelve el carácter que debe mostrarse.
string obtenerRepresentacionMina(int x, int y)
{
Celda c = this->contenido.at(y).at(x);
if (c.estaDescubierta() || this->modoProgramador)
{
if (c.tieneMina())
{
return "*";
}
else
{
int cantidad = this->minasCercanas(y, x);
string cantidadComoCadena = "";
stringstream ss;
ss << cantidad;
ss >> cantidadComoCadena;
return cantidadComoCadena;
}
}
else
{
return ".";
}
}
Si la mina está descubierta se hace otra verificación. En caso de que la celda tenga una mina, se muestra un asterisco. Si no, se muestra la cantidad de celdas con minas cercanas.
En caso de que la celda no esté descubierta o el modo programador esté desactivado entonces se muestra un punto.
Minas cercanas
Necesitamos una función que nos diga cuántas minas cercanas hay en una celda. Para ello recorremos y contamos todas las celdas contiguas, evitando los límites obviamente.
int minasCercanas(int fila, int columna)
{
int conteo = 0, filaInicio, filaFin, columnaInicio, columnaFin;
if (fila <= 0)
{
filaInicio = 0;
}
else
{
filaInicio = fila - 1;
}
if (fila + 1 >= this->altura)
{
filaFin = this->altura - 1;
}
else
{
filaFin = fila + 1;
}
if (columna <= 0)
{
columnaInicio = 0;
}
else
{
columnaInicio = columna - 1;
}
if (columna + 1 >= this->anchura)
{
columnaFin = this->anchura - 1;
}
else
{
columnaFin = columna + 1;
}
int m;
for (m = filaInicio; m <= filaFin; m++)
{
int l;
for (l = columnaInicio; l <= columnaFin; l++)
{
if (this->contenido.at(m).at(l).tieneMina())
{
conteo++;
}
}
}
return conteo;
}
Fíjate que en la línea 44 estamos usando los métodos de la celda y la programación orientada a objetos por ejemplo la función tieneMina
está definida en la clase Celda
.
Descubrir una mina
Tenemos la función que descubre una celda e indica si ahí había una mina o no:
/*
Regresa false si había una mina. true en caso contrario
*/
bool descubrir(int x, int y)
{
this->contenido.at(y).at(x).setDescubierta(true);
Celda celda = this->contenido.at(y).at(x);
if (celda.tieneMina())
{
return false;
}
return true;
}
En la línea 6 establecemos la celda como descubierta sin importar si tiene una mina o no. Luego comparamos si tiene mina para devolver un booleano indicando si había una mina.
Llenado del tablero
En el constructor de la clase Tablero
hacemos el llenado del vector:
Tablero(int altura, int anchura, bool modoProgramador)
{
this->altura = altura;
this->anchura = anchura;
this->modoProgramador = modoProgramador;
int x, y;
for (y = 0; y < this->altura; y++)
{
vector<Celda> fila;
for (x = 0; x < this->anchura; x++)
{
fila.push_back((Celda(x, y, false)));
}
this->contenido.push_back(fila);
}
}
Recuerda que con push_back
colocamos un elemento al final de un vector y que este elemento puede ser de cualquier tipo. Tampoco olvides que estamos trabajando con un vector de vectores o una matriz.
Contar celdas sin minas y sin descubrir
Para saber si el jugador gana vamos a necesitar contar las celdas que no tienen minas y que no han sido descubiertas.
Si el conteo es 0 es porque el jugador terminó de descubrir las celdas seguras y por lo tanto puede ganar el juego:
int contarCeldasSinMinasYSinDescubrir()
{
int x, y, conteo = 0;
for (y = 0; y < this->altura; y++)
{
for (x = 0; x < this->anchura; x++)
{
Celda c = this->contenido.at(y).at(x);
if (!c.estaDescubierta() && !c.tieneMina())
{
conteo++;
}
}
}
return conteo;
}
En este caso el algoritmo es simplemente recorrer el tablero, comprobar si la celda no está descubierta y no tiene mina para aumentar la variable de conteo.
Cuando acabamos de recorrer el tablero regresamos el conteo.
Clase juego
Veamos la última clase que compone a este juego de Buscaminas en C++ o CPP y es la del juego, que se encarga de conectar el tablero con lo que el usuario ingresa.
En esta clase es en donde se solicita la fila y columna al usuario, se imprime el tablero, etcétera.
También aquí es en donde se colocan las minas aleatoriamente dentro de las celdas, obteniendo las coordenadas como números aleatorios.
Nota: recuerda que si el usuario descubre una celda con mina, automáticamente pierde.
class Juego
{
private:
Tablero tablero;
int cantidadMinas;
int aleatorio_en_rango(int minimo, int maximo)
{
return minimo + rand() / (RAND_MAX / (maximo - minimo + 1) + 1);
}
int filaAleatoria()
{
return this->aleatorio_en_rango(0, this->tablero.obtenerAltura() - 1);
}
int columnaAleatoria()
{
return this->aleatorio_en_rango(0, this->tablero.obtenerAnchura() - 1);
}
public:
Juego(Tablero tablero, int cantidadMinas)
{
this->tablero = tablero;
this->cantidadMinas = cantidadMinas;
this->colocarMinasAleatoriamente();
}
void colocarMinasAleatoriamente()
{
int x, y, minasColocadas = 0;
while (minasColocadas < this->cantidadMinas)
{
x = this->columnaAleatoria();
y = this->filaAleatoria();
if (this->tablero.colocarMina(x, y))
{
minasColocadas++;
}
}
}
/*
solicitarFila y solicitarColumna piden la fila y columna del 1 al N, pero
recordemos que los índices se manejan del 0 al N-1, por eso es que se resta 1
*/
int solicitarFila()
{
int fila = 0;
cout << "Ingresa la fila: ";
cin >> fila;
return fila - 1;
}
int solicitarColumna()
{
int columna = 0;
cout << "Ingresa la columna: ";
cin >> columna;
return columna - 1;
}
bool jugadorGana()
{
int conteo = this->tablero.contarCeldasSinMinasYSinDescubrir();
if (conteo == 0)
{
return true;
}
else
{
return false;
}
}
void iniciar()
{
int fila, columna;
while (true)
{
this->tablero.imprimir();
fila = this->solicitarFila();
columna = this->solicitarColumna();
bool ok = this->tablero.descubrir(columna, fila);
if (!ok)
{
cout << "Perdiste\n";
// El modo programador te permite ver todo. Entonces lo activamos y volvemos a imprimir. No hay problema porque el jugador ya perdió
this->tablero.setModoProgramador(true);
this->tablero.imprimir();
break;
}
if (this->jugadorGana())
{
cout << "Ganaste\n";
this->tablero.setModoProgramador(true);
this->tablero.imprimir();
break;
}
}
}
};
En esta clase es en donde todos los elementos interactúan, se comprueba si el jugador gana, etcétera. Y podemos crear varias instancias de esta clase, lo cual es algo bonito de la programación orientada a objetos.
Poniendo todo junto
Ahora que tenemos nuestras clases del juego buscaminas programado en C++ podemos instanciarla desde el main y comenzar a jugar:
int main()
{
srand(getpid());
int filas = 3;
int columnas = 3;
int minas = 2;
bool modoProgramador = false; // Poner en true para depurar
Juego juego(Tablero(filas, columnas, modoProgramador), minas);
juego.iniciar();
}
Simplemente construimos nuestro objeto de la clase Juego
pasándole un tablero y la cantidad de minas que debe colocar.
Al tablero le indicamos las filas, columnas y si estamos en modo programador. En este caso para la salida de la imagen utilicé 6 filas, 6 columnas y 4 minas.
Código fuente
El código fuente completo con todos los archivos te lo dejo en GitHub.
Basta con compilar main.cpp
. Si tienes g++ entonces ejecuta g++ main.cpp -o main.exe
y luego ejecuta ./main.exe
. G++ es compatible con Windows, Mac y Linux. Incluso en Android.
Para terminar te dejo con más proyectos de C++ en mi blog.