Programming 3

University of Alicante, 2020–2021

Fourth Programming Assignment

Deadline: Your assignment must be submitted before Sunday, December 6th, 2020 at 23.59 (Alicante time). Late work will not be accepted
Relative weight of this assignment: 30%

Battleship : Interfaces, game and input/output

Introduction

This assignment will extend the previous one by adding two interfaces:

  • one called IPlayer for simulating a player shooting at the board of a second player;
  • another called IVisualiser for visualising the two boards used in the Battleship game, one board for each player.

The IPlayer interface will be implemented in this assignment by two classes, each one representing a different type of player. The IVisualiser interface will be implemented by two classes producing a different representation of the boards. Note, however, that since most of your code will use the interface types and not a particular sub-class, new players or visualisers could smoothly be added to your application with minor changes to the existing code.

As a complement to these interfaces, and with the aim of easily instantiating objects of the classes implementing them, new factory methods will be created. Also, new error situations may arise and, consequently, new exceptions will be added to the model.   Finally, we will implement a class Game for handling the game.

You will have to look up the Java 8 API Documentation to know how to work with some of the classes we will use in this practical assignment.

Class diagram

These are the UML class diagrams that represent the classes in our model:

The following sections describe all the methods that you have to modify or implement from scratch for this assignment. Attributes or relationships will not be covered as they are already shown in the UML diagram. Elements which do not change with regard to the previous assignment will not be explained again. Setters or getters which simply return the current value of a property, or set a property to a new value, will not be commented either. You will have to decide when to use defensive copy in a setter or getter by looking at the class diagram.

When indicated, your code must check that the argument values passed to a method are correct. The arguments that are references do not need to be checked, unless otherwise stated.

Package structure and directories

We will create three new packages:

  • model.io for the interfaces and the classes implementing them;
  • model.exceptions.io for a new class for input/output related errors;
  • model.io.gif for two classes, that you will not have to implement, to be used to generate animated GIF images.

Roadmap

It is strongly recommended that you implement your solution in the very same order followed in this document.

  1. Implement the classes dealing with new exceptions.
  2. Implement the factory method in CraftFactory.
  3. Implement the interface IPlayer, the classes implementing it and the corresponding factory methods.
  4. Implement the interface IVisualiser, the classes implementing it and the corresponding factory methods.
  5. Implement the class Game.

Exceptions

There are two new exceptions:

  • model.exceptions.io.BattleshipIOException will be used for input/output errors. Its constructor stores the message in the parameter into the instance attribute message, which is then used by getMessage(). All the situations handling this exception will be commented when appropriate.

  • model.exceptions.CoordinateException is a new abstract class. This class is identical to the class BattleshipException in the previous assignment. In this assignment BattleshipException is simply an abstract class with no methods and no attributes.

As in the previous assignment, unless explicitly stated, methods receiving object references as parameters do not need to check if they are null.

Class CraftFactory

This class belongs to package model and simply provides a factory method to create objects of the different classes that inherit from Ship and Aircraft.

Method createCraft(String, Orientation)

The first parameter is the type of craft (name of the class to be used to create it); the second one is the orientation to be used for creating the craft.

This methods creates the corresponding craft and returns it; if the type of craft to be created is not known, it returns null.

Interface IPlayer and related classes

Classes implementing the interface IPlayer must contain a method putCrafts(·) to add crafts to a board and a method nextShoot(·) to shoot at a board. Both methods may throw the exception BattleshipIOException.  

Class PlayerFile

This class implements the interface IPlayer. It allows to play the game by reading commands from a text file that will contain one line for each different command. Each command receives a different number of parameters separated by one or more whitespace characters. The following list illustrates the valid commands in a 2D board (their meaning is obvious and are not explained; note again that any number of whitespace characters may appear between a command and its parameters):

put Cruiser NORTH -1 2
put Destroyer EAST 3 3
put Carrier WEST 6 4
put Battleship SOUTH 1 0
endput
shoot 0 3
shoot 1 3
shoot 0 1
exit

Invalid input strings are “PUT Cruiser NORTH 0 1” (valid commands are case-sensitive), “put Cruiser NORTH 0” (a parameter is missing), “shooooot 0 3”, or “shoot a b c”.

On a 3D board the valid commands would look as follows:

put Cruiser NORTH -1 2 0
endput
shoot 0 1 1
exit

To take into account:

  • After “endput” no new “put” commands can be placed.
  • Before “endput” no “shoot” commands are allowed.
  • Any command after “exit” is ignored.
  • All commands are terminated with ‘\n’.

Method PlayerFile(String, BufferedReader)

This constructor receives the name of the player and an already initialised object of type BufferedReader and stores it into the instance attribute br.

Exceptions: If the reference to the BufferedReader is null, the exception NullPointerException must be thrown.

Method getName()

This method returns a string with the name of the player (the one received in the constructor) and the type of player. For example:

Martin (PlayerFile)

Method putCrafts(Board)

This methods reads the put commands one by one (one line at a time) from the attribute br (BufferedReader) and executes the corresponding actions (Board.addCraft(·)) on the board received as argument. Remember to use CraftFactory.createCraft(·) to create ships and aircfrats, and CoordinateFactory.createCoordinate(·) to create new coordinates.

The method stops reading commands from br when the commands endput or exit are read or there are no more lines to read.

Use String.split(·) in order to split each line into tokens as indicated here.
Use BufferedReader.readline(·) as discussed here in order to read the input file line by line.
Use Integer.parseInt(·) to convert a string into an integer. Bear in mind that if the string passed as argument cannot be parsed as an integer, the exception NumberFormatException is thrown.

Exceptions: Any exception thrown by Board.addCraft(·) must be re-thrown; i.e. exceptions thrown by Board.addCraft(·) do not have to be caught.

The method will throw the exception NullPointerException in the board received as argument is null, and the exception BattleshipIOException in any of the following situations:

  • An exception IOException is thrown when reading a line from br.
  • A command different from put, endput or exit is read.
  • The amount of parameters to put is not correct. It is worth noting that the orientation should be followed by two or three numbers and that both situations are correct. You do not have to worry here about the type of board and coordinates (2D or 3D) being used.
  • The orientation is none of those declared in the enum Orientation.
  • The parameters that should go after the orientation are not numbers.

Method nextShoot(Board)

This methods reads the next shoot command from the attribute br (BufferedReader) and executes the corresponding action (Board.hit(·)) on the board received as argument.

The method returns the coordinate used to hit the board or null if the command exit is read or there are no more lines to read.

Exceptions: Any exception thrown by Board.hit(·) must be re-thrown; i.e. exceptions thrown by Board.hit(·) do not have to be caught.

The method will throw the exception BattleshipIOException in any of the following situations:

  • An exception IOException is thrown when reading a line from br.
  • A command different from shoot or exit is read.
  • The amount of parameters to shoot is not correct. It is worth noting that the command shoot should be followed by two or three numbers and that both situations are correct. You do not have to worry here about the type of coordinates (2D or 3D) being used.
  • The parameters after shoot are not numbers.

Class PlayerRandom

This class implements the interface IPlayer. It behaves as a random blind player, randomly deciding the coordinates where ships and aircrafts are put and the coordinates to shoot.

Random numbers are strictly not possible with a computer if it does not have sensors connected to the outside world. In order to ensure some randomness, a seed is used in order to set the starting point for the formula which generates a sequence of pseudo-random numbers (this seed is usually given a value based on the current date and time). If the sequence generator is not changed, the same sequence will always be retrieved if the same seed is used. As your solution to this assignment will be automatically evaluated, the seed will be passed to the constructor so that each execution can be easily reproduced over and over provided that the seed is not changed.

The JDK provides the java.util.Random class for random number generation. The following code sets the seed to 12345 and generates a random integer between 0 and 99 (both numbers included):

Random random = new Random(12345);
int r = random.nextInt(100);

Method PlayerRandom(String, long)

This constructor receives the name of the player and the seed to be used to initialise the instance attribute random when calling the constructor of Random.

Method getName()

This method returns a string with the name of the player (the one received in the constructor) and the type of player. For example:

Martin (PlayerRandom)

Method genRandomInt(int, int)

This method generates a random integer in the interval [min, max) received as argument. Use the following implementation (just copy and paste from this assignment description):

private int genRandomInt(int min, int max) { 
    return random.nextInt(max-min)+min;
}

Method genRandomCoordinate(Board b, int offset)

This method generates and returns a random 2D or 3D coordinate, depending on the type of board. Call the method genRandomInt(·) to generate the value of the components of the coordinates as follows:

genRandomInt(0-offset, b.getSize());

The offset is used to generate coordinates with negative components so that they can be used to add crafts in the margins of the board.

Important: Never generate a new random number unless you are completely sure that it will be used to create a coordinate; otherwise, the games played by your random player will be different to those in the reference implementation and the tests used to grade your assignment will fail. In other words, only generate three components when the board is of type Board3D.

Method putCrafts(Board)

This method adds the ships/aircrafts to the board with a random orientation and on a random coordinate. Ships should be added in the following order: Battleship, Carrier, Cruiser, Destroyer. Aircrafts should be added (only if the board is of type Board3D) in the following order: Bomber, Fighter, Transport. It is worth noting that only one ship/aircraft of each class must be added.

For each ship/aircraft to be added the method should behave as follows:

  1. Create the ship/aircraft with a random orientation.
  2. Add the ship/aircraft to the board on a random coordinate generated with genRandomCoordinate(·) with an offset of Craft.BOUNDING_SQUARE_SIZE; to access Craft.BOUNDING_SQUARE_SIZE you will have to change its visibility from protected to public. It may happen that a ship/aircraft cannot be added to the board using the random coordinate generated; in those cases the method will keep generating random coordinates till the ship/aircraft to be added can actually be added or the amount of random coordinates generated equals 100.

Exceptions: This method throws no exceptions.

Method nextShoot(Board)

This methods generates a random coordinate with genRandomCoordinate(·) and an offset of zero, and uses it to shoot the board received as argument. The method returns the coordinate used.

Exceptions: Any exception thrown by Board.hit(·) must be re-thrown; i.e. exceptions thrown by Board.hit(·) do not have to be caught.

Class PlayerFactory

This class simply provides a factory method to create objects of the different classes that implement IPlayer.

Method createPlayer(String, String)

The first parameter is the name of the player; the second one is used to create a player of the appropriate type:

  • If the second parameter contains one of the characters ‘.’ (dot), ‘\’ or ‘/’, it is assumed to contain the path to a file containing commands and, consequently, an object of class PlayerFile is created and returned.
  • If the second parameter contains an long integer value, it is considered as the seed for the random number generator and an object of class PlayerRandom is created with it and then returned. In order to test whether a string contains a valid long number, you can use the method Long.parseLong(·). You can create an auxiliary (private) method isLong(·) which returns a boolean indicating whether a string contains a valid long number or not. 
  • If none of the two previous conditions are met, no player is created and null is returned.

Exceptions: This method throws BattleshipIOException (with an appropriate message) if the object of class BufferedReader to be passed to the constructor of PlayerFile cannot be created (for example, because the file with the commands does not exist).

Interface IVisualiser and related classes

Classes implementing this interface must define a method show() that will be called to visualise the current state of the game and a method close() that will be called when the visualiser will no longer be used.

Class VisualiserConsole

This class implements the interface IVisualiser. It allows us to produce a string representation of the game through the standard output.

Method VisualiserConsole(Game)

This constructor just stores the argument game in the corresponding attribute.

Exceptions: It throws exception NullPointerException if the game received as argument is null.

Method show()

It calls game.toString() in order to obtain a string representation of the current game which is then sent to the standard output by using System.out.println(·).

Method close()

In this class this method does nothing.

Class VisualiserGIF

This class implements the interface IVisualiser. It allows us to produce an animated GIF image representation of the game. In order to be able to generate these images you have to:

  1. Download the third-party library GIF4J, it is a JAR file. Copy it to a folder with name lib inside your Eclipse project and refresh the view of the project. Then, go to Project -> Properties, option Java Build Path, tab Libraries and press Add JARs…, then select the file gif4j_light_trial_1.0.jar.
  2. Create a package with name model.io.gif, download the files AnimatedGIF.java and FrameGIF.java and add them to the newly created package.

Method VisualiserGIF(Game)

This constructor stores the argument game in the corresponding attribute and initialises the AnimatedGIF attribute.

Exceptions: It throws exception NullPointerException if the game received as argument is null.

Method show()

It accesses the two game boards by means of the appropriate method of Game and gets their string representations using Board.show(false). It then processes these two string representations and generates a new frame to be added to the animated GIF. Each frame will contain the two boards separated by a dark grey line.

To generate a new frame do as follows:

FrameGIF frame = new FrameGIF(w, h*2+1);

where w is the width of the frame (the length of each line in the string representation of a board) and h*2+1 is the height of the frame (two times the number of lines in the string representation of a board plus one for the line separating the two boards). Notice that it is assumed that the two boards of the game are of equal size.

We will use the class java.awt.Color to specify the colours of the board positions in the generated image.

The method then processes the string representation of the first board and prints a square in light grey (Color.LIGHT_GRAY) for the non-seen positions (Board.NOTSEEN_SYMBOL), a square in blue (Color.BLUE) for the water positions (Board.WATER_SYMBOL), a square in red (Color.RED) for the hit (Board.HIT_SYMBOL) or destroyed positions, and a square in orange (Color.ORANGE) for the symbol used to separate the different 2D boards we have in a 3D board.

Here you have an example illustrating how to print a square in light grey:

frame.printSquare(column, row, Color.LIGHT_GRAY);

After processing the string representation of the first board it prints a row of squares in dark grey (Color.DARK_GRAY), and processes the string representation of the second board in a way analogous to that of the first board.

Finally, it adds the frame to the animated GIF as follows:

agif.addFrame(frame);

What follows is an example of an animated GIF obtained from a game with 3D boards of size 6: animatedgif

Exceptions: The methods AnimatedGIF.addFrame(·) and FramGIF.printSquare(·) may throw the exception BattleshipIOException; in that case it should be caught and re-thrown as a RuntimeException. This is something we do because those methods should not throw the BattleshipIOException exception, unless we have an error in our code.

Method close()

This method saves the GIF generated to a file with name output.gif in folder files:

agif.saveFile(new File("files/output.gif"));

Make sure the folder files exists inside your Eclipse project.

Exceptions: Any exception thrown by AnimatedGIF.saveFile(·) must be re-thrown as a RuntimeException.

Class VisualiserFactory

This class simply provides a factory method to create objects of the different classes that implement IVisualiser.

Method createVisualiser(String,Game)

If the value of the first parameter is “Console”, it creates and returns an object of class VisualiserConsole. If the value of the first parameter is “GIF”, it creates and returns an object of class VisualiserGIF. If the value of the first parameter is neither Console, nor GIF it returns null.

Class Game

This class has the responsibility of playing the Battleship game. The game is played by two players, each one putting ships/aircrafts in their respective boards and shooting at the opponent’s board. First the two players put their ships/aircrafts in their respective boards; then the game starts and the two players alternately shoot at the opponent’s board. The game ends when one of the players stops shooting, or if after one shot all ships/aircrafts of one of the players are shot down.

We will use the attribute nextToShoot to keep track of which player should shoot next (initially player1); the attribute gameStarted is used to flag whether the game has started or not; the attribute shootCounter will be used to keep track of the total amount of shots performed by the two players.

Method Game(Board, Board, IPlayer, IPlayer)

This constructor stores the two boards and players received as arguments in the corresponding attributes of the class. It also sets the flag gameStarted to false.

Exceptions: It throws the exception NullPointerException if any of its parameters is null.

Method start()

This method starts the game by making the players to put their ships/aircrafts on their boards. Player 1 puts her ships/aircrafts on board 1; player 2 puts her ships/aircrafts on board 2. It also initialises the attributes gameStarted, shootCounter and nextToShoot.

Exceptions: Any exception thrown by the calls to IPlayer.putCrafts(·) must be re-thrown as a RuntimeException.

Method gameEnded()

It returns true if the game has finished. A game is considered to be finished if it was started and all the crafts on one of the boards have been destroyed.

Method playNext()

This method makes the player that should play next to shoot at the opponent’s board. Player 1 shoots at board 2, and player 2 shoots at board 1. This method returns true if the player actually shot the opponent’s board (regardless of the result, even if she shot at a coordinate already hit), false otherwise.

If the method is to return true, it updates the shoot counter and the nextToShoot attribute before returning.

Exceptions: if the exceptions BattleshipIOException or InvalidCoordinateException are thrown by any of the methods used by this method, the exception is re-thrown as RuntimeException (and no update of the shoot counter and the nextToShoot attribute is performed). If the exception CoordinateAlreadyHitException is thrown, a message is print through the standard output; this message must include the text Action by, the name of the player that shot and the error message of the exception:

Action by Mary (PlayerRandom)...

Method getPlayerLastShoot()

This method returns the player that shot last or null if none of the player has shot yet.

Method playGame(IVisualiser)

This method starts the game and plays the game till the game is ended or the player that should shoot next has no more shots to perform (Game.playNext() returns false).

The method makes use of the visualiser received as argument to show the game as it progresses; to do so it calls the method IVisualiser.show(). Note that IVisualiser.show() must be called after starting the game (and before the first player starts shooting) and after each call to Game.playNext() if it returned true.

Once the game is ended, or the player that should shoot next has not more shots to perform, the method must close the visualiser (IVisualiser.close()).

Method toString()

This method returns a string representing the state of the game. What follows in a example of the string representing a game with 2D boards of size 10:

=== ONGOING GAME ===
==================================
John (PlayerRandom)
==================================
 ??   ?  ?
 ? ???    
 ?   •  ? 
   ? • ?••
    ?•? ? 
   ? ? •  
 ? ?   ? ?
      ?• ?
?      •  
 ?  ΩΩ •? 
==================================
Mary (PlayerRandom)
==================================
  •   ? ? 
  ?      ?
? •   ?   
? ?    ?  
  ?       
 •??     ?
   ?      
   ? ?    
 •?•••? Ω 
    ??  Ω 
==================================
Number of shots: 281

This string starts with === GAME NOT STARTED ===, === GAME ENDED === or === ONGOING GAME ===, depending on whether the games was not started, it already ended or is being played. It then contains the boards of the two players (together with the name of the players). After that the string includes the number of shots performed so far.

If the game was ended and all crafts in one of the boards were destroyed, a line with the name of the winner is finally added to the string representing the game. This line should look as follows:

Mary (PlayerRandom) wins

To take into account: The string representing the game is not terminated with ‘\n’.

Main Program

MainP4.java can be used as an example of a game using some of the classes implemented in this assignment. Keep in mind, however, that this code explores a very small subset of all possible game situations; therefore, it is recommended that you write your own tests in order to check your code. The code in MainP4.java uses two files (files/playerfile-john.txt and files/playerfile-mary.txt) that can be downloaded from here and here (create a regular folder with name files in the root folder of your Eclipse project and copy these text files there). You can download the expected output when running this code from here and the animated GIF image it generates from here.

Unit tests

Tests from the previous assignment must execute correctly for this assignment.

Previous tests

Here you can download the pre-tests. Copy the folders that are created when you unzip it into the folder test of your project.

There are some tests to be completed in each file (look for the comment //TODO). These will also be used to grade your assignment, so it is worth trying to complete them, so you can get a good score. Most probably, you will be required to write some unit tests in the practical exam, so make yourself sure you know how to code them!

Documentation

Your source files must include all the comments in Javadoc format as indicated in the first assignment. You do not need to include the HTML files generated by the Javadoc tool in the compressed file to deliver.

Minimal requirements for grading your assignment

  • Your program must run with no errors. 
  • Unless otherwise stated, your program must not emit any kind of message or text through the standard output or standard error streams.
  • The names of all properties (public, protected and private) of the classes, both in terms of scope of visibility and in terms of type and way of writing, must be rigorously respected, even if they are in Spanish. Make sure that you respect the distinction between class and instance attributes, as well as the uppercase and lowercase letters in the identifiers.
  • Your code must be conveniently documented and significant content has to be obtained after running the javadoc tool.   

Submission

Upload your work to the DLSI submission server.

You must upload a compressed file with your source code (only .java files). In a terminal, go to the ‘src’ folder of your Eclipse project and type

tar czvf prog3-battleship-p4.tgz model

Upload the file prog3-battleship-p4.tgz to the server. Follow the instructions on the page to log in and upload your work.

Evaluation

Testing of your assignment will be done automatically. This means that your program must strictly conform to the input and output formats given in this document, as well as to the public interfaces of all the classes: do not change the method signatures (name of the method, number, type and order of arguments, and data type returned) or their behaviour. For instance, method model.ship.Coordinate2D(int,int) must accept two arguments of type int and store them in the corresponding attributes.

You can find more information about the grading of programming assignments in the subject description sheet.

In addition to the automatic grading, a plagiarism detection application will be used. The applicable regulations of the University of Alicante Polytechnic School in the event of plagiarism are indicated below:

“Theoretical/practical work must be original. The detection of copy or plagiarism will suppose the qualification of”0" in the corresponding assignment. The corresponding Department and the University of Alicante Polytechnic School will be informed about the incident. Repeated conduct in this or any other subject will result in notification of the offences committed to the pertinent vice canchellor’s office so that they study the case and punish it in accordance with the legislation in force".

Clarifications

  • Although not recommended, you may add to your classes as many private attributes and methods as you wish. Notice, however, that you must implement all the methods indicated in this document and make sure that they work as expected, even if they are never called in your implementation. 

  • How to generate the orientation and coordinates in RandomPlayer:
    • The orientation must be generated by a single call to the method Random.nextInt(·).
    • The coordinates must be generated by calling PlayerRandom.genRandomCoordinate().
    • Generate first the orientation and then the coordinate
    • In PlayerRandom.genRandomCoordinate() generate first the ‘x’ component, then the ‘y’ component and finally the ‘z’ component; the latter only if nedded.
  • Any additional remark will be published in the Moodle forum for the discussion of the practical assignments; please subscribe to that forum.