PROG3 - Práctica 4

Práctica 4

Plazo de entrega: Hasta el domingo 12 de diciembre de 2021 a las 23:59h
Peso relativo de está práctica en la nota de prácticas: 35%
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

ImperialCommander: jugadores y mecánica de juego

Introducción

En esta práctica ampliaremos la práctica 3 con varios tipos de jugadores y unas clases para gestionar el juego. Con respecto a la práctica anterior, solamente hay cambios en las clases Ship y Board, en las que los atributos fleet y board pasan a ser protected porque vamos a derivar un par de clases de ellas que necesitan acceder a esos atributos.

Diagrama de clases

El siguiente diagrama de clases UML representa las clases de nuestro modelo. Solamente se muestran las clases nuevas (que pertenecen todas al paquete model.game), y de Ship y Board solamente se muestra el cambio de la visibilidad de los atributos fleet y board. Ten en cuenta que algunas relaciones entre las clases se implementan con atributos (privados todos, en este caso).

Diagrama de clases UML.
Diagrama de clases UML.

 

En esta práctica se añade una nueva excepción, WrongFighterIdException, que es similar a las de la práctica anterior e irá en su propio paquete model.game.exceptions:

Diagrama de clases de excepciones.
Diagrama de clases de excepciones.

 

Hoja de ruta

El orden aconsejado para ir construyendo tu práctica es:

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.

Cambios en las clases de la práctica anterior

Los cambios que debes hacer en las clases de la práctica anterior son:


Clases

A continuación se describen los métodos de cada una de las clases del modelo que cambian en esta práctica.

GameBoard

Se trata de una subclase de Board que usaremos para el juego. Sus métodos son:

GameBoard

El constructor simplemente llama al constructor de la clase padre con super(…)

numFighters

Devuelve el número de cazas que hay en el tablero del bando indicado por el argumento side (se usará en la clase Game)

toString()

Devuelve una cadena con una representación del tablero. Un ejemplo de tablero de 10x10 con varios cazas sería:

  0123456789
  ----------
0|   A    AX
1|          
2|    X     
3|       A  
4|          
5|   A      
6| i Y      
7|    Y b X 
8|          
9|          

Evidentemente, si el tamaño del tablero es superior a 10 la representación no será tan bonita, pero en este momento no nos preocupa que sea visualmente agradable.

GameShip

Es una clase para gestionar una nave en el juego. Es una subclase de Ship.

GameShip

Constructor que lo único que debe hacer es llamar al constructor de la clase padre

isFleetDestroyed

Devuelve true si no quedan cazas sin destruir en la flota de la nave, es decir, si no hay cazas o todos los que hay están destruidos; si hay algún caza sin destruir, devolverá false.

getFighter

Método privado que busca en la flota de la nave un caza cuyo identificador coincida con el argumento id. Si encuentra el caza y no ha sido destruido, lo devuelve; en caso contrario, lanza la WrongFighterIdException.

getFightersId

Devuelve una lista con los identificadores de los cazas (no destruidos) de la flota de la nave:

  • Si el argumento where es board devolverá solamente los identificadores de los cazas que estén en el tablero (es decir, los que tengan posición asignada)
  • si el argumento es ship, devolverá los identificadores de los cazas que estén en la nave (los que no tengan posición asignada, es decir, los que la tengan a null).
  • Con cualquier otro valor de where devolverá todos los cazas que pertenezcan a la nave, estén o no en el tablero.

En ningún caso se incluirán los cazas destruidos.

launch

Obtiene el caza indicado por el argumento id y lo lanza al tablero b en la coordenada c. Este método debe propagar las excepciones lanzadas por getFighter y Board.launch (para eso solamente hay que incluir las excepciones en la cláusula throws).

patrol

Obtiene el caza indicado por id y lo pone a patrullar en el tablero b. Como en el método anterior, propaga las excepciones que se lancen en su interior.

improveFighter

Obtiene el caza indicado por id, lo quita del tablero (si no estaba en el tablero se lanzará la FighterNotInBoardException, que se debe capturar para no hacer nada), y se mejorará el caza, sumándole la mitad de qty al ataque y la otra mitad al escudo; si por ejemplo qty es un valor impar como 67, se añadirá 33 al ataque y 34 al escudo. Propaga la WrongFighterIdException

IPlayer

Interfaz que modela un jugador del juego, que contiene los métodos necesarios para jugar. Como sabes, en el fichero IPlayer.java solamente se indica la signatura de los métodos (y su documentación en Javadoc), la implementación de los métodos debe hacerse en las clases que implementen el interfaz (en esta práctica serán PlayerFile y PlayerRandom), y se debe incluir la cláusula @Override antes de cada método. Lo que debe hacer cada uno de los métodos es:

setBoard

Asigna el tablero pasado como parámetro gb al atributo board del jugador

getGameShip

Devuelve la nave del jugador (sin hacer copia defensiva), se usará para los tests

initFighters

Obtiene una cadena similar a 7/XWing:4/AWing y llama al método addFighters de la nave del jugador. Más adelante se explica cómo se obtiene dicha cadena en las clases PlayerRandom y PlayerFile.

isFleetDestroyed, purgeFleet

Llama al método del mismo nombre de la nave del jugador (y devuelve su valor en el caso de isFleetDestroyed)

showShip

Devuelve una cadena formada por la cadena que devuelve el método toString de la nave, un cambio de línea y la cadena devuelta por el método showFleet.

nextPlay

Realiza la siguiente jugada del jugador, que puede ser una de estas:

  • launch: lanzar un caza al tablero
  • patrol: poner a patrullar un caza del tablero
  • improve: mejorar un caza (esté o no en el tablero)
  • exit: abandonar la partida

En la descripción de las clases PlayerRandom y PlayerFile se describirá con más detalle el funcionamiento de este método. Devuelve false si el jugador abandona, y true si sigue jugando

PlayerRandom

Es una clase para simular un jugador que juega al azar. Los métodos que son diferentes a los de otros tipos de jugadores son:

PlayerRandom

Construye la nave del jugador en función del argumento side (por ejemplo, si side es REBEL creará una nave rebelde con el nombre PlayerRamdom REBEL Ship), e inicializará el atributo numFighters con el valor del argumento numFighters

initFighters

Construye la cadena para llamar a Ship.addFighters de la siguiente forma:

  • con un bucle, recorre los distintos tipos de cazas: si la nave es imperial, los tipos son TIEFighter, TIEBomber y TIEInterceptor, en ese orden; si es rebelde los tipos son XWing, YWing y AWing
  • para cada tipo se obtiene un número aleatorio entre 0 y numFighters-1 (llamando a RandomNumber.newRandomNumber, como en la práctica 2)
  • si el número no es 0, se añade el número y tipo de caza a la cadena. Por ejemplo, si el número es 7 y el tipo YWing, se añadiría 7/YWing a la cadena

Una vez construida la cadena, se llama a Ship.addFighters (si la cadena no está vacía, claro)

nextPlay

Realiza un movimiento en el juego, para lo que lo primero que debe hacer es obtener un número aleatorio option entre 0 y 99 (ya sabes cómo).

  • si el número es 99, el movimiento será el abandono (exit).
  • en los casos restantes se debe obtener el identificador del caza sobre el que se desea actuar, para lo que primero hay que obtener una lista de identificadores de la nave, que podría estar vacía, en cuyo caso habría que emitir un mensaje de error (que debe comenzar por la cadena ERROR:) y no hacer nada más. La lista que hay que obtener depende de la opción, lógicamente, y el identificador se obtiene a partir de un número aleatorio que sea una posición válida de la lista.
  • si el número option está entre 85 y 98 (ambos incluidos), será un movimiento de mejora, para lo que hay que obtener el identificador del caza y mejorarlo usando el valor option como cantidad (qty)

  • si el número está entre 25 y 84, será un launch, para lo que hay que obtener el identificador del caza, obtener dos números aleatorios x e y para formar una coordenada, y lanzar el caza en esa coordenada. Las posibles excepciones que se puedan lanzar en el método GameShip.launch se capturarán y relanzarán como RuntimeException (son errores de programación):
   throw new RuntimeException(e); // 'e' es la excepción capturada
  • si el número option está entre 0 y 24, será un movimiento de patrulla, para lo que hay que obtener el identificador del caza y ponerlo a patrullar. Como en el caso de launch, las excepciones se deben relanzar como RuntimeException

En caso de que el jugador abandone (exit) el método debe devolver false; si el jugador sigue jugando hay que devolver true, aunque se emita un error y realmente no se haga ningún movimiento.

PlayerFile

Esta clase permite leer los movimientos de un jugador desde un fichero (o incluso desde la consola).

PlayerFile

Constructor que inicializa la nave del jugador (con el nombre PlayerFile IMPERIAL Ship si es una nave imperial), e inicializa el atributo br con el argumento br

initFighters

Leerá una línea con el método BufferedReader.readLine y la guardará en una cadena, y llamará a Ship.addFighters con esa cadena. La excepción que lanza readline se capturará y relanzará como RuntimeException (es complicado jugar si la primera lectura falla)

nextPlay

Se leerá una línea con readline (y se hará lo mismo que en initFighters si hay una excepción al leer), y se intentará ejecutar el movimiento contenido en la línea, que puede ser:

  • exit: el jugador abandona la partida.
  • improve id qty: donde id y qty son números, para convertir de cadena a número debes usar Integer.parseInt (que lanzará una excepción si no es un número, pero no debes hacer nada al respecto, ni capturarla ni propagarla). Si después de improve no hay exactamente dos cadenas más hay que dar un mensaje de error (que debe comenzar por ERROR:), y también debe emitirse un mensaje de error si la cantidad qty no es menor que 100; en caso de error no se puede mejorar el caza, por lo que no se hará ninguna mejora, se emitirá el mensaje de error y no se hará nada más. Finalmente, si al mejorar el caza se lanza la WrongFighterIdException se debe capturar y emitir un mensaje de error que incluya el mensaje de la excepción al final
  • patrol id: como en el caso anterior, id es un número. Si después de patrol no hay exactamente una cadena más (la del id), se debe emitir un mensaje de error. También como en el caso anterior, si la llamada al método patrol genera una excepción, se debe capturar y emitir un mensaje de error que incluya el mensaje de la excepción al final
  • launch x y, launch x y id, launch x y type: donde x e y son números. Si después de launch no hay 2 o 3 cadenas más, se emitirá un mensaje de error. Si solamente hay 2 cadenas (x e y), se lanzará el primer caza disponible (de cualquier tipo) a la coordenada formada por x e y del tablero. Si hay 3 cadenas, hay que intentar convertir la tercera cadena (la que va después de x e y) a número y capturar la NumberFormatException que lanza Integer.parseInt (sólo en este caso); si no se lanza la excepción, la tercera cadena es un número, y se asumirá que es el identificador de un caza de la nave, en cuyo caso se lanzará dicho caza a la coordenada indicada por x e y. Si no es un número, se asumirá que es un tipo de caza (p.ej. podría ser XWing) y se lanzará el primer caza disponible de ese tipo a la coordenada indicada por x e y. En todos los casos, las posibles excepciones lanzadas por los métodos (de Ship o GameShip) se capturarán, generando como en los casos anteriores un mensaje de error que incluya el mensaje de la excepción. Algunos ejemplos de movimientos launch serían:
launch 7 8                  (lanzar el primer caza disponible a la [7,8])
launch 6 7 23               (lanzar el caza con id=23 a la [6,7])
launch 3 2 AWing            (lanzar el primer AWing disponible a la [3,2])

(evidentemente, lo de (lanzar el ... no hay que ponerlo, claro)

Si la línea leida no empieza por ninguno de los movimientos descritos, se mostrará un mensaje de error y no se hará ningún movimiento.

Como en la clase PlayerRandom, en caso de que el jugador abandone (exit) el método debe devolver false; si el jugador sigue jugando hay que devolver true, aunque se emita un error y realmente no se haga ningún movimiento.

Game

Esta clase gestiona una partida entre dos jugadores, uno imperial y otro rebelde. Empieza jugando siempre el jugador imperial, y si hay algún error en el movimiento del jugador, simplemente pierde el turno (no se le vuelve a pedir).

Los métodos de esta clase son los siguientes (puede ser conveniente crear algunos métodos privados):

Game

Constructor que guarda en sus atributos los jugadores que se le pasan como argumentos, crea un tablero de tamaño BOARD_SIZE y se lo asigna a los jugadores. Si se lanza la InvalidSizeException en el constructor de Board debe relanzarse como una RuntimeException (sería un error de programación, porque el tamaño mínimo es 5 y la constante BOARD_SIZE es 10, que es mayor que 5, luego nunca debería lanzarse esa excepción).

getGameBoard

Devuelve el tablero (sin hacer copia defensiva), se usará para los tests

play

Al principio, inicializa los cazas de los jugadores imperial y rebelde. A continuación comienza el bucle del juego: en cada paso, se mostrará el tablero y la información de las naves de los jugadores en tres momentos:

  • antes del movimiento del jugador imperial (mostrando antes la cadena BEFORE IMPERIAL),
  • después del movimiento del jugador imperial y antes del movimiento del jugador rebelde (mostrando AFTER IMPERIAL, BEFORE REBEL),
  • después del movimiento del jugador rebelde (mostrando AFTER REBEL)

Antes del movimiento de cada jugador se mostrará un prompt que incluirá el bando y el número de cazas de ese bando en el tablero. Por ejemplo, si le toca jugar al jugador rebelde y tiene 3 cazas en el tablero, se mostraría:

REBEL(3): 

La partida termina cuando uno de los jugadores abandona (exit), o bien cuando la flota de alguno de los dos jugadores resulta destruida. Evidentemente, si después del movimiento del jugador imperial alguna de las dos flotas ha resultado destruida, al jugador rebelde no se le debe pedir el siguiente movimiento ni mostrarle el prompt (ni tampoco mostrar la información del tablero y de los jugadores).

Finalmente, después de mostrar la información del tablero y los jugadores después del movimiento del jugador rebelde, se eliminarán de las flotas los cazas destruidos. También se deben eliminar los cazas destruidos después de que la partida termine.

El método debe devolver el bando que ha ganado la partida (por abandono del contrario o porque ha destruido la flota enemiga).

Algoritmo en pseudo-código del método play
  • Inicializar los cazas del jugador IMPERIAL (llamando a initFighters)
  • Inicializar los del jugador REBEL de la misma forma
  • Bucle de juego
    • Imprimir BEFORE IMPERIAL, el tablero y las naves
    • Mostrar el prompt IMPERIAL(n): (donde n es el número de cazas en el tablero)
    • Realizar la jugada del jugador IMPERIAL; si es exit, salir del bucle
    • Imprimir AFTER IMPERIAL, BEFORE REBEL, el tablero y las naves
    • Si alguna de las flotas se ha destruido, salir del bucle
    • Mostrar el prompt REBEL(m):
    • Realizar la jugada del jugador REBEL; si es exit, salir del bucle
    • Imprimir AFTER REBEL, el tablero y las naves
    • Eliminar de las flotas los cazas destruidos (con purgeFleet)
    • Si ninguna de las dos flotas ha resultado destruida, volver al principio del bucle
  • Una vez acabado el bucle, eliminar los cazas destruidos (con purgeFleet)

Ejemplo

A continuación tienes un ejemplo de salida de una partida (el fichero MainP4min.java genera esta salida). En esa partida, la nave imperial empieza con 2 TIEInterceptor, que el jugador coloca en la [1,1] y en la [2,2] (en el siguiente turno), y el jugador rebelde empieza con 2 YWing, que coloca en posiciones cercanas ([1,2] y [2,3]); el jugador imperial pone a patrullar a su TIEInterceptor con identificador 2, que derrota a los dos YWing y gana la partida.

BEFORE IMPERIAL
  0123456789
  ----------
0|          
1|          
2|          
3|          
4|          
5|          
6|          
7|          
8|          
9|          

Ship [PlayerFile IMPERIAL Ship 0/0] 2/TIEInterceptor
(TIEInterceptor 1 IMPERIAL null {145,85,60})
(TIEInterceptor 2 IMPERIAL null {145,85,60})

Ship [PlayerFile REBEL Ship 0/0] 2/YWing
(YWing 3 REBEL null {80,70,110})
(YWing 4 REBEL null {80,70,110})

IMPERIAL(0): AFTER IMPERIAL, BEFORE REBEL
  0123456789
  ----------
0|          
1| i        
2|          
3|          
4|          
5|          
6|          
7|          
8|          
9|          

Ship [PlayerFile IMPERIAL Ship 0/0] 2/TIEInterceptor
(TIEInterceptor 1 IMPERIAL [1,1] {145,85,60})
(TIEInterceptor 2 IMPERIAL null {145,85,60})

Ship [PlayerFile REBEL Ship 0/0] 2/YWing
(YWing 3 REBEL null {80,70,110})
(YWing 4 REBEL null {80,70,110})

REBEL(0): AFTER REBEL
  0123456789
  ----------
0|          
1| i        
2| Y        
3|          
4|          
5|          
6|          
7|          
8|          
9|          

Ship [PlayerFile IMPERIAL Ship 0/0] 2/TIEInterceptor
(TIEInterceptor 1 IMPERIAL [1,1] {145,85,60})
(TIEInterceptor 2 IMPERIAL null {145,85,60})

Ship [PlayerFile REBEL Ship 0/0] 2/YWing
(YWing 3 REBEL [1,2] {80,70,110})
(YWing 4 REBEL null {80,70,110})

BEFORE IMPERIAL
  0123456789
  ----------
0|          
1| i        
2| Y        
3|          
4|          
5|          
6|          
7|          
8|          
9|          

Ship [PlayerFile IMPERIAL Ship 0/0] 2/TIEInterceptor
(TIEInterceptor 1 IMPERIAL [1,1] {145,85,60})
(TIEInterceptor 2 IMPERIAL null {145,85,60})

Ship [PlayerFile REBEL Ship 0/0] 2/YWing
(YWing 3 REBEL [1,2] {80,70,110})
(YWing 4 REBEL null {80,70,110})

IMPERIAL(1): AFTER IMPERIAL, BEFORE REBEL
  0123456789
  ----------
0|          
1| i        
2| Yi       
3|          
4|          
5|          
6|          
7|          
8|          
9|          

Ship [PlayerFile IMPERIAL Ship 0/0] 2/TIEInterceptor
(TIEInterceptor 1 IMPERIAL [1,1] {145,85,60})
(TIEInterceptor 2 IMPERIAL [2,2] {145,85,60})

Ship [PlayerFile REBEL Ship 0/0] 2/YWing
(YWing 3 REBEL [1,2] {80,70,110})
(YWing 4 REBEL null {80,70,110})

REBEL(1): AFTER REBEL
  0123456789
  ----------
0|          
1| i        
2| Yi       
3|  Y       
4|          
5|          
6|          
7|          
8|          
9|          

Ship [PlayerFile IMPERIAL Ship 0/0] 2/TIEInterceptor
(TIEInterceptor 1 IMPERIAL [1,1] {145,85,60})
(TIEInterceptor 2 IMPERIAL [2,2] {145,85,60})

Ship [PlayerFile REBEL Ship 0/0] 2/YWing
(YWing 3 REBEL [1,2] {80,70,110})
(YWing 4 REBEL [2,3] {80,70,110})

BEFORE IMPERIAL
  0123456789
  ----------
0|          
1| i        
2| Yi       
3|  Y       
4|          
5|          
6|          
7|          
8|          
9|          

Ship [PlayerFile IMPERIAL Ship 0/0] 2/TIEInterceptor
(TIEInterceptor 1 IMPERIAL [1,1] {145,85,60})
(TIEInterceptor 2 IMPERIAL [2,2] {145,85,60})

Ship [PlayerFile REBEL Ship 0/0] 2/YWing
(YWing 3 REBEL [1,2] {80,70,110})
(YWing 4 REBEL [2,3] {80,70,110})

IMPERIAL(2): AFTER IMPERIAL, BEFORE REBEL
  0123456789
  ----------
0|          
1| i        
2|  i       
3|          
4|          
5|          
6|          
7|          
8|          
9|          

Ship [PlayerFile IMPERIAL Ship 2/0] 2/TIEInterceptor
(TIEInterceptor 1 IMPERIAL [1,1] {145,85,60})
(TIEInterceptor 2 IMPERIAL [2,2] {145,85,9})

Ship [PlayerFile REBEL Ship 0/2] 
(YWing 3 REBEL null {80,70,-30}) (X)
(YWing 4 REBEL null {80,70,-20}) (X)

And the winner is IMPERIAL

Comprobación de parámetros no nulos

En la práctica 2 se indicaba que, en los métodos que reciben como parámetro algún objeto, se debería comprobar que el objeto no es null (excepto en algún caso en el que tenga sentido, como en Fighter.setPosition) llamando al método Objects.requireNonNull. A continuación tienes una tabla con todos los métodos de la práctica que deberían incluir esa comprobación:

Clase método
Fighter constructor
getDamage
fight
FactoryFighter createFighter
Board launch
patrol
removeFighter
getFighter
getNeighborhood
Ship constructor
addFighters
getFirstAvailableFighter
PlayerFile constructor
setBoard
PlayerRandom constructor
setBoard
GameShip launch
patrol
improveFighter
Game constructor

Ten en cuenta que:

Programa principal

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

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


Pruebas unitarias

El archivo prog3-ImperialCommander-p4-pretest.tgz contiene una carpeta test con un paquete model con pruebas unitarias, algunas de ellas usan ficheros que están en la carpeta files. Para que las pruebas funcionen debes copiar la carpeta files al mismo nivel que src y test.

Hay un par de pruebas más en el archivo prog3-ImperialCommander-p4-moretest.tgz

Documentación

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

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

En esta práctica crearemos dos nuevos paquetes:

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 que incluya el directorio model y toda la estructura de directorios y ficheros fuente tal y como se especifica en la práctica. En un terminal, sitúate en el directorio ‘src’ de tu proyecto Eclipse e introduce la orden

tar czvf prog3-imperialCommander-p4.tgz model

Sube este fichero prog3-imperialCommander-p4.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".