Buscaminas en C – Juego

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.

Características del juego

Buscaminas en C – Jugando en Ubuntu

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.

Versión de C++

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.

Explicación del algoritmo

El algoritmo es realmente sencillo. El tablero de juego será una matriz de tipo char en donde se almacenará determinado carácter. Puede haber:

  • Espacio sin descubrir
  • Espacio descubierto (aquí se dibuja la cantidad de minas que hay cerca)
  • Mina

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.

Dibujar tablero

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.

Colocar minas de manera aleatoria

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);
  }
}

Abrir casilla

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.

Obtener minas cercanas

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;
}

Saber si el usuario gana

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;
}

Funcionamiento del juego

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.

Constantes

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

Poniendo todo junto

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.

Vídeo de YouTube

En caso de que no hayas entendido algo, aquí tengo un vídeo explicando el juego y su funcionamiento:

Conclusión

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.

Estoy aquí para ayudarte 🤝💻


Estoy aquí para ayudarte en todo lo que necesites. Si requieres alguna modificación en lo presentado en este post, deseas asistencia con tu tarea, proyecto o precisas desarrollar un software a medida, no dudes en contactarme. Estoy comprometido a brindarte el apoyo necesario para que logres tus objetivos. Mi correo es parzibyte(arroba)gmail.com, estoy como@parzibyte en Telegram o en mi página de contacto

No te pierdas ninguno de mis posts 🚀🔔

Suscríbete a mi canal de Telegram para recibir una notificación cuando escriba un nuevo tutorial de programación.

Dejar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *