Programación 3

Universidad de Alicante, 2020–2021

Práctica 3

Plazo de entrega: Hasta el domingo 8 de noviembre de 2020 a las 23.59.
Peso relativo de está práctica en la nota de prácticas: 25%
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 : Aviones y tableros 2D/3D

Introducción

Esta práctica ampliará la anterior:

  1. Añadiendo tableros 3D y 2D con aviones y barcos de diferentes formas al juego de Hundir la flota por medio de mecanismos de herencia.
  2. Manejando situaciones de error a través de excepciones.

Para poder tener tableros 3D y 2D, así como barcos y aviones de diferentes formas, y al mismo tiempo evitar la duplicación de nuestro código, implementaremos una jerarquía de clases (ver el diagrama de clases más abajo), algunas de las cuales serán abstractas. La mayor parte del código que maneja la lógica del juego (añadir barcos o aviones, dispararles, etc.) estará en esas clases abstractas. Estas clases utilizarán los métodos de sus subclases para implementar la lógica del juego sin tener que preocuparse por el tipo de tablero (2D o 3D) y el tipo de nave (barco o avión) con el que trabajan.

Diagrama de clases

Estos son los diagramas de clases UML que representan las clases en nuestro modelo; los métodos en cursiva son métodos abstractos:

En las siguientes secciones se describen todos los métodos que tienes que modificar o implementar desde cero para esta práctica. No se describirán los atributos o relaciones como ya se muestran en el diagrama UML. Los elementos que no cambian con respecto a la práctica anterior no se explicarán de nuevo. Tampoco se comentarán los setters o getters que simplemente devuelven el valor actual de un atributo, o asignan un nuevo valor a un atributo. Tendrás que decidir cuándo usar copia defensiva en un setter o getter mirando el diagrama de clase.

Cuando se indique, tu código debe comprobar que los valores de los argumentos pasados a un método son correctos. Los argumentos que son referencias no necesitan ser comprobados, a menos que se indique lo contrario.

Estructura de paquetes y directorios

Crearemos tres nuevos paquetes:

  • model.ship para el tablero 2D y los barcos;
  • model.aircraft para el tablero 3D y los aviones;
  • model.exceptions para las clases que tratan de las excepciones.

Hoja de ruta

Te recomendamos que implementes tu solución en el mismo orden que se sigue en este documento.

  1. Implementar la jerarquía de clases para las excepciones: clase BattleshipException y sus subclases.
  2. Implementar la jerarquía de clases para las coordenadas: clases Coordinate, Coordinate2D, Coordinate3D y CoordinateFactory.
  3. Implementar la jerarquía de clases para los diferentes tipos de naves: clases Craft, Ship, Aircraft y sus subclases.
  4. Implementar la jerarquía de clases para los tableros: clases Board, Board2D y Board3D.

Excepciones

A partir de este trabajo práctico manejaremos las situaciones de error por medio de excepciones. Cuando se produce una excepción, se altera el flujo normal de ejecución del programa: el método que se está ejecutando aborta su ejecución y devuelve el control al método que lo invocó. Este método puede capturar y manejar la situación de error o abortar su ejecución y devolver el control al método que lo invocó. Si la excepción no es manejada por ningún método en la secuencia de invocaciones de métodos, el programa se aborta.

En Java, las excepciones se heredan de la clase java.lang.Exception. Crearemos una superclase abstracta con el nombre BattleshipException de la que heredarán el resto de excepciones. Todas las clases relacionadas con las excepciones pertenecerán al paquete model.exceptions.

Cada nueva clase de excepción tendrá un método getMessage(-) que devolverá una cadena que describirá el motivo por el que se lanza la excepción. El texto particular utilizado para el mensaje depende de ti: las pruebas de evaluación no asumirán ninguna cadena concreta para el mismo; sin embargo, se recomienda incluir la mayor cantidad de información posible para facilitar la identificación del error. Como todas las excepciones tienen que ver con las coordenadas, BattleshipException tendrá un atributo para almacenar una referencia a la coordenada que causa la situación de error. Esta coordenada será recibida por el constructor y almacenada en un atributo para que getMessage(-) pueda utilizarla.

El nombre y el propósito de las excepciones que deben ser creadas para esta práctica son:

  • IllegalArgumentException: el argumento de un método no es válido. Se trata de una excepción en tiempo de ejecución (no verificada) que pertenece al paquete java.lang y que no tiene que ser implementada (ya existe).
  • NullPointerException: el argumento de un método es nulo. Es una excepción en tiempo de ejecución (no verificada) que pertenece al paquete java.lang y que no tienes que implementar (ya existe).
  • BattleshipException: ancestro común de todas las nuevas clases de excepción; esta es una clase abstracta que extiende a java.lang.Excepción.
  • InvalidCoordinateException: la coordenada no es válida porque está fuera de los límites del tablero.
  • OccupiedCoordinateException: la coordenada que va a ser ocupada por una nave (barco o avión) ya está ocupada por otra nave.
  • NextToAnotherCraftException: la coordenada que va a ser ocupada por una nave (barco o avión) está junto a una coordenada ya ocupada por otra nave.
  • CoordinateAlreadyHitException: la coordenada que se está disparando ya fue alcanzada en un intento anterior.

Si un método pudiera lanzar, según estas instrucciones, dos o más excepciones de la lista anterior, la excepción que aparezca primero en la lista será la que se lance; en otras palabras, tu código tiene que tratar las situaciones de error en el mismo orden que el definido por la lista.

Eclipse probablemente mostrará una advertencia sobre un campo undeclared field serialVersionUID al definir las clases de excepción (por ejemplo, “The serializable class MyException does not declare a static final serialVersionUID field of type long”). Puedes seguir la recomendación de Eclipse y añadir este campo a tu clase:

private static final long serialVersionUID = 1L;

Alternativamente, puedes añadir la siguiente anotación justo antes de la línea donde se declara la clase:

@SuppressWarnings("serial")  
public class MyException extends Exception {
    ...
}

Captura de excepciones

En esta práctica, sólo el método main o las pruebas unitarias capturan las excepciones lanzadas por los métodos descritos en las secciones siguientes. Sin embargo, muchas de estas excepciones serán capturadas por métodos de algunas de las nuevas clases en prácticas posteriores.

La clase Coordinate y sus subclases

Tendremos una superclase abstracta con el nombre Coordinate que mantendrá la mayor parte del código que escribiste para esta clase en las prácticas anteriores. Esta clase tendrá dos subclases diferentes: Coordinate2D y Coordinate3D.

Los cambios a realizar en la clase Coordinate

  • Hacer que la clase Coordinate sea abstracta
public abstract class Coordinate {
   ...
}
  • Cambia su constructor para que reciba la cantidad de dimensiones de la coordenada, y asigne memoria para el atributo components.
  • Haz que los métodos copy() y adjacentCoordinates() sean abstractos (haz una copia de tu implementación, la usarás en Coordinate2D). Se implementarán en Coordinate2D y Coordinate3D. Estos métodos abstractos permitirán a los métodos del resto de clases, como Board y Craft, crear una copia de una coordenada, u obtener sus posiciones adyacentes, sin tener que preguntarse si es una coordenada 2D o 3D. De esta manera la mayor parte del código estará en las clases pertenecientes al paquete model.
  • Sustituye cualquier llamada al constructor de copia de Coordinate por una llamada al método copy()
  • Asegúrate de que los métodos add(-) y subtract(-) funcionen bien con todas las componentes disponibles. Cabe señalar que es posible sumar/restar coordenadas 2D y 3D; en ese caso la operación se hará sólo sobre las componentes disponibles (las dos primeras componentes de la coordenada); el tipo de coordenada a devolver será el del objeto que se está utilizando para invocar el método (this). Por ejemplo: (1, 4, 2) + (2, 3) = (3, 7, 2).
  • Elimina la implementación de toString(), se pasará a Coordinate2D.
  • Cambia la visibilidad del constructor y del constructor de copia a protected.
  • Cambia los métodos set(-) y get(-) para que la excepción IllegalArgumentException sea lanzada cuando la componente esté fuera de rango.
  • Cambia los métodos add(-) y subtract(-) para que comprueben si la coordenada recibida como argumento es null, en ese caso se debe lanzar la excepción NullPointerException. La comprobación y lanzamiento de la excepción puede hacerse fácilmente de la siguiente manera:
Objects.requireNonNull(c);

Clase Coordinate2D

Coordinate2D pertenecerá al paquete model.ship. Heredará de Coordinate y tiene que implementar los métodos que aparecen en el diagrama UML.

  • El constructor crea una coordenada con dos dimensiones y asigna los valores correspondientes.
  • El constructor de copia pasa el argumento recibido al constructor de copia de la superclase.
  • copy() hace uso del constructor de la copia.
  • La implementación de adjacentCoordinates() es casi idéntica a la de la clase Coordinate de la práctica anterior, pero utilizando el método CoordinateFactory.createCoordinate(-) (véase más abajo) para crear una coordenada 2D cuando sea necesario.
  • toString() es idéntico al de la clase Coordinate de la práctica anterior.

La clase Coordinate3D

Coordinate3D pertenecerá al paquete model.aircraft. Como Coordinate2D, hereda de Coordinate y tiene que implementar los métodos que aparecen en el diagrama UML. La implementación de sus métodos es similar a su implementación en Coordinate2D pero extendiéndolos a una tercera dimensión:

  • adjacentCoordinates() devolverá un conjunto con 26 posiciones.
  • toString() devolverá un String con las tres componentes de la coordenada, por ejemplo (1, 1, 4)

Clase CoordinateFactory

A menudo es muy útil tener un método que encapsula la creación de objetos. Esto se lleva a cabo generalmente implementando un método factoría. Para ello, añade la nueva clase CoordinateFactory a tu implementación. Esta clase sólo tiene un método estático, createCoordinate(-), que recibe un número variable de argumentos de tipo int y devuelve una coordenada 2D o 3D, dependiendo de cuántos argumentos reciba. La signatura del método es la siguiente:

public static Coordinate createCoordinate(int... coords) {
    ...
}

Dentro del método, coords será un vector. Si la cantidad de elementos en este vector está por debajo de 2 o por encima de 3, el método lanzará la excepción IllegalArgumentException. Recuerda que cada array tiene un atributo length que almacena la cantidad de elementos del array.

La clase Craft y sus subclases

Tendremos una superclase abstracta con el nombre Craft de la que heredarán dos clases abstractas, Ship y Aircraft. Los barcos y aviones específicos que se utilicen heredarán de una de estas dos clases abstractas.

Todo el código que tenías en la clase Ship en la práctica anterior estará en la clase Craft, que se encuentra en la parte superior de la jerarquía. Lo único que se mueve hacia abajo de la jerarquía es la forma (shape) de las diferentes naves y aviones.

La extracción de la superclase Craft

Vamos a extraer una superclase con el nombre Craft usando la opción Refactor proporcionada por Eclipse. Para extraer esta superclase haz lo siguiente:

  1. Haz clic con el botón derecho del ratón en la clase Ship, luego selecciona Refactor-> Extract Superclass…
  2. Indica el nombre de la superclase que se va a crear: Craft
  3. Selecciona todos los atributos y métodos para que sean trasladados a la superclase
  4. Asegúrate de que la opción Use the extracted class where possible está seleccionada
  5. Haz clic en el botón Finish

Como resultado tendremos dos clases, Craft y Ship, con Ship heredando de Craft. Si se mira la línea donde se declara la clase Ship, se verá que se utiliza la palabra reservada extends:

public class Ship extends Craft {
    ...
}

Cambios a realizar en la clase Craft

  • Elimina el constructor creado por Eclipse y crea uno nuevo copiando el constructor de Ship.
  • Haz que esta clase sea abstract.
  • Cambia la visibilidad de shape a protected y elimina su inicialización. A partir de ahora se inicializará en el constructor de las subclases.
  • En getShapeIndex(-) y getAbsolutePositions(-) comprueba que la coordenada recibida como argumento no es nula. Si fuera nula el método lanzará una NullPointerException (ver Sección Exceptions).- En hit(-) hay que lanzar la excepción CoordinateAlreadyHitException si la coordenada recibida como argumento ya fue alcanzada en un intento anterior.

Cambios a realizar en la clase Ship

  • Crear paquete model.ship
  • Mover esta clase al nuevo paquete. Puedes hacerlo con Refactor->Move….
  • Cambiar el constructor para que pase sus argumentos al constructor de su superclase usando super().
  • Hacer la clase abstracta.

Subclases de Ship

Ship tendrá un total de cuatro subclases, todas pertenecientes al paquete model.ship: Battleship, Carrier, Cruiser y Destroyer. Haz lo siguiente:

  1. Crea estas subclases.
  2. Crea sus constructores. Tienen que pasar la orientación, el símbolo usado para representarlos (ver abajo) y su nombre (el de la clase) a su superclase usando super(). Después, tienen que asignar memoria e inicializar el atributo shape (ver abajo).
Los símbolos que representan los diferentes barcos

Estos son los símbolos para representar los diferentes tipos de barcos, copialos de esta descripción a tu código:

  • Battleship: O
  • Carrier: ®
  • Cruiser: Ø
  • Destroyer: Ω
La forma de los barcos

El atributo shape debe ser inicializado en el constructor de cada subclase. Lo que cambia entre las subclases son las posiciones que tienen uno y cero. Puedes descargar la inicialización del atributo shape para cada barco (y avión) desde aquí.

Clase Aircraft

  • Crear paquete modelo.aircraft
  • Crear una clase abstracta con el nombre Aircraft que herede de Craft y pertenece al paquete model.aircraft.
  • Implementar el constructor para que se comporte como el constructor de Ship.

Subclases de Aircraft

Aircraft tendrá un total de tres subclases, todas pertenecientes al paquete modelo.aircraft: Bomber, Fighter y Transport. Como las subclases de Ship estas subclases tienen que inicializar el atributo shape en su constructor (puede descargar la inicialización del atributo shape desde aquí). Sus constructores también pasarán la orientación, el símbolo utilizado para representarlos (ver más adelante) y su nombre (el de la clase) a su superclase usando super().

Los símbolos que representan a los diferentes aviones

Estos son los símbolos para representar los diferentes tipos de aviones, copialos de esta descripción a tu código:

  • Bomber:
  • Fighter:
  • Transport:

La clase Board y sus subclases

Tendremos una superclase abstracta con el nombre Board que mantendrá la mayor parte del código de la clase Board de la práctica 2. Esta clase tendrá dos subclases diferentes: Board2D y Board3D. Los métodos que se implementarán en estas subclases son checkCoordinate(-) y show(-): su implementación depende del tipo de tablero (2D o 3D).

Extracción de la superclase Board

Lo que sigue es una explicación de cómo extraer la superclase Board

  1. Renombrar la clase Board a Board2D (Refactor -> Rename…).
  2. Extraer la superclase Board de Board2D (Refactor-> Extract Superclass…). Todos los miembros (atributos y métodos), excepto los métodos checkCoordinate(-) y show(-), deben ser movidos a la superclase.
  3. Mover Board2D al paquete model.ship (Refactor->Move…).

Cambios a hacer en la clase Board

  • Hacer la clase Board abstracta
  • Declarar un método abstracto con el nombre checkCoordinate(-)
abstract public boolean checkCoordinate(Coordinate c);
  • Declarar un método abstracto con el nombre show(-)
public abstract String show(boolean unveil);

Estos dos métodos se implementarán en Board2D y Board3D.

  • Añadir la constante Board_SEPARATOR con valor '|'.
  • Mover el código del constructor de Board2D al constructor de Board.
  • Cambiar el atributo board para que utilice naves (Craft) en lugar de barcos (Ship):
private Map<Coordinate,Craft> board;
  • Comprobar la visibilidad de los atributos de Board. Todos ellos deben ser privados con la excepción de las constantes, que deben ser públicas, como en la práctica 2.
  • En el constructor, si el tamaño del tablero no está dentro de los límites, se debe lanzar la excepción IllegalArgumentException.
  • En getNeighborhood(-) se debe comprobar que los argumentos no son nulos y si son nulos debe lanzar la excepción NullPointerException.
  • En hit(-) hay que comprobar que la coordenada recibida como argumento está dentro de los límites del tablero y si no lo está, debe lanzar la excepción InvalidCoordinateException. Si un método invocado por hit(-) lanza una excepción debe ser propagada (es decir, no debe ser capturada).
  • Renombrar el método getShip() a getCraft() (Refactor -> Rename…).
  • Cambios en el método addShip(-)
    • Renombrarlo, el nuevo nombre es addCraft(-)
    • Cambiar la signatura del método a public boolean addCraft(Craft craft, Coordinate position)
    • Si alguna de las coordenadas que ocupará la nueva nave (barco o avión) no está dentro de los límites del tablero, se debe lanzar la excepción InvalidCoordinateException; si alguna de ellas está ocupada por otra nave, lanzará la excepción OccupiedCoordinateException; si alguna de ellas está al lado de otra nave, lanzará la excepción NextToAnotherCraftException.

Cambios a hacer en la clase Board2D

  • Cambiar el constructor de Board2D para que pase el tamaño recibido como argumento al constructor de Board.
  • Anotar los métodos checkCoordinate(-) y show(-) con @Override para decirle al compilador que implementan los métodos heredados de Board:
@Override
public boolean checkCoordinate(Coordinate c) {
    ...
}
  • Cambiar la implementación de checkCoordinate(-) para que compruebe que la coordenada recibida como argumento es de tipo Coordinate2D. Si no es una instancia de Coordinate2D, debe lanzar una excepción del tipo IllegalArgumentException.
  • Cambiar la implementación de show(-) para que utilice CoordinateFactory.createCoordinate(-) para crear coordenadas de tipo Coordinate2D.

Clase Board3D

La nueva clase Boarda3D es análoga a la Board2D pero trabajando con coordenadas del tipo Coordinate3D. Esta clase hereda de Board y tiene que implementar su constructor y los métodos checkCoordinate(-) y show(-):

  • El constructor es idéntico al de Board2D
  • La implementación de checkCoordinate(-) es análoga a la de Board2D.checkCoordinate(-); recuerda comprobar que la coordenada recibida es del tipo Coordinate3D y comprobar que los tres valores de la coordenada están dentro de los límites del tablero.
  • Esta clase pertenece al paquete modelo.aircraft

Board3D.show(boolean unveil)

Este método es similar al de Board2D pero teniendo en cuenta que el tablero tiene tres dimensiones. Podemos pensar en un tablero 3D como una pila de tableros 2D; al representar el tablero 3D como una cadena, cada tablero 2D de esa pila se representará uno tras otro. Por ejemplo, un tablero 3D con tamaño 8 puede ser como una pila de 8 tableros 2D y será representada como (con unveil=false):

????????|????????|????????|????????|????????|????????|????????|????????
????????|????????|??? ????|????????|????????|????????|????????|????????
????????|????????|???•????|????????|????????|????????|????????|????????
????????|????????|??•••???|????????|????????|????????|????????|????????
????????|????????|???•????|????????|????????|????????|????????|????????
????????|????????|????????|????????|????????|????????|????????|????????
????????|????????|????????|????????|????????|????????|????????|????????
????????|????????|????????|????????|????????|????????|????????|????????

donde las coordenadas (3, 4, 2), (2, 3, 2), (3, 3, 2), (3, 2, 2), (4, 3, 2) han sido alcanzadas y la coordenada (3, 1, 2) fue disparada y el resultado fue AGUA.

Programa principal

Descarga este archivo con una clase que contiene un programa principal y copialo en la carpeta ‘src/mains’ de tu proyecto.

Puedes usar MainP3.java como un ejemplo inicial muy simple de una partida parcial usando las clases implementadas en esta práctica. Ten en cuenta que este código está lejos de ser exhaustivo y sólo explora un subconjunto muy pequeño de todas las situaciones posibles que pueden ocurrir en una partida. El archivo output-p3.txt contiene un ejemplo de la salida de este programa.


Pruebas unitarias

Aquí tienes los test de la práctica 2 adaptados a esta práctica. Copia la carpeta model que se crea al descomprimirla en la carpeta test de tu proyecto.

Pruebas previas

Aquí puedes descargar los pre-tests. Cópialos en la carpeta model en el directorio test de tu proyecto.

Hay algunas pruebas que deben ser completadas en cada archivo (busca el comentario //TODO). También se usarán para calificar tu práctica, así que vale la pena intentar completarlas para que puedas obtener una buena nota. Lo más probable es que se te pida que escribas algunas pruebas unitarias en el examen práctico, así que asegúrate de que sabes cómo hacerlo.


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.

Requisitos mínimos para evaluar la práctica

  • La práctica debe poder ejecutarse sin errores de compilación. 
  • Ninguna operación debe emitir ningún tipo de comentario o mensaje por salida estándar, a menos que se indique lo contrario. Evitad también los mensajes por la salida de error.

  • Se debe respetar de manera estricta el formato del nombre de todas las propiedades (públicas, protegidas y privadas) de las clases, tanto en cuanto a ámbito de visibilidad como en cuanto a tipo y forma de escritura. En particular se debe respetar escrupulosamente la distinción entre atributos de clase y de instancia, así como las mayúsculas y minúsculas en los identificadores.
  • La práctica debe estar suficientemente documentada, de manera que el contenido de la documentación que se genere mediante la herramienta javadoc sea significativo.  

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-p3.tgz model

Sube este fichero prog3-battleship-p3.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

  • Aunque no se recomienda, se pueden añadir los atributos y métodos privados que se considere oportuno a las clases. No obstante, eso no exime de implementar TODOS los métodos presentes en el enunciado, ni de asegurarse de que funcionan tal y como se espera, incluso si no se utilizan nunca en la implementación de la práctica. 
  • Cualquier aclaración adicional aparecerá publicada en el foro de Moodle para las prácticas; por favor, suscríbete a él.