PROG3 - Práctica 2

Práctica 2

Plazo de entrega: Hasta el domingo 18 de octubre de 2020 a las 23:59h
Peso relativo de está práctica en la nota de prácticas: 20%
IMPORTANTE: Todos tus ficheros de código fuente deben utilizar la codificación de caracteres UTF-8. No entregues código que use otra codificación (como Latin1 o iso-8859-1).
En caso de duda, consulta primero la sección ‘Aclaraciones’ al final de este enunciado. La iremos actualizando de vez en cuando.

Battleship : Barcos y un tablero

En esta práctica, partiendo de la clase Coordinate de la práctica anterior, construiremos las clases para simular el juego. Podremos crear barcos y añadirlos al tablero, y simular que un jugador intenta alcanzar un barco en una coordenada determinada.

El principal concepto de programación que trabajaremos es el de las relaciones entre objetos (composición y agregación).

Introducción

En esta práctica añadiremos algunos métodos a la clase Coordinate, y añadiremos una clase Board para manejar el tablero, y la clase Ship para crear barcos. Además, usaremos un par de tipos enumerados para la orientación de los barcos y para el estado de una posición del tablero. Consulta el diagrama de clases que puedes encontrar en la sección Diagrama de clases mientras lees esta introducción.

Diagrama de clases

El siguiente diagrama de clases UML representa las clases de nuestro modelo:

Diagrama de clases

Las nuevas clases pertenecen al paquete model, que ya ha sido creado en la práctica anterior.

Las siguientes secciones describen todos los métodos que hay que implementar. Los atributos y las relaciones no se incluyen, porque ya se muestran en el diagrama UML.

Allí donde se indique, tu código debe comprobar que los valores de los argumentos que se pasan a un método son correctos. En el caso concreto de argumentos que son referencias, no es necesario comprobar si la referencia es ‘null’ a menos que se indique.

Hoja de ruta

Te aconsejamos que sigas esta pequeña guía de implementación para ir construyendo tu práctica:

Recuerda, ve diseñando pruebas unitarias según vayas terminando cada clase, y utilízalas para depurar tu código antes de pasar a la siguiente clase.


Clases

A continuación se describen los métodos de cada una de las clases del modelo.

Coordinate

Se trata de la misma clase de la práctica anterior, pero con un par de métodos nuevos para obtener una copia del objeto, y para obtener las coordenadas adjacentes a una coordenada.

Ten en cuenta que en ningún momento se debe comprobar si las componentes son negativas o no, o si están dentro del tablero o no, esas comprobaciones se harán en otras clases, no en esta.

copy()

Devuelve una copia del objeto, invocando al constructor de copia (no dupliques el código del constructor de copia, simplemente llámalo y devuelve el objeto copia).

adjacentCoordinates()

Devuelve un conjunto con todas las coordenadas adyacentes a la coordenada que recibe la llamada, la cual no debe incluirse en dicho conjunto. Por ejemplo, para sistemas de coordenadas de 2 dimensiones como el de esta práctica siempre serán 8 las coordenadas adyacentes.

Este método devuelve un Set<Coordinate>, que es un conjunto de coordenadas. En realidad, Set<> es un interfaz, por lo que podemos asignar y devolver objetos de ese tipo, pero no podemos crear objetos; para eso es necesario usar una clase que implemente ese interfaz, como HashSet<>. En esta Guía fácil de Java Collection Framework (JCF) puedes aprender como se usan los conjuntos y otras estructuras de datos útiles que usaremos en las prácticas (listas y mapas). Luego puedes consultar la documentación de la API del JCF (paquete java.util) para conocer bien como funcionan estas estructuras de datos.


Ship

Esta clase representa un barco que será colocado en el tablero, y tiene varios atributos (todos privados):

shape

La forma del barco en el tablero se va a representar con una matriz en la que cada fila se corresponde con una de las orientaciones definidas, y tendrá una secuencia de ceros y unos formando un cuadrado de 5x5 (donde 5 es el valor de la constante BOUNDING_SQUARE_SIZE):

private int shape[][] = new int[][] {
          { 0, 0, 0, 0, 0,               // NORTH    ·····
            0, 0, 1, 0, 0,               //          ··#··
            0, 0, 1, 0, 0,               //          ··#··
            0, 0, 1, 0, 0,               //          ..#..
            0, 0, 0, 0, 0},              //          ·····

          { 0, 0, 0, 0, 0,               // EAST     ·····
            0, 0, 0, 0, 0,               //          ·····
            0, 1, 1, 1, 0,               //          ·###·
            0, 0, 0, 0, 0,               //          ·····
            0, 0, 0, 0, 0},              //          ·····

          { 0, 0, 0, 0, 0,               // SOUTH    ·····
            0, 0, 1, 0, 0,               //          ··#··
            0, 0, 1, 0, 0,               //          ··#··
            0, 0, 1, 0, 0,               //          ..#..
            0, 0, 0, 0, 0},              //          ·····

          { 0, 0, 0, 0, 0,               // WEST     ·····
            0, 0, 0, 0, 0,               //          ·····
            0, 1, 1, 1, 0,               //          ·###·
            0, 0, 0, 0, 0,               //          ·····
            0, 0, 0, 0, 0}};             //          ·····

En esta práctica, el valor inicial de shape será el mismo para todos los barcos, pero en prácticas posteriores cambiará, por lo que tu código no debe asumir que el barco tiene esta forma, debe usar el contenido de shape sin asumir ninguna distribución concreta. Tampoco debe asumir que siempre tendrá tamaño 5, aunque sí se puede asumir que será cuadrada.

Como se puede ver en el ejemplo, cada fila tiene 25 números que inicialmente valen 0 o 1, para indicar si una posición está ocupada por el barco o no; en el código se muestra como si fuera una matriz de números, aunque realmente es un vector y será necesario hacer una conversión (como se explica en el método getShapeIndex). La constante CRAFT_VALUE (que vale 1) se debe usar para representar que una posición está ocupada por el barco. Cuando, en el transcurso del juego, una posición del barco resulte alcanzada, el valor pasará de CRAFT_VALUE a HIT_VALUE (constante que vale -1). Como la orientación del barco no puede cambiar, sólo será necesario modificar la fila de shape correspondiente a la orientación definida en el constructor.

Además de los getters y setters básicos (que se pueden generar automáticamente con Eclipse), los métodos más importantes son:

Ship(Orientation orientation, char symbol, String name)

Constructor, asigna valores a los atributos.

getPosition()

Devuelve una copia defensiva de la posición del barco, o null si la posición no se ha asignado. A veces los objetos devuelven una copia de sus atributos para evitar que desde fuera de la clase se pueda alterar el atributo, esa copia se conoce como copia defensiva (el objeto se defiende de posibles cambios externos en su estado).

getShapeIndex(Coordinate c)

A partir de una coordenada relativa c (con valores entre 0 y BOUNDING_SQUARE_SIZE-1), obtiene la posición en el vector de números de shape. Por ejemplo, si la coordenada es (3,2) (cuarta columna de la tercera fila), devolverá 2*BOUNDING_SQUARE_SIZE+3 (que contiene un 0 en las orientaciones NORTH y SOUTH, y un 1 en EAST y WEST).

getAbsolutePositions(Coordinate position) / getAbsolutePositions()

A partir de una coordenada position, devuelve un conjunto con las coordenadas absolutas (referidas al tablero) ocupadas por el barco según lo que indique el atributo shape. Para calcular las coordenadas absolutas ocupadas por el barco se hace corresponder la posición que ocupa el barco en el tablero (position) con la posición (0,0) de shape.

En el caso del método sin parámetros se usará el atributo position del barco (llamando al otro método, por supuesto).

Ejemplo:

getAbsolutePositions

hit(Coordinate c)

Este método simula el impacto de un torpedo en el barco, si la coordenada absoluta c es una de las que ocupa el barco, y no es una posición ya alcanzada previamente, actualizará el estado interno del barco y devolverá true ; en caso contrario devolverá false.

isShotDown()

Devuelve true si todas las posiciones del barco han sido alcanzadas (es decir, si el barco se ha hundido).

isHit(Coordinate c)

Devuelve true si la coordenada absoluta c es una de las posiciones alcanzadas del barco.

toString()

Devuelve una cadena con una representación del estado actual del barco, usando el atributo symbol para las posiciones ocupadas por el barco, y las constantes de la clase Board HIT_SYMBOL y WATER_SYMBOL para las posiciones alcanzadas y libres, respectivamente. Por ejemplo, un barco llamado Kontiki con orientación SOUTH que ha sido alcanzado en la posición central se representaría con esta cadena (suponiendo que WATER_SYMBOL es un blanco, HIT_SYMBOL es y el atributo symbol es #):

Kontiki (SOUTH)
 -----
|     |
|  #  |
|  •  |
|  #  |
|     |
 -----

Board

El juego se desarrolla sobre un tablero. Por ejemplo, este tablero (obtenido de la paǵina de la Wikipedia para el Hundir la flota)

Ejemplo (Wikipedia)

se representaría de esta manera (las x son las columnas, y las y las filas):

x-> 0 1 2 3 4 5 6 7 8 9
0 # # # # # # #
1 #
2 # # # # # #
3 # #
4 #
5 # #
6 #
7 #
8
9 # # # # #
(y)

En el ejemplo de la Wikipedia aparecen marcadas las posiciones que ha jugado el adversario, pero en el ejemplo no se muestran porque se almacenan en otra estructura de datos (el conjunto seen).

La clase Board representa un tablero cuadrado en el que se situarán los barcos en el juego. Tiene varias constantes públicas y privadas:

Además tiene estos atributos:

Los métodos más importantes son:

Board(int size)

Constructor. Si size no está entre MIN_BOARD_SIZE y MAX_BOARD_SIZE se emitirá un mensaje de error por la salida de error estándard (usando System.err.println()) y se le asignará el valor de MIN_BOARD_SIZE. Como todas las referencias, tanto board como seen deben inicializarse asignándoles un nuevo objeto (en caso contrario valdrán null y el resto de métodos fallarán). Los atributos numCrafts y destroyedCrafts deben inicializarse a cero.

checkCoordinate(Coordinate c)

Comprueba si la coordenada c está dentro del tablero (los valores válidos van de 0 a size-1).

addShip(Ship ship, Coordinate position)

Añade un barco al tablero en la posición indicada (que corresponde a la esquina superior izquierda del barco), haciendo tres comprobaciones previamente:

  • Si alguna de las posiciones del barco está fuera del tablero se emitirá un error por la salida de error estándard (con System.err.println()) y el barco no se añadirá
  • Si alguna de las posiciones del barco ya está ocupada, se emitirá un error por la salida de error estándard y el barco no se añadirá
  • Si alguna de las posiciones alrededor del barco (su vecindad) está ocupada se emitirá un error por la salida de error estándard y no se añadirá el barco

Si las comprobaciones anteriores no generan ningún error, el barco se añadirá al mapa board usando como clave cada una de sus posiciones (se añadirá una entrada al mapa por cada posición, con la posición como clave y el barco como valor), se asignará la posición al barco, se incrementará el valor de numCrafts y el método devolverá true. Si el barco no ha podido ser añadido, el método devolverá false.

getShip(Coordinate c)

Devuelve el barco asociado a la coordenada c, o null si no hay barco en esa posición.

isSeen(Coordinate c)

Indica si una coordenada c ha sido descubierta por el adversario o no. Para esto hará uso del conjunto seen.

hit(Coordinate c)

Simula el lanzamiento de un torpedo en una coordenada c del tablero.

  • Si la coordenada está fuera del tablero emitirá un error por la salida de error estándard (con System.err.println()) y devolverá el valor WATER del enumerado CellStatus.
  • Si en la coordenada no hay ningún barco devolverá el valor WATER del enumerado CellStatus.
  • Si en la corrdenada hay un barco devolverá el valor HIT o DESTROYED del enumerado CellStatus, según el barco haya resultado alcanzado o hundido. Para determinar si el barco ha sido alcanzado o hundido hará uso del método correspondiente de la clase Ship.

En caso de que la coordenada sobre la que se lanza el torpedo se encuentre en el tablero, antes de devolver el valor correspondiente del enumerado CellStatus, deberá marcase esa coordenada como descubierta actulizando el conjunto seen. Además si el barco se ha hundido debe marcarse toda su vecindad como posiciones ya descubiertas, dado que no puede haber barcos en la vecindad de otro barco, e incrementar el valor de destroyedCrafts.

areAllCraftsDestroyed()

Indica si todos los barcos se han destruido.

getNeighborhood(Ship ship, Coordinate position)/getNeighborhood(Ship ship)

A partir de un barco y una posición, devuelve todas las posiciones del tablero de su vecindad, las que están alrededor del barco y pertenecen al trablero (las del propio barco se entiende que no están en su vecindad). En el método con un único parámetro se usará la posición del barco para llamar al otro método (no se debe duplicar código).

show(boolean unveil)

Devuelve una cadena que representa el tablero; si el parámetro unveil es true se mostrará el tablero completo, tal y como lo ve su propietario, y si es false se mostrará tal y como lo ve el adversario. Para las posiciones con agua se usará la constante WATER_SYMBOL, para las posiciones alcanzadas se usará HIT_SYMBOL.

Si unveil vale true, para las posiciones no alcanzadas de los barcos se usará su símbolo.

Si unveil vale false se usará NOTSEEN_SYMBOL para las posiciones no vistas, y si un barco ha sido hundido se mostrará el símbolo del barco en lugar de HIT_SYMBOL.

En el ejemplo anterior, con unveil a true el tablero se mostraría así (suponiendo que todos los barcos tienen el símbolo #):

## #  ####
   #      
#  # ### #
#        #
#         
#    #   •
     #   •
••       #
          
     #####

Con unveil a false, se mostraría así:

??????????
? ????????
??????????
??????????
??? ? ? ??
?? ??????•
    ?????•
## ???????
    ??????
??????????

toString()

Devuelve una cadena con los datos básicos del tablero: tamaño, barcos y barcos hundidos. Por ejemplo, para un tablero de tamaño 10 con 6 barcos de los cuales 2 han sido hundidos, la cadena sería Board 10; crafts: 6; destroyed: 2


Programa principal

Descarga este fichero MainP2.java y copialo en la carpeta ‘src/mains’ de tu proyecto.

Puedes usar MainP2.java como un ejemplo inicial muy sencillo de partida parcial que usa las clases implementadas en esta práctica. Ten presente, en cualquier caso, que este código explora un subconjunto muy reducido de todas las posibles situaciones que se pueden dar en el juego. El archivo partida1-SALIDA.txt contiene una salida de ejemplo de este programa.

Tienes disponible otro programa principal más extenso con el que se inicia una partida y produce esta salida estándar y esta salida de error. Cópialo en ‘src/mains’, como el anterior.


Pruebas unitarias

Aquí tienes, para empezar, los test de la práctica 1 adaptados a la práctica 2. Copia la carpeta model que se crea al descomprimirlo en la carpeta testde tu proyecto.

Tests previos

Aquí puedes descargar los test previos. Cópialos a una carpeta model en el directorio test de tu proyecto.

Hay algunos test que faltan por completar en cada fichero (busca el comentario //TODO). Estos test se usarán en la corrección final, por lo que te aconsejamos que los completes para asegurarte una buena nota. En el examen de prácticas es muy posible que se os pida implementar algún test unitario, ¡por lo cual os conviene practicar!


Documentación

Tu código fuente ha de incluir todos los comentarios de Javadoc que ya se indicaban en el enunciado de la primera práctica. No incluyas el HTML generado por Javadoc en el fichero comprimido que entregues.

Estructura de paquetes y directorios

Son los mismos que en la práctica anterior. Todas las nuevas clases pertenecen al paquete model.

Requisitos mínimos para evaluar la práctica

Entrega de la práctica

La práctica se entrega en el servidor de prácticas del DLSI.

Debes subir allí un archivo comprimido con tu código fuente (sólo archivos .java). En un terminal, sitúate en el directorio ‘src’ de tu proyecto Eclipse e introduce la orden

tar czvf prog3-battleship-p2.tgz model

Sube este fichero prog3-battleship-p2.tgz al servidor de prácticas. Sigue las instrucciones de la página para entrar como usuario y subir tu trabajo.

Evaluación

La corrección de la práctica es automática. Esto significa que se deben respetar estrictamente los formatos de entrada y salida especificados en los enunciados, así como la interfaz pública de las clases, tanto en la signatura de los métodos (nombre del método, número, tipo y orden de los argumentos de entrada y el tipo devuelto) como en el funcionamiento de éstos. Así, por ejemplo, el método model.Coordinate(int, int) debe tener dos enteros como argumento y guardarlos en los atributos correspondientes.

Tienes más información sobre el sistema de evaluación de prácticas en la ficha de la asignatura.

Además de la corrección automática, se va a utilizar una aplicación detectora de plagios. Se indica a continuación la normativa aplicable de la Escuela Politécnica Superior de la Universidad de Alicante en caso de plagio:

“Los trabajos teórico/prácticos realizados han de ser originales. La detección de copia o plagio supondrá la calificación de”0" en la prueba correspondiente. Se informará la dirección de Departamento y de la EPS sobre esta incidencia. La reiteración en la conducta en esta u otra asignatura conllevará la notificación al vicerrectorado correspondiente de las faltas cometidas para que estudien el caso y sancionen según la legislación vigente".

Aclaraciones