added contents

This commit is contained in:
Mark Minas
2024-09-18 17:04:31 +02:00
parent d28b17eba5
commit 71a4ac8d12
176 changed files with 51198 additions and 0 deletions

View File

@@ -0,0 +1,12 @@
plugins {
id 'buildlogic.java-library-conventions'
}
description = 'Battleship common model'
dependencies {
api project(":common")
api libs.jme3.networking
implementation libs.gson
testImplementation libs.mockito.core
}

View File

@@ -0,0 +1,103 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship;
import pp.util.config.Config;
import java.util.Map;
import java.util.TreeMap;
import static java.lang.Math.max;
/**
* Provides access to the configuration settings for the Battleship game.
* <p>
* This class allows for loading configuration settings from a properties file,
* including the server port, map dimensions, and the number of ships of various lengths.
* </p>
* <p>
* <b>Note:</b> Attributes of this class are not marked as {@code final} to allow
* for proper initialization when reading from a properties file.
* </p>
*/
public class BattleshipConfig extends Config {
/**
* The default port number for the Battleship server.
*/
@Property("port")
private int port = 1234;
/**
* The width of the game map in terms of grid units.
*/
@Property("map.width")
private int mapWidth = 10;
/**
* The height of the game map in terms of grid units.
*/
@Property("map.height")
private int mapHeight = 10;
/**
* An array representing the number of ships available for each length.
* The index corresponds to the ship length minus one, and the value at each index
* is the number of ships of that length.
*/
@Property("ship.nums")
private int[] shipNums = {4, 3, 2, 1};
/**
* Creates an instance of {@code BattleshipConfig} with default settings.
*/
public BattleshipConfig() {
// Default constructor
}
/**
* Returns the port number configured for the Battleship server.
*
* @return the port number
*/
public int getPort() {
return port;
}
/**
* Returns the width of the game map. The width is guaranteed to be at least 2 units.
*
* @return the width of the game map
*/
public int getMapWidth() {
return max(mapWidth, 2);
}
/**
* Returns the height of the game map. The height is guaranteed to be at least 2 units.
*
* @return the height of the game map
*/
public int getMapHeight() {
return max(mapHeight, 2);
}
/**
* Returns a map representing the number of ships for each length.
* The keys are ship lengths, and the values are the corresponding number of ships.
*
* @return a map of ship lengths to the number of ships
*/
public Map<Integer, Integer> getShipNums() {
final TreeMap<Integer, Integer> ships = new TreeMap<>();
for (int i = 0; i < shipNums.length; i++)
if (shipNums[i] > 0)
ships.put(i + 1, shipNums[i]);
return ships;
}
}

View File

@@ -0,0 +1,40 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship;
import java.util.ResourceBundle;
/**
* Provides access to the resource bundle of the game.
*
* @see #BUNDLE
*/
public class Resources {
/**
* The resource bundle for the Battleship game.
*/
public static final ResourceBundle BUNDLE = ResourceBundle.getBundle("battleship"); //NON-NLS
/**
* Gets a string for the given key from the resource bundle in {@linkplain #BUNDLE}.
*
* @param key the key for the desired string
* @return the string for the given key
* @throws NullPointerException if {@code key} is {@code null}
* @throws java.util.MissingResourceException if no object for the given key can be found
* @throws ClassCastException if the object found for the given key is not a string
*/
public static String lookup(String key) {
return BUNDLE.getString(key);
}
/**
* Private constructor to prevent instantiation.
*/
private Resources() { /* do not instantiate */ }
}

View File

@@ -0,0 +1,102 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.game.client;
import pp.battleship.message.client.ShootMessage;
import pp.battleship.message.server.EffectMessage;
import pp.battleship.model.IntPoint;
import pp.battleship.model.ShipMap;
import pp.battleship.notification.Sound;
import java.lang.System.Logger.Level;
/**
* Represents the state of the client where players take turns to attack each other's ships.
*/
class BattleState extends ClientState {
private boolean myTurn;
/**
* Constructs a new instance of {@link BattleState}.
*
* @param logic the game logic
* @param myTurn true if it is my turn
*/
public BattleState(ClientGameLogic logic, boolean myTurn) {
super(logic);
this.myTurn = myTurn;
}
@Override
public boolean showBattle() {
return true;
}
@Override
public void clickOpponentMap(IntPoint pos) {
if (!myTurn)
logic.setInfoText("wait.its.not.your.turn");
else if (logic.getOpponentMap().isValid(pos))
logic.send(new ShootMessage(pos));
}
/**
* Reports the effect of a shot based on the server message.
*
* @param msg the message containing the effect of the shot
*/
@Override
public void receivedEffect(EffectMessage msg) {
ClientGameLogic.LOGGER.log(Level.INFO, "report effect: {0}", msg); //NON-NLS
playSound(msg);
myTurn = msg.isMyTurn();
logic.setInfoText(msg.getInfoTextKey());
affectedMap(msg).add(msg.getShot());
if (destroyedOpponentShip(msg))
logic.getOpponentMap().add(msg.getDestroyedShip());
if (msg.isGameOver()) {
msg.getRemainingOpponentShips().forEach(logic.getOwnMap()::add);
logic.setState(new GameOverState(logic));
}
}
/**
* Determines which map (own or opponent's) should be affected by the shot based on the message.
*
* @param msg the effect message received from the server
* @return the map (either the opponent's or player's own map) that is affected by the shot
*/
private ShipMap affectedMap(EffectMessage msg) {
return msg.isOwnShot() ? logic.getOpponentMap() : logic.getOwnMap();
}
/**
* Checks if the opponent's ship was destroyed by the player's shot.
*
* @param msg the effect message received from the server
* @return true if the shot destroyed an opponent's ship, false otherwise
*/
private boolean destroyedOpponentShip(EffectMessage msg) {
return msg.getDestroyedShip() != null && msg.isOwnShot();
}
/**
* Plays a sound based on the outcome of the shot. Different sounds are played for a miss, hit,
* or destruction of a ship.
*
* @param msg the effect message containing the result of the shot
*/
private void playSound(EffectMessage msg) {
if (!msg.getShot().isHit())
logic.playSound(Sound.SPLASH);
else if (msg.getDestroyedShip() == null)
logic.playSound(Sound.EXPLOSION);
else
logic.playSound(Sound.DESTROYED_SHIP);
}
}

View File

@@ -0,0 +1,38 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.game.client;
import pp.battleship.game.singlemode.BattleshipClientConfig;
/**
* Interface representing a Battleship client.
* Provides methods to access game logic, configuration, and to enqueue tasks.
*/
public interface BattleshipClient {
/**
* Returns the game logic associated with this client.
*
* @return the ClientGameLogic instance
*/
ClientGameLogic getGameLogic();
/**
* Returns the configuration associated with this client.
*
* @return the BattleshipConfig instance
*/
BattleshipClientConfig getConfig();
/**
* Enqueues a task to be executed by the client.
*
* @param runnable the task to be executed
*/
void enqueue(Runnable runnable);
}

View File

@@ -0,0 +1,355 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.game.client;
import pp.battleship.message.client.ClientMessage;
import pp.battleship.message.server.EffectMessage;
import pp.battleship.message.server.GameDetails;
import pp.battleship.message.server.ServerInterpreter;
import pp.battleship.message.server.StartBattleMessage;
import pp.battleship.model.IntPoint;
import pp.battleship.model.ShipMap;
import pp.battleship.model.dto.ShipMapDTO;
import pp.battleship.notification.ClientStateEvent;
import pp.battleship.notification.GameEvent;
import pp.battleship.notification.GameEventBroker;
import pp.battleship.notification.GameEventListener;
import pp.battleship.notification.InfoTextEvent;
import pp.battleship.notification.Sound;
import pp.battleship.notification.SoundEvent;
import java.io.File;
import java.io.IOException;
import java.lang.System.Logger;
import java.lang.System.Logger.Level;
import java.util.ArrayList;
import java.util.List;
import static java.lang.Math.max;
/**
* Controls the client-side game logic for Battleship.
* Manages the player's ship placement, interactions with the map, and response to server messages.
*/
public class ClientGameLogic implements ServerInterpreter, GameEventBroker {
static final Logger LOGGER = System.getLogger(ClientGameLogic.class.getName());
private final ClientSender clientSender;
private final List<GameEventListener> listeners = new ArrayList<>();
private GameDetails details;
private ShipMap ownMap;
private ShipMap harbor;
private ShipMap opponentMap;
private ClientState state = new InitialState(this);
/**
* Constructs a ClientGameLogic with the specified sender object.
*
* @param clientSender the object used to send messages to the server
*/
public ClientGameLogic(ClientSender clientSender) {
this.clientSender = clientSender;
}
/**
* Returns the current state of the game logic.
*/
ClientState getState() {
return state;
}
/**
* Sets the current state of the game logic.
*
* @param newState the new state to be set
*/
void setState(ClientState newState) {
LOGGER.log(Level.DEBUG, "state transition {0} --> {1}", state.getName(), newState.getName()); //NON-NLS
state = newState;
notifyListeners(new ClientStateEvent());
state.entry();
}
/**
* Returns the game details.
*
* @return the game details
*/
GameDetails getDetails() {
return details;
}
/**
* Returns the player's own map.
*
* @return the player's own map
*/
public ShipMap getOwnMap() {
return ownMap;
}
/**
* Returns the opponent's map.
*
* @return the opponent's map
*/
public ShipMap getOpponentMap() {
return opponentMap;
}
/**
* Returns the harbor map.
*
* @return the harbor map
*/
public ShipMap getHarbor() {
return harbor;
}
/**
* Checks if the editor should be shown.
*
* @return true if the editor should be shown, false otherwise
*/
public boolean showEditor() {
return state.showEditor();
}
/**
* Checks if the battle state should be shown.
*
* @return true if the battle state should be shown, false otherwise
*/
public boolean showBattle() {
return state.showBattle();
}
/**
* Sets the game details provided by the server.
*
* @param details the game details including map size and ships
*/
@Override
public void received(GameDetails details) {
state.receivedGameDetails(details);
}
/**
* Moves the preview ship to the specified position.
*
* @param pos the new position for the preview ship
*/
public void movePreview(IntPoint pos) {
state.movePreview(pos);
}
/**
* Handles a click on the player's own map.
*
* @param pos the position where the click occurred
*/
public void clickOwnMap(IntPoint pos) {
state.clickOwnMap(pos);
}
/**
* Handles a click on the harbor map.
*
* @param pos the position where the click occurred
*/
public void clickHarbor(IntPoint pos) {
state.clickHarbor(pos);
}
/**
* Handles a click on the opponent's map.
*
* @param pos the position where the click occurred
*/
public void clickOpponentMap(IntPoint pos) {
state.clickOpponentMap(pos);
}
/**
* Rotates the preview ship.
*/
public void rotateShip() {
state.rotateShip();
}
/**
* Marks the player's map as finished.
*/
public void mapFinished() {
state.mapFinished();
}
/**
* Checks if the player's map is complete (i.e., all ships are placed).
*
* @return true if all ships are placed, false otherwise
*/
public boolean isMapComplete() {
return state.isMapComplete();
}
/**
* Checks if there is currently a preview ship.
*
* @return true if there is currently a preview ship, false otherwise
*/
public boolean movingShip() {
return state.movingShip();
}
/**
* Starts the battle based on the server message.
*
* @param msg the message indicating whose turn it is to shoot
*/
@Override
public void received(StartBattleMessage msg) {
state.receivedStartBattle(msg);
}
/**
* Reports the effect of a shot based on the server message.
*
* @param msg the message containing the effect of the shot
*/
@Override
public void received(EffectMessage msg) {
state.receivedEffect(msg);
}
/**
* Initializes the player's own map, opponent's map, and harbor based on the game details.
*
* @param details the game details including map size and ships
*/
void initializeMaps(GameDetails details) {
this.details = details;
final int numShips = details.getShipNums().values().stream().mapToInt(Integer::intValue).sum();
final int maxLength = details.getShipNums().keySet().stream().mapToInt(Integer::intValue).max().orElse(2);
ownMap = new ShipMap(details.getWidth(), details.getHeight(), this);
opponentMap = new ShipMap(details.getWidth(), details.getHeight(), this);
harbor = new ShipMap(max(maxLength, 2), max(numShips, details.getHeight()), this);
}
/**
* Sets the informational text to be displayed to the player.
*
* @param key the key for the info text
*/
void setInfoText(String key) {
notifyListeners(new InfoTextEvent(key));
}
/**
* Emits an event to play the specified sound.
*
* @param sound the sound to be played.
*/
public void playSound(Sound sound) {
notifyListeners(new SoundEvent(sound));
}
/**
* Loads a map from the specified file.
*
* @param file the file to load the map from
* @throws IOException if an I/O error occurs
*/
public void loadMap(File file) throws IOException {
state.loadMap(file);
}
/**
* Checks if the player's own map may be loaded from a file.
*
* @return true if the own map may be loaded from file, false otherwise
*/
public boolean mayLoadMap() {
return state.mayLoadMap();
}
/**
* Checks if the player's own map may be saved to a file.
*
* @return true if the own map may be saved to file, false otherwise
*/
public boolean maySaveMap() {
return state.maySaveMap();
}
/**
* Saves the player's own map to the specified file.
*
* @param file the file to save the map to
* @throws IOException if the map cannot be saved in the current state
*/
public void saveMap(File file) throws IOException {
if (ownMap != null && maySaveMap())
new ShipMapDTO(ownMap).saveTo(file);
else
throw new IOException("You are not allowed to save the map in this state of the game");
}
/**
* Sends a message to the server.
*
* @param msg the message to be sent
*/
void send(ClientMessage msg) {
if (clientSender == null)
LOGGER.log(Level.ERROR, "trying to send {0} with sender==null", msg); //NON-NLS
else
clientSender.send(msg);
}
/**
* Adds a listener to receive game events.
*
* @param listener the listener to add
*/
public synchronized void addListener(GameEventListener listener) {
listeners.add(listener);
}
/**
* Removes a listener from receiving game events.
*
* @param listener the listener to remove
*/
public synchronized void removeListener(GameEventListener listener) {
listeners.remove(listener);
}
/**
* Notifies all listeners of a game event.
*
* @param event the game event to notify listeners of
*/
@Override
public void notifyListeners(GameEvent event) {
final List<GameEventListener> copy;
synchronized (this) {
copy = new ArrayList<>(listeners);
}
for (GameEventListener listener : copy)
event.notifyListener(listener);
}
/**
* Called once per frame by the update loop.
*
* @param delta time in seconds since the last update call
*/
public void update(float delta) {
state.update(delta);
}
}

View File

@@ -0,0 +1,22 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.game.client;
import pp.battleship.message.client.ClientMessage;
/**
* Interface for sending messages to the server.
*/
public interface ClientSender {
/**
* Send the specified message to the server.
*
* @param message the message
*/
void send(ClientMessage message);
}

View File

@@ -0,0 +1,202 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.game.client;
import pp.battleship.message.server.EffectMessage;
import pp.battleship.message.server.GameDetails;
import pp.battleship.message.server.StartBattleMessage;
import pp.battleship.model.IntPoint;
import java.io.File;
import java.io.IOException;
import java.lang.System.Logger.Level;
/**
* Defines the behavior and state transitions for the client-side game logic.
* Different states of the game logic implement this interface to handle various game events and actions.
*/
abstract class ClientState {
/**
* The game logic object.
*/
final ClientGameLogic logic;
/**
* Constructs a client state of the specified game logic.
*
* @param logic the game logic
*/
ClientState(ClientGameLogic logic) {
this.logic = logic;
}
/**
* Method to be overridden by subclasses for post-transition initialization.
* By default, it does nothing, but it can be overridden in derived states.
*/
void entry() {
// Default implementation does nothing
}
/**
* Returns the name of the current state.
*
* @return the name of the current state
*/
String getName() {
return getClass().getSimpleName();
}
/**
* Checks if the editor should be shown.
*
* @return true if the editor should be shown, false otherwise
*/
boolean showEditor() {
return false;
}
/**
* Checks if the battle state should be shown.
*
* @return true if the battle state should be shown, false otherwise
*/
boolean showBattle() {
return false;
}
/**
* Checks if the player's map is complete (i.e., all ships are placed).
*
* @return true if all ships are placed, false otherwise
*/
boolean isMapComplete() {
return false;
}
/**
* Checks if there is currently a preview ship.
*
* @return true if there is currently a preview ship, false otherwise
*/
boolean movingShip() {
return false;
}
/**
* Handles a click on the player's own map.
*
* @param pos the position where the click occurred
*/
void clickOwnMap(IntPoint pos) {
ClientGameLogic.LOGGER.log(Level.DEBUG, "clickOwnMap has no effect in {0}", getName()); //NON-NLS
}
/**
* Handles a click on the harbor map.
*
* @param pos the position where the click occurred
*/
void clickHarbor(IntPoint pos) {
ClientGameLogic.LOGGER.log(Level.DEBUG, "clickHarbor has no effect in {0}", getName()); //NON-NLS
}
/**
* Handles a click on the opponent's map.
*
* @param pos the position where the click occurred
*/
void clickOpponentMap(IntPoint pos) {
ClientGameLogic.LOGGER.log(Level.DEBUG, "clickOpponentMap has no effect in {0}", getName()); //NON-NLS
}
/**
* Moves the preview ship to the specified position.
*
* @param pos the new position for the preview ship
*/
void movePreview(IntPoint pos) {
ClientGameLogic.LOGGER.log(Level.DEBUG, "movePreview has no effect in {0}", getName()); //NON-NLS
}
/**
* Rotates the preview ship.
*/
void rotateShip() {
ClientGameLogic.LOGGER.log(Level.DEBUG, "rotateShip has no effect in {0}", getName()); //NON-NLS
}
/**
* The user has marked the map as finished.
*/
void mapFinished() {
ClientGameLogic.LOGGER.log(Level.ERROR, "mapFinished not allowed in {0}", getName()); //NON-NLS
}
/**
* Sets the game details provided by the server.
*
* @param details the game details including map size and ships
*/
void receivedGameDetails(GameDetails details) {
ClientGameLogic.LOGGER.log(Level.ERROR, "receivedGameDetails not allowed in {0}", getName()); //NON-NLS
}
/**
* Starts the battle based on the server message.
*
* @param msg the message indicating whose turn it is to shoot
*/
void receivedStartBattle(StartBattleMessage msg) {
ClientGameLogic.LOGGER.log(Level.ERROR, "receivedStartBattle not allowed in {0}", getName()); //NON-NLS
}
/**
* Reports the effect of a shot based on the server message.
*
* @param msg the message containing the effect of the shot
*/
void receivedEffect(EffectMessage msg) {
ClientGameLogic.LOGGER.log(Level.ERROR, "receivedEffect not allowed in {0}", getName()); //NON-NLS
}
/**
* Loads a map from the specified file.
*
* @param file the file to load the map from
* @throws IOException if the map cannot be loaded in the current state
*/
void loadMap(File file) throws IOException {
throw new IOException("You are not allowed to load a map in this state of the game");
}
/**
* Checks if the own map may be loaded from file.
*
* @return true if the own map may be loaded from file, false otherwise
*/
boolean mayLoadMap() {
return false;
}
/**
* Checks if the own map may be saved to file.
*
* @return true if the own map may be saved to file, false otherwise
*/
boolean maySaveMap() {
return true;
}
/**
* Called once per frame by the update loop if this state is active.
*
* @param delta time in seconds since the last update call
*/
void update(float delta) { /* do nothing by default */ }
}

View File

@@ -0,0 +1,267 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.game.client;
import pp.battleship.message.client.MapMessage;
import pp.battleship.model.Battleship;
import pp.battleship.model.IntPoint;
import pp.battleship.model.ShipMap;
import pp.battleship.model.dto.ShipMapDTO;
import java.io.File;
import java.io.IOException;
import java.lang.System.Logger.Level;
import static pp.battleship.Resources.lookup;
import static pp.battleship.model.Battleship.Status.INVALID_PREVIEW;
import static pp.battleship.model.Battleship.Status.NORMAL;
import static pp.battleship.model.Battleship.Status.VALID_PREVIEW;
import static pp.battleship.model.Rotation.RIGHT;
/**
* Represents the state of the client setting up the ship map.
*/
class EditorState extends ClientState {
private Battleship preview;
private Battleship selectedInHarbor;
/**
* Constructs a new EditorState with the specified ClientGameLogic.
*
* @param logic the ClientGameLogic associated with this state
*/
public EditorState(ClientGameLogic logic) {
super(logic);
}
/**
* Returns true to indicate that the editor should be shown.
*
* @return true if the editor should be shown
*/
@Override
public boolean showEditor() {
return true;
}
/**
* Moves the preview ship to the specified position.
*
* @param pos the new position for the preview ship
*/
@Override
public void movePreview(IntPoint pos) {
ClientGameLogic.LOGGER.log(Level.DEBUG, "move preview to {0}", pos); //NON-NLS
if (preview == null || !ownMap().isValid(pos)) return;
preview.moveTo(pos);
setPreviewStatus(preview);
ownMap().remove(preview);
ownMap().add(preview);
}
/**
* Handles a click on the player's own map.
*
* @param pos the position where the click occurred
*/
@Override
public void clickOwnMap(IntPoint pos) {
ClientGameLogic.LOGGER.log(Level.DEBUG, "click at {0} in own map", pos); //NON-NLS
if (!ownMap().isValid(pos)) return;
if (preview == null)
modifyShip(pos);
else
placeShip(pos);
}
/**
* Modifies a ship on the map at the specified position.
*
* @param cursor the position of the ship to modify
*/
private void modifyShip(IntPoint cursor) {
preview = ownMap().findShipAt(cursor);
if (preview == null)
return;
preview.moveTo(cursor);
setPreviewStatus(preview);
ownMap().remove(preview);
ownMap().add(preview);
selectedInHarbor = new Battleship(preview.getLength(), 0, freeY(), RIGHT);
selectedInHarbor.setStatus(VALID_PREVIEW);
harbor().add(selectedInHarbor);
}
/**
* Places the preview ship at the specified position.
*
* @param cursor the position to place the ship
*/
private void placeShip(IntPoint cursor) {
ownMap().remove(preview);
preview.moveTo(cursor);
if (ownMap().isValid(preview)) {
preview.setStatus(NORMAL);
ownMap().add(preview);
harbor().remove(selectedInHarbor);
preview = null;
selectedInHarbor = null;
}
else {
preview.setStatus(INVALID_PREVIEW);
ownMap().add(preview);
}
}
/**
* Handles a click on the harbor map.
*
* @param pos the position where the click occurred
*/
@Override
public void clickHarbor(IntPoint pos) {
ClientGameLogic.LOGGER.log(Level.DEBUG, "click at {0} in harbor", pos); //NON-NLS
if (!harbor().isValid(pos)) return;
final Battleship shipAtCursor = harbor().findShipAt(pos);
if (preview != null) {
ownMap().remove(preview);
selectedInHarbor.setStatus(NORMAL);
harbor().remove(selectedInHarbor);
harbor().add(selectedInHarbor);
preview = null;
selectedInHarbor = null;
}
else if (shipAtCursor != null) {
selectedInHarbor = shipAtCursor;
selectedInHarbor.setStatus(VALID_PREVIEW);
harbor().remove(selectedInHarbor);
harbor().add(selectedInHarbor);
preview = new Battleship(selectedInHarbor.getLength(), 0, 0, RIGHT);
setPreviewStatus(preview);
ownMap().add(preview);
}
}
/**
* Rotates the preview ship.
*/
@Override
public void rotateShip() {
ClientGameLogic.LOGGER.log(Level.DEBUG, "pushed rotate"); //NON-NLS
if (preview == null) return;
preview.rotated();
ownMap().remove(preview);
ownMap().add(preview);
}
/**
* Finds a free position in the harbor to place a ship.
*
* @return the y coordinate of a free position in the harbor
*/
private int freeY() {
for (int i = 0; i < harbor().getHeight(); i++)
if (harbor().findShipAt(0, i) == null)
return i;
throw new RuntimeException("Cannot find a free slot in harbor");
}
/**
* Updates the status of the specified ship based on its validity.
*/
private void setPreviewStatus(Battleship ship) {
ship.setStatus(ownMap().isValid(ship) ? VALID_PREVIEW : INVALID_PREVIEW);
}
/**
* The user has marked the map as finished.
*/
@Override
public void mapFinished() {
if (!harbor().getItems().isEmpty()) return;
logic.send(new MapMessage(ownMap().getRemainingShips()));
logic.setInfoText("wait.for.opponent");
logic.setState(new WaitState(logic));
}
/**
* Checks if the player's map is complete (i.e., all ships are placed).
*
* @return true if all ships are placed, false otherwise
*/
@Override
public boolean isMapComplete() {
return harbor().getItems().isEmpty();
}
/**
* Checks if there is currently a preview ship.
*
* @return true if there is currently a preview ship, false otherwise
*/
@Override
public boolean movingShip() {
return preview != null;
}
/**
* Returns the player's own map.
*
* @return the player's own map
*/
private ShipMap ownMap() {
return logic.getOwnMap();
}
/**
* Returns the harbor map.
*
* @return the harbor map
*/
private ShipMap harbor() {
return logic.getHarbor();
}
/**
* Loads a map from the specified file.
*
* @param file the file to load the map from
* @throws IOException if the map cannot be loaded
*/
@Override
public void loadMap(File file) throws IOException {
final ShipMapDTO dto = ShipMapDTO.loadFrom(file);
if (!dto.fits(logic.getDetails()))
throw new IOException(lookup("map.doesnt.fit"));
ownMap().clear();
dto.getShips().forEach(ownMap()::add);
harbor().clear();
preview = null;
selectedInHarbor = null;
}
/**
* Checks if the player's own map may be loaded from a file.
*
* @return true if the own map may be loaded from file, false otherwise
*/
@Override
public boolean mayLoadMap() {
return true;
}
/**
* Checks if the player's own map may be saved to a file.
*
* @return true if the own map may be saved to file, false otherwise
*/
@Override
public boolean maySaveMap() {
return harbor().getItems().isEmpty();
}
}

View File

@@ -0,0 +1,32 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.game.client;
/**
* Represents the state of the client when the game is over.
*/
class GameOverState extends ClientState {
/**
* Constructs a new instance of GameOverState.
*
* @param logic the client game logic
*/
GameOverState(ClientGameLogic logic) {
super(logic);
}
/**
* Returns true to indicate that the battle state should be shown.
*
* @return true if the battle state should be shown
*/
@Override
public boolean showBattle() {
return true;
}
}

View File

@@ -0,0 +1,65 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.game.client;
import pp.battleship.message.server.GameDetails;
import pp.battleship.model.Battleship;
import pp.battleship.model.Rotation;
import java.util.Map.Entry;
/**
* Represents the state of the client waiting for the
* {@linkplain pp.battleship.message.server.GameDetails}
* from the server.
*/
class InitialState extends ClientState {
/**
* Creates a new initial state.
*
* @param logic the game logic
*/
public InitialState(ClientGameLogic logic) {
super(logic);
}
/**
* Sets the game details provided by the server.
*
* @param details the game details including map size and ships
*/
@Override
public void receivedGameDetails(GameDetails details) {
logic.initializeMaps(details);
fillHarbor(details);
logic.setInfoText(details.getInfoTextKey());
logic.setState(new EditorState(logic));
}
/**
* Fills the harbor with ships as specified by the game details.
*
* @param details the game details including map size and ships
*/
private void fillHarbor(GameDetails details) {
int y = 0;
for (Entry<Integer, Integer> entry : details.getShipNums().entrySet()) {
final int len = entry.getKey();
final int num = entry.getValue();
for (int i = 0; i < num; i++) {
final Battleship ship = new Battleship(len, 0, y++, Rotation.RIGHT);
logic.getHarbor().add(ship);
}
}
}
@Override
public boolean maySaveMap() {
return false;
}
}

View File

@@ -0,0 +1,32 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.game.client;
/**
* Interface representing a connection to the server.
* Extends ClientSender to allow sending messages to the server.
*/
public interface ServerConnection extends ClientSender {
/**
* Checks if the client is currently connected to the server.
*
* @return true if connected, false otherwise.
*/
boolean isConnected();
/**
* Establishes a connection to the server.
*/
void connect();
/**
* Disconnects from the server.
*/
void disconnect();
}

View File

@@ -0,0 +1,41 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.game.client;
import pp.battleship.message.server.StartBattleMessage;
import java.lang.System.Logger.Level;
class WaitState extends ClientState {
/**
* Creates a new instance of {@link WaitState}.
*
* @param logic the game logic
*/
public WaitState(ClientGameLogic logic) {
super(logic);
}
@Override
public boolean showEditor() {
return true;
}
/**
* Starts the battle based on the server message.
*
* @param msg the message indicating whose turn it is to shoot
*/
@Override
public void receivedStartBattle(StartBattleMessage msg) {
ClientGameLogic.LOGGER.log(Level.INFO, "start battle, {0} turn", msg.isMyTurn() ? "my" : "other's"); //NON-NLS
logic.setInfoText(msg.getInfoTextKey());
logic.setState(new BattleState(logic, msg.isMyTurn()));
}
}

View File

@@ -0,0 +1,54 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.game.server;
import pp.battleship.BattleshipConfig;
import pp.battleship.model.ShipMap;
/**
* Class representing a player
*/
public class Player {
private final ShipMap map;
private final String name;
private final int id;
/**
* Creates new Player
*
* @param id the id of the connection to the client represented by this player
* @param name the human-readable name of this player
* @param config model holding the player
*/
Player(int id, String name, BattleshipConfig config) {
this.id = id;
this.name = name;
map = new ShipMap(config.getMapWidth(), config.getMapHeight(), null);
}
/**
* Returns the id of the connection to the client represented by this player.
*
* @return the id
*/
public int getId() {
return id;
}
@Override
public String toString() {
return String.format("Player(%s,%s)", name, id); //NON-NLS
}
/**
* @return map containing own ships and shots
*/
public ShipMap getMap() {
return map;
}
}

View File

@@ -0,0 +1,220 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.game.server;
import pp.battleship.BattleshipConfig;
import pp.battleship.message.client.ClientInterpreter;
import pp.battleship.message.client.MapMessage;
import pp.battleship.message.client.ShootMessage;
import pp.battleship.message.server.EffectMessage;
import pp.battleship.message.server.GameDetails;
import pp.battleship.message.server.ServerMessage;
import pp.battleship.message.server.StartBattleMessage;
import pp.battleship.model.Battleship;
import pp.battleship.model.IntPoint;
import java.lang.System.Logger;
import java.lang.System.Logger.Level;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* Controls the server-side game logic for Battleship.
* Manages game states, player interactions, and message handling.
*/
public class ServerGameLogic implements ClientInterpreter {
private static final Logger LOGGER = System.getLogger(ServerGameLogic.class.getName());
private final BattleshipConfig config;
private final List<Player> players = new ArrayList<>(2);
private final Set<Player> readyPlayers = new HashSet<>();
private final ServerSender serverSender;
private Player activePlayer;
private ServerState state = ServerState.WAIT;
/**
* Constructs a ServerGameLogic with the specified sender and configuration.
*
* @param serverSender the sender used to send messages to clients
* @param config the game configuration
*/
public ServerGameLogic(ServerSender serverSender, BattleshipConfig config) {
this.serverSender = serverSender;
this.config = config;
}
/**
* Returns the state of the game.
*/
ServerState getState() {
return state;
}
/**
* Sets the new state of the game and logs the state transition.
*
* @param newState the new state to set
*/
void setState(ServerState newState) {
LOGGER.log(Level.DEBUG, "state transition {0} --> {1}", state, newState); //NON-NLS
state = newState;
}
/**
* Returns the opponent of the specified player.
*
* @param p the player
* @return the opponent of the player
*/
Player getOpponent(Player p) {
if (players.size() != 2)
throw new RuntimeException("trying to find opponent without having 2 players");
final int index = players.indexOf(p);
if (index < 0)
throw new RuntimeException("Nonexistent player " + p);
return players.get(1 - index);
}
/**
* Returns the player representing the client with the specified connection ID.
*
* @param id the ID of the client
* @return the player associated with the client ID, or null if not found
*/
public Player getPlayerById(int id) {
for (Player player : players)
if (player.getId() == id)
return player;
LOGGER.log(Level.ERROR, "no player found with connection {0}", id); //NON-NLS
return null;
}
/**
* Sends a message to the specified player.
*
* @param player the player to send the message to
* @param msg the message to send
*/
void send(Player player, ServerMessage msg) {
LOGGER.log(Level.INFO, "sending to {0}: {1}", player, msg); //NON-NLS
serverSender.send(player.getId(), msg);
}
/**
* Adds a new player to the game if there are less than two players.
* Transitions the state to SET_UP if two players are present.
*
* @param id the connection ID of the new player
* @return the player added to the game, or null if the game is not in the right state
*/
public Player addPlayer(int id) {
if (state != ServerState.WAIT) {
LOGGER.log(Level.ERROR, "addPlayer not allowed in {0}", state); //NON-NLS
return null;
}
final int n = players.size() + 1;
final Player player = new Player(id, "player " + n, config); //NON-NLS
LOGGER.log(Level.INFO, "adding {0}", player); //NON-NLS
players.add(player);
if (players.size() == 2) {
activePlayer = players.get(0);
for (Player p : players)
send(p, new GameDetails(config));
setState(ServerState.SET_UP);
}
return player;
}
/**
* Handles the reception of a MapMessage.
*
* @param msg the received MapMessage
* @param from the ID of the sender client
*/
@Override
public void received(MapMessage msg, int from) {
if (state != ServerState.SET_UP)
LOGGER.log(Level.ERROR, "playerReady not allowed in {0}", state); //NON-NLS
else
playerReady(getPlayerById(from), msg.getShips());
}
/**
* Handles the reception of a ShootMessage.
*
* @param msg the received ShootMessage
* @param from the ID of the sender client
*/
@Override
public void received(ShootMessage msg, int from) {
if (state != ServerState.BATTLE)
LOGGER.log(Level.ERROR, "shoot not allowed in {0}", state); //NON-NLS
else
shoot(getPlayerById(from), msg.getPosition());
}
/**
* Marks the player as ready and sets their ships.
* Transitions the state to PLAY if both players are ready.
*
* @param player the player who is ready
* @param ships the list of ships placed by the player
*/
void playerReady(Player player, List<Battleship> ships) {
if (!readyPlayers.add(player)) {
LOGGER.log(Level.ERROR, "{0} was already ready", player); //NON-NLS
return;
}
ships.forEach(player.getMap()::add);
if (readyPlayers.size() == 2) {
for (Player p : players)
send(p, new StartBattleMessage(p == activePlayer));
setState(ServerState.BATTLE);
}
}
/**
* Handles the shooting action by the player.
*
* @param p the player who shot
* @param pos the position of the shot
*/
void shoot(Player p, IntPoint pos) {
if (p != activePlayer) return;
final Player otherPlayer = getOpponent(activePlayer);
final Battleship selectedShip = otherPlayer.getMap().findShipAt(pos);
if (selectedShip == null) {
// shot missed
send(activePlayer, EffectMessage.miss(true, pos));
send(otherPlayer, EffectMessage.miss(false, pos));
activePlayer = otherPlayer;
}
else {
// shot hit a ship
selectedShip.hit(pos);
if (otherPlayer.getMap().getRemainingShips().isEmpty()) {
// game is over
send(activePlayer, EffectMessage.won(pos, selectedShip));
send(otherPlayer, EffectMessage.lost(pos, selectedShip, activePlayer.getMap().getRemainingShips()));
setState(ServerState.GAME_OVER);
}
else if (selectedShip.isDestroyed()) {
// ship has been destroyed, but game is not yet over
send(activePlayer, EffectMessage.shipDestroyed(true, pos, selectedShip));
send(otherPlayer, EffectMessage.shipDestroyed(false, pos, selectedShip));
}
else {
// ship has been hit, but it hasn't been destroyed
send(activePlayer, EffectMessage.hit(true, pos));
send(otherPlayer, EffectMessage.hit(false, pos));
}
}
}
}

View File

@@ -0,0 +1,23 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.game.server;
import pp.battleship.message.server.ServerMessage;
/**
* Interface for sending messages to a client.
*/
public interface ServerSender {
/**
* Send the specified message to the client.
*
* @param id the id of the client that shall receive the message
* @param message the message
*/
void send(int id, ServerMessage message);
}

View File

@@ -0,0 +1,33 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.game.server;
/**
* Represents the different states of the Battleship server during the game lifecycle.
*/
enum ServerState {
/**
* The server is waiting for clients to connect.
*/
WAIT,
/**
* The server is waiting for clients to set up their maps.
*/
SET_UP,
/**
* The battle of the game where players take turns to attack each other's ships.
*/
BATTLE,
/**
* The game has ended because all the ships of one player have been destroyed.
*/
GAME_OVER
}

View File

@@ -0,0 +1,114 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.game.singlemode;
import pp.battleship.BattleshipConfig;
import pp.battleship.model.IntPoint;
import java.io.File;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
/**
* Class providing access to the Battleship client configuration.
* Extends {@link BattleshipConfig} to include additional properties specific to the client.
* This class manages configuration settings related to the RobotClient's behavior
* and the game maps used in single mode.
* <p>
* <b>Note:</b> Attributes of this class should not be marked as {@code final}
* to ensure proper functionality when reading from a properties file.
* </p>
*/
public class BattleshipClientConfig extends BattleshipConfig {
/**
* Array representing the predefined shooting locations for the RobotClient.
* The array stores coordinates in pairs, where even indices represent x-coordinates
* and odd indices represent y-coordinates.
*/
@Property("robot.targets")
private int[] robotTargets = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
/**
* The delay (in milliseconds) between shots fired by the RobotClient.
*/
@Property("robot.delay")
private int delay = 750;
/**
* Path to the file representing the opponent's map.
*/
@Property("map.opponent")
private String opponentMap;
/**
* Path to the file representing the player's own map.
*/
@Property("map.own")
private String ownMap;
/**
* Creates a default {@code BattleshipClientConfig} with predefined values.
*/
public BattleshipClientConfig() {
// Default constructor
}
/**
* Returns an iterator of {@link IntPoint} objects representing the predefined
* shooting locations for the RobotClient.
*
* @return an iterator of {@code IntPoint} representing the shooting locations.
*/
public Iterator<IntPoint> getRobotTargets() {
List<IntPoint> targets = new ArrayList<>();
for (int i = 0; i < robotTargets.length; i += 2) {
int x = robotTargets[i];
int y = robotTargets[i + 1];
targets.add(new IntPoint(x, y));
}
return targets.iterator();
}
/**
* Returns the delay (in milliseconds) between shots by the RobotClient.
*
* @return the delay in milliseconds.
*/
public int getDelay() {
return delay;
}
/**
* Returns the file representing the opponent's map.
*
* @return the opponent's map file, or {@code null} if not set.
*/
public File getOpponentMap() {
return opponentMap == null ? null : new File(opponentMap);
}
/**
* Returns the file representing the player's own map.
*
* @return the player's own map file, or {@code null} if not set.
*/
public File getOwnMap() {
return ownMap == null ? null : new File(ownMap);
}
/**
* Determines if the game is in single mode based on the presence of an opponent map.
*
* @return {@code true} if the opponent map is set, indicating single mode; {@code false} otherwise.
*/
public boolean isSingleMode() {
return opponentMap != null;
}
}

View File

@@ -0,0 +1,75 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.game.singlemode;
import pp.battleship.message.client.ClientInterpreter;
import pp.battleship.message.client.ClientMessage;
import pp.battleship.message.client.MapMessage;
import pp.battleship.message.client.ShootMessage;
import pp.battleship.model.Battleship;
/**
* The {@code Copycat} class is a utility that creates a copy of a {@link ClientMessage}.
* It implements the {@link ClientInterpreter} interface to interpret and process
* different types of messages.
*/
class Copycat implements ClientInterpreter {
private ClientMessage copiedMessage;
/**
* Creates a copy of the provided {@link ClientMessage}.
*
* @param msg the message to be copied
* @return a copy of the provided message
*/
static ClientMessage copy(ClientMessage msg) {
final Copycat copycat = new Copycat();
msg.accept(copycat, 0);
return copycat.copiedMessage;
}
/**
* Private constructor to prevent direct instantiation of {@code Copycat}.
*/
private Copycat() { /* do nothing */ }
/**
* Handles the reception of a {@link ShootMessage}.
* Since a {@code ShootMessage} does not need to be copied, it is directly assigned.
*
* @param msg the received {@code ShootMessage}
* @param from the identifier of the sender
*/
@Override
public void received(ShootMessage msg, int from) {
// copying is not necessary
copiedMessage = msg;
}
/**
* Handles the reception of a {@link MapMessage}.
* Creates a deep copy of the {@code MapMessage} by copying each {@link Battleship} in the message.
*
* @param msg the received {@code MapMessage}
* @param from the identifier of the sender
*/
@Override
public void received(MapMessage msg, int from) {
copiedMessage = new MapMessage(msg.getShips().stream().map(Copycat::copy).toList());
}
/**
* Creates a copy of the provided {@link Battleship}.
*
* @param ship the battleship to be copied
* @return a copy of the provided battleship
*/
private static Battleship copy(Battleship ship) {
return new Battleship(ship.getLength(), ship.getX(), ship.getY(), ship.getRot());
}
}

View File

@@ -0,0 +1,93 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.game.singlemode;
import pp.battleship.game.client.BattleshipClient;
import pp.battleship.game.client.ClientGameLogic;
import pp.battleship.message.server.EffectMessage;
import pp.battleship.message.server.GameDetails;
import pp.battleship.message.server.ServerInterpreter;
import pp.battleship.message.server.ServerMessage;
import pp.battleship.message.server.StartBattleMessage;
import java.io.IOException;
/**
* A proxy class that interprets messages from the server and forwards them to the BattleshipClient.
* Implements the ServerInterpreter interface to handle specific server messages.
*/
class InterpreterProxy implements ServerInterpreter {
private final BattleshipClient playerClient;
/**
* Constructs an InterpreterProxy with the specified BattleshipClient.
*
* @param playerClient the client to which the server messages are forwarded
*/
InterpreterProxy(BattleshipClient playerClient) {
this.playerClient = playerClient;
}
/**
* Handles the received GameDetails message by accepting it with the client's game logic.
* If the client's own map option is set, it also loads the map.
*
* @param msg the GameDetails message received from the server
*/
@Override
public void received(GameDetails msg) {
msg.accept(playerClient.getGameLogic());
if (playerClient.getConfig().getOwnMap() != null)
playerClient.enqueue(this::loadMap);
}
/**
* Loads the map specified in the client's options and notifies the game logic that the map is finished.
*
* @throws RuntimeException if the map fails to load.
*/
private void loadMap() {
final ClientGameLogic clientGameLogic = playerClient.getGameLogic();
try {
clientGameLogic.loadMap(playerClient.getConfig().getOwnMap());
}
catch (IOException e) {
throw new RuntimeException("Failed to load PlayerClient map", e);
}
clientGameLogic.mapFinished();
}
/**
* Forwards the received StartBattleMessage to the client's game logic.
*
* @param msg the StartBattleMessage received from the server
*/
@Override
public void received(StartBattleMessage msg) {
forward(msg);
}
/**
* Forwards the received EffectMessage to the client's game logic.
*
* @param msg the EffectMessage received from the server
*/
@Override
public void received(EffectMessage msg) {
forward(msg);
}
/**
* Forwards the specified ServerMessage to the client's game logic by enqueuing the message acceptance.
*
* @param msg the ServerMessage to forward
*/
private void forward(ServerMessage msg) {
playerClient.enqueue(() -> msg.accept(playerClient.getGameLogic()));
}
}

View File

@@ -0,0 +1,127 @@
package pp.battleship.game.singlemode;
import pp.battleship.game.client.BattleshipClient;
import pp.battleship.message.client.MapMessage;
import pp.battleship.message.client.ShootMessage;
import pp.battleship.message.server.EffectMessage;
import pp.battleship.message.server.GameDetails;
import pp.battleship.message.server.ServerInterpreter;
import pp.battleship.message.server.StartBattleMessage;
import pp.battleship.model.IntPoint;
import pp.battleship.model.dto.ShipMapDTO;
import pp.util.RandomPositionIterator;
import java.lang.System.Logger;
import java.lang.System.Logger.Level;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;
import static pp.battleship.Resources.lookup;
/**
* RobotClient simulates a client in the Battleship game.
* It handles its own shooting targets and acts as a sender of client messages.
* The RobotClient can shoot at predefined targets or generate random targets if predefined ones are exhausted.
*/
class RobotClient implements ServerInterpreter {
private static final Logger LOGGER = System.getLogger(RobotClient.class.getName());
private final BattleshipClient app;
private final ServerConnectionMockup connection;
private final BattleshipClientConfig config;
private final ShipMapDTO dto;
private final Iterator<IntPoint> targetIterator;
private final Iterator<IntPoint> randomPositionIterator;
private final Set<IntPoint> shotTargets = new HashSet<>();
private final Timer timer = new Timer(true);
/**
* Constructs a RobotClient instance with the given connection and configuration.
* Initializes the shooting targets from the configuration.
*
* @param app The BattleshipApp instance, used to enqueue actions on the main thread
* @param connection The ServerConnectionMockup instance
* @param config The BattleshipClientConfig instance
* @param dto The ShipMap dto specified at app start
*/
RobotClient(BattleshipClient app, ServerConnectionMockup connection, BattleshipClientConfig config, ShipMapDTO dto) {
this.app = app;
this.connection = connection;
this.config = config;
this.dto = dto;
this.targetIterator = config.getRobotTargets();
this.randomPositionIterator = new RandomPositionIterator<>(IntPoint::new, config.getMapWidth(), config.getMapHeight());
}
/**
* Schedules the RobotClient to take a shot after the specified delay.
*/
private void shoot() {
timer.schedule(new TimerTask() {
@Override
public void run() {
app.enqueue(RobotClient.this::robotShot);
}
}, config.getDelay());
}
/**
* Makes the RobotClient take a shot by sending a ShootMessage with the target position.
*/
private void robotShot() {
connection.sendRobotMessage(new ShootMessage(getShotPosition()));
}
/**
* Determines the next shot position. If predefined targets are available, uses the next target.
* Otherwise, generates a random target that has not been shot at before.
*
* @return the next shot position as IntPosition
*/
private IntPoint getShotPosition() {
while (true) {
final IntPoint target = targetIterator.hasNext() ? targetIterator.next() : randomPositionIterator.next();
if (shotTargets.add(target)) return target;
}
}
/**
* Receives GameDetails, creates and sends the ShipMap to the mock server.
*
* @param details The game details
*/
@Override
public void received(GameDetails details) {
if (!dto.fits(details))
throw new RuntimeException(lookup("map.doesnt.fit"));
app.enqueue(() -> connection.sendRobotMessage(new MapMessage(dto.getShips())));
}
/**
* Receives the StartBattleMessage and updates the turn status.
* If it is RobotClient's turn to shoot, schedules a shot using shoot();
*
* @param msg The start battle message
*/
@Override
public void received(StartBattleMessage msg) {
LOGGER.log(Level.INFO, "Received StartBattleMessage: {0}", msg); //NON-NLS
if (msg.isMyTurn())
shoot();
}
/**
* Receives an effect message, logs it, and updates the turn status.
* If it is RobotClient's turn to shoot, schedules a shot using shoot();
*
* @param msg The effect message
*/
@Override
public void received(EffectMessage msg) {
LOGGER.log(Level.INFO, "Received EffectMessage: {0}", msg); //NON-NLS
if (msg.isMyTurn())
shoot();
}
}

View File

@@ -0,0 +1,145 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.game.singlemode;
import pp.battleship.game.client.BattleshipClient;
import pp.battleship.game.client.ServerConnection;
import pp.battleship.game.server.ServerGameLogic;
import pp.battleship.game.server.ServerSender;
import pp.battleship.message.client.ClientMessage;
import pp.battleship.message.server.ServerInterpreter;
import pp.battleship.message.server.ServerMessage;
import pp.battleship.model.dto.ShipMapDTO;
import java.io.IOException;
import static pp.battleship.game.singlemode.Copycat.copy;
/**
* A mock implementation of the ServerConnection interface for single mode.
* Simulates a server connection without actual network communication.
* Handles a mock player named RobotClient and schedules its shots when due.
*/
public class ServerConnectionMockup implements ServerConnection, ServerSender {
/**
* The id of the player client.
*/
private static final int PLAYER_CLIENT = 1;
/**
* The id of the robot client.
*/
private static final int ROBOT_CLIENT = 2;
private final BattleshipClient playerClient;
private final RobotClient robotClient;
private final InterpreterProxy playerProxy;
private final ServerGameLogic serverGameLogic;
/**
* Constructs a ServerConnectionMockup instance for the given Battleship application.
* Creates a RobotClient instance and an instance of ServerGameLogic.
*
* @param playerClient The Battleship client instance, e.g., a BattleshipApp instance.
*/
public ServerConnectionMockup(BattleshipClient playerClient) {
this.playerClient = playerClient;
robotClient = new RobotClient(playerClient, this, playerClient.getConfig(), getShipMapDTO());
serverGameLogic = new ServerGameLogic(this, playerClient.getConfig());
playerProxy = new InterpreterProxy(playerClient);
}
/**
* Always returns true as this is a mock connection.
*
* @return true, indicating the mock connection is always considered connected.
*/
@Override
public boolean isConnected() {
return true;
}
/**
* Simulates connecting to a server by adding the PlayerClient and the RobotClient to the serverGameLogic.
* Loads the map of the PlayerClient and triggers sending it to the serverGameLogic.
*/
@Override
public void connect() {
serverGameLogic.addPlayer(PLAYER_CLIENT);
serverGameLogic.addPlayer(ROBOT_CLIENT);
}
/**
* Does nothing upon shutdown of the app.
*/
@Override
public void disconnect() {
//do nothing
}
/**
* Forwards the specified message received from the player client to the server logic.
*
* @param message The message from the player client to be processed.
*/
@Override
public void send(ClientMessage message) { // from PlayerClient, as this is the PlayerClients 'serverConnection'
copy(message).accept(serverGameLogic, PLAYER_CLIENT);
}
/**
* Forwards the specified message received from the robot client to the server logic.
*
* @param message The message from the robot client to be processed.
*/
void sendRobotMessage(ClientMessage message) {
message.accept(serverGameLogic, ROBOT_CLIENT);
}
/**
* Forwards the specified message received from the server logic either to the player client or to the
* robot client, depending on the specified id.
*
* @param id The recipient id
* @param message The server message to be processed
* @see #PLAYER_CLIENT
* @see #ROBOT_CLIENT
*/
@Override
public void send(int id, ServerMessage message) {
message.accept(getInterpreter(id));
}
/**
* Retrieves the ServerInterpreter of the client with the specified id.
*
* @param clientId the id of the client whose ServerInterpreter shall be retrieved.
* @return the ServerInterpreter of the client
* @throws java.lang.IllegalArgumentException if there is no client with the specified id.
*/
private ServerInterpreter getInterpreter(int clientId) {
return switch (clientId) {
case PLAYER_CLIENT -> playerProxy;
case ROBOT_CLIENT -> robotClient;
default -> throw new IllegalArgumentException("Unexpected value: " + clientId);
};
}
/**
* Loads the ShipMapDTO from the opponent map file.
*
* @return the loaded ShipMapDTO.
*/
private ShipMapDTO getShipMapDTO() {
try {
return ShipMapDTO.loadFrom(playerClient.getConfig().getOpponentMap());
}
catch (IOException e) {
throw new RuntimeException("Failed to load RobotClient map", e);
}
}
}

View File

@@ -0,0 +1,29 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.message.client;
/**
* Visitor interface for processing all client messages.
*/
public interface ClientInterpreter {
/**
* Processes a received ShootMessage.
*
* @param msg the ShootMessage to be processed
* @param from the connection ID from which the message was received
*/
void received(ShootMessage msg, int from);
/**
* Processes a received MapMessage.
*
* @param msg the MapMessage to be processed
* @param from the connection ID from which the message was received
*/
void received(MapMessage msg, int from);
}

View File

@@ -0,0 +1,32 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.message.client;
import com.jme3.network.AbstractMessage;
/**
* An abstract base class for client messages used in network transfer.
* It extends the AbstractMessage class provided by the jme3-network library.
*/
public abstract class ClientMessage extends AbstractMessage {
/**
* Constructs a new ClientMessage instance.
*/
protected ClientMessage() {
super(true);
}
/**
* Accepts a visitor for processing this message.
*
* @param interpreter the visitor to be used for processing
* @param from the connection ID of the sender
*/
public abstract void accept(ClientInterpreter interpreter, int from);
}

View File

@@ -0,0 +1,66 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.message.client;
import com.jme3.network.serializing.Serializable;
import pp.battleship.model.Battleship;
import java.util.ArrayList;
import java.util.List;
/**
* A message sent by the client containing the positions of the ships on the player's map.
*/
@Serializable
public class MapMessage extends ClientMessage {
private List<Battleship> ships;
/**
* Default constructor for serialization purposes.
*/
private MapMessage() { /* empty */ }
/**
* Constructs a MapMessage with the specified list of ships.
*
* @param ships the list of ships placed on the player's map
*/
public MapMessage(List<Battleship> ships) {
this.ships = new ArrayList<>(ships);
}
/**
* Returns the list of ships on the player's map.
*
* @return the list of ships
*/
public List<Battleship> getShips() {
return ships;
}
/**
* Returns a string representation of the MapMessage.
*
* @return a string representation of the MapMessage
*/
@Override
public String toString() {
return "MapMessage{ships=" + ships + '}'; //NON-NLS
}
/**
* Accepts a visitor to process this message.
*
* @param interpreter the visitor to process this message
* @param from the connection ID from which the message was received
*/
@Override
public void accept(ClientInterpreter interpreter, int from) {
interpreter.received(this, from);
}
}

View File

@@ -0,0 +1,63 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.message.client;
import com.jme3.network.serializing.Serializable;
import pp.battleship.model.IntPoint;
/**
* A message sent by the client to indicate a shooting action in the game.
*/
@Serializable
public class ShootMessage extends ClientMessage {
private IntPoint position;
/**
* Default constructor for serialization purposes.
*/
private ShootMessage() { /* empty */ }
/**
* Constructs a ShootMessage with the specified position.
*
* @param position the position where the shot is fired
*/
public ShootMessage(IntPoint position) {
this.position = position;
}
/**
* Returns the position of the shot.
*
* @return the position of the shot
*/
public IntPoint getPosition() {
return position;
}
/**
* Returns a string representation of the ShootMessage.
*
* @return a string representation of the ShootMessage
*/
@Override
public String toString() {
return "ShootMessage{position=" + position + '}'; //NON-NLS
}
/**
* Accepts a visitor to process this message.
*
* @param interpreter the visitor to process this message
* @param from the connection ID from which the message was received
*/
@Override
public void accept(ClientInterpreter interpreter, int from) {
interpreter.received(this, from);
}
}

View File

@@ -0,0 +1,211 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.message.server;
import com.jme3.network.serializing.Serializable;
import pp.battleship.model.Battleship;
import pp.battleship.model.IntPoint;
import pp.battleship.model.Shot;
import java.util.Collections;
import java.util.List;
import static pp.util.Util.copy;
/**
* A message sent by the server to inform clients about the effects of a shot in the Battleship game.
*/
@Serializable
public class EffectMessage extends ServerMessage {
private boolean ownShot;
private Shot shot;
private Battleship destroyedShip;
private List<Battleship> remainingOpponentShips;
/**
* Creates an EffectMessage indicating a hit.
*
* @param ownShot true if the shot was fired by the player, false if by the opponent
* @param pos the position of the shot
* @return an EffectMessage indicating a hit
*/
public static EffectMessage hit(boolean ownShot, IntPoint pos) {
return new EffectMessage(ownShot, new Shot(pos, true), null, null);
}
/**
* Creates an EffectMessage indicating a miss.
*
* @param ownShot true if the shot was fired by the player, false if by the opponent
* @param pos the position of the shot
* @return an EffectMessage indicating a miss
*/
public static EffectMessage miss(boolean ownShot, IntPoint pos) {
return new EffectMessage(ownShot, new Shot(pos, false), null, null);
}
/**
* Creates an EffectMessage indicating a ship was destroyed.
*
* @param ownShot true if the shot was fired by the player, false if by the opponent
* @param pos the position of the shot
* @param destroyedShip the ship that was destroyed
* @return an EffectMessage indicating a ship was destroyed
*/
public static EffectMessage shipDestroyed(boolean ownShot, IntPoint pos, Battleship destroyedShip) {
return new EffectMessage(ownShot, new Shot(pos, true), destroyedShip, null);
}
/**
* Creates an EffectMessage indicating the player has won the game.
*
* @param pos the position of the shot
* @param destroyedShip the ship that was destroyed
* @return an EffectMessage indicating the player has won
*/
public static EffectMessage won(IntPoint pos, Battleship destroyedShip) {
return new EffectMessage(true, new Shot(pos, true), destroyedShip, Collections.emptyList());
}
/**
* Creates an EffectMessage indicating the player has lost the game.
*
* @param pos the position of the shot
* @param destroyedShip the ship that was destroyed
* @param remainingOpponentShips the list of opponent's remaining ships
* @return an EffectMessage indicating the player has lost
*/
public static EffectMessage lost(IntPoint pos, Battleship destroyedShip, List<Battleship> remainingOpponentShips) {
return new EffectMessage(false, new Shot(pos, true), destroyedShip, remainingOpponentShips);
}
/**
* Default constructor for serialization purposes.
*/
private EffectMessage() { /* empty */ }
/**
* Constructs an EffectMessage with the specified parameters.
*
* @param ownShot true if the shot was fired by the player, false if by the opponent
* @param shot the shot fired
* @param destroyedShip the ship that was destroyed by the shot, null if no ship was destroyed
* @param remainingOpponentShips the list of opponent's remaining ships after the shot
*/
private EffectMessage(boolean ownShot, Shot shot, Battleship destroyedShip, List<Battleship> remainingOpponentShips) {
this.ownShot = ownShot;
this.shot = shot;
this.destroyedShip = destroyedShip;
this.remainingOpponentShips = copy(remainingOpponentShips);
}
/**
* Accepts a visitor to process this message.
*
* @param interpreter the visitor to process this message
*/
@Override
public void accept(ServerInterpreter interpreter) {
interpreter.received(this);
}
/**
* Checks if the shot was fired by the player.
*
* @return true if the shot was fired by the player, false otherwise
*/
public boolean isOwnShot() {
return ownShot;
}
/**
* Returns the shot fired.
*
* @return the shot fired
*/
public Shot getShot() {
return shot;
}
/**
* Returns the ship that was destroyed by the shot.
*
* @return the destroyed ship, null if no ship was destroyed
*/
public Battleship getDestroyedShip() {
return destroyedShip;
}
/**
* Returns the list of opponent's remaining ships after the shot.
*
* @return the list of opponent's remaining ships, null if the game is not yet over
*/
public List<Battleship> getRemainingOpponentShips() {
return remainingOpponentShips;
}
/**
* Checks if the game is over.
*
* @return true if the game is over, false otherwise
*/
public boolean isGameOver() {
return remainingOpponentShips != null;
}
/**
* Checks if the game is won by the player.
*
* @return true if the game is won by the player, false otherwise
*/
public boolean isGameWon() {
return isGameOver() && isOwnShot();
}
/**
* Checks if the game is lost by the player.
*
* @return true if the game is lost by the player, false otherwise
*/
public boolean isGameLost() {
return isGameOver() && !isOwnShot();
}
/**
* Checks if it's currently the player's turn.
*
* @return true if it's the player's turn, false otherwise
*/
public boolean isMyTurn() {
return isOwnShot() == shot.isHit();
}
/**
* Returns a string representation of the EffectMessage.
*
* @return a string representation of the EffectMessage
*/
@Override
public String toString() {
return "EffectMessage{ownShot=" + ownShot + ", shot=" + shot + ", destroyedShip=" + destroyedShip + //NON-NLS
", remainingOpponentShips=" + remainingOpponentShips + '}'; //NON-NLS
}
/**
* Returns the key for the informational text associated with this message.
*
* @return the key for the informational text
*/
@Override
public String getInfoTextKey() {
if (isGameOver())
return isGameWon() ? "you.won.the.game" : "you.lost.the.game";
return isMyTurn() ? "its.your.turn" : "wait.for.opponent";
}
}

View File

@@ -0,0 +1,97 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.message.server;
import com.jme3.network.serializing.Serializable;
import pp.battleship.BattleshipConfig;
import java.util.Map;
/**
* A message sent by the server to provide details about the game configuration.
*/
@Serializable
public class GameDetails extends ServerMessage {
private Map<Integer, Integer> shipNums;
private int width;
private int height;
/**
* Default constructor for serialization purposes.
*/
private GameDetails() { /* empty */ }
/**
* Constructs a GameDetails message with the specified BattleshipConfig.
*
* @param config the BattleshipConfig containing game configuration details
*/
public GameDetails(BattleshipConfig config) {
this.shipNums = config.getShipNums();
this.width = config.getMapWidth();
this.height = config.getMapHeight();
}
/**
* Accepts a visitor to process this message.
*
* @param interpreter the visitor to process this message
*/
@Override
public void accept(ServerInterpreter interpreter) {
interpreter.received(this);
}
/**
* Returns a map where the keys represent ship lengths
* and the values represent the number of ships of that length.
*
* @return a map of ship lengths to the number of ships
*/
public Map<Integer, Integer> getShipNums() {
return shipNums;
}
/**
* Returns the width of the game map.
*
* @return the width of the game map
*/
public int getWidth() {
return width;
}
/**
* Returns the height of the game map.
*
* @return the height of the game map
*/
public int getHeight() {
return height;
}
/**
* Returns a string representation of the GameDetails message.
*
* @return a string representation of the GameDetails message
*/
@Override
public String toString() {
return "GameDetails{" + "ships=" + getShipNums() + ", width=" + width + ", height=" + height + '}'; //NON-NLS
}
/**
* Returns the key for the informational text associated with this message.
*
* @return the key for the informational text
*/
@Override
public String getInfoTextKey() {
return "place.ships.in.your.map";
}
}

View File

@@ -0,0 +1,36 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.message.server;
/**
* An interface for processing server messages.
* Implementations of this interface can be used to handle different types of server messages.
*/
public interface ServerInterpreter {
/**
* Handles a GameDetails message received from the server.
*
* @param msg the GameDetails message received
*/
void received(GameDetails msg);
/**
* Handles a StartBattleMessage received from the server.
*
* @param msg the StartBattleMessage received
*/
void received(StartBattleMessage msg);
/**
* Handles an EffectMessage received from the server.
*
* @param msg the EffectMessage received
*/
void received(EffectMessage msg);
}

View File

@@ -0,0 +1,39 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.message.server;
import com.jme3.network.AbstractMessage;
/**
* An abstract base class for server messages used in network transfer.
* It extends the AbstractMessage class provided by the jme3-network library.
*/
public abstract class ServerMessage extends AbstractMessage {
/**
* Constructs a new ServerMessage instance.
*/
protected ServerMessage() {
super(true);
}
/**
* Accepts a visitor for processing this message.
*
* @param interpreter the visitor to be used for processing
*/
public abstract void accept(ServerInterpreter interpreter);
/**
* Gets the bundle key of the informational text to be shown at the client.
* This key is used to retrieve the appropriate localized text for display.
*
* @return the bundle key of the informational text
*/
public abstract String getInfoTextKey();
}

View File

@@ -0,0 +1,71 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.message.server;
import com.jme3.network.serializing.Serializable;
/**
* A message sent by the server to inform clients about the start of the battle.
*/
@Serializable
public class StartBattleMessage extends ServerMessage {
private boolean myTurn;
/**
* Default constructor for serialization purposes.
*/
private StartBattleMessage() { /* empty */ }
/**
* Constructs a StartBattleMessage with the specified turn indicator.
*
* @param myTurn true if it's the client's turn to shoot, false otherwise
*/
public StartBattleMessage(boolean myTurn) {
this.myTurn = myTurn;
}
/**
* Accepts a visitor to process this message.
*
* @param interpreter the visitor to process this message
*/
@Override
public void accept(ServerInterpreter interpreter) {
interpreter.received(this);
}
/**
* Checks if it's the client's turn to shoot.
*
* @return true if it's the client's turn, false otherwise
*/
public boolean isMyTurn() {
return myTurn;
}
/**
* Returns a string representation of the StartBattleMessage.
*
* @return a string representation of the StartBattleMessage
*/
@Override
public String toString() {
return "StartBattleMessage{myTurn=" + myTurn + '}'; //NON-NLS
}
/**
* Returns the key for the informational text associated with this message.
*
* @return the key for the informational text
*/
@Override
public String getInfoTextKey() {
return isMyTurn() ? "its.your.turn" : "wait.for.opponent";
}
}

View File

@@ -0,0 +1,324 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.model;
import com.jme3.network.serializing.Serializable;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import static java.lang.Math.max;
import static java.lang.Math.min;
/**
* Represents a battleship in the game. A battleship is characterized by its length, position,
* rotation, and status. It can be moved, rotated, and hit during the game. This class also
* provides methods to check for collisions with other ships and to determine whether the
* battleship has been destroyed.
*/
@Serializable
public class Battleship implements Item {
/**
* Enumeration representing the different statuses a battleship can have during the game.
*/
public enum Status {
/**
* The ship is in its normal state, not being previewed for placement.
*/
NORMAL,
/**
* The ship is being previewed in a valid position for placement.
*/
VALID_PREVIEW,
/**
* The ship is being previewed in an invalid position for placement.
*/
INVALID_PREVIEW
}
private final int length; // The length of the battleship
private int x; // The x-coordinate of the battleship's position
private int y; // The y-coordinate of the battleship's position
private Rotation rot; // The rotation of the battleship
private Status status; // The current status of the battleship
private final Set<IntPoint> damaged = new HashSet<>(); // The set of positions that have been hit on this ship
/**
* Default constructor for serialization. Initializes a battleship with length 0,
* at position (0, 0), with a default rotation of RIGHT.
*/
private Battleship() {
this(0, 0, 0, Rotation.RIGHT);
}
/**
* Constructs a new Battleship with the specified length, position, and rotation.
*
* @param length the length of the battleship
* @param x the x-coordinate of the battleship's initial position
* @param y the y-coordinate of the battleship's initial position
* @param rot the rotation of the battleship
*/
public Battleship(int length, int x, int y, Rotation rot) {
this.x = x;
this.y = y;
this.rot = rot;
this.length = length;
this.status = Status.NORMAL;
}
/**
* Returns the current x-coordinate of the battleship's position.
*
* @return the x-coordinate of the battleship
*/
public int getX() {
return x;
}
/**
* Returns the current y-coordinate of the battleship's position.
*
* @return the y-coordinate of the battleship
*/
public int getY() {
return y;
}
/**
* Moves the battleship to the specified coordinates.
*
* @param x the new x-coordinate of the battleship's position
* @param y the new y-coordinate of the battleship's position
*/
public void moveTo(int x, int y) {
this.x = x;
this.y = y;
}
/**
* Moves the battleship to the specified position.
*
* @param pos the new position of the battleship
*/
public void moveTo(IntPosition pos) {
moveTo(pos.getX(), pos.getY());
}
/**
* Returns the current status of the battleship.
*
* @return the status of the battleship
*/
public Status getStatus() {
return status;
}
/**
* Sets the status of the battleship.
*
* @param status the new status to be set for the battleship
*/
public void setStatus(Status status) {
this.status = status;
}
/**
* Returns the length of the battleship.
*
* @return the length of the battleship
*/
public int getLength() {
return length;
}
/**
* Returns the minimum x-coordinate that the battleship occupies based on its current position and rotation.
*
* @return the minimum x-coordinate of the battleship
*/
public int getMinX() {
return x + min(0, (length - 1) * rot.dx());
}
/**
* Returns the maximum x-coordinate that the battleship occupies based on its current position and rotation.
*
* @return the maximum x-coordinate of the battleship
*/
public int getMaxX() {
return x + max(0, (length - 1) * rot.dx());
}
/**
* Returns the minimum y-coordinate that the battleship occupies based on its current position and rotation.
*
* @return the minimum y-coordinate of the battleship
*/
public int getMinY() {
return y + min(0, (length - 1) * rot.dy());
}
/**
* Returns the maximum y-coordinate that the battleship occupies based on its current position and rotation.
*
* @return the maximum y-coordinate of the battleship
*/
public int getMaxY() {
return y + max(0, (length - 1) * rot.dy());
}
/**
* Returns the current rotation of the battleship.
*
* @return the rotation of the battleship
*/
public Rotation getRot() {
return rot;
}
/**
* Sets the rotation of the battleship.
*
* @param rot the new rotation to be set for the battleship
*/
public void setRotation(Rotation rot) {
this.rot = rot;
}
/**
* Rotates the battleship by 90 degrees clockwise.
*/
public void rotated() {
setRotation(rot.rotate());
}
/**
* Attempts to hit the battleship at the specified position.
* If the position is part of the battleship, the hit is recorded.
*
* @param x the x-coordinate of the position to hit
* @param y the y-coordinate of the position to hit
* @return true if the position is part of the battleship, false otherwise
* @see #contains(int, int)
*/
public boolean hit(int x, int y) {
if (!contains(x, y))
return false;
damaged.add(new IntPoint(x, y));
return true;
}
/**
* Attempts to hit the battleship at the specified position.
* If the position is part of the battleship, the hit is recorded.
* This is a convenience method for {@linkplain #hit(int, int)}.
*
* @param position the position to hit
* @return true if the position is part of the battleship, false otherwise
*/
public boolean hit(IntPosition position) {
return hit(position.getX(), position.getY());
}
/**
* Returns the positions of this battleship that have been hit.
*
* @return a set of positions that have been hit
* @see #hit(int, int)
*/
public Set<IntPoint> getDamaged() {
return Collections.unmodifiableSet(damaged);
}
/**
* Checks whether the specified position is covered by the battleship. This method does
* not record a hit, only checks coverage.
* This is a convenience method for {@linkplain #contains(int, int)}.
*
* @param pos the position to check
* @return true if the position is covered by the battleship, false otherwise
*/
public boolean contains(IntPosition pos) {
return contains(pos.getX(), pos.getY());
}
/**
* Checks whether the specified position is covered by the battleship. This method does
* not record a hit, only checks coverage.
*
* @param x the x-coordinate of the position to check
* @param y the y-coordinate of the position to check
* @return true if the position is covered by the battleship, false otherwise
*/
public boolean contains(int x, int y) {
return getMinX() <= x && x <= getMaxX() &&
getMinY() <= y && y <= getMaxY();
}
/**
* Determines if the battleship has been completely destroyed. A battleship is considered
* destroyed if all of its positions have been hit.
*
* @return true if the battleship is destroyed, false otherwise
* @see #hit(int, int)
*/
public boolean isDestroyed() {
return damaged.size() == length;
}
/**
* Checks whether this battleship collides with another battleship. Two battleships collide
* if any of their occupied positions overlap.
*
* @param other the other battleship to check collision with
* @return true if the battleships collide, false otherwise
*/
public boolean collidesWith(Battleship other) {
return other.getMaxX() >= getMinX() && getMaxX() >= other.getMinX() &&
other.getMaxY() >= getMinY() && getMaxY() >= other.getMinY();
}
/**
* Returns a string representation of the battleship, including its length, position,
* and rotation.
*
* @return a string representation of the battleship
*/
@Override
public String toString() {
return "Ship{length=" + length + ", x=" + x + ", y=" + y + ", rot=" + rot + '}'; //NON-NLS
}
/**
* Accepts a visitor that returns a value of type {@code T}. This method is part of the
* Visitor design pattern.
*
* @param visitor the visitor to accept
* @param <T> the type of the value returned by the visitor
* @return the value returned by the visitor
*/
@Override
public <T> T accept(Visitor<T> visitor) {
return visitor.visit(this);
}
/**
* Accepts a visitor that does not return a value. This method is part of the
* Visitor design pattern.
*
* @param visitor the visitor to accept
*/
@Override
public void accept(VoidVisitor visitor) {
visitor.visit(this);
}
}

View File

@@ -0,0 +1,94 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.model;
import com.jme3.network.serializing.Serializable;
import java.util.Objects;
/**
* Represents a point in the two-dimensional plane with integer coordinates.
*/
@Serializable
public final class IntPoint implements IntPosition {
private int x;
private int y;
/**
* Default constructor for serialization purposes.
*/
private IntPoint() { /* do nothing */ }
/**
* Constructs a new IntPoint with the specified coordinates.
*
* @param x the x-coordinate of the point
* @param y the y-coordinate of the point
*/
public IntPoint(int x, int y) {
this.x = x;
this.y = y;
}
/**
* Gets the x-coordinate of the point.
*
* @return the x-coordinate
*/
@Override
public int getX() {
return x;
}
/**
* Gets the y-coordinate of the point.
*
* @return the y-coordinate
*/
@Override
public int getY() {
return y;
}
/**
* Indicates whether some other object is "equal to" this one.
*
* @param obj the reference object with which to compare
* @return true if this object is the same as the obj argument; false otherwise
*/
@Override
public boolean equals(Object obj) {
if (obj == this) return true;
if (obj == null || obj.getClass() != this.getClass()) return false;
var that = (IntPoint) obj;
return this.x == that.x &&
this.y == that.y;
}
/**
* Returns a hash code value for the IntPoint.
*
* @return a hash code value for this object
*/
@Override
public int hashCode() {
return Objects.hash(x, y);
}
/**
* Returns a string representation of the IntPoint.
*
* @return a string representation of the object
*/
@Override
public String toString() {
return "IntPoint[" + //NON-NLS
"x=" + x + ", " + //NON-NLS
"y=" + y + ']'; //NON-NLS
}
}

View File

@@ -0,0 +1,28 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.model;
/**
* Interface representing a position with X and Y coordinates.
*/
public interface IntPosition {
/**
* Returns the X coordinate of this position.
*
* @return the X coordinate.
*/
int getX();
/**
* Returns the Y coordinate of this position.
*
* @return the Y coordinate.
*/
int getY();
}

View File

@@ -0,0 +1,31 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.model;
/**
* An interface representing any item on a ship map.
* It extends the IntPosition interface to provide position information.
*/
public interface Item {
/**
* Accepts a visitor to perform operations on the item.
*
* @param visitor the visitor performing operations on the item
* @param <T> the type of result returned by the visitor
* @return the result of the visitor's operation on the item
*/
<T> T accept(Visitor<T> visitor);
/**
* Accepts a visitor to perform operations on the item without returning a result.
*
* @param visitor the visitor performing operations on the item
*/
void accept(VoidVisitor visitor);
}

View File

@@ -0,0 +1,67 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.model;
import java.io.Serializable;
/**
* Represents the rotation of a ship and provides functionality related to rotation.
*/
public enum Rotation implements Serializable {
/**
* Represents the ship facing upwards.
*/
UP,
/**
* Represents the ship facing rightwards.
*/
RIGHT,
/**
* Represents the ship facing downwards.
*/
DOWN,
/**
* Represents the ship facing leftwards.
*/
LEFT;
/**
* Gets the change in x-coordinate corresponding to this rotation.
*
* @return the change in x-coordinate
*/
public int dx() {
return switch (this) {
case UP, DOWN -> 0;
case RIGHT -> 1;
case LEFT -> -1;
};
}
/**
* Gets the change in y-coordinate corresponding to this rotation.
*
* @return the change in y-coordinate
*/
public int dy() {
return switch (this) {
case UP -> 1;
case LEFT, RIGHT -> 0;
case DOWN -> -1;
};
}
/**
* Rotates the orientation clockwise and returns the next rotation.
*
* @return the next rotation after rotating clockwise
*/
public Rotation rotate() {
return values()[(ordinal() + 1) % values().length];
}
}

View File

@@ -0,0 +1,251 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.model;
import pp.battleship.notification.GameEvent;
import pp.battleship.notification.GameEventBroker;
import pp.battleship.notification.ItemAddedEvent;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Stream;
/**
* Represents a rectangular map that holds ships and registers shots fired.
* It also supports event notification for game state changes such as item addition or removal.
* Valid positions on this map have x-coordinates in the range of 0 to width-1 and y-coordinates
* in the range of 0 to height-1.
*
* @see #getWidth()
* @see #getHeight()
*/
public class ShipMap {
/**
* A list of items (ships, shots, etc.) placed on the map.
*/
private final List<Item> items = new ArrayList<>();
/**
* The broker responsible for notifying registered listeners of events
* (such as when an item is added or removed from the map).
* Can be null, in which case no notifications will be sent.
*/
private final GameEventBroker eventBroker;
private final int width;
private final int height;
/**
* Constructs an empty map with the given dimensions. The specified event broker
* will handle the notification of changes in the map state, such as adding or removing items.
* Passing null as the event broker is allowed, but in that case, no notifications will occur.
*
* @param width the number of columns (width) of the map
* @param height the number of rows (height) of the map
* @param eventBroker the event broker used for notifying listeners, or null if event distribution is not needed
*/
public ShipMap(int width, int height, GameEventBroker eventBroker) {
if (width < 1 || height < 1)
throw new IllegalArgumentException("Invalid map size");
this.width = width;
this.height = height;
this.eventBroker = eventBroker;
}
/**
* Adds an item (e.g., a ship or a shot) to the map and triggers the appropriate event.
*
* @param item the item to be added to the map
*/
private void addItem(Item item) {
items.add(item);
notifyListeners(new ItemAddedEvent(item, this));
}
/**
* Adds a battleship to the map and triggers an item addition event.
*
* @param ship the battleship to be added to the map
*/
public void add(Battleship ship) {
addItem(ship);
}
/**
* Registers a shot on the map, updates the state of the affected ship (if any),
* and triggers an item addition event.
*
* @param shot the shot to be registered on the map
*/
public void add(Shot shot) {
final Battleship ship = findShipAt(shot);
if (ship != null)
ship.hit(shot);
addItem(shot);
}
/**
* Removes an item from the map and triggers an item removal event.
*
* @param item the item to be removed from the map
*/
public void remove(Item item) {
items.remove(item);
notifyListeners(new ItemAddedEvent(item, this));
}
/**
* Removes all items from the map and triggers corresponding removal events for each.
*/
public void clear() {
new ArrayList<>(items).forEach(this::remove);
}
/**
* Returns a stream of items of a specified type (class).
*
* @param clazz the class type to filter items by
* @param <T> the type of items to return
* @return a stream of items matching the specified class type
*/
private <T extends Item> Stream<T> getItems(Class<T> clazz) {
return items.stream().filter(clazz::isInstance).map(clazz::cast);
}
/**
* Returns a stream of all battleships currently on the map.
*
* @return a stream of battleships
*/
public Stream<Battleship> getShips() {
return getItems(Battleship.class);
}
/**
* Returns a list of all remaining battleships that have not been destroyed.
*
* @return a list of remaining battleships
*/
public List<Battleship> getRemainingShips() {
return getShips().filter(s -> !s.isDestroyed()).toList();
}
/**
* Returns a stream of all shots fired on the map.
*
* @return a stream of shots
*/
public Stream<Shot> getShots() {
return getItems(Shot.class);
}
/**
* Returns an unmodifiable list of all items currently on the map.
*
* @return an unmodifiable list of all items
*/
public List<Item> getItems() {
return Collections.unmodifiableList(items);
}
/**
* Returns the width (number of columns) of the map.
*
* @return the width of the map
*/
public int getWidth() {
return width;
}
/**
* Returns the height (number of rows) of the map.
*
* @return the height of the map
*/
public int getHeight() {
return height;
}
/**
* Checks if the given ship is in a valid position (within the map bounds and non-colliding with other ships).
*
* @param ship the battleship to validate
* @return true if the ship's position is valid, false otherwise
*/
public boolean isValid(Battleship ship) {
return isValid(ship.getMinX(), ship.getMinY()) &&
isValid(ship.getMaxX(), ship.getMaxY()) &&
getShips().filter(s -> s != ship).noneMatch(ship::collidesWith);
}
/**
* Finds a battleship at the specified coordinates.
*
* @param x the x-coordinate of the position
* @param y the y-coordinate of the position
* @return the ship at the specified coordinates, or null if none is found
*/
public Battleship findShipAt(int x, int y) {
return getShips().filter(ship -> ship.contains(x, y))
.findAny()
.orElse(null);
}
/**
* Finds a battleship at the specified position. This is a convenience method.
*
* @param position the position within the map
* @return the ship at the specified position, or null if none is found
*/
public Battleship findShipAt(IntPosition position) {
return findShipAt(position.getX(), position.getY());
}
/**
* Validates whether the specified position is within the map boundaries.
*
* @param pos the position to validate
* @return true if the position is within the map, false otherwise
*/
public boolean isValid(IntPosition pos) {
return isValid(pos.getX(), pos.getY());
}
/**
* Checks if the specified coordinates are within the map boundaries.
*
* @param x the x-coordinate to validate
* @param y the y-coordinate to validate
* @return true if the coordinates are valid, false otherwise
*/
public boolean isValid(int x, int y) {
return x >= 0 && x < width &&
y >= 0 && y < height;
}
/**
* Returns a string representation of the ship map.
*
* @return a string representation of the ship map
*/
@Override
public String toString() {
return "ShipMap{" + items + '}'; //NON-NLS
}
/**
* Notifies all registered listeners about a game event if the event broker is available.
*
* @param event the event to be distributed to listeners
*/
private void notifyListeners(GameEvent event) {
if (eventBroker != null)
eventBroker.notifyListeners(event);
}
}

View File

@@ -0,0 +1,140 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.model;
import com.jme3.network.serializing.Serializable;
import java.util.Objects;
/**
* Represents a shot in the Battleship game.
* A shot is defined by its coordinates and whether it was a hit or miss.
*/
@Serializable
public final class Shot implements Item, IntPosition {
private int x;
private int y;
private boolean hit;
// Private no-arg constructor for serialization purposes
private Shot() {}
/**
* Creates a new shot.
*
* @param x the x-coordinate of the shot
* @param y the y-coordinate of the shot
* @param hit indicates whether the shot was a hit
*/
public Shot(int x, int y, boolean hit) {
this.x = x;
this.y = y;
this.hit = hit;
}
/**
* Creates a new shot.
*
* @param pos the position of the shot
* @param hit indicates whether the shot was a hit
*/
public Shot(IntPosition pos, boolean hit) {
this(pos.getX(), pos.getY(), hit);
}
/**
* Gets the x-coordinate of the shot.
*
* @return the x-coordinate of the shot
*/
@Override
public int getX() {
return x;
}
/**
* Gets the y-coordinate of the shot.
*
* @return the y-coordinate of the shot
*/
@Override
public int getY() {
return y;
}
/**
* Checks if the shot was a hit.
*
* @return true if the shot was a hit, false otherwise
*/
public boolean isHit() {
return hit;
}
/**
* Checks if this shot is equal to another object.
* Two shots are considered equal if they have the same coordinates and hit status.
*
* @param obj the object to compare with
* @return true if the objects are equal, false otherwise
*/
@Override
public boolean equals(Object obj) {
if (obj == this) return true;
if (obj == null || obj.getClass() != this.getClass()) return false;
var that = (Shot) obj;
return this.x == that.x &&
this.y == that.y &&
this.hit == that.hit;
}
/**
* Computes the hash code of this shot.
*
* @return the hash code of this shot
*/
@Override
public int hashCode() {
return Objects.hash(x, y, hit);
}
/**
* Returns a string representation of the shot.
*
* @return a string representation of the shot
*/
@Override
public String toString() {
return "Shot[" + //NON-NLS
"x=" + x + ", " + //NON-NLS
"y=" + y + ", " + //NON-NLS
"hit=" + hit + ']'; //NON-NLS
}
/**
* Accepts a visitor with a return value.
*
* @param visitor the visitor to accept
* @param <T> the type of the return value
* @return the result of the visitor's visit method
*/
@Override
public <T> T accept(Visitor<T> visitor) {
return visitor.visit(this);
}
/**
* Accepts a visitor without a return value.
*
* @param visitor the visitor to accept
*/
@Override
public void accept(VoidVisitor visitor) {
visitor.visit(this);
}
}

View File

@@ -0,0 +1,31 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.model;
/**
* An interface for implementing the Visitor pattern for different types of elements in the Battleship model.
*
* @param <T> the type of result returned by the visit methods
*/
public interface Visitor<T> {
/**
* Visits a Shot element.
*
* @param shot the Shot element to visit
* @return the result of visiting the Shot element
*/
T visit(Shot shot);
/**
* Visits a Battleship element.
*
* @param ship the Battleship element to visit
* @return the result of visiting the Battleship element
*/
T visit(Battleship ship);
}

View File

@@ -0,0 +1,28 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.model;
/**
* An interface for implementing the Visitor pattern for different types of elements in the Battleship model
* without returning any result.
*/
public interface VoidVisitor {
/**
* Visits a Shot element.
*
* @param shot the Shot element to visit
*/
void visit(Shot shot);
/**
* Visits a Battleship element.
*
* @param ship the Battleship element to visit
*/
void visit(Battleship ship);
}

View File

@@ -0,0 +1,51 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.model.dto;
import pp.battleship.model.Battleship;
import pp.battleship.model.Rotation;
/**
* A class representing data transfer objects of battleships for JSON serialization and deserialization.
*/
class BattleshipDTO {
private final int length; // The length of the battleship
private final int x; // The x-coordinate of the battleship's position
private final int y; // The y-coordinate of the battleship's position
private final Rotation rot; // The rotation of the battleship
/**
* Constructs a BattleshipDTO object from a Battleship object.
*
* @param ship the Battleship object to be converted
*/
BattleshipDTO(Battleship ship) {
length = ship.getLength();
x = ship.getX();
y = ship.getY();
rot = ship.getRot();
}
/**
* Gets the length of the battleship.
*
* @return the length
*/
public int getLength() {
return length;
}
/**
* Converts this BattleshipDTO object to a Battleship object.
*
* @return the Battleship object
*/
Battleship toBattleship() {
return new Battleship(length, x, y, rot);
}
}

View File

@@ -0,0 +1,117 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.model.dto;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonParseException;
import pp.battleship.message.server.GameDetails;
import pp.battleship.model.Battleship;
import pp.battleship.model.ShipMap;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.lang.System.Logger;
import java.lang.System.Logger.Level;
import java.util.List;
/**
* A class representing data transfer objects of ship maps for JSON serialization and deserialization.
*/
public class ShipMapDTO {
// Logger instance for logging messages
static final Logger LOGGER = System.getLogger(ShipMapDTO.class.getName());
// Width of the ship map
private int width;
// Height of the ship map
private int height;
// List of ships in the map
private List<BattleshipDTO> ships;
/**
* Constructs a ShipMapDTO object from a ShipMap object.
*
* @param map the ShipMap object to be converted
*/
public ShipMapDTO(ShipMap map) {
this.width = map.getWidth();
this.height = map.getHeight();
this.ships = map.getShips().map(BattleshipDTO::new).toList();
}
/**
* Checks if the current ship map fits the game details provided.
*
* @param details the game details to be matched against
* @return true if the ship map fits the game details, false otherwise
*/
public boolean fits(GameDetails details) {
if (width != details.getWidth() || height != details.getHeight())
return false;
int numShips = details.getShipNums().values().stream().mapToInt(Integer::intValue).sum();
if (numShips != ships.size())
return false;
for (var e : details.getShipNums().entrySet()) {
final int shipLength = e.getKey();
final int requiredNum = e.getValue();
final int actualNum = (int) ships.stream()
.filter(s -> s.getLength() == shipLength)
.count();
if (requiredNum != actualNum)
return false;
}
return true;
}
/**
* Returns the ships stored in this DTO.
*
* @return the ships stored in this DTO
*/
public List<Battleship> getShips() {
return ships.stream().map(BattleshipDTO::toBattleship).toList();
}
/**
* Saves the current ShipMapDTO to a file in JSON format.
*
* @param file the file to which the ShipMapDTO will be saved
* @throws IOException if an I/O error occurs
*/
public void saveTo(File file) throws IOException {
try (FileWriter writer = new FileWriter(file)) {
final Gson gson = new GsonBuilder().setPrettyPrinting().create();
final String json = gson.toJson(this);
LOGGER.log(Level.DEBUG, "JSON of map: {0}", json); //NON-NLS
writer.write(json);
LOGGER.log(Level.INFO, "JSON written to {0}", file.getAbsolutePath()); //NON-NLS
}
}
/**
* Loads a ShipMapDTO from a file containing JSON data.
*
* @param file the file from which the ShipMapDTO will be loaded
* @return the loaded ShipMapDTO object
* @throws IOException if an I/O error occurs or if the JSON is invalid
*/
public static ShipMapDTO loadFrom(File file) throws IOException {
try (FileReader reader = new FileReader(file)) {
final Gson gson = new Gson();
return gson.fromJson(reader, ShipMapDTO.class);
}
catch (JsonParseException e) {
throw new IOException(e.getLocalizedMessage());
}
}
}

View File

@@ -0,0 +1,23 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.notification;
/**
* Event when an item is added to a map.
*/
public record ClientStateEvent() implements GameEvent {
/**
* Notifies the game event listener of this event.
*
* @param listener the game event listener
*/
@Override
public void notifyListener(GameEventListener listener) {
listener.receivedEvent(this);
}
}

View File

@@ -0,0 +1,20 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.notification;
/**
* An interface used for all game events.
*/
public interface GameEvent {
/**
* Notifies the game event listener of the event.
*
* @param listener the game event listener to be notified
*/
void notifyListener(GameEventListener listener);
}

View File

@@ -0,0 +1,20 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.notification;
/**
* Defines a broker for distributing game events to registered listeners.
*/
public interface GameEventBroker {
/**
* Notifies all registered listeners about the specified game event.
*
* @param event the game event to be broadcast to listeners
*/
void notifyListeners(GameEvent event);
}

View File

@@ -0,0 +1,48 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.notification;
/**
* Listener interface for all events implemented by subclasses of {@linkplain pp.battleship.notification.GameEvent}.
*/
public interface GameEventListener {
/**
* Indicates that an item has been destroyed
*
* @param event the received event
*/
default void receivedEvent(ItemRemovedEvent event) { /* do nothing */ }
/**
* Indicates that an item has been added to a map.
*
* @param event the received event
*/
default void receivedEvent(ItemAddedEvent event) { /* do nothing */ }
/**
* Indicates that an info text shall be shown.
*
* @param event the received event
*/
default void receivedEvent(InfoTextEvent event) { /* do nothing */ }
/**
* Indicates that a sound shall be played.
*
* @param event the received event
*/
default void receivedEvent(SoundEvent event) { /* do nothing */ }
/**
* Indicates that the client's state has changed.
*
* @param event the received event
*/
default void receivedEvent(ClientStateEvent event) { /* do nothing */ }
}

View File

@@ -0,0 +1,25 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.notification;
/**
* Event when an item is added to a map.
*
* @param key the bundle key for the message
*/
public record InfoTextEvent(String key) implements GameEvent {
/**
* Notifies the game event listener of this event.
*
* @param listener the game event listener
*/
@Override
public void notifyListener(GameEventListener listener) {
listener.receivedEvent(this);
}
}

View File

@@ -0,0 +1,29 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.notification;
import pp.battleship.model.Item;
import pp.battleship.model.ShipMap;
/**
* Event when an item is added to a map.
*
* @param item the added item
* @param map the map that got the additional item
*/
public record ItemAddedEvent(Item item, ShipMap map) implements GameEvent {
/**
* Notifies the game event listener of this event.
*
* @param listener the game event listener
*/
@Override
public void notifyListener(GameEventListener listener) {
listener.receivedEvent(this);
}
}

View File

@@ -0,0 +1,28 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.notification;
import pp.battleship.model.Item;
import pp.battleship.model.ShipMap;
/**
* Event when an item gets removed.
*
* @param item the destroyed item
*/
public record ItemRemovedEvent(Item item, ShipMap map) implements GameEvent {
/**
* Notifies the game event listener of this event.
*
* @param listener the game event listener
*/
@Override
public void notifyListener(GameEventListener listener) {
listener.receivedEvent(this);
}
}

View File

@@ -0,0 +1,26 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.notification;
/**
* Enumeration representing different types of sounds used in the game.
*/
public enum Sound {
/**
* Sound of an explosion.
*/
EXPLOSION,
/**
* Sound of a splash.
*/
SPLASH,
/**
* Sound of a ship being destroyed.
*/
DESTROYED_SHIP
}

View File

@@ -0,0 +1,26 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.notification;
/**
* Event when an item is added to a map.
*
* @param sound the sound to be played
*/
public record SoundEvent(Sound sound) implements GameEvent {
/**
* Notifies the game event listener of this event.
*
* @param listener the game event listener
*/
@Override
public void notifyListener(GameEventListener listener) {
listener.receivedEvent(this);
}
}

View File

@@ -0,0 +1,39 @@
########################################
## Programming project code
## UniBw M, 2022, 2023, 2024
## www.unibw.de/inf2
## (c) Mark Minas (mark.minas@unibw.de)
########################################
#
battleship.name=Battleship
button.ready=Ready
button.rotate=Rotate
server.connection.failed=Failed to establish a server connection.
its.your.turn=It's your turn! Click on the opponent's field to shoot...
lost.connection.to.server=Lost connection to server. The game terminated.
place.ships.in.your.map=Place ships in your map.
wait.for.an.opponent=Wait for an opponent!
wait.for.opponent=Wait for your opponent!
confirm.leaving=Would you really like to leave the game?
you.lost.the.game=You lost the game!
you.won.the.game=You won the game!
button.yes=Yes
button.no=No
button.ok=Ok
button.connect=Connect
button.cancel=Cancel
server.dialog=Server
host.name=Host
port.number=Port
wait.its.not.your.turn=Wait, it's not your turn!!
menu.quit=Quit game
menu.return-to-game=Return to game
menu.sound-enabled=Sound switched on
menu.map.load=Load map from file...
menu.map.save=Save map in file...
label.file=File:
label.connecting=Connecting...
dialog.error=Error
dialog.question=Question
port.must.be.integer=Port must be an integer number
map.doesnt.fit=The map doesn't fit to this game

View File

@@ -0,0 +1,39 @@
########################################
## Programming project code
## UniBw M, 2022, 2023, 2024
## www.unibw.de/inf2
## (c) Mark Minas (mark.minas@unibw.de)
########################################
#
battleship.name=Schiffe versenken
button.ready=Bereit
button.rotate=Rotiere
server.connection.failed=Verbindung zum Server fehlgeschlagen.
its.your.turn=Du bist dran! Klicke in der gegnerischen Karte...
lost.connection.to.server=Verbindung zum Server abgebrochen. Das Spiel ist damit beendet.
place.ships.in.your.map=Positioniere die Schiffe in Deiner Karte
wait.for.an.opponent=Warte auf einen Gegner!
wait.for.opponent=Warte auf Deinen Gegner!
confirm.leaving=Willst Du wirklich das Spiel verlassen?
you.lost.the.game=Leider verloren!
you.won.the.game=Du hast gewonnen!
button.yes=Ja
button.no=Nein
button.ok=Ok
button.connect=Verbinde
button.cancel=Abbruch
server.dialog=Server
host.name=Host
port.number=Port
wait.its.not.your.turn=Warte, Du bist nicht dran!!
menu.quit=Spiel beenden
menu.return-to-game=Zurück zum Spiel
menu.sound-enabled=Sound eingeschaltet
menu.map.load=Karte von Datei laden...
menu.map.save=Karte in Datei speichern...
label.file=Datei:
label.connecting=Verbindung wird aufgebaut...
dialog.error=Fehler
dialog.question=Frage
port.must.be.integer=Der Port muss eine ganze Zahl sein
map.doesnt.fit=Diese Karte passt nicht zu diesem Spiel

View File

@@ -0,0 +1,157 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.client;
import org.junit.Before;
import org.junit.Test;
import pp.battleship.BattleshipConfig;
import pp.battleship.game.client.ClientGameLogic;
import pp.battleship.message.server.GameDetails;
import pp.battleship.model.Battleship;
import pp.battleship.model.Battleship.Status;
import pp.battleship.model.IntPoint;
import pp.battleship.model.Rotation;
import java.util.Properties;
import static java.util.Collections.emptyList;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
public class EditorTest {
private static final BattleshipConfig CONFIG = new BattleshipConfig();
static {
final Properties props = new Properties();
props.setProperty("map.width", "10");
props.setProperty("map.height", "10");
props.setProperty("harbor.width", "6");
props.setProperty("harbor.height", "10");
props.setProperty("ship.nums", "1,1");
CONFIG.readFrom(props);
}
private ClientGameLogic logic;
private static IntPoint p(int x, int y) {
return new IntPoint(x, y);
}
@Before
public void setUp() {
logic = new ClientGameLogic(null);
logic.received(new GameDetails(CONFIG));
}
@Test
public void testInit() {
assertEquals(emptyList(), logic.getOwnMap().getItems());
assertEquals(2, logic.getHarbor().getItems().size());
final Battleship ship1 = (Battleship) logic.getHarbor().getItems().get(0);
checkShip(ship1, 1, 0, 0, Rotation.RIGHT, Status.NORMAL);
final Battleship ship2 = (Battleship) logic.getHarbor().getItems().get(1);
checkShip(ship2, 2, 0, 1, Rotation.RIGHT, Status.NORMAL);
}
private void checkShip(Battleship ship, int length, int x, int y, Rotation rot, Status status) {
assertEquals(length, ship.getLength());
assertEquals(x, ship.getX());
assertEquals(y, ship.getY());
assertEquals(rot, ship.getRot());
assertEquals(status, ship.getStatus());
}
@Test
public void testRun1() {
logic.clickHarbor(p(0, 0));
logic.clickOwnMap(p(0, 0)); // place ship 1 at (0,0)
assertEquals(1, logic.getOwnMap().getShips().count());
assertEquals(1, logic.getHarbor().getShips().count());
checkShip((Battleship) logic.getHarbor().getItems().get(0),
2, 0, 1, Rotation.RIGHT, Status.NORMAL);
checkShip((Battleship) logic.getOwnMap().getItems().get(0),
1, 0, 0, Rotation.RIGHT, Status.NORMAL);
assertEquals(logic.getOwnMap().findShipAt(0, 0), logic.getOwnMap().getShips().findFirst().get());
assertNull(logic.getOwnMap().findShipAt(1, 0));
assertNull(logic.getOwnMap().findShipAt(0, 1));
logic.clickOwnMap(p(0, 0)); // select ship1
logic.clickHarbor(p(0, 0)); // return it to harbor
assertEquals(0, logic.getOwnMap().getShips().count());
assertEquals(2, logic.getHarbor().getShips().count());
logic.clickHarbor(p(0, 0)); // select ship 1 in harbor
logic.clickOwnMap(p(0, 0)); // place it at (0,0)
checkShip((Battleship) logic.getHarbor().getItems().get(0),
2, 0, 1, Rotation.RIGHT, Status.NORMAL);
checkShip((Battleship) logic.getOwnMap().getItems().get(0),
1, 0, 0, Rotation.RIGHT, Status.NORMAL);
logic.clickHarbor(p(0, 1)); // select ship 2 in harbor
logic.clickOwnMap(p(0, 0)); // try to place it at (0,0); this is too close to ship 1
assertEquals(2, logic.getOwnMap().getShips().count());
assertEquals(1, logic.getHarbor().getShips().count());
checkShip((Battleship) logic.getHarbor().getItems().get(0),
2, 0, 1, Rotation.RIGHT, Status.VALID_PREVIEW);
checkShip((Battleship) logic.getOwnMap().getItems().get(0),
1, 0, 0, Rotation.RIGHT, Status.NORMAL);
checkShip((Battleship) logic.getOwnMap().getItems().get(1),
2, 0, 0, Rotation.RIGHT, Status.INVALID_PREVIEW);
logic.rotateShip(); // rotate selected ship 2
logic.clickOwnMap(p(5, 5)); // place it at (5,5)
assertEquals(2, logic.getOwnMap().getShips().count());
assertEquals(0, logic.getHarbor().getShips().count());
checkShip((Battleship) logic.getOwnMap().getItems().get(0),
1, 0, 0, Rotation.RIGHT, Status.NORMAL);
checkShip((Battleship) logic.getOwnMap().getItems().get(1),
2, 5, 5, Rotation.DOWN, Status.NORMAL);
assertEquals(logic.getOwnMap().getShips().toList().get(1), logic.getOwnMap().findShipAt(5, 5));
assertEquals(logic.getOwnMap().getShips().toList().get(1), logic.getOwnMap().findShipAt(5, 4));
assertTrue(logic.isMapComplete());
}
@Test
public void testRun2() {
logic.clickHarbor(p(0, 0));
logic.clickOwnMap(p(9, 9)); // place ship 1 at (9,9)
assertEquals(1, logic.getOwnMap().getShips().count());
assertEquals(1, logic.getHarbor().getShips().count());
checkShip((Battleship) logic.getHarbor().getItems().get(0),
2, 0, 1, Rotation.RIGHT, Status.NORMAL);
checkShip((Battleship) logic.getOwnMap().getItems().get(0),
1, 9, 9, Rotation.RIGHT, Status.NORMAL);
assertEquals(logic.getOwnMap().findShipAt(9, 9), logic.getOwnMap().getShips().findFirst().get());
assertNull(logic.getOwnMap().findShipAt(10, 9));
assertNull(logic.getOwnMap().findShipAt(9, 10));
assertFalse(logic.isMapComplete());
logic.clickHarbor(p(0, 1)); // select ship 2 in harbor of player 2
logic.clickOwnMap(p(0, 0)); // place it at (0,0)
assertEquals(2, logic.getOwnMap().getShips().count());
assertEquals(0, logic.getHarbor().getShips().count());
assertEquals(logic.getOwnMap().getShips().toList().get(0), logic.getOwnMap().findShipAt(9, 9));
assertEquals(logic.getOwnMap().getShips().toList().get(1), logic.getOwnMap().findShipAt(0, 0));
assertEquals(logic.getOwnMap().getShips().toList().get(1), logic.getOwnMap().findShipAt(1, 0));
assertTrue(logic.isMapComplete());
}
}

View File

@@ -0,0 +1,63 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.game.client;
import org.junit.Before;
import pp.battleship.message.client.ClientMessage;
import pp.battleship.model.Battleship;
import pp.battleship.model.Battleship.Status;
import pp.battleship.model.IntPoint;
import pp.battleship.model.Rotation;
import pp.battleship.notification.GameEventListener;
import pp.battleship.notification.InfoTextEvent;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.LogManager;
import java.util.logging.Logger;
import static org.junit.Assert.assertEquals;
public abstract class AbstractClientGameTest {
final Deque<String> infoTexts = new ArrayDeque<>();
final Deque<ClientMessage> messages = new ArrayDeque<>();
final ClientGameLogic clientLogic = new ClientGameLogic(messages::offer);
static {
// Configure logging
final Logger rootLogger = LogManager.getLogManager().getLogger("");
rootLogger.setLevel(Level.WARNING);
for (Handler h : rootLogger.getHandlers()) {
h.setLevel(Level.WARNING);
}
}
@Before
public void setup() {
clientLogic.addListener(new GameEventListener() {
@Override
public void receivedEvent(InfoTextEvent event) {
infoTexts.offer(event.key());
}
});
}
static IntPoint p(int x, int y) {
return new IntPoint(x, y);
}
void checkShip(Battleship ship, int length, int x, int y, Rotation rot, Status status) {
assertEquals(length, ship.getLength());
assertEquals(x, ship.getX());
assertEquals(y, ship.getY());
assertEquals(rot, ship.getRot());
assertEquals(status, ship.getStatus());
}
}

View File

@@ -0,0 +1,243 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.game.client;
import org.junit.Test;
import pp.battleship.BattleshipConfig;
import pp.battleship.message.client.MapMessage;
import pp.battleship.message.client.ShootMessage;
import pp.battleship.message.server.EffectMessage;
import pp.battleship.message.server.GameDetails;
import pp.battleship.message.server.StartBattleMessage;
import pp.battleship.model.Battleship;
import java.util.List;
import java.util.Properties;
import static org.junit.Assert.assertEquals;
import static pp.battleship.model.Battleship.Status.NORMAL;
import static pp.battleship.model.Battleship.Status.VALID_PREVIEW;
import static pp.battleship.model.Rotation.DOWN;
import static pp.battleship.model.Rotation.RIGHT;
/**
* Integration test cases for the client game logic of Player 1 in a small game, called "Game 1".
* The test cases for Player 2 are contained in {@linkplain ClientGame1Player2Test}
* and the test cases for the server in {@linkplain pp.battleship.game.server.ServerGame1Test}.
*
* @see ClientGame1Player2Test
* @see pp.battleship.game.server.ServerGame1Test
*/
public class ClientGame1Player1Test extends AbstractClientGameTest {
@Test
public void testClient() {
BattleshipConfig config;
Properties props;
List<Battleship> ships;
MapMessage mapMsg;
ShootMessage shootMsg;
config = new BattleshipConfig();
props = new Properties();
props.setProperty("map.width", "6");
props.setProperty("map.height", "6");
props.setProperty("harbor.width", "6");
props.setProperty("harbor.height", "6");
props.setProperty("ship.nums", "1,1");
config.readFrom(props);
clientLogic.received(new GameDetails(config));
assertEquals("place.ships.in.your.map", infoTexts.poll());
clientLogic.clickHarbor(p(0, 1));
ships = clientLogic.getOwnMap().getRemainingShips();
assertEquals(1, ships.size());
checkShip(ships.get(0), 2, 0, 0, RIGHT, VALID_PREVIEW);
ships = clientLogic.getHarbor().getRemainingShips();
assertEquals(2, ships.size());
checkShip(ships.get(0), 1, 0, 0, RIGHT, NORMAL);
checkShip(ships.get(1), 2, 0, 1, RIGHT, VALID_PREVIEW);
ships = clientLogic.getOwnMap().getRemainingShips();
assertEquals(1, ships.size());
checkShip(ships.get(0), 2, 0, 0, RIGHT, VALID_PREVIEW);
ships = clientLogic.getHarbor().getRemainingShips();
assertEquals(2, ships.size());
checkShip(ships.get(0), 1, 0, 0, RIGHT, NORMAL);
checkShip(ships.get(1), 2, 0, 1, RIGHT, VALID_PREVIEW);
clientLogic.movePreview(p(5, 1));
clientLogic.movePreview(p(4, 1));
clientLogic.movePreview(p(4, 1));
clientLogic.movePreview(p(3, 1));
clientLogic.movePreview(p(2, 1));
clientLogic.movePreview(p(2, 1));
clientLogic.movePreview(p(1, 1));
clientLogic.movePreview(p(1, 1));
clientLogic.movePreview(p(1, 1));
clientLogic.movePreview(p(1, 1));
clientLogic.movePreview(p(1, 1));
clientLogic.movePreview(p(1, 1));
clientLogic.movePreview(p(1, 1));
clientLogic.movePreview(p(1, 1));
clientLogic.movePreview(p(0, 1));
clientLogic.movePreview(p(1, 1));
clientLogic.movePreview(p(1, 1));
clientLogic.movePreview(p(1, 1));
clientLogic.movePreview(p(1, 1));
clientLogic.movePreview(p(1, 1));
clientLogic.movePreview(p(1, 1));
clientLogic.movePreview(p(1, 1));
clientLogic.movePreview(p(1, 1));
clientLogic.movePreview(p(1, 1));
clientLogic.movePreview(p(1, 1));
clientLogic.movePreview(p(1, 1));
clientLogic.movePreview(p(1, 1));
clientLogic.movePreview(p(1, 1));
clientLogic.movePreview(p(1, 1));
clientLogic.movePreview(p(1, 1));
clientLogic.movePreview(p(1, 1));
clientLogic.movePreview(p(1, 1));
clientLogic.movePreview(p(1, 1));
clientLogic.movePreview(p(1, 1));
clientLogic.movePreview(p(1, 1));
clientLogic.movePreview(p(1, 1));
clientLogic.movePreview(p(1, 1));
clientLogic.movePreview(p(1, 1));
clientLogic.movePreview(p(1, 1));
clientLogic.movePreview(p(2, 1));
clientLogic.movePreview(p(2, 1));
clientLogic.movePreview(p(2, 1));
clientLogic.movePreview(p(2, 1));
clientLogic.movePreview(p(2, 1));
clientLogic.movePreview(p(2, 1));
ships = clientLogic.getOwnMap().getRemainingShips();
assertEquals(1, ships.size());
checkShip(ships.get(0), 2, 2, 1, RIGHT, VALID_PREVIEW);
ships = clientLogic.getHarbor().getRemainingShips();
assertEquals(2, ships.size());
checkShip(ships.get(0), 1, 0, 0, RIGHT, NORMAL);
checkShip(ships.get(1), 2, 0, 1, RIGHT, VALID_PREVIEW);
clientLogic.clickOwnMap(p(2, 1));
ships = clientLogic.getOwnMap().getRemainingShips();
assertEquals(1, ships.size());
checkShip(ships.get(0), 2, 2, 1, RIGHT, NORMAL);
ships = clientLogic.getHarbor().getRemainingShips();
assertEquals(1, ships.size());
checkShip(ships.get(0), 1, 0, 0, RIGHT, NORMAL);
clientLogic.clickHarbor(p(0, 0));
ships = clientLogic.getOwnMap().getRemainingShips();
assertEquals(2, ships.size());
checkShip(ships.get(0), 2, 2, 1, RIGHT, NORMAL);
checkShip(ships.get(1), 1, 0, 0, RIGHT, VALID_PREVIEW);
ships = clientLogic.getHarbor().getRemainingShips();
assertEquals(1, ships.size());
checkShip(ships.get(0), 1, 0, 0, RIGHT, VALID_PREVIEW);
ships = clientLogic.getOwnMap().getRemainingShips();
assertEquals(2, ships.size());
checkShip(ships.get(0), 2, 2, 1, RIGHT, NORMAL);
checkShip(ships.get(1), 1, 0, 0, RIGHT, VALID_PREVIEW);
ships = clientLogic.getHarbor().getRemainingShips();
assertEquals(1, ships.size());
checkShip(ships.get(0), 1, 0, 0, RIGHT, VALID_PREVIEW);
clientLogic.movePreview(p(5, 1));
clientLogic.movePreview(p(5, 1));
clientLogic.movePreview(p(5, 1));
clientLogic.movePreview(p(5, 1));
clientLogic.movePreview(p(5, 1));
clientLogic.movePreview(p(5, 1));
clientLogic.movePreview(p(5, 1));
clientLogic.movePreview(p(5, 1));
clientLogic.movePreview(p(5, 1));
clientLogic.movePreview(p(5, 1));
clientLogic.movePreview(p(5, 1));
clientLogic.movePreview(p(5, 1));
clientLogic.movePreview(p(5, 1));
ships = clientLogic.getOwnMap().getRemainingShips();
assertEquals(2, ships.size());
checkShip(ships.get(0), 2, 2, 1, RIGHT, NORMAL);
checkShip(ships.get(1), 1, 5, 1, RIGHT, VALID_PREVIEW);
ships = clientLogic.getHarbor().getRemainingShips();
assertEquals(1, ships.size());
checkShip(ships.get(0), 1, 0, 0, RIGHT, VALID_PREVIEW);
clientLogic.clickOwnMap(p(5, 1));
ships = clientLogic.getOwnMap().getRemainingShips();
assertEquals(2, ships.size());
checkShip(ships.get(0), 2, 2, 1, RIGHT, NORMAL);
checkShip(ships.get(1), 1, 5, 1, RIGHT, NORMAL);
ships = clientLogic.getHarbor().getRemainingShips();
assertEquals(0, ships.size());
clientLogic.mapFinished();
mapMsg = (MapMessage) messages.poll();
assertEquals(2, mapMsg.getShips().size());
checkShip(mapMsg.getShips().get(0), 2, 2, 1, RIGHT, NORMAL);
checkShip(mapMsg.getShips().get(1), 1, 5, 1, RIGHT, NORMAL);
assertEquals("wait.for.opponent", infoTexts.poll());
clientLogic.received(new StartBattleMessage(true));
assertEquals("its.your.turn", infoTexts.poll());
clientLogic.clickOpponentMap(p(3, 3));
shootMsg = (ShootMessage) messages.poll();
assertEquals(p(3, 3), shootMsg.getPosition());
clientLogic.received(EffectMessage.miss(true, p(3, 3)));
assertEquals("wait.for.opponent", infoTexts.poll());
clientLogic.received(EffectMessage.miss(false, p(2, 2)));
assertEquals("its.your.turn", infoTexts.poll());
clientLogic.clickOpponentMap(p(1, 3));
shootMsg = (ShootMessage) messages.poll();
assertEquals(p(1, 3), shootMsg.getPosition());
clientLogic.clickOpponentMap(p(1, 3));
shootMsg = (ShootMessage) messages.poll();
assertEquals(p(1, 3), shootMsg.getPosition());
clientLogic.received(EffectMessage.miss(true, p(1, 3)));
assertEquals("wait.for.opponent", infoTexts.poll());
clientLogic.received(EffectMessage.hit(false, p(2, 1)));
assertEquals("wait.for.opponent", infoTexts.poll());
clientLogic.received(EffectMessage.shipDestroyed(false, p(3, 1), new Battleship(2, 2, 1, RIGHT)));
assertEquals("wait.for.opponent", infoTexts.poll());
clientLogic.received(EffectMessage.miss(false, p(5, 3)));
assertEquals("its.your.turn", infoTexts.poll());
clientLogic.clickOpponentMap(p(2, 3));
shootMsg = (ShootMessage) messages.poll();
assertEquals(p(2, 3), shootMsg.getPosition());
clientLogic.clickOpponentMap(p(2, 3));
shootMsg = (ShootMessage) messages.poll();
assertEquals(p(2, 3), shootMsg.getPosition());
clientLogic.received(EffectMessage.miss(true, p(2, 3)));
assertEquals("wait.for.opponent", infoTexts.poll());
clientLogic.received(EffectMessage.miss(false, p(3, 3)));
assertEquals("its.your.turn", infoTexts.poll());
clientLogic.clickOpponentMap(p(1, 4));
shootMsg = (ShootMessage) messages.poll();
assertEquals(p(1, 4), shootMsg.getPosition());
clientLogic.clickOpponentMap(p(1, 4));
shootMsg = (ShootMessage) messages.poll();
assertEquals(p(1, 4), shootMsg.getPosition());
clientLogic.received(EffectMessage.hit(true, p(1, 4)));
assertEquals("its.your.turn", infoTexts.poll());
clientLogic.received(EffectMessage.hit(true, p(1, 4)));
assertEquals("its.your.turn", infoTexts.poll());
clientLogic.clickOpponentMap(p(1, 5));
shootMsg = (ShootMessage) messages.poll();
assertEquals(p(1, 5), shootMsg.getPosition());
clientLogic.received(EffectMessage.shipDestroyed(true, p(1, 5), new Battleship(2, 1, 5, DOWN)));
assertEquals("its.your.turn", infoTexts.poll());
ships = clientLogic.getOpponentMap().getShips().toList();
assertEquals(1, ships.size());
checkShip(ships.get(0), 2, 1, 5, DOWN, NORMAL);
clientLogic.clickOpponentMap(p(2, 2));
shootMsg = (ShootMessage) messages.poll();
assertEquals(p(2, 2), shootMsg.getPosition());
clientLogic.received(EffectMessage.miss(true, p(2, 2)));
assertEquals("wait.for.opponent", infoTexts.poll());
clientLogic.received(EffectMessage.lost(p(5, 1), new Battleship(1, 5, 1, RIGHT), List.of(new Battleship(1, 1, 2, RIGHT))));
assertEquals("you.lost.the.game", infoTexts.poll());
ships = clientLogic.getOpponentMap().getShips().toList();
assertEquals(2, ships.size());
checkShip(ships.get(0), 2, 1, 5, DOWN, NORMAL);
checkShip(ships.get(1), 1, 1, 2, RIGHT, NORMAL);
assertEquals(0, infoTexts.size());
assertEquals(0, messages.size());
}
}

View File

@@ -0,0 +1,389 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.game.client;
import org.junit.Test;
import pp.battleship.BattleshipConfig;
import pp.battleship.message.client.MapMessage;
import pp.battleship.message.client.ShootMessage;
import pp.battleship.message.server.EffectMessage;
import pp.battleship.message.server.GameDetails;
import pp.battleship.message.server.StartBattleMessage;
import pp.battleship.model.Battleship;
import java.util.List;
import java.util.Properties;
import static org.junit.Assert.assertEquals;
import static pp.battleship.model.Battleship.Status.NORMAL;
import static pp.battleship.model.Battleship.Status.VALID_PREVIEW;
import static pp.battleship.model.Rotation.DOWN;
import static pp.battleship.model.Rotation.RIGHT;
/**
* Integration test cases for the client game logic of Player 2 in a small game, called "Game 1".
* The test cases for Player 1 are contained in {@linkplain ClientGame1Player1Test}
* and the test cases for the server in {@linkplain pp.battleship.game.server.ServerGame1Test}.
*
* @see ClientGame1Player1Test
* @see pp.battleship.game.server.ServerGame1Test
*/
public class ClientGame1Player2Test extends AbstractClientGameTest {
@Test
public void testClient() {
BattleshipConfig config;
Properties props;
List<Battleship> ships;
MapMessage mapMsg;
ShootMessage shootMsg;
config = new BattleshipConfig();
props = new Properties();
props.setProperty("map.width", "6");
props.setProperty("map.height", "6");
props.setProperty("harbor.width", "6");
props.setProperty("harbor.height", "6");
props.setProperty("ship.nums", "1,1");
config.readFrom(props);
clientLogic.received(new GameDetails(config));
assertEquals("place.ships.in.your.map", infoTexts.poll());
clientLogic.clickHarbor(p(1, 1));
ships = clientLogic.getOwnMap().getRemainingShips();
assertEquals(1, ships.size());
checkShip(ships.get(0), 2, 0, 0, RIGHT, VALID_PREVIEW);
ships = clientLogic.getHarbor().getRemainingShips();
assertEquals(2, ships.size());
checkShip(ships.get(0), 1, 0, 0, RIGHT, NORMAL);
checkShip(ships.get(1), 2, 0, 1, RIGHT, VALID_PREVIEW);
ships = clientLogic.getOwnMap().getRemainingShips();
assertEquals(1, ships.size());
checkShip(ships.get(0), 2, 0, 0, RIGHT, VALID_PREVIEW);
ships = clientLogic.getHarbor().getRemainingShips();
assertEquals(2, ships.size());
checkShip(ships.get(0), 1, 0, 0, RIGHT, NORMAL);
checkShip(ships.get(1), 2, 0, 1, RIGHT, VALID_PREVIEW);
clientLogic.movePreview(p(5, 2));
clientLogic.movePreview(p(5, 2));
clientLogic.movePreview(p(4, 2));
clientLogic.movePreview(p(4, 2));
clientLogic.movePreview(p(4, 2));
clientLogic.movePreview(p(4, 2));
clientLogic.movePreview(p(4, 2));
clientLogic.movePreview(p(4, 2));
clientLogic.movePreview(p(3, 3));
clientLogic.movePreview(p(3, 3));
clientLogic.movePreview(p(3, 3));
clientLogic.movePreview(p(3, 3));
clientLogic.movePreview(p(2, 3));
clientLogic.movePreview(p(2, 3));
clientLogic.movePreview(p(2, 3));
clientLogic.movePreview(p(1, 3));
clientLogic.movePreview(p(1, 3));
clientLogic.movePreview(p(1, 3));
clientLogic.movePreview(p(1, 3));
clientLogic.movePreview(p(1, 3));
clientLogic.movePreview(p(1, 3));
clientLogic.movePreview(p(1, 3));
clientLogic.movePreview(p(1, 3));
clientLogic.movePreview(p(1, 3));
clientLogic.movePreview(p(1, 3));
clientLogic.movePreview(p(1, 3));
clientLogic.movePreview(p(1, 3));
clientLogic.movePreview(p(1, 3));
clientLogic.movePreview(p(1, 3));
clientLogic.movePreview(p(1, 3));
clientLogic.movePreview(p(1, 3));
clientLogic.movePreview(p(1, 3));
clientLogic.movePreview(p(1, 3));
clientLogic.movePreview(p(1, 2));
clientLogic.movePreview(p(1, 2));
clientLogic.movePreview(p(2, 2));
clientLogic.movePreview(p(2, 2));
clientLogic.movePreview(p(2, 2));
clientLogic.movePreview(p(2, 2));
clientLogic.movePreview(p(2, 2));
clientLogic.movePreview(p(2, 2));
clientLogic.movePreview(p(2, 2));
clientLogic.movePreview(p(2, 2));
clientLogic.movePreview(p(2, 2));
clientLogic.movePreview(p(2, 2));
clientLogic.movePreview(p(2, 2));
clientLogic.movePreview(p(2, 2));
ships = clientLogic.getOwnMap().getRemainingShips();
assertEquals(1, ships.size());
checkShip(ships.get(0), 2, 2, 2, RIGHT, VALID_PREVIEW);
ships = clientLogic.getHarbor().getRemainingShips();
assertEquals(2, ships.size());
checkShip(ships.get(0), 1, 0, 0, RIGHT, NORMAL);
checkShip(ships.get(1), 2, 0, 1, RIGHT, VALID_PREVIEW);
clientLogic.clickOwnMap(p(2, 2));
ships = clientLogic.getOwnMap().getRemainingShips();
assertEquals(1, ships.size());
checkShip(ships.get(0), 2, 2, 2, RIGHT, NORMAL);
ships = clientLogic.getHarbor().getRemainingShips();
assertEquals(1, ships.size());
checkShip(ships.get(0), 1, 0, 0, RIGHT, NORMAL);
clientLogic.clickHarbor(p(0, 0));
ships = clientLogic.getOwnMap().getRemainingShips();
assertEquals(2, ships.size());
checkShip(ships.get(0), 2, 2, 2, RIGHT, NORMAL);
checkShip(ships.get(1), 1, 0, 0, RIGHT, VALID_PREVIEW);
ships = clientLogic.getHarbor().getRemainingShips();
assertEquals(1, ships.size());
checkShip(ships.get(0), 1, 0, 0, RIGHT, VALID_PREVIEW);
ships = clientLogic.getOwnMap().getRemainingShips();
assertEquals(2, ships.size());
checkShip(ships.get(0), 2, 2, 2, RIGHT, NORMAL);
checkShip(ships.get(1), 1, 0, 0, RIGHT, VALID_PREVIEW);
ships = clientLogic.getHarbor().getRemainingShips();
assertEquals(1, ships.size());
checkShip(ships.get(0), 1, 0, 0, RIGHT, VALID_PREVIEW);
clientLogic.movePreview(p(5, 1));
clientLogic.movePreview(p(5, 1));
clientLogic.movePreview(p(5, 1));
clientLogic.movePreview(p(5, 1));
clientLogic.movePreview(p(5, 1));
clientLogic.movePreview(p(5, 2));
clientLogic.movePreview(p(5, 2));
clientLogic.movePreview(p(5, 2));
clientLogic.movePreview(p(5, 2));
clientLogic.movePreview(p(5, 2));
clientLogic.movePreview(p(5, 2));
clientLogic.movePreview(p(5, 2));
clientLogic.movePreview(p(5, 2));
clientLogic.movePreview(p(5, 2));
clientLogic.movePreview(p(5, 2));
clientLogic.movePreview(p(5, 2));
clientLogic.movePreview(p(5, 2));
clientLogic.movePreview(p(5, 2));
clientLogic.movePreview(p(5, 2));
clientLogic.movePreview(p(5, 2));
clientLogic.movePreview(p(5, 2));
clientLogic.movePreview(p(5, 2));
ships = clientLogic.getOwnMap().getRemainingShips();
assertEquals(2, ships.size());
checkShip(ships.get(0), 2, 2, 2, RIGHT, NORMAL);
checkShip(ships.get(1), 1, 5, 2, RIGHT, VALID_PREVIEW);
ships = clientLogic.getHarbor().getRemainingShips();
assertEquals(1, ships.size());
checkShip(ships.get(0), 1, 0, 0, RIGHT, VALID_PREVIEW);
clientLogic.clickOwnMap(p(5, 2));
ships = clientLogic.getOwnMap().getRemainingShips();
assertEquals(2, ships.size());
checkShip(ships.get(0), 2, 2, 2, RIGHT, NORMAL);
checkShip(ships.get(1), 1, 5, 2, RIGHT, NORMAL);
ships = clientLogic.getHarbor().getRemainingShips();
assertEquals(0, ships.size());
ships = clientLogic.getOwnMap().getRemainingShips();
assertEquals(2, ships.size());
checkShip(ships.get(0), 2, 2, 2, RIGHT, NORMAL);
checkShip(ships.get(1), 1, 5, 2, RIGHT, NORMAL);
ships = clientLogic.getHarbor().getRemainingShips();
assertEquals(0, ships.size());
clientLogic.clickOwnMap(p(2, 2));
ships = clientLogic.getOwnMap().getRemainingShips();
assertEquals(2, ships.size());
checkShip(ships.get(0), 1, 5, 2, RIGHT, NORMAL);
checkShip(ships.get(1), 2, 2, 2, RIGHT, VALID_PREVIEW);
ships = clientLogic.getHarbor().getRemainingShips();
assertEquals(1, ships.size());
checkShip(ships.get(0), 2, 0, 0, RIGHT, VALID_PREVIEW);
clientLogic.movePreview(p(2, 2));
clientLogic.movePreview(p(2, 2));
clientLogic.movePreview(p(3, 2));
clientLogic.movePreview(p(3, 2));
clientLogic.movePreview(p(3, 2));
clientLogic.movePreview(p(4, 2));
clientLogic.movePreview(p(4, 2));
clientLogic.movePreview(p(5, 2));
clientLogic.clickHarbor(p(1, 1));
ships = clientLogic.getOwnMap().getRemainingShips();
assertEquals(1, ships.size());
checkShip(ships.get(0), 1, 5, 2, RIGHT, NORMAL);
ships = clientLogic.getHarbor().getRemainingShips();
assertEquals(1, ships.size());
checkShip(ships.get(0), 2, 0, 0, RIGHT, NORMAL);
ships = clientLogic.getOwnMap().getRemainingShips();
assertEquals(1, ships.size());
checkShip(ships.get(0), 1, 5, 2, RIGHT, NORMAL);
ships = clientLogic.getHarbor().getRemainingShips();
assertEquals(1, ships.size());
checkShip(ships.get(0), 2, 0, 0, RIGHT, NORMAL);
ships = clientLogic.getOwnMap().getRemainingShips();
assertEquals(1, ships.size());
checkShip(ships.get(0), 1, 5, 2, RIGHT, NORMAL);
ships = clientLogic.getHarbor().getRemainingShips();
assertEquals(1, ships.size());
checkShip(ships.get(0), 2, 0, 0, RIGHT, NORMAL);
clientLogic.clickOwnMap(p(5, 2));
ships = clientLogic.getOwnMap().getRemainingShips();
assertEquals(1, ships.size());
checkShip(ships.get(0), 1, 5, 2, RIGHT, VALID_PREVIEW);
ships = clientLogic.getHarbor().getRemainingShips();
assertEquals(2, ships.size());
checkShip(ships.get(0), 2, 0, 0, RIGHT, NORMAL);
checkShip(ships.get(1), 1, 0, 1, RIGHT, VALID_PREVIEW);
clientLogic.movePreview(p(5, 2));
clientLogic.movePreview(p(5, 2));
clientLogic.movePreview(p(5, 2));
clientLogic.movePreview(p(5, 2));
clientLogic.movePreview(p(5, 2));
clientLogic.movePreview(p(4, 2));
clientLogic.movePreview(p(4, 2));
clientLogic.movePreview(p(4, 2));
clientLogic.movePreview(p(3, 2));
clientLogic.movePreview(p(3, 2));
clientLogic.movePreview(p(3, 2));
clientLogic.movePreview(p(3, 2));
clientLogic.movePreview(p(3, 2));
clientLogic.movePreview(p(3, 2));
clientLogic.movePreview(p(2, 2));
clientLogic.movePreview(p(2, 2));
clientLogic.movePreview(p(2, 2));
clientLogic.movePreview(p(2, 2));
clientLogic.movePreview(p(2, 2));
clientLogic.movePreview(p(2, 2));
clientLogic.movePreview(p(1, 2));
clientLogic.movePreview(p(1, 2));
clientLogic.movePreview(p(1, 2));
clientLogic.movePreview(p(1, 2));
clientLogic.movePreview(p(1, 2));
clientLogic.movePreview(p(1, 2));
clientLogic.movePreview(p(1, 2));
clientLogic.movePreview(p(1, 2));
ships = clientLogic.getOwnMap().getRemainingShips();
assertEquals(1, ships.size());
checkShip(ships.get(0), 1, 1, 2, RIGHT, VALID_PREVIEW);
ships = clientLogic.getHarbor().getRemainingShips();
assertEquals(2, ships.size());
checkShip(ships.get(0), 2, 0, 0, RIGHT, NORMAL);
checkShip(ships.get(1), 1, 0, 1, RIGHT, VALID_PREVIEW);
clientLogic.clickOwnMap(p(1, 2));
ships = clientLogic.getOwnMap().getRemainingShips();
assertEquals(1, ships.size());
checkShip(ships.get(0), 1, 1, 2, RIGHT, NORMAL);
ships = clientLogic.getHarbor().getRemainingShips();
assertEquals(1, ships.size());
checkShip(ships.get(0), 2, 0, 0, RIGHT, NORMAL);
clientLogic.clickHarbor(p(0, 0));
ships = clientLogic.getOwnMap().getRemainingShips();
assertEquals(2, ships.size());
checkShip(ships.get(0), 1, 1, 2, RIGHT, NORMAL);
checkShip(ships.get(1), 2, 0, 0, RIGHT, VALID_PREVIEW);
ships = clientLogic.getHarbor().getRemainingShips();
assertEquals(1, ships.size());
checkShip(ships.get(0), 2, 0, 0, RIGHT, VALID_PREVIEW);
ships = clientLogic.getOwnMap().getRemainingShips();
assertEquals(2, ships.size());
checkShip(ships.get(0), 1, 1, 2, RIGHT, NORMAL);
checkShip(ships.get(1), 2, 0, 0, RIGHT, VALID_PREVIEW);
ships = clientLogic.getHarbor().getRemainingShips();
assertEquals(1, ships.size());
checkShip(ships.get(0), 2, 0, 0, RIGHT, VALID_PREVIEW);
clientLogic.movePreview(p(5, 0));
clientLogic.movePreview(p(5, 0));
clientLogic.movePreview(p(4, 0));
clientLogic.movePreview(p(4, 0));
clientLogic.movePreview(p(3, 0));
clientLogic.movePreview(p(3, 0));
clientLogic.rotateShip();
clientLogic.movePreview(p(1, 0));
clientLogic.movePreview(p(1, 1));
clientLogic.movePreview(p(1, 2));
clientLogic.movePreview(p(1, 3));
clientLogic.movePreview(p(1, 4));
clientLogic.movePreview(p(1, 4));
clientLogic.movePreview(p(1, 5));
clientLogic.movePreview(p(1, 5));
clientLogic.movePreview(p(1, 5));
clientLogic.movePreview(p(1, 5));
clientLogic.movePreview(p(1, 5));
ships = clientLogic.getOwnMap().getRemainingShips();
assertEquals(2, ships.size());
checkShip(ships.get(0), 1, 1, 2, RIGHT, NORMAL);
checkShip(ships.get(1), 2, 1, 5, DOWN, VALID_PREVIEW);
ships = clientLogic.getHarbor().getRemainingShips();
assertEquals(1, ships.size());
checkShip(ships.get(0), 2, 0, 0, RIGHT, VALID_PREVIEW);
clientLogic.clickOwnMap(p(1, 5));
ships = clientLogic.getOwnMap().getRemainingShips();
assertEquals(2, ships.size());
checkShip(ships.get(0), 1, 1, 2, RIGHT, NORMAL);
checkShip(ships.get(1), 2, 1, 5, DOWN, NORMAL);
ships = clientLogic.getHarbor().getRemainingShips();
assertEquals(0, ships.size());
clientLogic.mapFinished();
mapMsg = (MapMessage) messages.poll();
assertEquals(2, mapMsg.getShips().size());
checkShip(mapMsg.getShips().get(0), 1, 1, 2, RIGHT, NORMAL);
checkShip(mapMsg.getShips().get(1), 2, 1, 5, DOWN, NORMAL);
assertEquals("wait.for.opponent", infoTexts.poll());
clientLogic.received(new StartBattleMessage(false));
assertEquals("wait.for.opponent", infoTexts.poll());
clientLogic.received(EffectMessage.miss(false, p(3, 3)));
assertEquals("its.your.turn", infoTexts.poll());
clientLogic.clickOpponentMap(p(2, 2));
shootMsg = (ShootMessage) messages.poll();
assertEquals(p(2, 2), shootMsg.getPosition());
clientLogic.clickOpponentMap(p(2, 2));
shootMsg = (ShootMessage) messages.poll();
assertEquals(p(2, 2), shootMsg.getPosition());
clientLogic.received(EffectMessage.miss(true, p(2, 2)));
assertEquals("wait.for.opponent", infoTexts.poll());
clientLogic.received(EffectMessage.miss(false, p(1, 3)));
assertEquals("its.your.turn", infoTexts.poll());
clientLogic.clickOpponentMap(p(2, 1));
shootMsg = (ShootMessage) messages.poll();
assertEquals(p(2, 1), shootMsg.getPosition());
clientLogic.received(EffectMessage.hit(true, p(2, 1)));
assertEquals("its.your.turn", infoTexts.poll());
clientLogic.clickOpponentMap(p(3, 1));
shootMsg = (ShootMessage) messages.poll();
assertEquals(p(3, 1), shootMsg.getPosition());
clientLogic.received(EffectMessage.shipDestroyed(true, p(3, 1), new Battleship(2, 2, 1, RIGHT)));
assertEquals("its.your.turn", infoTexts.poll());
ships = clientLogic.getOpponentMap().getShips().toList();
assertEquals(1, ships.size());
checkShip(ships.get(0), 2, 2, 1, RIGHT, NORMAL);
clientLogic.clickOpponentMap(p(5, 3));
shootMsg = (ShootMessage) messages.poll();
assertEquals(p(5, 3), shootMsg.getPosition());
clientLogic.received(EffectMessage.miss(true, p(5, 3)));
assertEquals("wait.for.opponent", infoTexts.poll());
clientLogic.received(EffectMessage.miss(false, p(2, 3)));
assertEquals("its.your.turn", infoTexts.poll());
clientLogic.clickOpponentMap(p(3, 3));
shootMsg = (ShootMessage) messages.poll();
assertEquals(p(3, 3), shootMsg.getPosition());
clientLogic.received(EffectMessage.miss(true, p(3, 3)));
assertEquals("wait.for.opponent", infoTexts.poll());
clientLogic.received(EffectMessage.hit(false, p(1, 4)));
assertEquals("wait.for.opponent", infoTexts.poll());
clientLogic.received(EffectMessage.hit(false, p(1, 4)));
assertEquals("wait.for.opponent", infoTexts.poll());
clientLogic.received(EffectMessage.shipDestroyed(false, p(1, 5), new Battleship(2, 1, 5, DOWN)));
assertEquals("wait.for.opponent", infoTexts.poll());
clientLogic.received(EffectMessage.miss(false, p(2, 2)));
assertEquals("its.your.turn", infoTexts.poll());
clientLogic.clickOpponentMap(p(5, 1));
shootMsg = (ShootMessage) messages.poll();
assertEquals(p(5, 1), shootMsg.getPosition());
clientLogic.clickOpponentMap(p(5, 1));
shootMsg = (ShootMessage) messages.poll();
assertEquals(p(5, 1), shootMsg.getPosition());
clientLogic.received(EffectMessage.won(p(5, 1), new Battleship(1, 5, 1, RIGHT)));
assertEquals("you.won.the.game", infoTexts.poll());
ships = clientLogic.getOpponentMap().getShips().toList();
assertEquals(2, ships.size());
checkShip(ships.get(0), 2, 2, 1, RIGHT, NORMAL);
checkShip(ships.get(1), 1, 5, 1, RIGHT, NORMAL);
assertEquals(0, infoTexts.size());
assertEquals(0, messages.size());
}
}

View File

@@ -0,0 +1,187 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.game.client;
import org.junit.Before;
import org.junit.Test;
import pp.battleship.message.client.ClientMessage;
import pp.battleship.message.server.EffectMessage;
import pp.battleship.message.server.GameDetails;
import pp.battleship.message.server.StartBattleMessage;
import pp.battleship.model.IntPoint;
import java.io.File;
import java.io.IOException;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class ClientGameLogicTest {
private ClientGameLogic logic;
private ClientSender clientSender;
private ClientState initialState;
private ClientState editorState;
private ClientState gameOverState;
@Before
public void setUp() {
clientSender = mock(ClientSender.class);
initialState = mock(ClientState.class);
editorState = mock(ClientState.class);
gameOverState = mock(ClientState.class);
logic = new ClientGameLogic(clientSender);
logic.setState(initialState);
}
@Test
public void testSetState() {
logic.setState(editorState);
assertEquals(editorState, logic.getState());
}
@Test
public void testShowEditor() {
when(initialState.showEditor()).thenReturn(true);
when(editorState.showEditor()).thenReturn(false);
assertTrue(logic.showEditor());
logic.setState(editorState);
assertFalse(logic.showEditor());
}
@Test
public void testShowBattle() {
when(initialState.showBattle()).thenReturn(false);
when(gameOverState.showBattle()).thenReturn(true);
assertFalse(logic.showBattle());
logic.setState(gameOverState);
assertTrue(logic.showBattle());
}
@Test
public void testReceivedGameDetails() {
final GameDetails details = mock(GameDetails.class);
logic.received(details);
verify(initialState).receivedGameDetails(details);
}
@Test
public void testMovePreview() {
final IntPoint pos = new IntPoint(42, 42);
logic.movePreview(pos);
verify(initialState).movePreview(pos);
}
@Test
public void testClickOwnMap() {
final IntPoint pos = new IntPoint(42, 42);
logic.clickOwnMap(pos);
verify(initialState).clickOwnMap(pos);
}
@Test
public void testClickHarbor() {
final IntPoint pos = new IntPoint(42, 42);
logic.clickHarbor(pos);
verify(initialState).clickHarbor(pos);
}
@Test
public void testClickOpponentMap() {
final IntPoint pos = new IntPoint(42, 42);
logic.clickOpponentMap(pos);
verify(initialState).clickOpponentMap(pos);
}
@Test
public void testRotateShip() {
logic.rotateShip();
verify(initialState).rotateShip();
}
@Test
public void testMapFinished() {
logic.mapFinished();
verify(initialState).mapFinished();
}
@Test
public void testIsMapComplete() {
logic.setState(editorState);
when(editorState.isMapComplete()).thenReturn(true);
assertTrue(logic.isMapComplete());
when(editorState.isMapComplete()).thenReturn(false);
assertFalse(logic.isMapComplete());
}
@Test
public void testMovingShip() {
logic.setState(editorState);
when(editorState.movingShip()).thenReturn(true);
assertTrue(logic.movingShip());
when(editorState.movingShip()).thenReturn(false);
assertFalse(logic.movingShip());
}
@Test
public void testReceivedStartShooting() {
final StartBattleMessage msg = mock(StartBattleMessage.class);
logic.received(msg);
verify(initialState).receivedStartBattle(msg);
}
@Test
public void testReceivedEffectMessage() {
final EffectMessage msg = mock(EffectMessage.class);
logic.received(msg);
verify(initialState).receivedEffect(msg);
}
@Test
public void testLoadMap() throws IOException {
final File file = mock(File.class);
logic.loadMap(file);
verify(initialState).loadMap(file);
}
@Test
public void testMayLoadMap() {
logic.setState(editorState);
when(editorState.mayLoadMap()).thenReturn(true);
assertTrue(logic.mayLoadMap());
when(editorState.mayLoadMap()).thenReturn(false);
assertFalse(logic.mayLoadMap());
}
@Test
public void testMaySaveMap() {
logic.setState(editorState);
when(editorState.maySaveMap()).thenReturn(true);
assertTrue(logic.maySaveMap());
when(editorState.maySaveMap()).thenReturn(false);
assertFalse(logic.maySaveMap());
}
@Test(expected = IOException.class)
public void testSaveMap() throws IOException {
final File file = mock(File.class);
logic.setState(editorState);
when(editorState.maySaveMap()).thenReturn(false);
logic.saveMap(file);
}
@Test
public void testSend() {
final ClientMessage msg = mock(ClientMessage.class);
logic.send(msg);
verify(clientSender).send(msg);
}
}

View File

@@ -0,0 +1,55 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.game.server;
import pp.battleship.BattleshipConfig;
import pp.battleship.message.server.ServerMessage;
import pp.battleship.model.Battleship;
import pp.battleship.model.Battleship.Status;
import pp.battleship.model.IntPoint;
import pp.battleship.model.Rotation;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.LogManager;
import java.util.logging.Logger;
import static org.junit.Assert.assertEquals;
public abstract class AbstractServerGameTest {
static final BattleshipConfig CONFIG = new BattleshipConfig();
static {
// Configure logging
final Logger rootLogger = LogManager.getLogManager().getLogger("");
rootLogger.setLevel(Level.WARNING);
for (Handler h : rootLogger.getHandlers()) {
h.setLevel(Level.WARNING);
}
}
record ServerMessageWrapper(int id, ServerMessage msg) {}
final Deque<ServerMessageWrapper> messages = new ArrayDeque<>();
final ServerSender serverSender = (id, msg) -> messages.offer(new ServerMessageWrapper(id, msg));
final ServerGameLogic serverLogic = new ServerGameLogic(serverSender, CONFIG);
static IntPoint p(int x, int y) {
return new IntPoint(x, y);
}
void checkShip(Battleship ship, int length, int x, int y, Rotation rot, Status status) {
assertEquals(length, ship.getLength());
assertEquals(x, ship.getX());
assertEquals(y, ship.getY());
assertEquals(rot, ship.getRot());
assertEquals(status, ship.getStatus());
}
}

View File

@@ -0,0 +1,254 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.game.server;
import org.junit.Test;
import pp.battleship.message.client.MapMessage;
import pp.battleship.message.client.ShootMessage;
import pp.battleship.message.server.EffectMessage;
import pp.battleship.message.server.GameDetails;
import pp.battleship.message.server.StartBattleMessage;
import pp.battleship.model.Battleship;
import pp.battleship.model.Shot;
import java.util.List;
import java.util.Properties;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static pp.battleship.model.Battleship.Status.NORMAL;
import static pp.battleship.model.Rotation.DOWN;
import static pp.battleship.model.Rotation.RIGHT;
/**
* Integration test cases for the server game logic in a small game, called "Game 1", where Player 2 (with id 1) wins.
* The test cases for the players are contained in {@linkplain pp.battleship.game.client.ClientGame1Player1Test}
* and {@linkplain pp.battleship.game.client.ClientGame1Player2Test}.
*
* @see pp.battleship.game.client.ClientGame1Player1Test
* @see pp.battleship.game.client.ClientGame1Player2Test
*/
public class ServerGame1Test extends AbstractServerGameTest {
static {
final Properties props = new Properties();
props.setProperty("map.width", "6");
props.setProperty("map.height", "6");
props.setProperty("ship.nums", "1,1");
CONFIG.readFrom(props);
}
@Test
public void testRun() {
EffectMessage effectMsg;
GameDetails details;
StartBattleMessage startShooting;
serverLogic.addPlayer(0); // Player 1
serverLogic.addPlayer(1); // Player 2
assertEquals(0, messages.peek().id());
details = (GameDetails) messages.poll().msg();
assertEquals(6, details.getWidth());
assertEquals(6, details.getHeight());
assertEquals(2, details.getShipNums().size());
assertEquals(Integer.valueOf(1), details.getShipNums().get(1));
assertEquals(Integer.valueOf(1), details.getShipNums().get(2));
assertEquals(1, messages.peek().id());
details = (GameDetails) messages.poll().msg();
assertEquals(6, details.getWidth());
assertEquals(6, details.getHeight());
assertEquals(2, details.getShipNums().size());
assertEquals(Integer.valueOf(1), details.getShipNums().get(1));
assertEquals(Integer.valueOf(1), details.getShipNums().get(2));
serverLogic.received(new MapMessage(List.of(new Battleship(1, 1, 2, RIGHT), new Battleship(2, 1, 5, DOWN))), 1);
serverLogic.received(new MapMessage(List.of(new Battleship(2, 2, 1, RIGHT), new Battleship(1, 5, 1, RIGHT))), 0);
assertEquals(0, messages.peek().id());
startShooting = (StartBattleMessage) messages.poll().msg();
assertTrue(startShooting.isMyTurn());
assertEquals(1, messages.peek().id());
startShooting = (StartBattleMessage) messages.poll().msg();
assertFalse(startShooting.isMyTurn());
serverLogic.received(new ShootMessage(p(3, 3)), 0);
assertEquals(0, messages.peek().id());
effectMsg = (EffectMessage) messages.poll().msg();
assertTrue(effectMsg.isOwnShot());
assertEquals(new Shot(3, 3, false), effectMsg.getShot());
assertNull(effectMsg.getDestroyedShip());
assertNull(effectMsg.getRemainingOpponentShips());
assertEquals(1, messages.peek().id());
effectMsg = (EffectMessage) messages.poll().msg();
assertFalse(effectMsg.isOwnShot());
assertEquals(new Shot(3, 3, false), effectMsg.getShot());
assertNull(effectMsg.getDestroyedShip());
assertNull(effectMsg.getRemainingOpponentShips());
serverLogic.received(new ShootMessage(p(2, 2)), 1);
assertEquals(1, messages.peek().id());
effectMsg = (EffectMessage) messages.poll().msg();
assertTrue(effectMsg.isOwnShot());
assertEquals(new Shot(2, 2, false), effectMsg.getShot());
assertNull(effectMsg.getDestroyedShip());
assertNull(effectMsg.getRemainingOpponentShips());
assertEquals(0, messages.peek().id());
effectMsg = (EffectMessage) messages.poll().msg();
assertFalse(effectMsg.isOwnShot());
assertEquals(new Shot(2, 2, false), effectMsg.getShot());
assertNull(effectMsg.getDestroyedShip());
assertNull(effectMsg.getRemainingOpponentShips());
serverLogic.received(new ShootMessage(p(2, 2)), 1);
serverLogic.received(new ShootMessage(p(1, 3)), 0);
assertEquals(0, messages.peek().id());
effectMsg = (EffectMessage) messages.poll().msg();
assertTrue(effectMsg.isOwnShot());
assertEquals(new Shot(1, 3, false), effectMsg.getShot());
assertNull(effectMsg.getDestroyedShip());
assertNull(effectMsg.getRemainingOpponentShips());
assertEquals(1, messages.peek().id());
effectMsg = (EffectMessage) messages.poll().msg();
assertFalse(effectMsg.isOwnShot());
assertEquals(new Shot(1, 3, false), effectMsg.getShot());
assertNull(effectMsg.getDestroyedShip());
assertNull(effectMsg.getRemainingOpponentShips());
serverLogic.received(new ShootMessage(p(1, 3)), 0);
serverLogic.received(new ShootMessage(p(2, 1)), 1);
assertEquals(1, messages.peek().id());
effectMsg = (EffectMessage) messages.poll().msg();
assertTrue(effectMsg.isOwnShot());
assertEquals(new Shot(2, 1, true), effectMsg.getShot());
assertNull(effectMsg.getDestroyedShip());
assertNull(effectMsg.getRemainingOpponentShips());
assertEquals(0, messages.peek().id());
effectMsg = (EffectMessage) messages.poll().msg();
assertFalse(effectMsg.isOwnShot());
assertEquals(new Shot(2, 1, true), effectMsg.getShot());
assertNull(effectMsg.getDestroyedShip());
assertNull(effectMsg.getRemainingOpponentShips());
serverLogic.received(new ShootMessage(p(3, 1)), 1);
assertEquals(1, messages.peek().id());
effectMsg = (EffectMessage) messages.poll().msg();
assertTrue(effectMsg.isOwnShot());
assertEquals(new Shot(3, 1, true), effectMsg.getShot());
checkShip(effectMsg.getDestroyedShip(), 2, 2, 1, RIGHT, NORMAL);
assertNull(effectMsg.getRemainingOpponentShips());
assertEquals(0, messages.peek().id());
effectMsg = (EffectMessage) messages.poll().msg();
assertFalse(effectMsg.isOwnShot());
assertEquals(new Shot(3, 1, true), effectMsg.getShot());
checkShip(effectMsg.getDestroyedShip(), 2, 2, 1, RIGHT, NORMAL);
assertNull(effectMsg.getRemainingOpponentShips());
serverLogic.received(new ShootMessage(p(5, 3)), 1);
assertEquals(1, messages.peek().id());
effectMsg = (EffectMessage) messages.poll().msg();
assertTrue(effectMsg.isOwnShot());
assertEquals(new Shot(5, 3, false), effectMsg.getShot());
assertNull(effectMsg.getDestroyedShip());
assertNull(effectMsg.getRemainingOpponentShips());
assertEquals(0, messages.peek().id());
effectMsg = (EffectMessage) messages.poll().msg();
assertFalse(effectMsg.isOwnShot());
assertEquals(new Shot(5, 3, false), effectMsg.getShot());
assertNull(effectMsg.getDestroyedShip());
assertNull(effectMsg.getRemainingOpponentShips());
serverLogic.received(new ShootMessage(p(2, 3)), 0);
assertEquals(0, messages.peek().id());
effectMsg = (EffectMessage) messages.poll().msg();
assertTrue(effectMsg.isOwnShot());
assertEquals(new Shot(2, 3, false), effectMsg.getShot());
assertNull(effectMsg.getDestroyedShip());
assertNull(effectMsg.getRemainingOpponentShips());
assertEquals(1, messages.peek().id());
effectMsg = (EffectMessage) messages.poll().msg();
assertFalse(effectMsg.isOwnShot());
assertEquals(new Shot(2, 3, false), effectMsg.getShot());
assertNull(effectMsg.getDestroyedShip());
assertNull(effectMsg.getRemainingOpponentShips());
serverLogic.received(new ShootMessage(p(2, 3)), 0);
serverLogic.received(new ShootMessage(p(3, 3)), 1);
assertEquals(1, messages.peek().id());
effectMsg = (EffectMessage) messages.poll().msg();
assertTrue(effectMsg.isOwnShot());
assertEquals(new Shot(3, 3, false), effectMsg.getShot());
assertNull(effectMsg.getDestroyedShip());
assertNull(effectMsg.getRemainingOpponentShips());
assertEquals(0, messages.peek().id());
effectMsg = (EffectMessage) messages.poll().msg();
assertFalse(effectMsg.isOwnShot());
assertEquals(new Shot(3, 3, false), effectMsg.getShot());
assertNull(effectMsg.getDestroyedShip());
assertNull(effectMsg.getRemainingOpponentShips());
serverLogic.received(new ShootMessage(p(1, 4)), 0);
assertEquals(0, messages.peek().id());
effectMsg = (EffectMessage) messages.poll().msg();
assertTrue(effectMsg.isOwnShot());
assertEquals(new Shot(1, 4, true), effectMsg.getShot());
assertNull(effectMsg.getDestroyedShip());
assertNull(effectMsg.getRemainingOpponentShips());
assertEquals(1, messages.peek().id());
effectMsg = (EffectMessage) messages.poll().msg();
assertFalse(effectMsg.isOwnShot());
assertEquals(new Shot(1, 4, true), effectMsg.getShot());
assertNull(effectMsg.getDestroyedShip());
assertNull(effectMsg.getRemainingOpponentShips());
serverLogic.received(new ShootMessage(p(1, 4)), 0);
assertEquals(0, messages.peek().id());
effectMsg = (EffectMessage) messages.poll().msg();
assertTrue(effectMsg.isOwnShot());
assertEquals(new Shot(1, 4, true), effectMsg.getShot());
assertNull(effectMsg.getDestroyedShip());
assertNull(effectMsg.getRemainingOpponentShips());
assertEquals(1, messages.peek().id());
effectMsg = (EffectMessage) messages.poll().msg();
assertFalse(effectMsg.isOwnShot());
assertEquals(new Shot(1, 4, true), effectMsg.getShot());
assertNull(effectMsg.getDestroyedShip());
assertNull(effectMsg.getRemainingOpponentShips());
serverLogic.received(new ShootMessage(p(1, 5)), 0);
assertEquals(0, messages.peek().id());
effectMsg = (EffectMessage) messages.poll().msg();
assertTrue(effectMsg.isOwnShot());
assertEquals(new Shot(1, 5, true), effectMsg.getShot());
checkShip(effectMsg.getDestroyedShip(), 2, 1, 5, DOWN, NORMAL);
assertNull(effectMsg.getRemainingOpponentShips());
assertEquals(1, messages.peek().id());
effectMsg = (EffectMessage) messages.poll().msg();
assertFalse(effectMsg.isOwnShot());
assertEquals(new Shot(1, 5, true), effectMsg.getShot());
checkShip(effectMsg.getDestroyedShip(), 2, 1, 5, DOWN, NORMAL);
assertNull(effectMsg.getRemainingOpponentShips());
serverLogic.received(new ShootMessage(p(2, 2)), 0);
assertEquals(0, messages.peek().id());
effectMsg = (EffectMessage) messages.poll().msg();
assertTrue(effectMsg.isOwnShot());
assertEquals(new Shot(2, 2, false), effectMsg.getShot());
assertNull(effectMsg.getDestroyedShip());
assertNull(effectMsg.getRemainingOpponentShips());
assertEquals(1, messages.peek().id());
effectMsg = (EffectMessage) messages.poll().msg();
assertFalse(effectMsg.isOwnShot());
assertEquals(new Shot(2, 2, false), effectMsg.getShot());
assertNull(effectMsg.getDestroyedShip());
assertNull(effectMsg.getRemainingOpponentShips());
serverLogic.received(new ShootMessage(p(5, 1)), 1);
assertEquals(1, messages.peek().id());
effectMsg = (EffectMessage) messages.poll().msg();
assertTrue(effectMsg.isOwnShot());
assertEquals(new Shot(5, 1, true), effectMsg.getShot());
checkShip(effectMsg.getDestroyedShip(), 1, 5, 1, RIGHT, NORMAL);
assertEquals(0, effectMsg.getRemainingOpponentShips().size());
assertEquals(0, messages.peek().id());
effectMsg = (EffectMessage) messages.poll().msg();
assertFalse(effectMsg.isOwnShot());
assertEquals(new Shot(5, 1, true), effectMsg.getShot());
checkShip(effectMsg.getDestroyedShip(), 1, 5, 1, RIGHT, NORMAL);
assertEquals(1, effectMsg.getRemainingOpponentShips().size());
checkShip(effectMsg.getRemainingOpponentShips().get(0), 1, 1, 2, RIGHT, NORMAL);
// Player 2 wins
assertEquals(0, messages.size());
}
}

View File

@@ -0,0 +1,574 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.game.server;
import org.junit.Test;
import pp.battleship.message.client.MapMessage;
import pp.battleship.message.client.ShootMessage;
import pp.battleship.message.server.EffectMessage;
import pp.battleship.message.server.GameDetails;
import pp.battleship.message.server.StartBattleMessage;
import pp.battleship.model.Battleship;
import pp.battleship.model.Shot;
import java.util.List;
import java.util.Properties;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static pp.battleship.model.Battleship.Status.NORMAL;
import static pp.battleship.model.Rotation.DOWN;
import static pp.battleship.model.Rotation.RIGHT;
/**
* Integration test cases for the server game logic in a larger game, called "Game 2", where Player 1 (with id 0) wins.
* The test cases for the players are contained in {@linkplain pp.battleship.game.client.ClientGame2Player1Test}
* and {@linkplain pp.battleship.game.client.ClientGame2Player2Test}.
*
* @see pp.battleship.game.client.ClientGame2Player1Test
* @see pp.battleship.game.client.ClientGame2Player2Test
*/
public class ServerGame2Test extends AbstractServerGameTest {
static {
final Properties props = new Properties();
props.setProperty("map.width", "10");
props.setProperty("map.height", "10");
props.setProperty("ship.nums", "4,3,2,1");
CONFIG.readFrom(props);
}
@Test
public void testRun() {
EffectMessage effectMsg;
GameDetails details;
StartBattleMessage startShooting;
serverLogic.addPlayer(0); // Player 1
serverLogic.addPlayer(1); // Player 2
assertEquals(0, messages.peek().id());
details = (GameDetails) messages.poll().msg();
assertEquals(10, details.getWidth());
assertEquals(10, details.getHeight());
assertEquals(4, details.getShipNums().size());
assertEquals(Integer.valueOf(4), details.getShipNums().get(1));
assertEquals(Integer.valueOf(3), details.getShipNums().get(2));
assertEquals(Integer.valueOf(2), details.getShipNums().get(3));
assertEquals(Integer.valueOf(1), details.getShipNums().get(4));
assertEquals(1, messages.peek().id());
details = (GameDetails) messages.poll().msg();
assertEquals(10, details.getWidth());
assertEquals(10, details.getHeight());
assertEquals(4, details.getShipNums().size());
assertEquals(Integer.valueOf(4), details.getShipNums().get(1));
assertEquals(Integer.valueOf(3), details.getShipNums().get(2));
assertEquals(Integer.valueOf(2), details.getShipNums().get(3));
assertEquals(Integer.valueOf(1), details.getShipNums().get(4));
serverLogic.received(new MapMessage(List.of(new Battleship(3, 2, 3, DOWN), new Battleship(2, 2, 6, DOWN), new Battleship(2, 4, 6, RIGHT), new Battleship(2, 4, 4, RIGHT), new Battleship(1, 7, 6, RIGHT), new Battleship(1, 7, 4, RIGHT), new Battleship(1, 4, 2, RIGHT), new Battleship(1, 6, 2, RIGHT), new Battleship(4, 2, 8, RIGHT), new Battleship(3, 7, 8, RIGHT))), 1);
serverLogic.received(new MapMessage(List.of(new Battleship(4, 0, 5, DOWN), new Battleship(3, 0, 9, DOWN), new Battleship(3, 0, 0, RIGHT), new Battleship(2, 4, 0, RIGHT), new Battleship(2, 2, 9, RIGHT), new Battleship(2, 5, 9, RIGHT), new Battleship(1, 8, 9, RIGHT), new Battleship(1, 7, 0, RIGHT), new Battleship(1, 9, 0, RIGHT), new Battleship(1, 9, 7, RIGHT))), 0);
assertEquals(0, messages.peek().id());
startShooting = (StartBattleMessage) messages.poll().msg();
assertTrue(startShooting.isMyTurn());
assertEquals(1, messages.peek().id());
startShooting = (StartBattleMessage) messages.poll().msg();
assertFalse(startShooting.isMyTurn());
serverLogic.received(new ShootMessage(p(5, 5)), 0);
assertEquals(0, messages.peek().id());
effectMsg = (EffectMessage) messages.poll().msg();
assertTrue(effectMsg.isOwnShot());
assertEquals(new Shot(5, 5, false), effectMsg.getShot());
assertNull(effectMsg.getDestroyedShip());
assertNull(effectMsg.getRemainingOpponentShips());
assertEquals(1, messages.peek().id());
effectMsg = (EffectMessage) messages.poll().msg();
assertFalse(effectMsg.isOwnShot());
assertEquals(new Shot(5, 5, false), effectMsg.getShot());
assertNull(effectMsg.getDestroyedShip());
assertNull(effectMsg.getRemainingOpponentShips());
serverLogic.received(new ShootMessage(p(5, 5)), 1);
assertEquals(1, messages.peek().id());
effectMsg = (EffectMessage) messages.poll().msg();
assertTrue(effectMsg.isOwnShot());
assertEquals(new Shot(5, 5, false), effectMsg.getShot());
assertNull(effectMsg.getDestroyedShip());
assertNull(effectMsg.getRemainingOpponentShips());
assertEquals(0, messages.peek().id());
effectMsg = (EffectMessage) messages.poll().msg();
assertFalse(effectMsg.isOwnShot());
assertEquals(new Shot(5, 5, false), effectMsg.getShot());
assertNull(effectMsg.getDestroyedShip());
assertNull(effectMsg.getRemainingOpponentShips());
serverLogic.received(new ShootMessage(p(5, 6)), 0);
assertEquals(0, messages.peek().id());
effectMsg = (EffectMessage) messages.poll().msg();
assertTrue(effectMsg.isOwnShot());
assertEquals(new Shot(5, 6, true), effectMsg.getShot());
assertNull(effectMsg.getDestroyedShip());
assertNull(effectMsg.getRemainingOpponentShips());
assertEquals(1, messages.peek().id());
effectMsg = (EffectMessage) messages.poll().msg();
assertFalse(effectMsg.isOwnShot());
assertEquals(new Shot(5, 6, true), effectMsg.getShot());
assertNull(effectMsg.getDestroyedShip());
assertNull(effectMsg.getRemainingOpponentShips());
serverLogic.received(new ShootMessage(p(6, 6)), 0);
assertEquals(0, messages.peek().id());
effectMsg = (EffectMessage) messages.poll().msg();
assertTrue(effectMsg.isOwnShot());
assertEquals(new Shot(6, 6, false), effectMsg.getShot());
assertNull(effectMsg.getDestroyedShip());
assertNull(effectMsg.getRemainingOpponentShips());
assertEquals(1, messages.peek().id());
effectMsg = (EffectMessage) messages.poll().msg();
assertFalse(effectMsg.isOwnShot());
assertEquals(new Shot(6, 6, false), effectMsg.getShot());
assertNull(effectMsg.getDestroyedShip());
assertNull(effectMsg.getRemainingOpponentShips());
serverLogic.received(new ShootMessage(p(5, 6)), 1);
assertEquals(1, messages.peek().id());
effectMsg = (EffectMessage) messages.poll().msg();
assertTrue(effectMsg.isOwnShot());
assertEquals(new Shot(5, 6, false), effectMsg.getShot());
assertNull(effectMsg.getDestroyedShip());
assertNull(effectMsg.getRemainingOpponentShips());
assertEquals(0, messages.peek().id());
effectMsg = (EffectMessage) messages.poll().msg();
assertFalse(effectMsg.isOwnShot());
assertEquals(new Shot(5, 6, false), effectMsg.getShot());
assertNull(effectMsg.getDestroyedShip());
assertNull(effectMsg.getRemainingOpponentShips());
serverLogic.received(new ShootMessage(p(4, 6)), 0);
assertEquals(0, messages.peek().id());
effectMsg = (EffectMessage) messages.poll().msg();
assertTrue(effectMsg.isOwnShot());
assertEquals(new Shot(4, 6, true), effectMsg.getShot());
checkShip(effectMsg.getDestroyedShip(), 2, 4, 6, RIGHT, NORMAL);
assertNull(effectMsg.getRemainingOpponentShips());
assertEquals(1, messages.peek().id());
effectMsg = (EffectMessage) messages.poll().msg();
assertFalse(effectMsg.isOwnShot());
assertEquals(new Shot(4, 6, true), effectMsg.getShot());
checkShip(effectMsg.getDestroyedShip(), 2, 4, 6, RIGHT, NORMAL);
assertNull(effectMsg.getRemainingOpponentShips());
serverLogic.received(new ShootMessage(p(4, 8)), 0);
assertEquals(0, messages.peek().id());
effectMsg = (EffectMessage) messages.poll().msg();
assertTrue(effectMsg.isOwnShot());
assertEquals(new Shot(4, 8, true), effectMsg.getShot());
assertNull(effectMsg.getDestroyedShip());
assertNull(effectMsg.getRemainingOpponentShips());
assertEquals(1, messages.peek().id());
effectMsg = (EffectMessage) messages.poll().msg();
assertFalse(effectMsg.isOwnShot());
assertEquals(new Shot(4, 8, true), effectMsg.getShot());
assertNull(effectMsg.getDestroyedShip());
assertNull(effectMsg.getRemainingOpponentShips());
serverLogic.received(new ShootMessage(p(5, 8)), 0);
assertEquals(0, messages.peek().id());
effectMsg = (EffectMessage) messages.poll().msg();
assertTrue(effectMsg.isOwnShot());
assertEquals(new Shot(5, 8, true), effectMsg.getShot());
assertNull(effectMsg.getDestroyedShip());
assertNull(effectMsg.getRemainingOpponentShips());
assertEquals(1, messages.peek().id());
effectMsg = (EffectMessage) messages.poll().msg();
assertFalse(effectMsg.isOwnShot());
assertEquals(new Shot(5, 8, true), effectMsg.getShot());
assertNull(effectMsg.getDestroyedShip());
assertNull(effectMsg.getRemainingOpponentShips());
serverLogic.received(new ShootMessage(p(6, 8)), 0);
assertEquals(0, messages.peek().id());
effectMsg = (EffectMessage) messages.poll().msg();
assertTrue(effectMsg.isOwnShot());
assertEquals(new Shot(6, 8, false), effectMsg.getShot());
assertNull(effectMsg.getDestroyedShip());
assertNull(effectMsg.getRemainingOpponentShips());
assertEquals(1, messages.peek().id());
effectMsg = (EffectMessage) messages.poll().msg();
assertFalse(effectMsg.isOwnShot());
assertEquals(new Shot(6, 8, false), effectMsg.getShot());
assertNull(effectMsg.getDestroyedShip());
assertNull(effectMsg.getRemainingOpponentShips());
serverLogic.received(new ShootMessage(p(3, 7)), 1);
assertEquals(1, messages.peek().id());
effectMsg = (EffectMessage) messages.poll().msg();
assertTrue(effectMsg.isOwnShot());
assertEquals(new Shot(3, 7, false), effectMsg.getShot());
assertNull(effectMsg.getDestroyedShip());
assertNull(effectMsg.getRemainingOpponentShips());
assertEquals(0, messages.peek().id());
effectMsg = (EffectMessage) messages.poll().msg();
assertFalse(effectMsg.isOwnShot());
assertEquals(new Shot(3, 7, false), effectMsg.getShot());
assertNull(effectMsg.getDestroyedShip());
assertNull(effectMsg.getRemainingOpponentShips());
serverLogic.received(new ShootMessage(p(3, 8)), 0);
assertEquals(0, messages.peek().id());
effectMsg = (EffectMessage) messages.poll().msg();
assertTrue(effectMsg.isOwnShot());
assertEquals(new Shot(3, 8, true), effectMsg.getShot());
assertNull(effectMsg.getDestroyedShip());
assertNull(effectMsg.getRemainingOpponentShips());
assertEquals(1, messages.peek().id());
effectMsg = (EffectMessage) messages.poll().msg();
assertFalse(effectMsg.isOwnShot());
assertEquals(new Shot(3, 8, true), effectMsg.getShot());
assertNull(effectMsg.getDestroyedShip());
assertNull(effectMsg.getRemainingOpponentShips());
serverLogic.received(new ShootMessage(p(2, 8)), 0);
assertEquals(0, messages.peek().id());
effectMsg = (EffectMessage) messages.poll().msg();
assertTrue(effectMsg.isOwnShot());
assertEquals(new Shot(2, 8, true), effectMsg.getShot());
checkShip(effectMsg.getDestroyedShip(), 4, 2, 8, RIGHT, NORMAL);
assertNull(effectMsg.getRemainingOpponentShips());
assertEquals(1, messages.peek().id());
effectMsg = (EffectMessage) messages.poll().msg();
assertFalse(effectMsg.isOwnShot());
assertEquals(new Shot(2, 8, true), effectMsg.getShot());
checkShip(effectMsg.getDestroyedShip(), 4, 2, 8, RIGHT, NORMAL);
assertNull(effectMsg.getRemainingOpponentShips());
serverLogic.received(new ShootMessage(p(0, 8)), 0);
assertEquals(0, messages.peek().id());
effectMsg = (EffectMessage) messages.poll().msg();
assertTrue(effectMsg.isOwnShot());
assertEquals(new Shot(0, 8, false), effectMsg.getShot());
assertNull(effectMsg.getDestroyedShip());
assertNull(effectMsg.getRemainingOpponentShips());
assertEquals(1, messages.peek().id());
effectMsg = (EffectMessage) messages.poll().msg();
assertFalse(effectMsg.isOwnShot());
assertEquals(new Shot(0, 8, false), effectMsg.getShot());
assertNull(effectMsg.getDestroyedShip());
assertNull(effectMsg.getRemainingOpponentShips());
serverLogic.received(new ShootMessage(p(5, 9)), 1);
assertEquals(1, messages.peek().id());
effectMsg = (EffectMessage) messages.poll().msg();
assertTrue(effectMsg.isOwnShot());
assertEquals(new Shot(5, 9, true), effectMsg.getShot());
assertNull(effectMsg.getDestroyedShip());
assertNull(effectMsg.getRemainingOpponentShips());
assertEquals(0, messages.peek().id());
effectMsg = (EffectMessage) messages.poll().msg();
assertFalse(effectMsg.isOwnShot());
assertEquals(new Shot(5, 9, true), effectMsg.getShot());
assertNull(effectMsg.getDestroyedShip());
assertNull(effectMsg.getRemainingOpponentShips());
serverLogic.received(new ShootMessage(p(6, 9)), 1);
assertEquals(1, messages.peek().id());
effectMsg = (EffectMessage) messages.poll().msg();
assertTrue(effectMsg.isOwnShot());
assertEquals(new Shot(6, 9, true), effectMsg.getShot());
checkShip(effectMsg.getDestroyedShip(), 2, 5, 9, RIGHT, NORMAL);
assertNull(effectMsg.getRemainingOpponentShips());
assertEquals(0, messages.peek().id());
effectMsg = (EffectMessage) messages.poll().msg();
assertFalse(effectMsg.isOwnShot());
assertEquals(new Shot(6, 9, true), effectMsg.getShot());
checkShip(effectMsg.getDestroyedShip(), 2, 5, 9, RIGHT, NORMAL);
assertNull(effectMsg.getRemainingOpponentShips());
serverLogic.received(new ShootMessage(p(7, 7)), 1);
assertEquals(1, messages.peek().id());
effectMsg = (EffectMessage) messages.poll().msg();
assertTrue(effectMsg.isOwnShot());
assertEquals(new Shot(7, 7, false), effectMsg.getShot());
assertNull(effectMsg.getDestroyedShip());
assertNull(effectMsg.getRemainingOpponentShips());
assertEquals(0, messages.peek().id());
effectMsg = (EffectMessage) messages.poll().msg();
assertFalse(effectMsg.isOwnShot());
assertEquals(new Shot(7, 7, false), effectMsg.getShot());
assertNull(effectMsg.getDestroyedShip());
assertNull(effectMsg.getRemainingOpponentShips());
serverLogic.received(new ShootMessage(p(0, 6)), 0);
assertEquals(0, messages.peek().id());
effectMsg = (EffectMessage) messages.poll().msg();
assertTrue(effectMsg.isOwnShot());
assertEquals(new Shot(0, 6, false), effectMsg.getShot());
assertNull(effectMsg.getDestroyedShip());
assertNull(effectMsg.getRemainingOpponentShips());
assertEquals(1, messages.peek().id());
effectMsg = (EffectMessage) messages.poll().msg();
assertFalse(effectMsg.isOwnShot());
assertEquals(new Shot(0, 6, false), effectMsg.getShot());
assertNull(effectMsg.getDestroyedShip());
assertNull(effectMsg.getRemainingOpponentShips());
serverLogic.received(new ShootMessage(p(2, 5)), 1);
assertEquals(1, messages.peek().id());
effectMsg = (EffectMessage) messages.poll().msg();
assertTrue(effectMsg.isOwnShot());
assertEquals(new Shot(2, 5, false), effectMsg.getShot());
assertNull(effectMsg.getDestroyedShip());
assertNull(effectMsg.getRemainingOpponentShips());
assertEquals(0, messages.peek().id());
effectMsg = (EffectMessage) messages.poll().msg();
assertFalse(effectMsg.isOwnShot());
assertEquals(new Shot(2, 5, false), effectMsg.getShot());
assertNull(effectMsg.getDestroyedShip());
assertNull(effectMsg.getRemainingOpponentShips());
serverLogic.received(new ShootMessage(p(7, 8)), 0);
assertEquals(0, messages.peek().id());
effectMsg = (EffectMessage) messages.poll().msg();
assertTrue(effectMsg.isOwnShot());
assertEquals(new Shot(7, 8, true), effectMsg.getShot());
assertNull(effectMsg.getDestroyedShip());
assertNull(effectMsg.getRemainingOpponentShips());
assertEquals(1, messages.peek().id());
effectMsg = (EffectMessage) messages.poll().msg();
assertFalse(effectMsg.isOwnShot());
assertEquals(new Shot(7, 8, true), effectMsg.getShot());
assertNull(effectMsg.getDestroyedShip());
assertNull(effectMsg.getRemainingOpponentShips());
serverLogic.received(new ShootMessage(p(8, 8)), 0);
assertEquals(0, messages.peek().id());
effectMsg = (EffectMessage) messages.poll().msg();
assertTrue(effectMsg.isOwnShot());
assertEquals(new Shot(8, 8, true), effectMsg.getShot());
assertNull(effectMsg.getDestroyedShip());
assertNull(effectMsg.getRemainingOpponentShips());
assertEquals(1, messages.peek().id());
effectMsg = (EffectMessage) messages.poll().msg();
assertFalse(effectMsg.isOwnShot());
assertEquals(new Shot(8, 8, true), effectMsg.getShot());
assertNull(effectMsg.getDestroyedShip());
assertNull(effectMsg.getRemainingOpponentShips());
serverLogic.received(new ShootMessage(p(9, 8)), 0);
assertEquals(0, messages.peek().id());
effectMsg = (EffectMessage) messages.poll().msg();
assertTrue(effectMsg.isOwnShot());
assertEquals(new Shot(9, 8, true), effectMsg.getShot());
checkShip(effectMsg.getDestroyedShip(), 3, 7, 8, RIGHT, NORMAL);
assertNull(effectMsg.getRemainingOpponentShips());
assertEquals(1, messages.peek().id());
effectMsg = (EffectMessage) messages.poll().msg();
assertFalse(effectMsg.isOwnShot());
assertEquals(new Shot(9, 8, true), effectMsg.getShot());
checkShip(effectMsg.getDestroyedShip(), 3, 7, 8, RIGHT, NORMAL);
assertNull(effectMsg.getRemainingOpponentShips());
serverLogic.received(new ShootMessage(p(2, 6)), 0);
assertEquals(0, messages.peek().id());
effectMsg = (EffectMessage) messages.poll().msg();
assertTrue(effectMsg.isOwnShot());
assertEquals(new Shot(2, 6, true), effectMsg.getShot());
assertNull(effectMsg.getDestroyedShip());
assertNull(effectMsg.getRemainingOpponentShips());
assertEquals(1, messages.peek().id());
effectMsg = (EffectMessage) messages.poll().msg();
assertFalse(effectMsg.isOwnShot());
assertEquals(new Shot(2, 6, true), effectMsg.getShot());
assertNull(effectMsg.getDestroyedShip());
assertNull(effectMsg.getRemainingOpponentShips());
serverLogic.received(new ShootMessage(p(2, 5)), 0);
assertEquals(0, messages.peek().id());
effectMsg = (EffectMessage) messages.poll().msg();
assertTrue(effectMsg.isOwnShot());
assertEquals(new Shot(2, 5, true), effectMsg.getShot());
checkShip(effectMsg.getDestroyedShip(), 2, 2, 6, DOWN, NORMAL);
assertNull(effectMsg.getRemainingOpponentShips());
assertEquals(1, messages.peek().id());
effectMsg = (EffectMessage) messages.poll().msg();
assertFalse(effectMsg.isOwnShot());
assertEquals(new Shot(2, 5, true), effectMsg.getShot());
checkShip(effectMsg.getDestroyedShip(), 2, 2, 6, DOWN, NORMAL);
assertNull(effectMsg.getRemainingOpponentShips());
serverLogic.received(new ShootMessage(p(3, 3)), 0);
assertEquals(0, messages.peek().id());
effectMsg = (EffectMessage) messages.poll().msg();
assertTrue(effectMsg.isOwnShot());
assertEquals(new Shot(3, 3, false), effectMsg.getShot());
assertNull(effectMsg.getDestroyedShip());
assertNull(effectMsg.getRemainingOpponentShips());
assertEquals(1, messages.peek().id());
effectMsg = (EffectMessage) messages.poll().msg();
assertFalse(effectMsg.isOwnShot());
assertEquals(new Shot(3, 3, false), effectMsg.getShot());
assertNull(effectMsg.getDestroyedShip());
assertNull(effectMsg.getRemainingOpponentShips());
serverLogic.received(new ShootMessage(p(8, 9)), 1);
assertEquals(1, messages.peek().id());
effectMsg = (EffectMessage) messages.poll().msg();
assertTrue(effectMsg.isOwnShot());
assertEquals(new Shot(8, 9, true), effectMsg.getShot());
checkShip(effectMsg.getDestroyedShip(), 1, 8, 9, RIGHT, NORMAL);
assertNull(effectMsg.getRemainingOpponentShips());
assertEquals(0, messages.peek().id());
effectMsg = (EffectMessage) messages.poll().msg();
assertFalse(effectMsg.isOwnShot());
assertEquals(new Shot(8, 9, true), effectMsg.getShot());
checkShip(effectMsg.getDestroyedShip(), 1, 8, 9, RIGHT, NORMAL);
assertNull(effectMsg.getRemainingOpponentShips());
serverLogic.received(new ShootMessage(p(5, 3)), 1);
assertEquals(1, messages.peek().id());
effectMsg = (EffectMessage) messages.poll().msg();
assertTrue(effectMsg.isOwnShot());
assertEquals(new Shot(5, 3, false), effectMsg.getShot());
assertNull(effectMsg.getDestroyedShip());
assertNull(effectMsg.getRemainingOpponentShips());
assertEquals(0, messages.peek().id());
effectMsg = (EffectMessage) messages.poll().msg();
assertFalse(effectMsg.isOwnShot());
assertEquals(new Shot(5, 3, false), effectMsg.getShot());
assertNull(effectMsg.getDestroyedShip());
assertNull(effectMsg.getRemainingOpponentShips());
serverLogic.received(new ShootMessage(p(2, 3)), 0);
assertEquals(0, messages.peek().id());
effectMsg = (EffectMessage) messages.poll().msg();
assertTrue(effectMsg.isOwnShot());
assertEquals(new Shot(2, 3, true), effectMsg.getShot());
assertNull(effectMsg.getDestroyedShip());
assertNull(effectMsg.getRemainingOpponentShips());
assertEquals(1, messages.peek().id());
effectMsg = (EffectMessage) messages.poll().msg();
assertFalse(effectMsg.isOwnShot());
assertEquals(new Shot(2, 3, true), effectMsg.getShot());
assertNull(effectMsg.getDestroyedShip());
assertNull(effectMsg.getRemainingOpponentShips());
serverLogic.received(new ShootMessage(p(2, 2)), 0);
assertEquals(0, messages.peek().id());
effectMsg = (EffectMessage) messages.poll().msg();
assertTrue(effectMsg.isOwnShot());
assertEquals(new Shot(2, 2, true), effectMsg.getShot());
assertNull(effectMsg.getDestroyedShip());
assertNull(effectMsg.getRemainingOpponentShips());
assertEquals(1, messages.peek().id());
effectMsg = (EffectMessage) messages.poll().msg();
assertFalse(effectMsg.isOwnShot());
assertEquals(new Shot(2, 2, true), effectMsg.getShot());
assertNull(effectMsg.getDestroyedShip());
assertNull(effectMsg.getRemainingOpponentShips());
serverLogic.received(new ShootMessage(p(2, 1)), 0);
assertEquals(0, messages.peek().id());
effectMsg = (EffectMessage) messages.poll().msg();
assertTrue(effectMsg.isOwnShot());
assertEquals(new Shot(2, 1, true), effectMsg.getShot());
checkShip(effectMsg.getDestroyedShip(), 3, 2, 3, DOWN, NORMAL);
assertNull(effectMsg.getRemainingOpponentShips());
assertEquals(1, messages.peek().id());
effectMsg = (EffectMessage) messages.poll().msg();
assertFalse(effectMsg.isOwnShot());
assertEquals(new Shot(2, 1, true), effectMsg.getShot());
checkShip(effectMsg.getDestroyedShip(), 3, 2, 3, DOWN, NORMAL);
assertNull(effectMsg.getRemainingOpponentShips());
serverLogic.received(new ShootMessage(p(4, 4)), 0);
assertEquals(0, messages.peek().id());
effectMsg = (EffectMessage) messages.poll().msg();
assertTrue(effectMsg.isOwnShot());
assertEquals(new Shot(4, 4, true), effectMsg.getShot());
assertNull(effectMsg.getDestroyedShip());
assertNull(effectMsg.getRemainingOpponentShips());
assertEquals(1, messages.peek().id());
effectMsg = (EffectMessage) messages.poll().msg();
assertFalse(effectMsg.isOwnShot());
assertEquals(new Shot(4, 4, true), effectMsg.getShot());
assertNull(effectMsg.getDestroyedShip());
assertNull(effectMsg.getRemainingOpponentShips());
serverLogic.received(new ShootMessage(p(5, 4)), 0);
assertEquals(0, messages.peek().id());
effectMsg = (EffectMessage) messages.poll().msg();
assertTrue(effectMsg.isOwnShot());
assertEquals(new Shot(5, 4, true), effectMsg.getShot());
checkShip(effectMsg.getDestroyedShip(), 2, 4, 4, RIGHT, NORMAL);
assertNull(effectMsg.getRemainingOpponentShips());
assertEquals(1, messages.peek().id());
effectMsg = (EffectMessage) messages.poll().msg();
assertFalse(effectMsg.isOwnShot());
assertEquals(new Shot(5, 4, true), effectMsg.getShot());
checkShip(effectMsg.getDestroyedShip(), 2, 4, 4, RIGHT, NORMAL);
assertNull(effectMsg.getRemainingOpponentShips());
serverLogic.received(new ShootMessage(p(7, 4)), 0);
assertEquals(0, messages.peek().id());
effectMsg = (EffectMessage) messages.poll().msg();
assertTrue(effectMsg.isOwnShot());
assertEquals(new Shot(7, 4, true), effectMsg.getShot());
checkShip(effectMsg.getDestroyedShip(), 1, 7, 4, RIGHT, NORMAL);
assertNull(effectMsg.getRemainingOpponentShips());
assertEquals(1, messages.peek().id());
effectMsg = (EffectMessage) messages.poll().msg();
assertFalse(effectMsg.isOwnShot());
assertEquals(new Shot(7, 4, true), effectMsg.getShot());
checkShip(effectMsg.getDestroyedShip(), 1, 7, 4, RIGHT, NORMAL);
assertNull(effectMsg.getRemainingOpponentShips());
serverLogic.received(new ShootMessage(p(9, 4)), 0);
assertEquals(0, messages.peek().id());
effectMsg = (EffectMessage) messages.poll().msg();
assertTrue(effectMsg.isOwnShot());
assertEquals(new Shot(9, 4, false), effectMsg.getShot());
assertNull(effectMsg.getDestroyedShip());
assertNull(effectMsg.getRemainingOpponentShips());
assertEquals(1, messages.peek().id());
effectMsg = (EffectMessage) messages.poll().msg();
assertFalse(effectMsg.isOwnShot());
assertEquals(new Shot(9, 4, false), effectMsg.getShot());
assertNull(effectMsg.getDestroyedShip());
assertNull(effectMsg.getRemainingOpponentShips());
serverLogic.received(new ShootMessage(p(8, 4)), 1);
assertEquals(1, messages.peek().id());
effectMsg = (EffectMessage) messages.poll().msg();
assertTrue(effectMsg.isOwnShot());
assertEquals(new Shot(8, 4, false), effectMsg.getShot());
assertNull(effectMsg.getDestroyedShip());
assertNull(effectMsg.getRemainingOpponentShips());
assertEquals(0, messages.peek().id());
effectMsg = (EffectMessage) messages.poll().msg();
assertFalse(effectMsg.isOwnShot());
assertEquals(new Shot(8, 4, false), effectMsg.getShot());
assertNull(effectMsg.getDestroyedShip());
assertNull(effectMsg.getRemainingOpponentShips());
serverLogic.received(new ShootMessage(p(7, 6)), 0);
assertEquals(0, messages.peek().id());
effectMsg = (EffectMessage) messages.poll().msg();
assertTrue(effectMsg.isOwnShot());
assertEquals(new Shot(7, 6, true), effectMsg.getShot());
checkShip(effectMsg.getDestroyedShip(), 1, 7, 6, RIGHT, NORMAL);
assertNull(effectMsg.getRemainingOpponentShips());
assertEquals(1, messages.peek().id());
effectMsg = (EffectMessage) messages.poll().msg();
assertFalse(effectMsg.isOwnShot());
assertEquals(new Shot(7, 6, true), effectMsg.getShot());
checkShip(effectMsg.getDestroyedShip(), 1, 7, 6, RIGHT, NORMAL);
assertNull(effectMsg.getRemainingOpponentShips());
serverLogic.received(new ShootMessage(p(4, 2)), 0);
assertEquals(0, messages.peek().id());
effectMsg = (EffectMessage) messages.poll().msg();
assertTrue(effectMsg.isOwnShot());
assertEquals(new Shot(4, 2, true), effectMsg.getShot());
checkShip(effectMsg.getDestroyedShip(), 1, 4, 2, RIGHT, NORMAL);
assertNull(effectMsg.getRemainingOpponentShips());
assertEquals(1, messages.peek().id());
effectMsg = (EffectMessage) messages.poll().msg();
assertFalse(effectMsg.isOwnShot());
assertEquals(new Shot(4, 2, true), effectMsg.getShot());
checkShip(effectMsg.getDestroyedShip(), 1, 4, 2, RIGHT, NORMAL);
assertNull(effectMsg.getRemainingOpponentShips());
serverLogic.received(new ShootMessage(p(6, 2)), 0);
assertEquals(0, messages.peek().id());
effectMsg = (EffectMessage) messages.poll().msg();
assertTrue(effectMsg.isOwnShot());
assertEquals(new Shot(6, 2, true), effectMsg.getShot());
checkShip(effectMsg.getDestroyedShip(), 1, 6, 2, RIGHT, NORMAL);
assertEquals(0, effectMsg.getRemainingOpponentShips().size());
assertEquals(1, messages.peek().id());
effectMsg = (EffectMessage) messages.poll().msg();
assertFalse(effectMsg.isOwnShot());
assertEquals(new Shot(6, 2, true), effectMsg.getShot());
checkShip(effectMsg.getDestroyedShip(), 1, 6, 2, RIGHT, NORMAL);
assertEquals(8, effectMsg.getRemainingOpponentShips().size());
checkShip(effectMsg.getRemainingOpponentShips().get(0), 4, 0, 5, DOWN, NORMAL);
checkShip(effectMsg.getRemainingOpponentShips().get(1), 3, 0, 9, DOWN, NORMAL);
checkShip(effectMsg.getRemainingOpponentShips().get(2), 3, 0, 0, RIGHT, NORMAL);
checkShip(effectMsg.getRemainingOpponentShips().get(3), 2, 4, 0, RIGHT, NORMAL);
checkShip(effectMsg.getRemainingOpponentShips().get(4), 2, 2, 9, RIGHT, NORMAL);
checkShip(effectMsg.getRemainingOpponentShips().get(5), 1, 7, 0, RIGHT, NORMAL);
checkShip(effectMsg.getRemainingOpponentShips().get(6), 1, 9, 0, RIGHT, NORMAL);
checkShip(effectMsg.getRemainingOpponentShips().get(7), 1, 9, 7, RIGHT, NORMAL);
// Player 1 wins
assertEquals(0, messages.size());
}
}

View File

@@ -0,0 +1,122 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.game.server;
import org.junit.Before;
import org.junit.Test;
import pp.battleship.BattleshipConfig;
import pp.battleship.message.client.MapMessage;
import pp.battleship.message.client.ShootMessage;
import pp.battleship.message.server.EffectMessage;
import pp.battleship.message.server.GameDetails;
import pp.battleship.message.server.StartBattleMessage;
import pp.battleship.model.Battleship;
import pp.battleship.model.IntPoint;
import pp.battleship.model.Rotation;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.anyInt;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
public class ServerGameLogicTest {
private ServerGameLogic serverGameLogic;
private ServerSender mockServerSender;
@Before
public void setUp() {
mockServerSender = mock(ServerSender.class);
serverGameLogic = new ServerGameLogic(mockServerSender, new BattleshipConfig());
}
@Test
public void testAddPlayer() {
serverGameLogic.addPlayer(1);
serverGameLogic.addPlayer(2);
verify(mockServerSender, times(2)).send(anyInt(), any(GameDetails.class));
assertEquals(ServerState.SET_UP, serverGameLogic.getState());
}
@Test
public void testGetPlayerById() {
final Player player1 = serverGameLogic.addPlayer(1);
final Player player2 = serverGameLogic.addPlayer(2);
assertEquals(player1, serverGameLogic.getPlayerById(1));
assertEquals(player2, serverGameLogic.getPlayerById(2));
}
@Test
public void testReceivedMapMessage() {
serverGameLogic.addPlayer(1);
serverGameLogic.addPlayer(2);
List<Battleship> ships = List.of(new Battleship(3, 2, 2, Rotation.RIGHT));
MapMessage mapMessage = new MapMessage(ships);
serverGameLogic.received(mapMessage, 1);
serverGameLogic.received(mapMessage, 2);
verify(mockServerSender, times(2)).send(anyInt(), any(StartBattleMessage.class));
assertEquals(ServerState.BATTLE, serverGameLogic.getState());
}
@Test
public void testReceivedShootMessage() {
serverGameLogic.addPlayer(1);
serverGameLogic.addPlayer(2);
List<Battleship> ships = List.of(new Battleship(3, 2, 2, Rotation.RIGHT));
MapMessage mapMessage = new MapMessage(ships);
serverGameLogic.received(mapMessage, 1);
serverGameLogic.received(mapMessage, 2);
ShootMessage shootMessage = new ShootMessage(p(2, 2));
serverGameLogic.received(shootMessage, 1);
verify(mockServerSender, times(2)).send(anyInt(), any(EffectMessage.class));
}
@Test
public void testPlayerReady() {
final Player player1 = serverGameLogic.addPlayer(1);
final Player player2 = serverGameLogic.addPlayer(2);
List<Battleship> ships = List.of(new Battleship(3, 2, 2, Rotation.RIGHT));
serverGameLogic.playerReady(player1, ships);
serverGameLogic.playerReady(player2, ships);
verify(mockServerSender, times(2)).send(anyInt(), any(StartBattleMessage.class));
assertEquals(ServerState.BATTLE, serverGameLogic.getState());
}
@Test
public void testShoot() {
final Player player1 = serverGameLogic.addPlayer(1);
final Player player2 = serverGameLogic.addPlayer(2);
List<Battleship> ships = List.of(new Battleship(3, 2, 2, Rotation.RIGHT));
serverGameLogic.playerReady(player1, ships);
serverGameLogic.playerReady(player2, ships);
serverGameLogic.shoot(player1, p(2, 2));
verify(mockServerSender, times(2)).send(anyInt(), any(EffectMessage.class));
}
@Test
public void testSetState() {
serverGameLogic.setState(ServerState.BATTLE);
assertEquals(ServerState.BATTLE, serverGameLogic.getState());
}
@Test
public void testGetOpponent() {
final Player player1 = serverGameLogic.addPlayer(1);
final Player player2 = serverGameLogic.addPlayer(2);
assertEquals(player2, serverGameLogic.getOpponent(player1));
}
static IntPoint p(int x, int y) {
return new IntPoint(x, y);
}
}

View File

@@ -0,0 +1,145 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.model;
import org.junit.Before;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
public class BattleshipTest {
private Battleship ship;
@Before
public void setUp() {
ship = new Battleship(3, 2, 2, Rotation.RIGHT);
}
@Test
public void testDefaultConstructor() {
Battleship defaultShip = new Battleship(1, 0, 0, Rotation.RIGHT);
assertEquals(1, defaultShip.getLength());
assertEquals(0, defaultShip.getX());
assertEquals(0, defaultShip.getY());
assertEquals(Rotation.RIGHT, defaultShip.getRot());
assertEquals(Battleship.Status.NORMAL, defaultShip.getStatus());
}
@Test
public void testConstructorWithParameters() {
assertEquals(3, ship.getLength());
assertEquals(2, ship.getX());
assertEquals(2, ship.getY());
assertEquals(Rotation.RIGHT, ship.getRot());
assertEquals(Battleship.Status.NORMAL, ship.getStatus());
}
@Test
public void testSetPosition() {
ship.moveTo(4, 5);
assertEquals(4, ship.getX());
assertEquals(5, ship.getY());
}
@Test
public void testSetPositionWithIntPosition() {
IntPoint pos = p(5, 6);
ship.moveTo(pos);
assertEquals(5, ship.getX());
assertEquals(6, ship.getY());
}
@Test
public void testGetStatus() {
assertEquals(Battleship.Status.NORMAL, ship.getStatus());
}
@Test
public void testWithStatus() {
ship.setStatus(Battleship.Status.VALID_PREVIEW);
assertEquals(Battleship.Status.VALID_PREVIEW, ship.getStatus());
}
@Test
public void testGetLength() {
assertEquals(3, ship.getLength());
}
@Test
public void testGetMinX() {
assertEquals(2, ship.getMinX());
}
@Test
public void testGetMaxX() {
assertEquals(4, ship.getMaxX());
}
@Test
public void testGetMaxY() {
assertEquals(2, ship.getMaxY());
}
@Test
public void testGetMinY() {
assertEquals(2, ship.getMinY());
}
@Test
public void testGetRotation() {
assertEquals(Rotation.RIGHT, ship.getRot());
}
@Test
public void testSetRotation() {
ship.setRotation(Rotation.DOWN);
assertEquals(Rotation.DOWN, ship.getRot());
}
@Test
public void testRotated() {
ship.rotated();
assertEquals(Rotation.DOWN, ship.getRot());
}
@Test
public void testHit() {
assertTrue(ship.hit(2, 2));
assertTrue(ship.hit(2, 2));
}
@Test
public void testContains() {
IntPoint pos = p(3, 2);
assertTrue(ship.contains(pos));
}
@Test
public void testIsDestroyed() {
ship.hit(2, 2);
ship.hit(3, 2);
ship.hit(4, 2);
assertTrue(ship.isDestroyed());
}
@Test
public void testCollidesWith() {
Battleship otherShip = new Battleship(3, 3, 2, Rotation.RIGHT);
assertTrue(ship.collidesWith(otherShip));
}
@Test
public void testToString() {
assertEquals("Ship{length=3, x=2, y=2, rot=RIGHT}", ship.toString());
}
static IntPoint p(int x, int y) {
return new IntPoint(x, y);
}
}

View File

@@ -0,0 +1,37 @@
package pp.battleship.model;
import org.junit.Test;
import pp.util.RandomPositionIterator;
import java.util.HashSet;
import java.util.Set;
import static org.junit.Assert.fail;
public class RandomPositionIteratorTest {
@Test
public void testEqualsOccurs() {
final var it = new RandomPositionIterator<>(IntPoint::new, 2, 2);
final Set<IntPoint> positions = new HashSet<>();
positions.add(new IntPoint(0, 0));
positions.add(new IntPoint(1, 1));
int n = 0;
while (it.hasNext() && ++n <= 3) {
if (!positions.add(it.next()))
return;
}
fail("not found");
}
@Test
public void testNoEquals() {
final var it = new RandomPositionIterator<>(IntPoint::new, 2, 2);
final Set<IntPoint> positions = new HashSet<>();
while (it.hasNext()) {
if (!positions.add(it.next()))
fail("not found");
}
}
}

View File

@@ -0,0 +1,116 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.model;
import org.junit.Before;
import org.junit.Test;
import pp.battleship.notification.GameEventBroker;
import pp.battleship.notification.ItemAddedEvent;
import pp.battleship.notification.ItemRemovedEvent;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static pp.battleship.model.BattleshipTest.p;
public class ShipMapTest {
private ShipMap map;
private GameEventBroker mockBroker;
private Battleship battleship;
@Before
public void setUp() {
mockBroker = mock(GameEventBroker.class);
map = new ShipMap(10, 10, mockBroker);
battleship = new Battleship(3, 2, 2, Rotation.RIGHT);
}
@Test
public void testConstructorWithValidSize() {
assertEquals(10, map.getWidth());
assertEquals(10, map.getHeight());
}
@Test(expected = IllegalArgumentException.class)
public void testConstructorWithInvalidSize() {
new ShipMap(0, 10, null);
}
@Test
public void testAddItem() {
map.add(battleship);
List<Item> items = map.getItems();
assertTrue(items.contains(battleship));
verify(mockBroker).notifyListeners(any(ItemAddedEvent.class));
}
@Test
public void testRemoveItem() {
map.add(battleship);
map.remove(battleship);
final List<Item> items = map.getItems();
assertFalse(items.contains(battleship));
verify(mockBroker).notifyListeners(any(ItemAddedEvent.class));
verify(mockBroker).notifyListeners(any(ItemRemovedEvent.class));
}
@Test
public void testClear() {
map.add(battleship);
map.clear();
final List<Item> items = map.getItems();
assertTrue(items.isEmpty());
verify(mockBroker).notifyListeners(any(ItemAddedEvent.class));
verify(mockBroker).notifyListeners(any(ItemRemovedEvent.class));
}
@Test
public void testGetRemainingShips() {
map.add(battleship);
final List<Battleship> remainingShips = map.getRemainingShips();
assertTrue(remainingShips.contains(battleship));
}
@Test
public void testGetShots() {
final Shot shot = new Shot(1, 1, false);
map.add(shot);
final List<Shot> shots = map.getShots().toList();
assertTrue(shots.contains(shot));
}
@Test
public void testGetItems() {
map.add(battleship);
final List<Item> items = map.getItems();
assertTrue(items.contains(battleship));
}
@Test
public void testFindShipAt() {
map.add(battleship);
Battleship foundShip = map.findShipAt(2, 2);
assertEquals(battleship, foundShip);
}
@Test
public void testIsValidPosition() {
assertTrue(map.isValid(p(5, 5)));
}
@Test
public void testIsValidCoordinates() {
assertTrue(map.isValid(5, 5));
assertFalse(map.isValid(10, 10));
}
}