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,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