Hoy vamos a ver cómo implementar el juego buscaminas en C. Pasa que por diversión quise hacerlo y quedó muy bien a mi parecer.
Se trata del juego buscaminas en donde el usuario elige una casilla para descubrir lo que hay bajo la misma. Si hay una mina, el usuario pierde. En caso de que no, se le muestra al usuario el número de minas que hay cerca a esa casilla.
Lo que haremos será mostrar el tablero de juego, solicitar al usuario la casilla, ocultar las minas de manera aleatoria y saber si el usuario gana o pierde. Todo esto será modificable dentro del código fuente.
Está escrito completamente en C estándar. Compila como un encanto en Windows y Linux (probado en Windows 10 y en Ubuntu, así como en mi Android usando Termux).
Permite personalizar el número de minas, número de filas y columnas
El modo debug permite mostrar las minas, de este modo, puedes ver la ubicación de las minas al realizar pruebas de software
Ahora sí veamos el código fuente y algoritmo.
Ahora también existe la versión nativa para C++. El código es distinto, pues se usa la programación orientada a objetos pero es interesante de analizar.
El algoritmo es realmente sencillo. El tablero de juego será una matriz de tipo char en donde se almacenará determinado carácter. Puede haber:
Para que sea fácil para el usuario, la columna se pide como número, y la fila como letra. Luego, esa letra se convierte a número en donde la A es un 0, la B es un 1, etcétera.
Al dibujar el escenario, se ocultan las minas al usuario. Es decir, no se dibuja el escenario original, sino uno dependiendo del estado del juego. Si se ha ganado o perdido, se muestran las minas.
Lo demás es simplemente el recorrido de la matriz, comprobación de cada char en determinadas coordenadas, etcétera.
Para el tablero dibujamos el encabezado, un separador por cada fila y los datos en sí. También colocamos la letra de la fila en cada paso. Todas las funciones reciben el tablero:
void imprimirSeparadorEncabezado() {
int m;
for (m = 0; m <= COLUMNAS; m++) {
printf("----");
if (m + 2 == COLUMNAS) {
printf("-");
}
}
printf("\n");
}
void imprimirSeparadorFilas() {
int m;
for (m = 0; m <= COLUMNAS; m++) {
printf("+---");
if (m == COLUMNAS) {
printf("+");
}
}
printf("\n");
}
void imprimirEncabezado() {
imprimirSeparadorEncabezado();
printf("| ");
int l;
for (l = 0; l < COLUMNAS; l++) {
printf("| %d ", l + 1);
if (l + 1 == COLUMNAS) {
printf("|");
}
}
printf("\n");
}
// Convierte un int a un char. Por ejemplo 0 a '0'
char enteroACaracter(int numero) {
return numero + '0';
}
void imprimirTablero(char tablero[FILAS][COLUMNAS], int deberiaMostrarMinas) {
imprimirEncabezado();
imprimirSeparadorEncabezado();
char letra = 'A';
int l;
for (l = 0; l < FILAS; l++) {
int m;
// Imprimir la letra de la fila
printf("| %c ", letra);
letra++;
for (m = 0; m < COLUMNAS; m++) {
// No le vamos a mostrar al usuario si hay una mina...
char verdaderaLetra = ESPACIO_SIN_DESCUBRIR;
char letraActual = tablero[l][m];
if (letraActual == MINA) {
verdaderaLetra = ESPACIO_SIN_DESCUBRIR;
} else if (letraActual == ESPACIO_DESCUBIERTO) {
// Si ya lo abrió, entonces mostramos las minas cercanas
int minasCercanas = obtenerMinasCercanas(l, m, tablero);
verdaderaLetra = enteroACaracter(minasCercanas);
}
// Si DEBUG está en 1, o debería mostrar las minas (porque perdió o ganó)
// mostramos la mina original
if (letraActual == MINA && (DEBUG || deberiaMostrarMinas)) {
verdaderaLetra = MINA;
}
printf("| %c ", verdaderaLetra);
if (m + 1 == COLUMNAS) {
printf("|");
}
}
printf("\n");
imprimirSeparadorFilas();
}
}
Si en el código notas que hay funciones o constantes que no están definidas todavía, no te preocupes, al final dejaré el código completo. Lo que hace este código es dibujar el tablero, ocultar o mostrar las minas y dibujar las minas cercanas en caso de que el usuario haya descubierto una casilla.
Las minas se colocan en el tablero de manera aleatoria. Podemos cambiar el número de las mismas y así agregar dificultar al juego. Lo que se hace es obtener obtener un número aleatorio tanto para la fila como para la columna.
// Coloca una mina en las coordenadas indicadas
void colocarMina(int fila, int columna, char tablero[FILAS][COLUMNAS]) {
tablero[fila][columna] = MINA;
}
// Coloca minas de manera aleatoria. El número depende de CANTIDAD_MINAS
void colocarMinasAleatoriamente(char tablero[FILAS][COLUMNAS]) {
int l;
for (l = 0; l < CANTIDAD_MINAS; l++) {
int fila = aleatorioEnRango(0, FILAS - 1);
int columna = aleatorioEnRango(0, COLUMNAS - 1);
colocarMina(fila, columna, tablero);
}
}
Esta función es la que invoca el usuario al momento de jugar. Se trata de abrir una casilla y dependiendo de lo que haya en la misma, se devuelve un estado.
Por ejemplo, si hay una mina, se indica. Si la casilla ya estaba abierta también se indica. Estos estados son capturados para saber si el usuario ha perdido o si todo ha ido bien.
// Recibe la fila, columna y tablero. La fila y columna deben ser tal y como las
// proporciona el usuario. Es decir, la columna debe comenzar en 1 (no en cero
// como si fuera un índice) y la fila debe ser una letra
int abrirCasilla(char filaLetra, int columna, char tablero[FILAS][COLUMNAS]) {
// Convertir a mayúscula
filaLetra = toupper(filaLetra);
// Restamos 1 porque usamos la columna como índice
columna--;
// Convertimos la letra a índice
int fila = filaLetra - 'A';
assert(columna < COLUMNAS && columna >= 0);
assert(fila < FILAS && fila >= 0);
if (tablero[fila][columna] == MINA) {
return ERROR_MINA_ENCONTRADA;
}
if (tablero[fila][columna] == ESPACIO_DESCUBIERTO) {
return ERROR_ESPACIO_YA_DESCUBIERTO;
}
// Si no hay error, colocamos el espacio descubierto
tablero[fila][columna] = ESPACIO_DESCUBIERTO;
return ERROR_NINGUNO;
}
Esta función recibe las coordenadas tal y como el usuario las ingresa. Convierte la letra a mayúscula, y después la convierte a índice. A la columna le resta 1, pues el usuario final no sabe que los arreglos comienzan en 0.
Por cierto, también estoy usando assert
para validar que la columna y la fila son correctos; es decir, que están dentro del rango válido.
Para las minas cercanas se visitan las casillas que están junto a la casilla de la coordenada indicada. No importa si está en una esquina o en un borde, esto está validado:
// Devuelve el número de minas que hay cercanas en determinada coordenada
int obtenerMinasCercanas(int fila, int columna, char tablero[FILAS][COLUMNAS]) {
int conteo = 0, filaInicio, filaFin, columnaInicio, columnaFin;
if (fila <= 0) {
filaInicio = 0;
} else {
filaInicio = fila - 1;
}
if (fila + 1 >= FILAS) {
filaFin = FILAS - 1;
} else {
filaFin = fila + 1;
}
if (columna <= 0) {
columnaInicio = 0;
} else {
columnaInicio = columna - 1;
}
if (columna + 1 >= COLUMNAS) {
columnaFin = COLUMNAS - 1;
} else {
columnaFin = columna + 1;
}
int m;
for (m = filaInicio; m <= filaFin; m++) {
int l;
for (l = columnaInicio; l <= columnaFin; l++) {
if (tablero[m][l] == MINA) {
conteo++;
}
}
}
return conteo;
}
El usuario gana cuando ya no hay espacios por descubrir. Por lo tanto lo único que hay que hacer es comprobar que ya no haya ninguna casilla con esa característica, recorriendo el tablero.
// Para saber si el usuario ganó
int noHayCasillasSinAbrir(char tablero[FILAS][COLUMNAS]) {
int l;
for (l = 0; l < FILAS; l++) {
int m;
for (m = 0; m < COLUMNAS; m++) {
char actual = tablero[l][m];
if (actual == ESPACIO_SIN_DESCUBRIR) {
return 0;
}
}
}
return 1;
}
Para el juego hacemos un ciclo infinito que se romperá cuando el jugador pierda o gane. En el mismo solicitamos las coordenadas e indicamos el estado. También dibujamos el tablero.
printf("** BUSCAMINAS **\nBy Parzibyte\n");
char tablero[FILAS][COLUMNAS];
int deberiaMostrarMinas = 0;
// Alimentar rand
srand(getpid());
iniciarTablero(tablero);
colocarMinasAleatoriamente(tablero);
// Ciclo infinito. Se rompe si gana o pierde, y eso se define con
// "deberiaMostrarMinas"
while (1) {
imprimirTablero(tablero, deberiaMostrarMinas);
if (deberiaMostrarMinas) {
break;
}
int columna;
char fila;
printf("Ingresa la fila: ");
scanf(" %c", &fila);
printf("Ingresa la columna: ");
scanf("%d", &columna);
int status = abrirCasilla(fila, columna, tablero);
if (noHayCasillasSinAbrir(tablero)) {
printf("Has ganado\n");
deberiaMostrarMinas = 1;
} else if (status == ERROR_ESPACIO_YA_DESCUBIERTO) {
printf("Ya has abierto esta casilla\n");
} else if (status == ERROR_MINA_ENCONTRADA) {
printf("Has perdido\n");
deberiaMostrarMinas = 1;
}
}
Fíjate en que el fin del juego se controla con la variable deberiaMostrarMinas
que se activa cuando el jugador pierde o gana, y que ocasiona que se muestren las minas del tablero además de romper el ciclo.
Bien, ahora veamos las constantes. Aquí se pueden configurar varios aspectos del juego como el tamaño del tablero, el modo debug (que muestra las minas) o la cantidad de minas.
#define COLUMNAS 5
#define FILAS 5
#define ESPACIO_SIN_DESCUBRIR '.'
#define ESPACIO_DESCUBIERTO ' '
#define MINA '*'
#define CANTIDAD_MINAS \
5 // ¿cuántas minas colocar en el tablero de manera aleatoria? va a fallar si
// hay menos espacio que el número de minas
#define DEBUG 0 // Si lo pones en 1, se van a desocultar las minas
Creo que ya he explicado lo suficiente. He aquí el código completo:
/*
____ _____ _ _ _
| _ \ | __ \ (_) | | |
| |_) |_ _ | |__) |_ _ _ __ _____| |__ _ _| |_ ___
| _ <| | | | | ___/ _` | '__|_ / | '_ \| | | | __/ _ \
| |_) | |_| | | | | (_| | | / /| | |_) | |_| | || __/
|____/ \__, | |_| \__,_|_| /___|_|_.__/ \__, |\__\___|
__/ | __/ |
|___/ |___/
Blog: https://parzibyte.me/blog
Ayuda: https://parzibyte.me/blog/contrataciones-ayuda/
Contacto: https://parzibyte.me/blog/contacto/
Copyright (c) 2020 Luis Cabrera Benito
Licenciado bajo la licencia MIT
El texto de arriba debe ser incluido en cualquier redistribución
Errores esperados, documentación y esas cosas...
- No puede haber más filas que letras del abecedario, porque solo se imprime
hasta la Z
- No puede haber más de 9 columnas porque ya no se imprimiría bien, y porque
ya no se convertiría correctamente el entero a cadena para mostrar las minas
cercanas
- Al colocar las minas se puede colocar una mina sobre otra mina, ya que no se
verifica lo que hay antes de colocarla. Por lo tanto, en ocasiones se podrían
poner menos minas de las que se configuran
*/
#include <assert.h> // assert
#include <ctype.h> // toupper
#include <stdio.h> // printf, scanf
#include <stdlib.h> // rand
#include <unistd.h> // getpid
// Cosas que no deberías modificar si no sabes lo que haces
#define ERROR_MINA_ENCONTRADA 1
#define ERROR_ESPACIO_YA_DESCUBIERTO 2
#define ERROR_NINGUNO 3
// Cosas que puedes modificar ;)
#define COLUMNAS 5
#define FILAS 5
#define ESPACIO_SIN_DESCUBRIR '.'
#define ESPACIO_DESCUBIERTO ' '
#define MINA '*'
#define CANTIDAD_MINAS \
5 // ¿cuántas minas colocar en el tablero de manera aleatoria? va a fallar si
// hay menos espacio que el número de minas
#define DEBUG 0 // Si lo pones en 1, se van a desocultar las minas
// Devuelve el número de minas que hay cercanas en determinada coordenada
int obtenerMinasCercanas(int fila, int columna, char tablero[FILAS][COLUMNAS]) {
int conteo = 0, filaInicio, filaFin, columnaInicio, columnaFin;
if (fila <= 0) {
filaInicio = 0;
} else {
filaInicio = fila - 1;
}
if (fila + 1 >= FILAS) {
filaFin = FILAS - 1;
} else {
filaFin = fila + 1;
}
if (columna <= 0) {
columnaInicio = 0;
} else {
columnaInicio = columna - 1;
}
if (columna + 1 >= COLUMNAS) {
columnaFin = COLUMNAS - 1;
} else {
columnaFin = columna + 1;
}
int m;
for (m = filaInicio; m <= filaFin; m++) {
int l;
for (l = columnaInicio; l <= columnaFin; l++) {
if (tablero[m][l] == MINA) {
conteo++;
}
}
}
return conteo;
}
// Devuelve un número aleatorio entre minimo y maximo, incluyendo a minimo y
// maximo
// https://parzibyte.me/blog/2019/03/21/obtener-numeros-aleatorios-c/
int aleatorioEnRango(int minimo, int maximo) {
return minimo + rand() / (RAND_MAX / (maximo - minimo + 1) + 1);
}
// Rellena el tablero de espacios sin descubrir
void iniciarTablero(char tablero[FILAS][COLUMNAS]) {
int l;
for (l = 0; l < FILAS; l++) {
int m;
for (m = 0; m < COLUMNAS; m++) {
tablero[l][m] = ESPACIO_SIN_DESCUBRIR;
}
}
}
// Coloca una mina en las coordenadas indicadas
void colocarMina(int fila, int columna, char tablero[FILAS][COLUMNAS]) {
tablero[fila][columna] = MINA;
}
// Coloca minas de manera aleatoria. El número depende de CANTIDAD_MINAS
void colocarMinasAleatoriamente(char tablero[FILAS][COLUMNAS]) {
int l;
for (l = 0; l < CANTIDAD_MINAS; l++) {
int fila = aleatorioEnRango(0, FILAS - 1);
int columna = aleatorioEnRango(0, COLUMNAS - 1);
colocarMina(fila, columna, tablero);
}
}
void imprimirSeparadorEncabezado() {
int m;
for (m = 0; m <= COLUMNAS; m++) {
printf("----");
if (m + 2 == COLUMNAS) {
printf("-");
}
}
printf("\n");
}
void imprimirSeparadorFilas() {
int m;
for (m = 0; m <= COLUMNAS; m++) {
printf("+---");
if (m == COLUMNAS) {
printf("+");
}
}
printf("\n");
}
void imprimirEncabezado() {
imprimirSeparadorEncabezado();
printf("| ");
int l;
for (l = 0; l < COLUMNAS; l++) {
printf("| %d ", l + 1);
if (l + 1 == COLUMNAS) {
printf("|");
}
}
printf("\n");
}
// Convierte un int a un char. Por ejemplo 0 a '0'
char enteroACaracter(int numero) {
return numero + '0';
}
void imprimirTablero(char tablero[FILAS][COLUMNAS], int deberiaMostrarMinas) {
imprimirEncabezado();
imprimirSeparadorEncabezado();
char letra = 'A';
int l;
for (l = 0; l < FILAS; l++) {
int m;
// Imprimir la letra de la fila
printf("| %c ", letra);
letra++;
for (m = 0; m < COLUMNAS; m++) {
// No le vamos a mostrar al usuario si hay una mina...
char verdaderaLetra = ESPACIO_SIN_DESCUBRIR;
char letraActual = tablero[l][m];
if (letraActual == MINA) {
verdaderaLetra = ESPACIO_SIN_DESCUBRIR;
} else if (letraActual == ESPACIO_DESCUBIERTO) {
// Si ya lo abrió, entonces mostramos las minas cercanas
int minasCercanas = obtenerMinasCercanas(l, m, tablero);
verdaderaLetra = enteroACaracter(minasCercanas);
}
// Si DEBUG está en 1, o debería mostrar las minas (porque perdió o ganó)
// mostramos la mina original
if (letraActual == MINA && (DEBUG || deberiaMostrarMinas)) {
verdaderaLetra = MINA;
}
printf("| %c ", verdaderaLetra);
if (m + 1 == COLUMNAS) {
printf("|");
}
}
printf("\n");
imprimirSeparadorFilas();
}
}
// Recibe la fila, columna y tablero. La fila y columna deben ser tal y como las
// proporciona el usuario. Es decir, la columna debe comenzar en 1 (no en cero
// como si fuera un índice) y la fila debe ser una letra
int abrirCasilla(char filaLetra, int columna, char tablero[FILAS][COLUMNAS]) {
// Convertir a mayúscula
filaLetra = toupper(filaLetra);
// Restamos 1 porque usamos la columna como índice
columna--;
// Convertimos la letra a índice
int fila = filaLetra - 'A';
assert(columna < COLUMNAS && columna >= 0);
assert(fila < FILAS && fila >= 0);
if (tablero[fila][columna] == MINA) {
return ERROR_MINA_ENCONTRADA;
}
if (tablero[fila][columna] == ESPACIO_DESCUBIERTO) {
return ERROR_ESPACIO_YA_DESCUBIERTO;
}
// Si no hay error, colocamos el espacio descubierto
tablero[fila][columna] = ESPACIO_DESCUBIERTO;
return ERROR_NINGUNO;
}
// Para saber si el usuario ganó
int noHayCasillasSinAbrir(char tablero[FILAS][COLUMNAS]) {
int l;
for (l = 0; l < FILAS; l++) {
int m;
for (m = 0; m < COLUMNAS; m++) {
char actual = tablero[l][m];
if (actual == ESPACIO_SIN_DESCUBRIR) {
return 0;
}
}
}
return 1;
}
int main() {
printf("** BUSCAMINAS **\nBy Parzibyte\n");
char tablero[FILAS][COLUMNAS];
int deberiaMostrarMinas = 0;
// Alimentar rand
srand(getpid());
iniciarTablero(tablero);
colocarMinasAleatoriamente(tablero);
// Ciclo infinito. Se rompe si gana o pierde, y eso se define con
// "deberiaMostrarMinas"
while (1) {
imprimirTablero(tablero, deberiaMostrarMinas);
if (deberiaMostrarMinas) {
break;
}
int columna;
char fila;
printf("Ingresa la fila: ");
scanf(" %c", &fila);
printf("Ingresa la columna: ");
scanf("%d", &columna);
int status = abrirCasilla(fila, columna, tablero);
if (noHayCasillasSinAbrir(tablero)) {
printf("Has ganado\n");
deberiaMostrarMinas = 1;
} else if (status == ERROR_ESPACIO_YA_DESCUBIERTO) {
printf("Ya has abierto esta casilla\n");
} else if (status == ERROR_MINA_ENCONTRADA) {
printf("Has perdido\n");
deberiaMostrarMinas = 1;
}
}
return 0;
}
Si quieres compilarlo instala gcc u otro compilador de C (debería funcionar también en el editor Dev C++). Luego compila con:
gcc main.c
Ejecuta el archivo de salida y listo.
Por cierto, en los comentarios he colocado algunos errores esperados.
En caso de que no hayas entendido algo, aquí tengo un vídeo explicando el juego y su funcionamiento:
Nota: el código fuente actualizado está en mi GitHub. Si vas por ahí, déjale una estrella y sígueme, que nada te cuesta.
Oh, y si te lo preguntas, lo he escrito en C para que sea sencillo portarlo a otros lenguajes de programación.
Hoy te voy a presentar un creador de credenciales que acabo de programar y que…
Ya te enseñé cómo convertir una aplicación web de Vue 3 en una PWA. Al…
En este artículo voy a documentar la arquitectura que yo utilizo al trabajar con WebAssembly…
En un artículo anterior te enseñé a crear un PWA. Al final, cualquier aplicación que…
Al usar Comlink para trabajar con los workers usando JavaScript me han aparecido algunos errores…
En este artículo te voy a enseñar cómo usar un "top level await" esperando a…
Esta web usa cookies.