Programación 3

Universidad de Alicante, 2021–2022

Práctica 2

Plazo de entrega: Hasta el domingo 24 de octubre de 2021 a las 23:59h
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).

Aclaraciones

  • Corrección: en el método Board.removeFighter decía “… si el caza situado en esa posición es exactamente el mismo que se pasa como parámetro …”, y ahora dice “… si el caza situado en esa posición es igual que el que se pasa como parámetro …” . A nivel de implementación esta corrección implica que hay que usar equals
  • Corrección: la clase RandomNumber no tenía documentación, ahora ya la tiene, vuelve a descargarla
  • (cualquier aclaración adicional aparecerá en este apartado)

ImperialCommander: cazas, naves y tablero

Introducción

En esta práctica implementaremos algunas clases en las que se va a basar nuestro juego, que tendrá cazas (imperiales o rebeldes), naves que dirigirán los cazas, y un tablero en el que podremos situar cazas. La clase Coordinate de la práctica anterior se utilizará para representar las posiciones de los cazas en el tablero.

Diagrama de clases

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

Diagrama de clases UML.
Diagrama de clases UML.

Las nuevas clases pertenecen al paquete model, que ya ha sido creado en la práctica anterior y al que pertenece la clase Coordinate, que es necesario modificar en esta práctica. Ten en cuenta que en el diagrama UML hay atributos (privados, en este caso) asociados a relaciones entre objetos.

Hoja de ruta

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

  • Empieza por añadir el tipo enumerado Side, con ayuda de Eclipse. En esta página sobre Tipos enumerados en Java tienes más información, si tienes curiosidad por todo lo que se puede hacer en Java con tipos enumerados, aunque el tipo enumerado de esta práctica es muy básico y no vamos a usar ninguna característica avanzada de estos tipos
  • Incorpora al proyecto la clase RandomNumber, como se indica más abajo. Recuerda que no debes modificarla
  • Implementa el interfaz Comparable en la clase Coordinate, como se indica más abajo, e implementa los métodos nuevos
  • Crea con ayuda de Eclipse la clase Fighter y sus métodos (muchos se pueden generar desde Eclipse, como se explica más adelante), y sus pruebas unitarias
  • Sigue con la clase Ship, y sus pruebas unitarias
  • Finalmente, implementa la clase Board, sus pruebas unitarias y algún programa principal para simular una partida sencilla con un par de naves y algunos cazas (mira antes la sección Programa principal)

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, con la única diferencia de que hay un método nuevo, getNeighborhood, y se debe implementar el interfaz Comparable para que sea posible construir un conjunto ordenado de coordenadas (más adelante se explica con más detalle). Para que esta clase implemente dicho interfaz, hay que hacer dos cosas:

  1. Añadir en la declaración de la clase que implementa el interfaz:
public class Coordinate implements Comparable<Coordinate> {
...
  1. Implementar el método compareTo:
    @Override
    public int compareTo(Coordinate otra) {

Este método debe devolver un valor negativo si la x de la coordenada es menor que la x de otra, un valor positivo si x es mayor, y si ambas x son iguales debe proceder de la misma forma con las y; si ambas componentes, x e y, son iguales, debe devolver 0

getNeighborhood()

Este método debe devolver un TreeSet con las coordenadas que tiene alrededor la coordenada que recibe la llamada. 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. Se usa un TreeSet para que el conjunto esté ordenado (para esto es para lo que se necesita que la clase Coordinate implemente el interfaz Comparable), y de esta forma el conjunto de posiciones vecinas obtenido sea el mismo independientemente de en qué orden se recorran las posiciones vecinas para añadirlas al conjunto. Nota: aunque en la signatura del método pone Set, el objeto realmente devuelto no puede ser de la clase Set, tiene que ser de una de sus subclases, en este caso TreeSet

RandomNumber

Esta clase no debes implementarla, descarga el fichero RandomNumber.java en tu ordenador, abre el navegador de archivos y cópialo, y finalmente pégalo en el proyecto desde Eclipse en el paquete model. IMPORTANTE: no debes modificar este fichero en absoluto, solamente debes usarlo cuando sea necesario. Tiene tres métodos:

newRandomNumber(int max)

Genera un número aleatorio entre 0 y max-1, si la llamada es newRandomNumber(15) se generará un número entre 0 y 14.

getRandomNumberList()

Devuelve una lista de números aleatorios generados. No debes usarlo en la práctica, se incluye solamente para ayudar en la depuración de errores (a veces algunas prácticas generan más números aleatorios de los necesarios y los resultados no coinciden con los esperados).

resetRandomCounter()

Reinicializa la lista y el generador de números aleatorios, debe usarse únicamente en los tests unitarios.

Fighter

Esta clase refleja el comportamiento de los cazas, y tiene varios atributos (todos ellos privados):

  • type: una cadena que indica el tipo de caza: XWing, TIEFighter, …
  • velocity: velocidad del caza (no debe ser negativa)
  • attack: capacidad de ataque del caza (no debe ser negativa)
  • shield: escudo del caza, si llega a ser 0 o negativo se considerará que el caza está destruido
  • id: identificador, único para cada caza. Cuando se crea un nuevo caza, se le asigna como identificador el valor de nextId y se incrementa nextId; como se puede ver en el diagrama, inicialmente nextId vale 1
  • position: coordenada que representa la posición del caza en el tablero; inicialmente no tendrá valor (valdrá null), y se le asignará valor cuando se coloque el caza en el tablero; cuando el caza resulte destruido y se borre del tablero, debe volver a valer null
  • motherShip: nave a la que pertenece el caza

En el diagrama UML aparecen los métodos de esta clase; muchos de ellos son getters y setters simples, que puedes generar automáticamente con Eclipse. Los métodos addAttack, addVelocity y addShield son en el fondo también setters, puedes generarlos como setters con Eclipse para luego cambiarles el nombre (de setAttack a addAttack, etc) y modificar el código para que sumen el valor recibido como argumento al atributo (como el nombre del método indica); además, en caso de que la velocidad o la capacidad de ataque resulten negativas después de la suma (el argumento podria ser negativo), deben ponerse a 0. También debes generar con Eclipse los métodos hashCode y equals tomando únicamente el atributo id para ambos métodos.

Los demás métodos se describen a continuación:

Fighter(String type,Ship mother)

Constructor que inicializa un caza, asignando un valor de 100 a la velocidad, 80 al ataque y 80 al escudo. Además, guarda el atributo type y asigna valor al identificador. El atributo motherShip lo obtiene del segundo parámetro, y el atributo position se debe dejar a null. Este método tiene visibilidad de paquete, es decir, solamente se puede usar dentro del paquete model (de hecho, sólo debe usarse en el método addFighters de la clase Ship). Para indicar que tiene visibilidad de paquete simplemente no hay que poner nada antes del método, ni public, ni private ni protected

NOTA: cuando vayas a implementar este método la clase Ship aún no la habrás hecho, por lo que Eclipse te dará error de compilación. Para evitarlo, genera la clase Ship y su constructor (aunque esté vacío), y con eso es suficiente para que el constructor de Fighter compile. Además, si quieres hacer pruebas unitarias para el constructor de Fighter necesitarás también completar el constructor de Ship y el método getSide de Ship

Fighter(Fighter f)

Constructor de copia que devuelve un caza con los mismos valores que el caza recibido como argumento f (incluso con el mismo ìd). Se utilizará para realizar en algún caso una copia defensiva, que se utiliza cuando un objeto A devuelve en algún getter un objeto B parte de su composición que no quiere que se pueda modificar, es decir, no se devuelve el objeto B si no una copia para que si se modifica el objeto devuelto no resulte modificado el objeto B que forma parte de A (el objeto A se defiende de una posible modificación de su objeto B, por eso se llama copia defensiva). Este método se usará en el método getFighter de la clase Board.

Puesto que el objeto de la clase Fighter tiene como atributos otros objetos (mothership y position), realmente habría que hacer una copia también de dichos objetos (esto es lo que se conoce como una copia profunda), pero, por simplificar, en este método no hay que hacer copia profunda, simplemente hay que asignar los valores.

resetNextId()

Método estático que inicializa el valor del atributo estático nextId a 1. No se debe utilizar en la práctica, pero para realizar pruebas unitarias es necesario

isDestroyed()

Devuelve true si el atributo shield es 0 o menor, y false en otro caso

getSide()

Devuelve el bando del caza, que es el bando de la nave a la que pertenece

int getDamage(int n,Fighter enemy)

Devuelve el daño infligido por el caza al caza enemigo, que en esta práctica será siempre (n*attack)/300 (puedes declarar 300 como constante privada si lo deseas). En esta práctica, el argumento enemy no se utiliza, pero sí lo usaremos en la siguiente práctica.

toString()

Este método (que sobreescribe el método con el mismo nombre de la clase Object) debe devolver una cadena con la descripción del caza. Por ejemplo, si un XWing rebelde colocado en la posición (2,3) tiene como identificador 27 y le queda un valor de 36 en su escudo, la cadena que debe devolver sería:

(XWing 27 REBEL [2,3] {100,80,36})

Si no tuviera posición asignada, simplemente saldría null en vez de [2,3]

fight(Fighter enemy)

Este método simula la lucha entre dos cazas, el que recibe la llamada (el caza) y un caza enemigo (el enemigo). Inicialmente, si alguno de los dos cazas ha sido destruido, devuelve 0; en caso contrario, inicia un bucle que terminará cuando uno de los dos cazas resulte destruido (es una lucha a muerte). En cada paso del bucle uno de los cazas atacará al otro; para ello se obtendrá un número aleatorio n entre 0 y 99 (llamando al método newRandomNumber de la clase RandomNumber), y se comparará dicho valor con un umbral, que se obtiene multiplicando 100 por la velocidad del caza y dividiendo el valor resultante por la suma de las velocidades de ambos cazas; como todos los valores son enteros, la división será entera (no uses números reales). Si el umbral es menor o igual que n, el atacante será el caza y se restará al escudo del enemigo el daño causado por el caza (que será el valor devuelto por el método getDamage); en caso contrario, el atacante será el enemigo y se restará al escudo del caza el daño causado por el enemigo (llamando también a getDamage del enemigo, pero en este caso pasando 100-n como primer parámetro). Si después de un ataque el caza atacado resulta destruido, la lucha terminará y el método devolverá 1 si ha ganado el caza o -1 si ha ganado el enemigo; si ninguno de los cazas ha sido destruido, se seguirá dentro del bucle y se volverá a obtener otro número aleatorio. IMPORTANTE: asegúrate de que no llamas a newRandomNumber más de una vez dentro del bucle, si haces más llamadas el resultado de esta lucha (y las siguientes) será probablemente diferente y tu práctica funcionará mal.

Ship

Esta clase permite gestionar naves imperiales o rebeldes, que tendrán varios atributos (privados):

  • name: nombre de la nave
  • side: como en los cazas, bando al que pertenece la nave, IMPERIAL o REBEL
  • wins: victorias obtenidas por los cazas de la nave
  • losses: derrotas de los cazas de la nave
  • fleet: flota de cazas de la nave (debes usar un ArrayList para almacenar los cazas)

Aparte de los getters triviales, los métodos de esta clase son:

Ship(String name,Side side)

Constructor que inicializa los datos de la nave (obviamente los atributos wins y losses deben inicializarse a 0)

getFleetTest()

Devuelve el valor del atributo fleet (sin hacer copia defensiva), se usará solamente en las pruebas unitarias

addFighters(String fd)

A partir de una cadena como por ejemplo "5/XWing:12/AWing:3/YWing:2/XWing", debe construir los cazas indicados y añadirlos a la flota de cazas de la nave. En la cadena de ejemplo, se crearían 5 XWing, 12 AWing", 3 YWing" y otros 2 XWing. Se puede asumir que la cadena no tendrá errores (aunque si sólo hay un tipo de caza es posible que no aparezca el separador :, p.ej. en "123/AWing"). Todos los cazas creados deben pertenecer a la nave y obviamente deben ser del mismo bando. Puedes utilizar el método String.split para trocear la cadena, y el método Integer.parseInt para convertir una cadena a un entero.

updateResults(int r)

Debe actualizar los valores de wins o losses en función del valor del argumento r; si vale 1, se incrementará wins, si vale -1 se incrementará losses. Si r tiene cualquier otro valor, el método no hará nada.

getFirstAvailableFighter(String type)

Devuelve el primer caza (no destruido) de la flota del tipo indicado por el argumento type; si type es una cadena vacía, devuelve el primer caza (no destruido) de cualquier tipo. Si no hay cazas no destruidos o directamente no hay cazas, devuelve null

purgeFleet()

Borra de la flota los cazas destruidos

showFleet()

Devuelve una cadena en la que en cada línea se muestra un caza de la flota; si el caza ha sido destruido, al final de la línea se añadirá la cadena "(X)" para indicar que se ha destruido, como en este ejemplo:

(XWing 24 REBEL null {100,80,-24}) (X)
(XWing 25 REBEL null {100,80,80})
(XWing 26 REBEL null {100,80,80})
(XWing 27 REBEL null {100,80,80})
(XWing 28 REBEL null {100,80,80})

Si la flota no tiene cazas, devuelve una cadena vacía

myFleet()

Devuelve una cadena con el mismo formato que admite el método addFighters con los cazas no destruidos de la flota. Por ejemplo, si a una nave le quedan 7 TIEFighter y 3 TIEBomber, devolvería la cadena "7/TIEFighter:3/TIEBomber" (o la cadena "3/TIEBomber:7/TIEFighter", según si el primer caza no destruido es un TIEFighter o un TIEBomber). Como en el método showFleet, si la flota no tiene cazas, devuelve una cadena vacía. Debe tenerse en cuenta que deben mostrarse los cazas en el orden que tienen en fleet y que deben acumularse todos los cazas del mismo tipo, si por ejemplo una nave tiene en su flota 3 XWing, 2 AWing, 4 XWing, 5 BWing y 2 XWing, la cadena a devolver debe ser "9/XWing:2/AWing:5/BWing"

toString()

Este método (que debe sobreescribir el método toString de la clase Object) muestra los datos de la nave en una cadena. Por ejemplo, si una nave llamada Alderaan ha conseguido 35 victorias y ha tenido 10 derrotas, y tiene una flota de 12 XWing y 7 AWing (no destruidos), la cadena sería:

Ship [Alderaan 35/10] 12/XWing:7/AWing

Board

Esta clase representa el tablero cuadrado en el que se desarrollará el juego, y tiene dos atributos:

  • size: tamaño del tablero (en esta práctica asumiremos que es positivo, en prácticas posteriores veremos cómo tratar un posible tamaño incorrecto). Las coordenadas dentro del tablero irán de 0 a size-1
  • board: mapa para almacenar los cazas en posiciones del tablero. En la Guía fácil de Java Collection Framework (JCF) puedes aprender más sobre los mapas. La idea es que en vez de guardar una matriz cuadrada de posiciones que pueden o no tener un caza, se guarda una estructura con únicamente las posiciones ocupadas por cazas. Es muy importante que el método hashCode de la clase Coordinate esté bien definido.

En varios métodos de esta clase se reciben como argumentos objetos de las clases Coordinate y Fighter, y si no se indica lo contrario, como cualquier objeto puede valer null, se debe poner al principio de cada uno de estos métodos una comprobación estándar:

Objects.requireNonNull( objetoArgumento )

Poniendo una llamada como esa para cada argumento (que debe estar al principio del método), si hubiera algún error y el argumento fuera null, el error se detectaría al principio del método, no en otro momento más adelante (si no se espera que el argumento sea null fallará en algún momento, pero es mejor que el fallo se detecte lo antes posible). Por ejemplo, al principio del método launch hay que poner:

Objects.requireNonNull(c);
Objects.requireNonNull(f);

Los métodos de esta clase son:

Board(int size)

Constructor que inicializa los datos del tablero. El atributo board debes declararlo como un Map<Coordinate,Fighter>, y debes inicializarlo con una instancia de un HashMap (no se pueden crear objetos de la clase Map)

getFighter(Coordinate c)

Getter que devuelve el contenido del tablero en la posición indicada, que será el caza que la ocupa, o null si no hubiera nada. Devuelve una copia defensiva del caza porque fuera del tablero no se puede modificar el caza.

removeFighter(Fighter f)

A partir de la posición del caza f, si el caza situado en esa posición es igual que el que se pasa como parámetro se borrará del tablero y se devolverá true; en caso contrario, devolverá false (no debería ocurrir, pero es posible que el caza tenga la posición incorrecta). Evidentemente, si el caza f no ha sido posicionado en el tablero, este método devolverá false

inside(Coordinate c)

Devuelve true si la coordenada está dentro del tablero, y false en otro caso (si c es null también debe devolver false). Las coordenadas están dentro del tablero si las componentes de la coordenada están entre 0 y size-1 (ambos valores incluidos)

getNeighborhood(Coordinate c)

Devuelve un TreeSet con las posiciones vecinas a la coordenada c que estén dentro del tablero; en un caso normal, se devolverán 8 posiciones, pero por ejemplo en una esquina serían 3. Este conjunto se usará posteriormente en el método patrol

launch(Coordinate c,Fighter f)

Intenta colocar un caza en una posición del tablero. Obviamente, si la coordenada no está dentro del tablero no hará nada, como tampoco hará nada si la posición está ocupada por otro caza del mismo bando. Sin embargo, si la posición está ocupada por un caza enemigo, el nuevo caza peleará con el que ocupaba dicha posición. Al acabar la lucha (en la que siempre gana uno de los cazas y el otro resulta destruido), se actualizarán las estadísticas de victorias/derrotas de las naves de ambos cazas, y se quedará en la posición el caza ganador. Finalmente, si la posición estaba vacía, el caza se colocará en dicha posición (actualizando también la posición del caza con el setter correspondiente).

Debe devolver el resultado de la lucha con el caza enemigo, o 0 en cualquier otro caso (no hay caza, es amigo, o la coordenada no está en el tablero).

patrol(Fighter f)

El caza (si está en el tablero) recorre su vecindad (en el orden que tienen las coordenadas que devuelve getNeighborhood) y pelea con los cazas enemigos que se encuentre. Igual que en el método anterior, hay que actualizar las estadísticas de las naves a las que pertenecen los cazas, y el caza que resulte destruido debe eliminarse del tablero, pero en este método el caza atacante (que es el que patrulla) no cambia de posición. Evidentemente, si el caza que está patrullando resulta destruido en una lucha, no sigue patrullando y se elimina del tablero

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 salida-MainP2.txt contiene la salida de este programa.


Pruebas unitarias

El archivo prog3-ImperialCommander-p2-pretest.tgz contiene una carpeta model con pruebas unitarias, algunas debes completarlas tú.

Documentación

Debéis incluir en los ficheros fuente todos los comentarios necesarios en formato Javadoc. Estos comentarios deben definirse para:  

  • Ficheros: debe incluir el nombre y dni del autor de la práctica usando la anotación @author
  • Clases: propósito de la clase
  • Métodos: propósito del método + parámetros de entrada (@param), valor de retorno (@return) y funciones dependientes para operaciones más complejas, y excepciones cuando corresponda (@throws).
  • Atributos: propósito de cada uno de ellos

No se debe entregar los ficheros HTML que genera Javadoc, y tampoco debes generar la documentación automáticamente con plugins, es una buena práctica generar la documentación a mano, justo en el momento en que se empieza a escribir el método.

Estructura de paquetes y directorios

Esta práctica tiene la misma estructura de paquetes de la práctica anterior, no hay que modificar nada.

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.  
  • 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. 

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 del paquete model). En un terminal, sitúate en el directorio ‘src’ de tu proyecto Eclipse e introduce la orden

tar czvf prog3-imperialCommander-p2.tgz model

Sube este fichero prog3-imperialCommander-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 exactamente dos argumentos de tipo int.

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".