mirror of
https://athene2.informatik.unibw-muenchen.de/progproj/gruppen-ht24/Gruppe-02.git
synced 2025-08-08 07:33:31 +02:00
added contents
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
@@ -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 */ }
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
@@ -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 */ }
|
||||
}
|
@@ -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();
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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();
|
||||
}
|
@@ -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()));
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
@@ -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
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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());
|
||||
}
|
||||
}
|
@@ -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()));
|
||||
}
|
||||
}
|
@@ -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();
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
@@ -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);
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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";
|
||||
}
|
||||
}
|
@@ -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";
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
@@ -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();
|
||||
}
|
@@ -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";
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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
|
||||
}
|
||||
}
|
@@ -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();
|
||||
}
|
@@ -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);
|
||||
}
|
@@ -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];
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
@@ -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);
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
@@ -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);
|
||||
}
|
@@ -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 */ }
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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
|
@@ -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
|
Reference in New Issue
Block a user