Todo el programa ha sido creado usando programación orientada a objetos, y se compone de varias clases que se explican a continuación.
Existen dos, uno para codificar direcciones, que se aplica a la orientación de los barcos, o a los movimientos a través del tablero (veremos esto con más detalle).
El otro sirve para codificar el contenido de cada casilla, dentro de los seis posibles valores.
enum direccion {derecha=0, abajo, izquierda, arriba};
enum contenido {libre=0, agua, buque, tocado, hundido, novalido};
Las constantes del juego de definen y declaran en un espacio con nombre llamado cte de modo que todos los jugadores y el programa que los enfrenta usen los mismos valores. Estas definiciones están en el fichero "const.h".
namespace cte {
const int alto = 10;
const int ancho = 10;
const int nBarcos = 10;
const int lBarco[nBarcos] = {4, 3, 3, 2, 2, 2, 1, 1, 1, 1};
const int nPartidas = 10;
};
Estos valores de constantes corresponden al juego tradicional de "Hundir la flota", es decir, un tablero de 10 x 10 casillas, y 10 barcos, uno de cuatro casillas, dos de tres casillas, tres de dos casillas y cuatro de una.
Es fácil modificar esas constantes para todo el juego editando este fichero, de modo que podemos usar tableros más grandes o más barcos de distintos tamaños.
Todos los jugadores deben usar estas constantes mediante las variables de este espacio con nombre, y nunca como constantes literales, no hacerlo así puede provocar errores y por supuesto, la descalificación del jugador.
Hemos definido varias clases para manejar objetos de uso frecuente en el juego. Las declaraciones están incluidas en el fichero "clases.h", y las definiciones en "clases.cpp".
class Coordenada {
public:
Coordenada(int _x=0, int _y=0) : x(_x), y(_y) {}
Coordenada operator+(const Coordenada &c) {
Coordenada temp;
temp.x = x + c.x;
temp.y = y + c.y;
return temp;
}
bool operator==(const Coordenada &c) {
return (x == c.x) && (y == c.y);
}
void Incrementar(direccion, int=1);
int X() const {return x;}
int Y() const {return y;}
private:
int x;
int y;
};
Sirve para almacenar y manejar coordenadas de dos dimensiones correspondientes al tablero de juego. Dispone de un constructor con valores por defecto para los parámetros, dos operadores, el de suma y el de igualdad, y tres funciones, para incrementar y obtener las componentes x e y.
La función de incrementar incrementa la coordenada en la dirección indicada y el número de unidades indicado, (1 por defecto).
struct DatosBarco {
Coordenada inicio;
direccion dir;
};
Usaremos esta estructura para pedir los datos de un barco a los jugadores.
class ErrorBarco: public std::exception {
public:
ErrorBarco() : exception() {}
const char* what() const throw() {
return "Barco fuera de tablero";
}
};
Clase derivada de "exception", nos servirá para indicar errores en la colocación de barcos.
class Barco {
public:
Barco(Coordenada c, int l, direccion d) throw(ErrorBarco, std::bad_alloc);
~Barco();
// Devuelve -1 si la coordenada no pertenece al barco
// o la posición si pertenece
int Pertenece(Coordenada c) const;
Coordenada Posicion(int) const;
void Marcar(Coordenada);
bool Hundido() const;
private:
int longitud;
Coordenada *posicion;
bool *estado;
direccion dir;
};
El constructor admite como parámetros una coordenada, una longitud y una dirección. Entre las tres definen la posición, longitud y dirección del barco. El constructor puede fallar porque el barco resulte estar fuera del tablero o por un error en la asignación de memoria dinámica.
El método Pertenece nos indica si la coordenada que recibimos como parámetro pertenece o no al barco. En caso de pertenecer nos devuelve la posición del barco concreta, empezando en 0 para la coordenada recibida como parámetro en el constructor. Si no pertenece devuelve el valor -1.
En este ejemplo, si llamamos a Pertenece con las coordenadas "D3", (3,2), tendremos como resultado el valor 2, ya que en esa casilla está la sección número 2 del barco.
Posicion devuelve la coordenada de la casilla que ocupa el orden indicado en el parámetro.
Marcar sirve para marcar la casilla correspondiente a la coordenada indicada como tocado, si es que pertenece al barco, claro.
Hundido devuelve "true" si al barco está hundido, es decir, si todas sus posiciones han sido tocadas.
Los datos almacenados nos permiten controlar el estado y posición de cada una de las casillas ocupadas por el barco.
posicion es un array dinámico con las coordenadas de cada casilla. estado es un array dinámico con el estado de cada casilla. longitud es el tamaño del barco.
La clase que cada competidor creará debe estar basada en esta clase base virtual pura. La declaración está incluida en el fichero "jugador.h", y la definición depende de ti.
Con el proyecto se suministran dos clases derivadas, una como ejemplo: "Jugador1", y otra para depuración, que permite jugar de forma interactiva: "Humano".
class Jugador {
public:
virtual void NuevaPartida()=0;
virtual DatosBarco PedirBarco(int n)=0;
virtual Coordenada PedirCoordenada()=0;
virtual void Informar(Coordenada)=0;
virtual void Responder(Coordenada, contenido)=0;
};
Estas funciones definen el interfaz entre el programa que enfrenta a los distintos jugadores y el jugador.
NuevaPartida será invocada por el programa para que el jugador prepare su estado para comenzar una nueva partida. Deberá inicializar sus variables, y realizar las tareas que consideres necesarias como diseñador de tu clase derivada.
PedirBarco será invocada repetidamente por el programa para obtener las coordenadas de cada uno de los barcos del jugador. El parámetro indica el número del barco, según las constantes definidas en el espacio con nombre "cte". Es recomendable que parte de las tareas a realizar por NuevaPartida sea colocar todos los barcos, y esta función se limite a consultarlas y devolverlas. Para la devolución de los datos del barco se usa una estructura DatosBarco que contiene la coordenada de inicio y dirección.
PedirCoordenada será invocada por el programa para obtener la coordenada de disparo del "jugador". La coordenada debe estar dentro del tablero, y no puede pertenecer a una coordenada de un barco donde se haya disparado previamente.
Informar informa al jugador de la coordenada donde ha disparado el jugador contrario. Esto no influye, en principio, en el juego del jugador que recibe la información, a no ser que se use esa información en un algoritmo de inteligencia artificial con el fin de adaptarse a la estrategia del jugador contrario.
Responder sirve para informar a un jugador del contenido de la casilla donde ha disparado. El programa invocará esta función después de haber llamado a PedirCoordenada, usando como parámetros la misma coordenada obtenida mediante PerdirCoordenada y con el contenido de esa coordenada en el tablero de jugador contrario.
Con lo anterior deberías tener suficiente información para diseñar tu clase derivada de Jugador, sin embargo creemos que es interesante proporcionar dos clases como ejemplo. La clase Humano es una clase interactiva, que permite a un jugador humano participar en el juego. Te será muy útil para depurar tu jugador, viendo dónde falla y por dónde se puede mejorar.
class Humano : public Jugador {
public:
void NuevaPartida();
DatosBarco PedirBarco(int n);
Coordenada PedirCoordenada();
void Informar(Coordenada);
void Responder(Coordenada, contenido);
private:
contenido tablero[2+cte::alto][2+cte::ancho];
contenido contrario[2+cte::alto][2+cte::ancho];
Coordenada coor[cte::nBarcos];
direccion dir[cte::nBarcos];
// Devuleve un valor aleatorio entre min y max-1
int Alea(int min, int max);
// Comprueba si un barco colisiona con los existentes
bool Colision(int);
};
Se trata de una clase derivada de Jugador, por supuesto. A las funciones heredadas añadimos dos más, además de las variables necerasias para mantener el juego.
Alea es una función imprescindible en cualquier juego, nos devuelve
un valor aleatorio entre "min" y "max". El valor "max" nunca se alcanza,
por ejemplo Alea(1,10) proporcionará valores entre 1 y 9
(incluidos).
Colision la usamos para colocar cada uno de los barcos. La técnica consiste en colocar cada uno de los barcos de forma aleatoria. Una vez elegidos las coordenadas y la dirección de un barco, si entra en colisión con alguno de los anteriores, se vuelve a intentar colocar.
Esto es una representación del tablero que se usa para colocar los barcos de forma automática. Los cuadros blancos son los que se usan para jugar, los cuadros de alrededor, en gris claro, se usan para evitar casos especiales.
En el tablero se ha colocado un barco, marcado en oscuro, y alrededor de él se han marcado todas las casillas como usadas. Cualquier barco que intente colocarse después generará una colisión si cualquiera de sus casillas cae en una casilla usada. Es decir, para colocar el resto de los barcos sólo disponemos de las casillas en blanco.
Como se ve, tener una casilla alrededor del tablero visible nos facilita el marcado de las casillas alrededor de cada barco, ya que no existen excepciones cuando el barco toca un borde.
Los datos que se almacenan son cuatro arrays:
tablero es un array que contiene las casillas del jugador, incluyendo los barcos y si el algoritmo del jugador lo requiere, el estado de la partida del jugador contrario (que se desarrolla en nuestro tablero).
contrario es el array que contiene las casillas del jugador contrario, en ese array se almacena el estado de la partida que jugamos, indicando el resultado de cada una de nuestras jugadas.
Observarás que los dos arrays de los tableros tienen dos casillas más de ancho y alto que el tablero de juego real. Esto es porque de ese modo es más sencillo rellenar de agua las casillas de alrededor de cada barco, sin preocuparse de si el barco está o no junto al borde. Las casillas del borde de los arrays no pueden nunca contener barcos.
Los otros dos arrays coor y dir contienen las coordendas y direcciones de cada barco, que en esta versión se colocan automáticamente.
En cuanto a la funciones heredadas, realizan las siguientes tareas:
NuevaPartida inicializa los arrays tablero y contrario y coloca los barcos.
PedirBarco se limita a consultar los arrays de coordenadas y direcciones.
PedirCoordenada lo primero que hace es visualizar los dos tableros tal como los ve el jugador, es decir, el estado del tablero del jugador contrario, con las casillas donde hemos disparado, y si son o no barcos. También se muestra el nuestro tablero, con las posiciones de nuestros barcos y su estado, así como las posiciones donde ha disparado el jugador contrario.
Turno de jugador: 1: 0,5...Agua Turno de jugador: 2 ABCDEFGHIJ ABCDEFGHIJ 1 . . 1 .X. . OOO 2 .X 2 .O .. 3 . . . 3 . 4 . .... 4 O. OO.OO 5 . 5 O . 6 ...... 6 .X O 7 7 O O. O . 8 .. 8 O .X 9 . 9 O . . 10 . 10 . O Introduce coordendas (Ejemplo A8, C1, J10): _
El tablero de la izquierda es el del jugador contrario, el de la derecha el del que está jugando. Los puntos representan disparos que han fallado, las 'X' disparos acertados y las 'O' barcos sin tocar.
Esto nos permite elegir nuestra próxima coordenada y ver el estado de nuestra flota.
Finalmente se leen las coordenadas de disparo, en el caso del jugador humano no se permite elegir coordenadas prohibidas: fuera del tablero o donde cuyo contenido ya se conoce.
Responder actualiza el tablero contrario en función del contenido que nos indica el programa. Si se trata de un tocado o hundido además de marcarlo, marcaremos con agua las cuatro esquinas. En el caso de hundido, marcaremos como agua las casillas alrededor que no estén marcadas ya.
En este ejemplo se ve que alrededor de cualquier barco todas las casillas son "agua", por lo tanto, una vez "hundido" el barco podemos marcarlas como "agua" y no necesitamos volver a disparar sobre ellas.
Del mismo modo, cuando "tocamos" un barco por primera vez, podemos estar seguros de que no existirán casillas con barco en ninguna de las diagonales. Podemos, por lo tanto, marcarlas como "agua".
Informar marca en el tablero propio los resultados de los disparos del jugador contrario.
class Jugador1 : public Jugador {
public:
void NuevaPartida();
DatosBarco PedirBarco(int n);
Coordenada PedirCoordenada();
void Informar(Coordenada);
void Responder(Coordenada, contenido);
private:
contenido tablero[2+cte::alto][2+cte::ancho];
contenido contrario[2+cte::alto][2+cte::ancho];
Coordenada coor[cte::nBarcos];
direccion dir[cte::nBarcos];
// Devuleve un valor aleatorio entre min y max-1
int Alea(int min, int max);
bool Colision(int);
};
Como verás, la declaración de la clase es idéntica a la de la clase Humano, se trata de un jugador automático sin ninguna inteligencia para el juego, se limitará a elegir casillas vacías de forma aleatoria para cada disparo.
Las únicas funciones diferentes son:
NuevaPartida, no necesitamos colocar nuestros barcos en el tablero, ya que no monitorizamos la partida del contrario.
PedirCoordenada, no necesitamos mostrar los tableros, nos limitamos a elegir una coordenada correspondiente a una casilla libre.
Informar, no hace nada, ya que no tenemos en cuenta qué hace el jugador contrario.
El programa que define el centro de control, y que se suministra junto con las clases anteriores, está diseñado para enfrentar a dos jugadores, pero usa ciertas clases que se usarán en el centro de control final: una versión más completa para jugar una liga.
class Tablero {
public:
Tablero() throw(std::bad_alloc);
~Tablero();
void Iniciar() throw(std::bad_alloc);
contenido LeerCelda(Coordenada) const;
contenido ModificarCelda(Coordenada, contenido);
bool ColocarBarco(int, Coordenada, int, direccion) throw(std::bad_alloc);
bool Colision();
void Mostrar();
bool ComprobarBarco(Coordenada);
private:
contenido celda[cte::ancho][cte::alto];
Barco **barco; // Array dinámico de punteros de barcos
};
Esta clase almacena y manipula todos los datos relativos a una partida y a un jugador.
Los datos que se almacenan son un array de celdas, con el contenido del tablero del jugador, donde se guardan los barcos y el estado de cada celda. Puesto que estos datos son inaccesibles para los jugadores, se almacena el estado real actual de cada jugador: posición de los barcos, coordenadas de los disparos, celdas con agua y tocados.
El otro dato es un array dinámico de objetos de la clase Barco, cada uno de los objetos contendrá los datos de uno de los barcos del jugador. Estos objetos permiten a un objeto de esta clase averiguar si un barco ha sido hundido o tocado.
Entre las funciones tenemos el constructor y destructor.
La función Iniciar limpia el array celda, y destruye y crea un nuevo array dinámico de Barcos, que inicialmente contendrá sólo punteros nulos.
LeerCelda devuelve el contenido de la celda cuya coordenada se pasa como parámetro.
ModificarCelda modifica el contenido de la celda cuya coordenada se pasa como parámetro, con el valor del segundo parámetro, y devuelve el valor previo de esa celda.
ColocarBarco crea un objeto de la clase Barco, que se añade al array dinámico de barcos, en la posición del primer parámetro. El resto de los parámetros indican la posición, tamaño y dirección del barco.
Colision verifica si los barcos del jugador entran en colisión. Esta función será invocada una vez se tengan los datos de todos los barcos.
Mostrar muestra el tablero en pantalla.
ComprobarBarco actualiza la información del barco tocado, siempre que sea necesario. Es decir, si la coordenada recibida como parámetro pertenece a un barco, y no ha sido tocada previamente. Devuelve true si ha sido hundido y false si no es un barco o si sólo ha sido tocado.
class Torneo {
public:
Torneo(Jugador *j1, Jugador *j2);
int Iniciar() throw(std::bad_alloc);
int Partida(int);
void Resultado();
int Ganadas() const;
private:
Jugador *jugador[2];
Tablero tablero[2];
int ganadas[2];
};
Contiene la información de un torneo que enfrenta a dos jugadores, un número determinado de partidas, alternandose en el comienzo.
Entre los datos que contiene hay un array de dos punteros a objetos derivados de la clase base abstracta "Jugador", que se reciben como parámetros en el constructor.
También contiene un array de dos objetos Tablero, para almacenar datos durante la partida.
Un tercer array de enteros almacena el número de partidas ganadas por cada jugador.
El interfaz se compone de tres funciones y el constructor.
El constructor, recibe dos punteros correspondientes a cada uno de los jugadores, que serán de una clase derivada de la clase virtual Jugador. También se encarga de inicializar el array jugador, asignándole los parámetros recibidos y el array ganadas, con cero.
Iniciar inicia una partida, para lo cual inicia los tableros, llama a la función NuevaPartida de cada uno de los jugadores y les pide los datos de los barcos, actualiza el tablero, y comprueba si la colocación de los barcos es legal.
El valor de retorno indica si alguno o ambos jugadores ha cometido una infracción al colocar los barcos.
Partida juega una partida completa. El parámetro indica cual de los jugadores empieza a jugar. El valor de retorno indica cual de los jugadores gana.
Una partida puede terminar bien porque uno de los jugadores haya hundido todos los barcos del contrincante, o bien porque haya cometido un error: coordenada fuera del tablero o disparo en una coordenada previamente tocada.
El bucle principal se repite hasta que se termine la partida por uno de esos motivos, y consiste en:
Resultado muestra el resultado de partidas ganadas por cada jugador.
Ganadas devuelve el número de partidas ganadas por el jugador que más partidas lleve ganadas hasta el momento.
La función main inicializa la semilla del generador de números aleatorios, crea un torneo con los dos jugadores correspondientes, y los enfrenta un número de veces, hasta que uno de ellos llegue al valor determinado por la constante cte::nPartidas.
Al final, muestra el número de partidas ganadas por cada jugador.
Es posible enfrentar a un jugador consigo mismo, creando un torneo con dos objetos Jugador de la misma clase:
Torneo torneo(new Jugador1, new Jugador1);Competir