PROG3 - Third programming assignment

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:

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:

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

public abstract class Coordinate {
   ...
}
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.

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:

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

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

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

abstract public boolean checkCoordinate(Coordinate c);
public abstract String show(boolean unveil);

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

private Map<Coordinate,Craft> board;

Cambios a hacer en la clase Board2D

@Override
public boolean checkCoordinate(Coordinate c) {
    ...
}

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(-):

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

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