[C++] [Aporte] Buscaminas por consola

Iniciado por Wofo, 11 Marzo 2013, 16:20 PM

0 Miembros y 1 Visitante están viendo este tema.

Wofo

Buenas gente, luego de algunos días trabajando en un buscaminas para aprender, les dejo el código para que lo puedan aprovechar.

Lo programé poniendo especial empeño en que fuera orientado a objetos y exclusivamente C++ (intenté no usar nada de C). También escribí muchos comentarios para que quede todo más claro y sea muy fácil de entender.

Espero que a alguien le sirva para aprender. Por lo menos a mí me sirvió bastante.

Una particularidad del programa es que, gracias a que está orientado a objetos, es posible crear una interfaz gráfica sin tener que modificar ni una sola línea de código dentro de las clases.

Conceptos implementados:
-Herencia
-Manejo de errores (excepciones)
-Uso de la clase vector
-Generación de números aleatorios




*Nota: este es un segundo upload con correcciones para mejorar el manejo de excepciones y una mejor orientación a objetos
Programa: Buscaminas
IDE: Ninguna (Notepad++) *Se puede compilar escribiendo g++ main.cpp -o archivo_compilado
Link: http://depositfiles.com/files/z79itfr7f

________

Por si alguien prefiere copiar el código en vez de descargarlo de depositfiles, lo dejo aquí también:
*Es importante guardar los archivos con el mismo nombre, para que funcionen los includes correctamente.

main.cpp
Código (cpp) [Seleccionar]
#include <iostream> //Necesario para mostrar el contenido por la consola
#include <stdexcept> //Necesario para manejo de errores
#include <cstdlib> //Necesario para rand() en la clase Random_Number
#include <time.h> //Necesario para la clase Random_Number
#include <vector> //Necesario para usar vectores

#include "console_board.cpp"

using namespace std;

int main() {
try {
Console_Board board;
int level = -1;

//Menú principal
cout << "Bienvenido al Buscaminas. Elige el nivel que deseas jugar" << endl
<< "1- Principiante" << endl
<< "2- Intermedio" << endl << endl
<< "Escribe el numero del nivel: ";
cin >> level;

//Creo el nivel
if(level == 2)
board.create(16, 16, 40); //16x16 y 40 minas
else
board.create(9, 9, 10); //9x9 y 10 minas

//Muestro las instrucciones
cout << endl << "--INSTRUCCIONES--";
cout << endl << "Elegir un cuadrado: e x y (ejemplo: e 2 3).";
cout << endl << "Marcar un cuadrado como mina: m x y (ejemplo: m 2 3). " << endl << endl;

system("pause");

//Preparo el game_loop y lo comienzo
bool game_loop = true, victory = false;
int x = 0, y = 0;
char option = '-';
while(game_loop == true && victory == false) {
system("CLS");
board.print();
cout << "Minas restantes: " << board.get_left_mines() << endl;
cout << "Ultima eleccion: " << option << ' ' << x << ", " << y << endl << endl;
cout << "Comando: ";
cin >> option >> x >> y;
/*Por si no introdujo un número limpio el cin. De lo contrario, al introducir un string empieza a correr el while de modo infinito sin volver a preguntar por las coordenadas*/
cin.clear(); cin.ignore(99,'\n');
if(option == 'm')
board.mark_square(x-1, y-1);
else {
board.choose_square(x-1, y-1);
if( board.is_valid(x-1, y-1) && board.square(x-1, y-1).is_mine() ) game_loop = false;
}
if( board.no_squares_left() ) {
board.mark_all_mines();
victory = true;
}
}

//Muestro el tablero por última vez
system("CLS");
board.print();
cout << endl << endl;

if(victory == false)
cout << "Has perdido!";
else
cout << "Felicitaciones! Has ganado!";

cout << endl;

return 0;
}
catch(exception& ex) {
cerr << "Se ha procucido un error: " << ex.what();
return 1;
}
}


board.cpp
Código (cpp) [Seleccionar]
/* Clase Board.
* Maneja los elementos del juego Buscaminas. Consiste en un tablero (Board en inglés)
* que tiene cuadrados (Squares).
*
*
* Includes necesarios para el funcionamiento de esta clase
*
* #include <iostream>  | Para usar cout
* #include <vector>    | Para usar vector
* #include <stdexcept> | Para manejo de errores */

#include "square.cpp"
#include "random_number.cpp"

class Board {
std::vector< std::vector<Square> > squares;
int width, height;
int left_mines; //Minas que quedan (va descontando cuando el usuario marca una)
int total_mines; //Cantidad de cuadrados que contienen una mina
int total_number_squares; //Cantidad de cuadrados que contienen un número
bool first_choosing;

//Métodos privados
void set_size(int width, int height);
void set_mines(int x, int y);
void set_numbers();
void show_empty_squares(int x, int y);
int count_surrounding_mines(int x, int y);
int shown_squares();

//Métodos públicos
public:
void create(int width, int height, int param_mines);
void choose_square(int x, int y);
void mark_square(int x, int y);
void mark_all_mines();

bool is_valid(int x, int y);
bool no_squares_left();

Square square(int x, int y);
inline int get_width() { return width; }
inline int get_height() { return height; }
inline int get_left_mines() { return left_mines; }
};

void Board::set_size(int param_width, int param_height) {
//Asigna el tamaño al tablero
//Condición: los argumentos deben ser positivos
if(param_width <= 0 || param_height <= 0)
throw std::runtime_error("No puedes crear un tablero con ancho o alto negativos.");

//Método de la clase vector que sirve para inicializarlo. En este caso creo un vector bidimensional con el ancho "param_width" y alto "param_height"
squares.assign(param_width, std::vector<Square>(param_height));

//Guardo el tamaño del tablero en atributos para usarlo más tarde
width = param_width;
height = param_height;
}

void Board::set_mines(int x, int y) {
//Asigna las minas de manera aleatoria a partir del atributo total_mines
//No se pone ninguna mina en el punto x, y
Random_Number random_number = Random_Number();
int x_aux;
int y_aux;

for(int i = 0; i < total_mines;) {
x_aux = random_number.get(0, width-1);
y_aux = random_number.get(0, height-1);

if( !squares[x_aux][y_aux].is_mine() && !squares[x][y].is_mine() ) {
//Si el punto aleatorio no es mina NI el punto entregado como argumento.
squares[x_aux][y_aux].set_mine();
++i;
}
}
}

void Board::set_numbers() {
//Calcula y guarda los números que van en el tablero (y que indican el número de minas que hay alrededor de un cuadrado)
for(int x = 0; x < width; ++x)
for(int y = 0; y < height; ++y) {
//Con estos dos for recorro el tablero completo (se toman todas las combinaciones posibles de x e y)

if( !squares[x][y].is_mine() )
//Si el cuadrado actual es una mina no es necesario asignarle un número.
//Si no es una mina, cuento las minas alrededor y le asigno un número.
//Recordar que x, y son las coordenadas del punto
squares[x][y].set_value( count_surrounding_mines(x, y) );
}
}

void Board::show_empty_squares(int x, int y) {
//Función recursiva que revisa si un cuadrado está vacío y lo muestra. En caso afirmativo revisa los que lo rodean y así sucesivamente.
if(is_valid(x, y) && squares[x][y].get_value() == 0 && squares[x][y].is_hidden()) {
squares[x][y].show();
for(int x_axis = x-1; x_axis <= x+1; ++x_axis)
for(int y_axis = y-1; y_axis <= y+1; ++y_axis) {
//Con estos dos for puedo moverme por los 8 cuadrados circundantes
if(is_valid(x_axis, y_axis) && squares[x_axis][y_axis].get_value() > 0 && !squares[x][y].is_mine())
//Muestro los números que hayan
squares[x_axis][y_axis].show();
show_empty_squares(x_axis, y_axis);
}
}
}

int Board::count_surrounding_mines(int x, int y) {
//Cuenta el número de minas alrededor de un punto
//Condición: el punto debe estar dentro del tabelero
if( !is_valid(x, y) )
throw std::runtime_error("No puedes contar las minas alrededor de un punto que se encuentra fuera del tablero.");

int count = 0;
for(int x_axis = x-1; x_axis <= x+1; ++x_axis)
for(int y_axis = y-1; y_axis <= y+1; ++y_axis)
/*Recorro los 9 puntos, incluyendo el del centro. Pierdo un mínimo de eficiencia, pero el código queda más ordenado. Además, sé que el número del centro no es una mina, por lo que el resultado de la función no cambia*/
if( is_valid(x_axis, y_axis) && squares[x_axis][y_axis].is_mine() )
++count;
return count;
}

int Board::shown_squares() {
int total = 0;

for(int x = 0; x < width; ++x)
for(int y = 0; y < height; ++y)
if( !squares[x][y].is_hidden() && !squares[x][y].is_mine() )
++total;

return total;
}

void Board::create(int param_width, int param_height, int param_mines) {
//Crea el tablero de buscaminas con el tamaño adecuado, asigna las minas y asigna los números.
//Condición: los argumentos deben ser positivos (se comprueba en las otras funciones)
//Condición: la cantidad de minas debe ser positiva (se comprueba aquí)
if(param_mines < 0) throw std::runtime_error("No puedes crear un tablero con un número negativo de minas");
set_size(param_width, param_height); //Asigna al tablero el tamaño adecuado
left_mines = param_mines;
total_mines = param_mines;
total_number_squares = param_width*param_height - param_mines;
first_choosing = true;
}

void Board::choose_square(int x, int y) {
//Hace que el cuadrado elegido muestre su contenido.
//Si el punto x, y está fuera del tablero, la función no hace nada.
if( is_valid(x, y) ) {
if( first_choosing ) {
//Si es la primera vez que se elige un número, pongo las minas excepto en ese lugar y luego proceso la elección.
//De esta manera se hace imposible perder en la primera jugada (es imposible elegir una mina el primer turno)
set_mines(x, y);
set_numbers();
first_choosing = false;
}

if(squares[x][y].get_value() == 0)
//Si el cuadrado elegido está vacío (sin minas ni números)
show_empty_squares(x, y);
else
//Si es un número o una mina
squares[x][y].show();
//En ambos casos, si el cuadrado estaba marcado como mina sumo uno al número de minas restantes
if( squares[x][y].is_marked() ) ++left_mines;
}
}

void Board::mark_square(int x, int y) {
//Marca un cuadrado con un signo para ayudar al usuario a identificar minas.
//La función no hace nada si el punto x, y: está fuera del tablero / no está oculto / ya está marcado
if( is_valid(x, y) && squares[x][y].is_hidden() && !squares[x][y].is_marked() ) {
squares[x][y].mark();
--left_mines;
}
}

void Board::mark_all_mines() {
//Marca todas las minas que no estén marcadas. Así, cuando el usuario gana las puede ver en el tablero.
for(int x = 0; x < width; ++x)
for(int y = 0; y < height; ++y)
if( squares[x][y].is_mine() )
squares[x][y].mark();
left_mines = 0;
}

bool Board::no_squares_left() {
//Calcula el número de cuadrados que están ocultos y no son minas. Devuelve true si no queda ninguno.
return shown_squares() == total_number_squares;
}

bool Board::is_valid(int x, int y) {
//Devuelve true si el punto está dentro del tablero y false si no lo está
return (-1 < x && x < width) && (-1 < y && y < height);
}

Square Board::square(int x, int y) {
//Función que retorna el objeto "Square" dadas las coordenadas x e y.
//Condición: las coordenadas deben apuntar a un "Square" existente.
if( !is_valid(x, y) )
throw std::runtime_error("Board::square no acepta coordenadas de puntos inexistentes.");
return squares[x][y];
}


console_board.cpp
Código (cpp) [Seleccionar]
/* Clase Console_Board derivada de la clase Console
* Añade el método "print", que permite mostrar el tablero por consola */

#include "board.cpp"

class Console_Board : public Board {
public: void print();
};

void Console_Board::print() {
std::cout << "     ";
for(int i = 0; i < get_width(); ++i) {
if(i < 9)
std::cout << "  " << i+1;
else
std::cout << " " << i+1;
}
std::cout << std::endl << "     ";
for(int i = 0; i < get_width(); ++i)
std::cout << "___";
std::cout << std::endl;

//Imprime los números de la izquierda (coordenadas del eje y)
for(int y = 0; y < get_height(); ++y) {

if(y < 9)
std::cout << "  " << y+1 << " |";
else
std::cout << ' ' << y+1 << " |";

//Imprimo el contenido del tablero. Es necesario que este for esté dentro del anterior.
for(int x = 0; x < get_width(); ++x)
if( square(x, y).is_hidden() ) {
if( square(x, y).is_marked() ) std::cout << "  #";
else std::cout << "  *";
}
else if(square(x, y).get_value() == 0) std::cout << "   ";
else if(square(x, y).get_value() == 9) std::cout << "  X";
else std::cout << "  " << square(x, y).get_value();
std::cout << "\n\n";
}
}


square.cpp
Código (cpp) [Seleccionar]
/* Clase Square
* El tablero (clase Board) se forma a partir de cuadrados (clase Square).
* Un cuadrado puede contener una mina o un número. */

class Square {
//Atributos
int value;
bool hidden;
bool marked;

//Métodos públicos
public:
Square();
inline int get_value() { return value; }
inline bool is_mine() { return value == 9; }
inline bool is_hidden() { return hidden; }
inline bool is_marked() { return marked; }
inline void set_mine() { value = 9; } //Asigna al cuadrado el valor correspondiente a una mina
inline void set_value(int number) { value = number; }
inline void show() { hidden = false; }
inline void mark() { marked = true; }
};

Square::Square() {
//Esconde el valor del cuadrado (así, todo cuadrado que se crea viene escondido)
hidden = true;
marked = false;
}


random_number.cpp
Código (cpp) [Seleccionar]
/* Clase Random_number
* Simplifica el cálculo de números "aleatorios"
*
*
* Includes necesarios para que esta clase funcione
* #include <cstdlib> -> Para usar rand()
* #include <time.h> -> Para usar time_t y time() */

class Random_Number {
public:
Random_Number();
int get(int min, int max);
};

Random_Number::Random_Number() {
//Proceso necesario para después generar números (no entiendo exactamente cómo funciona)
time_t seconds; time(&seconds);
srand((unsigned int)seconds);
}

int Random_Number::get(int min, int max) {
//Devuelver un número "aleatorio" entre el rango dado
return rand() % (max - min + 1) + min;
}

daryo

excelente aporte seguro aprendere mucho de este code  ;-)
buenas

amchacon

#2
[code=cpp]class Random_Number {
unsigned int seed;

public:
Random_Number();
int get(int min, int max);
};


Para que usas seed? *_*

Por otro lado:

Código (cpp) [Seleccionar]
throw "No puedes contar las minas alrededor de un punto que se encuentra fuera del tablero.";
Lanzar cadenas sueltas puede resultar complejo de capturar además pueden colisionar con las excepciones de otras clases.. Lo mejor es hacerte una clase para las excepciones:

Código (cpp) [Seleccionar]
struct Excepcion
   {

       unsigned int N_Error;
       string Mensaje;

       Excepcion(string mensaje,unsigned int Error) : Mensaje(mensaje),N_Error(Error) {};
   };


Siguiendo ese ejemplo:

Código (cpp) [Seleccionar]
throw Excepcion("No puedes contar las minas alrededor de un punto que se encuentra fuera del tablero.",FUERA_DE_RANGO);
// Fuera de rango será una constante o una macro que definas


Se pueden hacer clases que hereden de esta si quieres ser más especifico (por ejemplo, Excepcion_Minas, Excepcion_Aleatorios...).

Otra cuestión es:

Código (cpp) [Seleccionar]
void render(Board board);

Entiendo que no la has metido dentro de la clase para que sea más general. No obstante creo, que al ser una función relacionada con el tablero debería ir dentro. Para asegurarnos la independencia de cout, lo que haremos es devolver un string:

Código (cpp) [Seleccionar]
std::string Board::ToString() // Devuelve un string con la representacion del campo
{
 std::stringstream Devolver;

//Muestra el tablero
//Imprime los números de arriba (coordenadas del eje x)
Devolver<<  "     ";
for(int i = 0; i < this.get_width(); ++i) {
if(i < 9)
Devolver<<"  " << i+1;
else
Devolver<< " " << i+1;
}
Devolver<< std::endl << "     ";
for(int i = 0; i < this.get_width(); ++i)
Devolver<< "___";
Devolver<< std::endl;

//Imprime los números de la izquierda (coordenadas del eje y)
for(int y = 0; y < this.get_height(); ++y) {

if(y < 9)
Devolver << "  " << y+1 << " |";
else
Devolver<< ' ' << y+1 << " |";

//Imprimo el contenido del tablero. Es necesario que este for esté dentro del anterior.
for(int x = 0; x < this.get_width(); ++x)
if(this.square(x, y).is_hidden() ) {
if(this.square(x, y).is_marked() ) Devolver<< "  #";
else Devolver << "  *";
}
else if(board.square(x, y).get_value() == 0) Devolver << "   ";
else if(board.square(x, y).get_value() == 9)Devolver<< "  X";
else Devolver << "  " << board.square(x, y).get_value();
Devolver<< "\n\n";
}

       return Devolver.str(); // Devolvemos el string
}


El programador usuario podrá optar por mostrarlo con cout, interpetrarlo para una interfaz gráfica o bien usarlo en un archivo de depuración.

Por lo demás muy bueno y muy buen ordenado. Me gusta :)[/code]
Por favor, no me manden MP con dudas. Usen el foro, gracias.

¡Visita mi programa estrella!

Rar File Missing: Esteganografía en un Rar

Wofo

Gracias amchacon por tus comentarios.

La verdad es que no me había dado cuenta de que ese "seed" estaba ahí. Habrá que eliminarlo.

Lo de las excepciones me parece una excelente idea, pero no entiendo por qué usas "struct" en vez de "class".

Por otro lado, meter el render dentro de la clase no me termina de convencer por la dificultad que significa procesar ese string para transformarlo en una interfaz visual. Creo que es excesivamente complicado (mucho más que usar los métodos que provee la clase Board). Quizá una buena alternativa sería crear una tercera clase que se llame Screen o algo por el estilo, donde esté el método render.

Gracias denuevo!
Wofo.

amchacon

#4
Cita de: Wofo en 12 Marzo 2013, 17:37 PMLo de las excepciones me parece una excelente idea, pero no entiendo por qué usas "struct" en vez de "class".
Cuando todos los miembros de un objeto son públicos, uso struct en vez de class... Simple manía, no hay ninguna otra razón.

Cita de: Wofo en 12 Marzo 2013, 17:37 PMPor otro lado, meter el render dentro de la clase no me termina de convencer por la dificultad que significa procesar ese string para transformarlo en una interfaz visual. Creo que es excesivamente complicado (mucho más que usar los métodos que provee la clase Board). Quizá una buena alternativa sería crear una tercera clase que se llame Screen o algo por el estilo, donde esté el método render.
Cierto para lo visual puede ser complicado pero aún así no lo descataría... Para el modo consola sería bastante sencillo la representacion (cout<<board.ToString(); ) y tener un "mapa" en modo texto puede ser útil para una depuración en una interfaz visual.

Puedes crear un método virtual "Dibujar", que el programador herede la clase y lo implemente...
Por favor, no me manden MP con dudas. Usen el foro, gracias.

¡Visita mi programa estrella!

Rar File Missing: Esteganografía en un Rar

Wofo

#5
Me parece excelente la idea de crear una subclase. Acabo de hacerlo y de subir el code.

Las excepciones las hice usando la clase estándar runtime_exception (derivada de exception).

¡Muchas gracias!