added contents

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

View File

@@ -0,0 +1,22 @@
plugins {
id 'buildlogic.jme-application-conventions'
}
description = 'Battleship Client'
dependencies {
implementation project(":jme-common")
implementation project(":battleship:model")
implementation libs.jme3.desktop
runtimeOnly libs.jme3.awt.dialogs
runtimeOnly libs.jme3.plugins
runtimeOnly libs.jme3.jogg
runtimeOnly libs.jme3.testdata
}
application {
mainClass = 'pp.battleship.client.BattleshipApp'
applicationName = 'battleship'
}

View File

@@ -0,0 +1,73 @@
########################################
## Programming project code
## UniBw M, 2022, 2023, 2024
## www.unibw.de/inf2
## (c) Mark Minas (mark.minas@unibw.de)
########################################
#
# Battleship client configuration
#
# Specifies the map used by the opponent in single mode.
# Single mode is activated if this property is set.
#map.opponent=maps/map2.json
#
# Specifies the map used by the player in single mode.
# The player must define their own map if this property is not set.
map.own=maps/map1.json
#
# Coordinates of the shots fired by the RobotClient in the order listed.
# Example:
# 2, 0,\
# 2, 1,\
# 2, 2,\
# 2, 3
# defines four shots, namely at the coordinates
# (x=2, y=0), (x=2, y=1), (x=2, y=2), and (x=2, y=3)
robot.targets=2, 0,\
2, 1,\
2, 2,\
2, 3
#
# Delay in milliseconds between each shot fired by the RobotClient.
robot.delay=500
#
# The dimensions of the game map used in single mode.
# 'map.width' defines the number of columns, and 'map.height' defines the number of rows.
map.width=10
map.height=10
#
# The number of ships of each length available in single mode.
# The value is a comma-separated list where each element corresponds to the number of ships
# with a specific length. For example:
# ship.nums=4, 3, 2, 1
# This configuration means:
# - 4 ships of length 1
# - 3 ships of length 2
# - 2 ships of length 3
# - 1 ship of length 4
ship.nums=4, 3, 2, 1
#
# Screen settings
#
# Color of the text displayed at the top of the overlay.
# The format is (red, green, blue, alpha) where each value ranges from 0 to 1.
overlay.top.color=1, 1, 1, 1
#
# Application settings configuration
# Determines whether the settings window is shown at startup.
settings.show=false
#
# Specifies the width of the application window in pixels.
settings.resolution.width=1200
#
# Specifies the height of the application window in pixels.
settings.resolution.height=800
#
# Determines whether the application runs in full-screen mode.
settings.full-screen=false
#
# Enables or disables gamma correction to improve color accuracy.
settings.use-gamma-correction=true
#
# Indicates whether the statistics window is displayed during gameplay.
statistics.show=false

View File

@@ -0,0 +1,8 @@
handlers=java.util.logging.ConsoleHandler
.level=INFO
pp.level=FINE
com.jme3.network.level=INFO
;com.jme3.util.TangentBinormalGenerator.level=SEVERE
java.util.logging.ConsoleHandler.level=FINER
java.util.logging.ConsoleHandler.formatter=java.util.logging.SimpleFormatter
;java.util.logging.SimpleFormatter.format=[%4$s %2$s] %5$s%n

View File

@@ -0,0 +1,66 @@
{
"width": 10,
"height": 10,
"ships": [
{
"length": 4,
"x": 2,
"y": 8,
"rot": "RIGHT"
},
{
"length": 3,
"x": 2,
"y": 5,
"rot": "DOWN"
},
{
"length": 3,
"x": 5,
"y": 6,
"rot": "RIGHT"
},
{
"length": 2,
"x": 4,
"y": 4,
"rot": "RIGHT"
},
{
"length": 2,
"x": 7,
"y": 4,
"rot": "RIGHT"
},
{
"length": 2,
"x": 4,
"y": 2,
"rot": "DOWN"
},
{
"length": 1,
"x": 6,
"y": 2,
"rot": "RIGHT"
},
{
"length": 1,
"x": 8,
"y": 2,
"rot": "RIGHT"
},
{
"length": 1,
"x": 6,
"y": 0,
"rot": "RIGHT"
},
{
"length": 1,
"x": 8,
"y": 0,
"rot": "RIGHT"
}
]
}

View File

@@ -0,0 +1,66 @@
{
"width": 10,
"height": 10,
"ships": [
{
"length": 4,
"x": 0,
"y": 5,
"rot": "DOWN"
},
{
"length": 3,
"x": 0,
"y": 9,
"rot": "DOWN"
},
{
"length": 3,
"x": 2,
"y": 6,
"rot": "RIGHT"
},
{
"length": 2,
"x": 4,
"y": 8,
"rot": "RIGHT"
},
{
"length": 2,
"x": 2,
"y": 4,
"rot": "DOWN"
},
{
"length": 2,
"x": 2,
"y": 1,
"rot": "DOWN"
},
{
"length": 1,
"x": 6,
"y": 2,
"rot": "RIGHT"
},
{
"length": 1,
"x": 8,
"y": 2,
"rot": "RIGHT"
},
{
"length": 1,
"x": 6,
"y": 0,
"rot": "RIGHT"
},
{
"length": 1,
"x": 8,
"y": 0,
"rot": "RIGHT"
}
]
}

View File

@@ -0,0 +1,429 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.client;
import com.jme3.app.DebugKeysAppState;
import com.jme3.app.SimpleApplication;
import com.jme3.app.StatsAppState;
import com.jme3.font.BitmapFont;
import com.jme3.font.BitmapText;
import com.jme3.input.KeyInput;
import com.jme3.input.MouseInput;
import com.jme3.input.controls.ActionListener;
import com.jme3.input.controls.KeyTrigger;
import com.jme3.input.controls.MouseButtonTrigger;
import com.jme3.system.AppSettings;
import com.simsilica.lemur.GuiGlobals;
import com.simsilica.lemur.style.BaseStyles;
import pp.battleship.client.gui.BattleAppState;
import pp.battleship.client.gui.EditorAppState;
import pp.battleship.client.gui.SeaAppState;
import pp.battleship.game.client.BattleshipClient;
import pp.battleship.game.client.ClientGameLogic;
import pp.battleship.game.client.ServerConnection;
import pp.battleship.game.singlemode.BattleshipClientConfig;
import pp.battleship.game.singlemode.ServerConnectionMockup;
import pp.battleship.notification.ClientStateEvent;
import pp.battleship.notification.GameEventListener;
import pp.battleship.notification.InfoTextEvent;
import pp.dialog.DialogBuilder;
import pp.dialog.DialogManager;
import pp.graphics.Draw;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.lang.System.Logger;
import java.lang.System.Logger.Level;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.logging.LogManager;
import static pp.battleship.Resources.lookup;
/**
* The main class for the Battleship client application.
* It manages the initialization, input setup, GUI setup, and game states for the client.
*/
public class BattleshipApp extends SimpleApplication implements BattleshipClient, GameEventListener {
/**
* Logger for logging messages within the application.
*/
private static final Logger LOGGER = System.getLogger(BattleshipApp.class.getName());
/**
* Path to the styles script for GUI elements.
*/
private static final String STYLES_SCRIPT = "Interface/Lemur/pp-styles.groovy"; //NON-NLS
/**
* Path to the font resource used in the GUI.
*/
private static final String FONT = "Interface/Fonts/Default.fnt"; //NON-NLS
/**
* Path to the client configuration file, if one exists.
*/
private static final File CONFIG_FILE = new File("client.properties");
/**
* Input mapping name for mouse clicks.
*/
public static final String CLICK = "CLICK";
/**
* Input mapping name for the Escape key.
*/
private static final String ESC = "ESC";
/**
* Manager for handling dialogs within the application.
*/
private final DialogManager dialogManager = new DialogManager(this);
/**
* The server connection instance, used for communicating with the game server.
*/
private final ServerConnection serverConnection;
/**
* Instance of the {@link Draw} class for rendering graphics.
*/
private Draw draw;
/**
* Text display at the top of the GUI for showing information to the user.
*/
private BitmapText topText;
/**
* Executor service for handling asynchronous tasks within the application.
*/
private ExecutorService executor;
/**
* Handler for managing the client's game logic.
*/
private final ClientGameLogic logic;
/**
* Configuration settings for the Battleship client application.
*/
private final BattleshipAppConfig config;
/**
* Listener for handling actions triggered by the Escape key.
*/
private final ActionListener escapeListener = (name, isPressed, tpf) -> escape(isPressed);
static {
// Configure logging
LogManager manager = LogManager.getLogManager();
try {
manager.readConfiguration(new FileInputStream("logging.properties"));
LOGGER.log(Level.INFO, "Successfully read logging properties"); //NON-NLS
}
catch (IOException e) {
LOGGER.log(Level.INFO, e.getMessage());
}
}
/**
* Starts the Battleship application.
*
* @param args Command-line arguments for launching the application.
*/
public static void main(String[] args) {
new BattleshipApp().start();
}
/**
* Constructs a new {@code BattleshipApp} instance.
* Initializes the configuration, server connection, and game logic listeners.
*/
private BattleshipApp() {
config = new BattleshipAppConfig();
config.readFromIfExists(CONFIG_FILE);
serverConnection = makeServerConnection();
logic = new ClientGameLogic(serverConnection);
logic.addListener(this);
setShowSettings(config.getShowSettings());
setSettings(makeSettings());
}
/**
* Creates and configures application settings from the client configuration.
*
* @return A configured {@link AppSettings} object.
*/
private AppSettings makeSettings() {
final AppSettings settings = new AppSettings(true);
settings.setTitle(lookup("battleship.name"));
settings.setResolution(config.getResolutionWidth(), config.getResolutionHeight());
settings.setFullscreen(config.fullScreen());
settings.setUseRetinaFrameBuffer(config.useRetinaFrameBuffer());
settings.setGammaCorrection(config.useGammaCorrection());
return settings;
}
/**
* Factory method for creating a server connection based on the current
* client configuration.
*
* @return A {@link ServerConnection} instance, which could be a real or mock server.
*/
private ServerConnection makeServerConnection() {
if (config.isSingleMode())
return new ServerConnectionMockup(this);
return new NetworkSupport(this);
}
/**
* Returns the dialog manager responsible for managing in-game dialogs.
*
* @return The {@link DialogManager} instance.
*/
DialogManager getDialogManager() {
return dialogManager;
}
/**
* Returns the game logic handler for the client.
*
* @return The {@link ClientGameLogic} instance.
*/
@Override
public ClientGameLogic getGameLogic() {
return logic;
}
/**
* Returns the current configuration settings for the Battleship client.
*
* @return The {@link BattleshipClientConfig} instance.
*/
@Override
public BattleshipAppConfig getConfig() {
return config;
}
/**
* Initializes the application.
* Sets up input mappings, GUI, game states, and connects to the server.
*/
@Override
public void simpleInitApp() {
setPauseOnLostFocus(false);
draw = new Draw(assetManager);
setupInput();
setupStates();
setupGui();
serverConnection.connect();
}
/**
* Sets up the graphical user interface (GUI) for the application.
*/
private void setupGui() {
GuiGlobals.initialize(this);
BaseStyles.loadStyleResources(STYLES_SCRIPT);
GuiGlobals.getInstance().getStyles().setDefaultStyle("pp"); //NON-NLS
final BitmapFont normalFont = assetManager.loadFont(FONT); //NON-NLS
topText = new BitmapText(normalFont);
final int height = context.getSettings().getHeight();
topText.setLocalTranslation(10f, height - 10f, 0f);
topText.setColor(config.getTopColor());
guiNode.attachChild(topText);
}
/**
* Configures input mappings and sets up listeners for user interactions.
*/
private void setupInput() {
inputManager.deleteMapping(INPUT_MAPPING_EXIT);
inputManager.setCursorVisible(false);
inputManager.addMapping(ESC, new KeyTrigger(KeyInput.KEY_ESCAPE));
inputManager.addMapping(CLICK, new MouseButtonTrigger(MouseInput.BUTTON_LEFT));
inputManager.addListener(escapeListener, ESC);
}
/**
* Initializes and attaches the necessary application states for the game.
*/
private void setupStates() {
if (config.getShowStatistics()) {
final BitmapFont normalFont = assetManager.loadFont(FONT); //NON-NLS
final StatsAppState stats = new StatsAppState(guiNode, normalFont);
stateManager.attach(stats);
}
flyCam.setEnabled(false);
stateManager.detach(stateManager.getState(StatsAppState.class));
stateManager.detach(stateManager.getState(DebugKeysAppState.class));
attachGameSound();
stateManager.attachAll(new EditorAppState(), new BattleAppState(), new SeaAppState());
}
/**
* Attaches the game sound state and sets its initial enabled state.
*/
private void attachGameSound() {
final GameSound gameSound = new GameSound();
logic.addListener(gameSound);
gameSound.setEnabled(GameSound.enabledInPreferences());
stateManager.attach(gameSound);
}
/**
* Updates the application state every frame.
* This method is called once per frame during the game loop.
*
* @param tpf Time per frame in seconds.
*/
@Override
public void simpleUpdate(float tpf) {
super.simpleUpdate(tpf);
dialogManager.update(tpf);
logic.update(tpf);
}
/**
* Handles the Escape key action to either close the top dialog or show the main menu.
*
* @param isPressed Indicates whether the Escape key is pressed.
*/
private void escape(boolean isPressed) {
if (!isPressed) return;
if (dialogManager.showsDialog())
dialogManager.escape();
else
new Menu(this).open();
}
/**
* Returns the {@link Draw} instance used for rendering graphical elements in the game.
*
* @return The {@link Draw} instance.
*/
public Draw getDraw() {
return draw;
}
/**
* Handles a request to close the application.
* If the request is initiated by pressing ESC, this parameter is true.
*
* @param esc If true, the request is due to the ESC key being pressed.
*/
@Override
public void requestClose(boolean esc) { /* do nothing */ }
/**
* Closes the application, displaying a confirmation dialog if the client is connected to a server.
*/
public void closeApp() {
if (serverConnection.isConnected())
confirmDialog(lookup("confirm.leaving"), this::close);
else
close();
}
/**
* Closes the application, disconnecting from the server and stopping the application.
*/
private void close() {
serverConnection.disconnect();
stop();
}
/**
* Updates the informational text displayed in the GUI.
*
* @param text The information text to display.
*/
void setInfoText(String text) {
LOGGER.log(Level.DEBUG, "setInfoText {0}", text); //NON-NLS
topText.setText(text);
}
/**
* Updates the informational text in the GUI based on the key received in an {@link InfoTextEvent}.
*
* @param event The {@link InfoTextEvent} containing the key for the text to display.
*/
@Override
public void receivedEvent(InfoTextEvent event) {
LOGGER.log(Level.DEBUG, "received info text {0}", event.key()); //NON-NLS
setInfoText(lookup(event.key()));
}
/**
* Handles client state events to update the game states accordingly.
*
* @param event The {@link ClientStateEvent} representing the state change.
*/
@Override
public void receivedEvent(ClientStateEvent event) {
stateManager.getState(EditorAppState.class).setEnabled(logic.showEditor());
stateManager.getState(BattleAppState.class).setEnabled(logic.showBattle());
stateManager.getState(SeaAppState.class).setEnabled(logic.showBattle());
}
/**
* Returns the executor service used for handling multithreaded tasks.
*
* @return The {@link ExecutorService} instance.
*/
public ExecutorService getExecutor() {
if (executor == null)
executor = Executors.newCachedThreadPool();
return executor;
}
/**
* Stops the application, shutting down the executor service and halting execution.
*
* @param waitFor If true, waits for the application to stop before returning.
*/
@Override
public void stop(boolean waitFor) {
if (executor != null) executor.shutdownNow();
super.stop(waitFor);
}
/**
* Displays a confirmation dialog with a specified question and action for the "Yes" button.
*
* @param question The question to display in the dialog.
* @param yesAction The action to perform if "Yes" is selected.
*/
void confirmDialog(String question, Runnable yesAction) {
DialogBuilder.simple(dialogManager)
.setTitle(lookup("dialog.question"))
.setText(question)
.setOkButton(lookup("button.yes"), yesAction)
.setNoButton(lookup("button.no"))
.build()
.open();
}
/**
* Displays an error dialog with the specified error message.
*
* @param errorMessage The error message to display in the dialog.
*/
void errorDialog(String errorMessage) {
DialogBuilder.simple(dialogManager)
.setTitle(lookup("dialog.error"))
.setText(errorMessage)
.setOkButton(lookup("button.ok"))
.build()
.open();
}
}

View File

@@ -0,0 +1,196 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.client;
import com.jme3.math.ColorRGBA;
import pp.battleship.game.singlemode.BattleshipClientConfig;
/**
* Provides access to the Battleship application configuration.
* Extends {@link BattleshipClientConfig} to include additional properties specific to the client,
* particularly those related to screen settings and visual customization.
* <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 BattleshipAppConfig extends BattleshipClientConfig {
/**
* Converts a string value found in the properties file into an object of the specified type.
* Extends the superclass method to support conversion to {@link ColorRGBA}.
*
* @param value the string value to be converted
* @param targetType the target type into which the value string is converted
* @return the converted object of the specified type
*/
@Override
protected Object convertToType(String value, Class<?> targetType) {
if (targetType == ColorRGBA.class)
return makeColorRGBA(value);
return super.convertToType(value, targetType);
}
/**
* Converts the specified string value to a corresponding {@link ColorRGBA} object.
*
* @param value the color in the format "red, green, blue, alpha" with all values in the range [0..1]
* @return a {@link ColorRGBA} object representing the color
* @throws IllegalArgumentException if the input string is not in the expected format
*/
private static ColorRGBA makeColorRGBA(String value) {
String[] split = value.split(",", -1);
try {
if (split.length == 4)
return new ColorRGBA(Float.parseFloat(split[0]),
Float.parseFloat(split[1]),
Float.parseFloat(split[2]),
Float.parseFloat(split[3]));
}
catch (NumberFormatException e) {
// deliberately left empty
}
throw new IllegalArgumentException(value + " should consist of exactly 4 numbers");
}
/**
* The width of the game view resolution in pixels.
*/
@Property("settings.resolution.width") //NON-NLS
private int resolutionWidth = 1200;
/**
* The height of the game view resolution in pixels.
*/
@Property("settings.resolution.height") //NON-NLS
private int resolutionHeight = 800;
/**
* Specifies whether the game should start in full-screen mode.
*/
@Property("settings.full-screen") //NON-NLS
private boolean fullScreen = false;
/**
* Specifies whether gamma correction should be enabled.
* If enabled, the main framebuffer is configured for sRGB colors,
* and sRGB images are linearized.
* <p>
* Requires a GPU that supports GL_ARB_framebuffer_sRGB; otherwise, this setting will be ignored.
* </p>
*/
@Property("settings.use-gamma-correction") //NON-NLS
private boolean useGammaCorrection = true;
/**
* Specifies whether full resolution framebuffers should be used on Retina displays.
* This setting is ignored on non-Retina platforms.
*/
@Property("settings.use-retina-framebuffer") //NON-NLS
private boolean useRetinaFrameBuffer = false;
/**
* Specifies whether the settings window should be shown for configuring the game.
*/
@Property("settings.show") //NON-NLS
private boolean showSettings = false;
/**
* Specifies whether the JME statistics window should be shown in the lower left corner of the screen.
*/
@Property("statistics.show") //NON-NLS
private boolean showStatistics = false;
/**
* The color of the top text during gameplay, represented as a {@link ColorRGBA} object.
*/
@Property("overlay.top.color") //NON-NLS
private ColorRGBA topColor = ColorRGBA.White;
/**
* Creates a default {@code BattleshipAppConfig} with predefined values.
*/
public BattleshipAppConfig() {
// Default constructor
}
/**
* Returns the width of the game view resolution in pixels.
*
* @return the width of the game view resolution in pixels
*/
public int getResolutionWidth() {
return resolutionWidth;
}
/**
* Returns the height of the game view resolution in pixels.
*
* @return the height of the game view resolution in pixels
*/
public int getResolutionHeight() {
return resolutionHeight;
}
/**
* Returns whether the game should start in full-screen mode.
*
* @return {@code true} if the game should start in full-screen mode; {@code false} otherwise
*/
public boolean fullScreen() {
return fullScreen;
}
/**
* Returns whether gamma correction is enabled.
* If enabled, the main framebuffer is configured for sRGB colors,
* and sRGB images are linearized.
*
* @return {@code true} if gamma correction is enabled; {@code false} otherwise
*/
public boolean useGammaCorrection() {
return useGammaCorrection;
}
/**
* Returns whether full resolution framebuffers should be used on Retina displays.
* This setting is ignored on non-Retina platforms.
*
* @return {@code true} if full resolution framebuffers should be used on Retina displays; {@code false} otherwise
*/
public boolean useRetinaFrameBuffer() {
return useRetinaFrameBuffer;
}
/**
* Returns whether the settings window should be shown for configuring the game.
*
* @return {@code true} if the settings window should be shown; {@code false} otherwise
*/
public boolean getShowSettings() {
return showSettings;
}
/**
* Returns whether the JME statistics window should be shown in the lower left corner of the screen.
*
* @return {@code true} if the statistics window should be shown; {@code false} otherwise
*/
public boolean getShowStatistics() {
return showStatistics;
}
/**
* Returns the color of the top text during gameplay as a {@link ColorRGBA} object.
*
* @return the color of the top text during gameplay
*/
public ColorRGBA getTopColor() {
return topColor;
}
}

View File

@@ -0,0 +1,102 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.client;
import com.jme3.app.Application;
import com.jme3.app.state.AbstractAppState;
import com.jme3.app.state.AppStateManager;
import pp.battleship.game.client.ClientGameLogic;
/**
* Abstract class representing a state in the Battleship game.
* Extends the AbstractAppState from jMonkeyEngine to manage state behavior.
*/
public abstract class BattleshipAppState extends AbstractAppState {
private BattleshipApp app;
/**
* Creates a new BattleshipAppState that is initially disabled.
*
* @see #setEnabled(boolean)
*/
protected BattleshipAppState() {
setEnabled(false);
}
/**
* Initializes the state manager and application.
*
* @param stateManager The state manager
* @param application The application instance
*/
@Override
public void initialize(AppStateManager stateManager, Application application) {
super.initialize(stateManager, application);
this.app = (BattleshipApp) application;
if (isEnabled()) enableState();
}
/**
* Returns the BattleshipApp instance associated with this BattleshipAppState.
*
* @return The BattleshipApp instance.
*/
public BattleshipApp getApp() {
return app;
}
/**
* Returns the client game logic handler.
*
* @return the client game logic handler
*/
public ClientGameLogic getGameLogic() {
return app.getGameLogic();
}
/**
* Checks if any dialog is currently displayed.
*
* @return true if any dialog is currently shown, false otherwise
*/
public boolean showsDialog() {
return app.getDialogManager().showsDialog();
}
/**
* Sets the enabled state of the BattleshipAppState.
* If the new state is the same as the current state, the method returns.
*
* @param enabled The new enabled state.
*/
@Override
public void setEnabled(boolean enabled) {
if (isEnabled() == enabled) return;
super.setEnabled(enabled);
if (app != null) {
if (enabled)
enableState();
else
disableState();
}
}
/**
* This method is called when the state is enabled.
* It is meant to be overridden by subclasses to perform
* specific actions when the state is enabled.
*/
protected abstract void enableState();
/**
* This method is called when the state is disabled.
* It is meant to be overridden by subclasses to perform
* specific actions when the state is disabled.
*/
protected abstract void disableState();
}

View File

@@ -0,0 +1,135 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.client;
import com.jme3.app.Application;
import com.jme3.app.state.AbstractAppState;
import com.jme3.app.state.AppStateManager;
import com.jme3.asset.AssetLoadException;
import com.jme3.asset.AssetNotFoundException;
import com.jme3.audio.AudioData;
import com.jme3.audio.AudioNode;
import pp.battleship.notification.GameEventListener;
import pp.battleship.notification.SoundEvent;
import java.lang.System.Logger;
import java.lang.System.Logger.Level;
import java.util.prefs.Preferences;
import static pp.util.PreferencesUtils.getPreferences;
/**
* An application state that plays sounds.
*/
public class GameSound extends AbstractAppState implements GameEventListener {
private static final Logger LOGGER = System.getLogger(GameSound.class.getName());
private static final Preferences PREFERENCES = getPreferences(GameSound.class);
private static final String ENABLED_PREF = "enabled"; //NON-NLS
private AudioNode splashSound;
private AudioNode shipDestroyedSound;
private AudioNode explosionSound;
/**
* Checks if sound is enabled in the preferences.
*
* @return {@code true} if sound is enabled, {@code false} otherwise.
*/
public static boolean enabledInPreferences() {
return PREFERENCES.getBoolean(ENABLED_PREF, true);
}
/**
* Toggles the game sound on or off.
*/
public void toggleSound() {
setEnabled(!isEnabled());
}
/**
* Sets the enabled state of this AppState.
* Overrides {@link com.jme3.app.state.AbstractAppState#setEnabled(boolean)}
*
* @param enabled {@code true} to enable the AppState, {@code false} to disable it.
*/
@Override
public void setEnabled(boolean enabled) {
if (isEnabled() == enabled) return;
super.setEnabled(enabled);
LOGGER.log(Level.INFO, "Sound enabled: {0}", enabled); //NON-NLS
PREFERENCES.putBoolean(ENABLED_PREF, enabled);
}
/**
* Initializes the sound effects for the game.
* Overrides {@link AbstractAppState#initialize(AppStateManager, Application)}
*
* @param stateManager The state manager
* @param app The application
*/
@Override
public void initialize(AppStateManager stateManager, Application app) {
super.initialize(stateManager, app);
shipDestroyedSound = loadSound(app, "Sound/Effects/sunken.wav"); //NON-NLS
splashSound = loadSound(app, "Sound/Effects/splash.wav"); //NON-NLS
explosionSound = loadSound(app, "Sound/Effects/explosion.wav"); //NON-NLS
}
/**
* Loads a sound from the specified file.
*
* @param app The application
* @param name The name of the sound file.
* @return The loaded AudioNode.
*/
private AudioNode loadSound(Application app, String name) {
try {
final AudioNode sound = new AudioNode(app.getAssetManager(), name, AudioData.DataType.Buffer);
sound.setLooping(false);
sound.setPositional(false);
return sound;
}
catch (AssetLoadException | AssetNotFoundException ex) {
LOGGER.log(Level.ERROR, ex.getMessage(), ex);
}
return null;
}
/**
* Plays the splash sound effect.
*/
public void splash() {
if (isEnabled() && splashSound != null)
splashSound.playInstance();
}
/**
* Plays the explosion sound effect.
*/
public void explosion() {
if (isEnabled() && explosionSound != null)
explosionSound.playInstance();
}
/**
* Plays sound effect when a ship has been destroyed.
*/
public void shipDestroyed() {
if (isEnabled() && shipDestroyedSound != null)
shipDestroyedSound.playInstance();
}
@Override
public void receivedEvent(SoundEvent event) {
switch (event.sound()) {
case EXPLOSION -> explosion();
case SPLASH -> splash();
case DESTROYED_SHIP -> shipDestroyed();
}
}
}

View File

@@ -0,0 +1,143 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.client;
import com.simsilica.lemur.Button;
import com.simsilica.lemur.Checkbox;
import com.simsilica.lemur.Label;
import com.simsilica.lemur.style.ElementId;
import pp.dialog.Dialog;
import pp.dialog.StateCheckboxModel;
import pp.dialog.TextInputDialog;
import java.io.File;
import java.io.IOException;
import java.util.prefs.Preferences;
import static pp.battleship.Resources.lookup;
import static pp.util.PreferencesUtils.getPreferences;
/**
* The Menu class represents the main menu in the Battleship game application.
* It extends the Dialog class and provides functionalities for loading, saving,
* returning to the game, and quitting the application.
*/
class Menu extends Dialog {
private static final Preferences PREFERENCES = getPreferences(Menu.class);
private static final String LAST_PATH = "last.file.path";
private final BattleshipApp app;
private final Button loadButton = new Button(lookup("menu.map.load"));
private final Button saveButton = new Button(lookup("menu.map.save"));
/**
* Constructs the Menu dialog for the Battleship application.
*
* @param app the BattleshipApp instance
*/
public Menu(BattleshipApp app) {
super(app.getDialogManager());
this.app = app;
addChild(new Label(lookup("battleship.name"), new ElementId("header"))); //NON-NLS
addChild(new Checkbox(lookup("menu.sound-enabled"),
new StateCheckboxModel(app, GameSound.class)));
addChild(loadButton)
.addClickCommands(s -> ifTopDialog(this::loadDialog));
addChild(saveButton)
.addClickCommands(s -> ifTopDialog(this::saveDialog));
addChild(new Button(lookup("menu.return-to-game")))
.addClickCommands(s -> ifTopDialog(this::close));
addChild(new Button(lookup("menu.quit")))
.addClickCommands(s -> ifTopDialog(app::closeApp));
update();
}
/**
* Updates the state of the load and save buttons based on the game logic.
*/
@Override
public void update() {
loadButton.setEnabled(app.getGameLogic().mayLoadMap());
saveButton.setEnabled(app.getGameLogic().maySaveMap());
}
/**
* As an escape action, this method closes the menu if it is the top dialog.
*/
@Override
public void escape() {
close();
}
/**
* Functional interface for file actions.
*/
@FunctionalInterface
private interface FileAction {
/**
* Executes a file action.
*
* @param file the file to be processed
* @throws IOException if an I/O error occurs
*/
void run(File file) throws IOException;
}
/**
* Handles the file action for the provided dialog.
*
* @param fileAction the file action to be executed
* @param dialog the dialog providing the file input
*/
private void handle(FileAction fileAction, TextInputDialog dialog) {
try {
final String path = dialog.getInput().getText();
PREFERENCES.put(LAST_PATH, path);
fileAction.run(new File(path));
dialog.close();
}
catch (IOException e) {
app.errorDialog(e.getLocalizedMessage());
}
}
/**
* Shows a file dialog for loading or saving files.
*
* @param fileAction the action to perform with the selected file
* @param label the label for the dialog
*/
private void fileDialog(FileAction fileAction, String label) {
final TextInputDialog dialog =
TextInputDialog.builder(app.getDialogManager())
.setLabel(lookup("label.file"))
.setFocus(TextInputDialog::getInput)
.setTitle(label)
.setOkButton(lookup("button.ok"), d -> handle(fileAction, d))
.setNoButton(lookup("button.cancel"))
.setOkClose(false)
.build();
final String path = PREFERENCES.get(LAST_PATH, null);
if (path != null)
dialog.getInput().setText(path.trim());
dialog.open();
}
/**
* Shows the load dialog for loading maps.
*/
private void loadDialog() {
fileDialog(app.getGameLogic()::loadMap, lookup("menu.map.load"));
}
/**
* Shows the save dialog for saving maps.
*/
private void saveDialog() {
fileDialog(app.getGameLogic()::saveMap, lookup("menu.map.save"));
}
}

View File

@@ -0,0 +1,153 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.client;
import com.simsilica.lemur.Container;
import com.simsilica.lemur.Label;
import com.simsilica.lemur.TextField;
import com.simsilica.lemur.component.SpringGridLayout;
import pp.dialog.Dialog;
import pp.dialog.DialogBuilder;
import pp.dialog.SimpleDialog;
import java.lang.System.Logger;
import java.lang.System.Logger.Level;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import static pp.battleship.Resources.lookup;
/**
* Represents a dialog for setting up a network connection in the Battleship game.
* Allows users to specify the host and port for connecting to a game server.
*/
class NetworkDialog extends SimpleDialog {
private static final Logger LOGGER = System.getLogger(NetworkDialog.class.getName());
private static final String LOCALHOST = "localhost"; //NON-NLS
private static final String DEFAULT_PORT = "1234"; //NON-NLS
private final NetworkSupport network;
private final TextField host = new TextField(LOCALHOST);
private final TextField port = new TextField(DEFAULT_PORT);
private String hostname;
private int portNumber;
private Future<Object> connectionFuture;
private Dialog progressDialog;
/**
* Constructs a new NetworkDialog.
*
* @param network The NetworkSupport instance to be used for network operations.
*/
NetworkDialog(NetworkSupport network) {
super(network.getApp().getDialogManager());
this.network = network;
host.setSingleLine(true);
host.setPreferredWidth(400f);
port.setSingleLine(true);
final BattleshipApp app = network.getApp();
final Container input = new Container(new SpringGridLayout());
input.addChild(new Label(lookup("host.name") + ": "));
input.addChild(host, 1);
input.addChild(new Label(lookup("port.number") + ": "));
input.addChild(port, 1);
DialogBuilder.simple(app.getDialogManager())
.setTitle(lookup("server.dialog"))
.setExtension(d -> d.addChild(input))
.setOkButton(lookup("button.connect"), d -> connect())
.setNoButton(lookup("button.cancel"), app::closeApp)
.setOkClose(false)
.setNoClose(false)
.build(this);
}
/**
* Handles the action for the connect button in the connection dialog.
* Tries to parse the port number and initiate connection to the server.
*/
private void connect() {
LOGGER.log(Level.INFO, "connect to host={0}, port={1}", host, port); //NON-NLS
try {
hostname = host.getText().trim().isEmpty() ? LOCALHOST : host.getText();
portNumber = Integer.parseInt(port.getText());
openProgressDialog();
connectionFuture = network.getApp().getExecutor().submit(this::initNetwork);
}
catch (NumberFormatException e) {
network.getApp().errorDialog(lookup("port.must.be.integer"));
}
}
/**
* Creates a dialog indicating that the connection is in progress.
*/
private void openProgressDialog() {
progressDialog = DialogBuilder.simple(network.getApp().getDialogManager())
.setText(lookup("label.connecting"))
.build();
progressDialog.open();
}
/**
* Tries to initialize the network connection.
*
* @throws RuntimeException If an error occurs when creating the client.
*/
private Object initNetwork() {
try {
network.initNetwork(hostname, portNumber);
return null;
}
catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* This method is called by {@linkplain pp.dialog.DialogManager#update(float)} for periodically
* updating this dialog. T
*/
@Override
public void update(float delta) {
if (connectionFuture != null && connectionFuture.isDone())
try {
connectionFuture.get();
success();
}
catch (ExecutionException e) {
failure(e.getCause());
}
catch (InterruptedException e) {
LOGGER.log(Level.WARNING, "Interrupted!", e); //NON-NLS
Thread.currentThread().interrupt();
}
}
/**
* Handles a successful connection to the game server.
*/
private void success() {
connectionFuture = null;
progressDialog.close();
this.close();
network.getApp().setInfoText(lookup("wait.for.an.opponent"));
}
/**
* Handles a failed connection attempt.
*
* @param e The cause of the failure.
*/
private void failure(Throwable e) {
connectionFuture = null;
progressDialog.close();
network.getApp().errorDialog(lookup("server.connection.failed"));
network.getApp().setInfoText(e.getLocalizedMessage());
}
}

View File

@@ -0,0 +1,152 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.client;
import com.jme3.network.Client;
import com.jme3.network.ClientStateListener;
import com.jme3.network.Message;
import com.jme3.network.MessageListener;
import com.jme3.network.Network;
import pp.battleship.game.client.ServerConnection;
import pp.battleship.message.client.ClientMessage;
import pp.battleship.message.server.ServerMessage;
import java.io.IOException;
import java.lang.System.Logger;
import java.lang.System.Logger.Level;
import static pp.battleship.Resources.lookup;
/**
* Manages the network connection for the Battleship application.
* Handles connecting to and disconnecting from the server, and sending messages.
*/
class NetworkSupport implements MessageListener<Client>, ClientStateListener, ServerConnection {
private static final Logger LOGGER = System.getLogger(NetworkSupport.class.getName());
private final BattleshipApp app;
private Client client;
/**
* Constructs a NetworkSupport instance for the given Battleship application.
*
* @param app The Battleship application instance.
*/
public NetworkSupport(BattleshipApp app) {
this.app = app;
}
/**
* Returns the Battleship application instance.
*
* @return Battleship application instance
*/
BattleshipApp getApp() {
return app;
}
/**
* Checks if there is a connection to the game server.
*
* @return true if there is a connection to the game server, false otherwise.
*/
@Override
public boolean isConnected() {
return client != null && client.isConnected();
}
/**
* Attempts to join the game if there is no connection yet.
* Opens a dialog for the user to enter the host and port information.
*/
@Override
public void connect() {
if (client == null)
new NetworkDialog(this).open();
}
/**
* Closes the client connection.
*/
@Override
public void disconnect() {
if (client == null) return;
client.close();
client = null;
LOGGER.log(Level.INFO, "client closed"); //NON-NLS
}
/**
* Initializes the network connection.
*
* @param host The server's address.
* @param port The server's port.
* @throws IOException If an I/O error occurs when creating the client.
*/
void initNetwork(String host, int port) throws IOException {
if (client != null)
throw new IllegalStateException("trying to join a game again");
client = Network.connectToServer(host, port);
client.start();
client.addMessageListener(this);
client.addClientStateListener(this);
}
/**
* Called when a message is received from the server.
*
* @param client The client instance that received the message.
* @param message The message received from the server.
*/
@Override
public void messageReceived(Client client, Message message) {
LOGGER.log(Level.INFO, "message received from server: {0}", message); //NON-NLS
if (message instanceof ServerMessage serverMessage)
app.enqueue(() -> serverMessage.accept(app.getGameLogic()));
}
/**
* Called when the client has successfully connected to the server.
*
* @param client The client that connected to the server.
*/
@Override
public void clientConnected(Client client) {
LOGGER.log(Level.INFO, "Client connected: {0}", client); //NON-NLS
}
/**
* Called when the client is disconnected from the server.
*
* @param client The client that was disconnected.
* @param disconnectInfo Information about the disconnection.
*/
@Override
public void clientDisconnected(Client client, DisconnectInfo disconnectInfo) {
LOGGER.log(Level.INFO, "Client {0} disconnected: {1}", client, disconnectInfo); //NON-NLS
if (this.client != client)
throw new IllegalArgumentException("parameter value must be client");
LOGGER.log(Level.INFO, "client still connected: {0}", client.isConnected()); //NON-NLS
this.client = null;
disconnect();
app.enqueue(() -> app.setInfoText(lookup("lost.connection.to.server")));
}
/**
* Sends the specified message to the server.
*
* @param message The message to be sent to the server.
*/
@Override
public void send(ClientMessage message) {
LOGGER.log(Level.INFO, "sending {0}", message); //NON-NLS
if (client == null)
app.errorDialog(lookup("lost.connection.to.server"));
else
client.send(message);
}
}

View File

@@ -0,0 +1,122 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.client.gui;
import com.jme3.input.controls.ActionListener;
import com.jme3.scene.Node;
import com.jme3.system.AppSettings;
import pp.battleship.client.BattleshipAppState;
import pp.battleship.model.IntPoint;
import java.lang.System.Logger;
import java.lang.System.Logger.Level;
import static pp.battleship.client.BattleshipApp.CLICK;
/**
* Represents the state responsible for managing the battle interface within the Battleship game.
* This state handles the display and interaction of the battle map, including the opponent's map.
* It manages GUI components, input events, and the layout of the interface when this state is enabled.
*/
public class BattleAppState extends BattleshipAppState {
private static final Logger LOGGER = System.getLogger(BattleAppState.class.getName());
private static final float DEPTH = 0f;
private static final float GAP = 20f;
/**
* A listener for handling click events in the battle interface.
* When a click is detected, it triggers the corresponding actions on the opponent's map.
*/
private final ActionListener clickListener = (name, isPressed, tpf) -> click(isPressed);
/**
* The root node for all GUI components in the battle state.
*/
private final Node battleNode = new Node("Battle"); //NON-NLS
/**
* A view representing the opponent's map in the GUI.
*/
private MapView opponentMapView;
/**
* Enables the battle state by initializing, laying out, and adding GUI components.
* Attaches the components to the GUI node and registers input listeners.
*/
@Override
protected void enableState() {
battleNode.detachAllChildren();
initializeGuiComponents();
layoutGuiComponents();
addGuiComponents();
getApp().getGuiNode().attachChild(battleNode);
getApp().getInputManager().addListener(clickListener, CLICK);
}
/**
* Disables the battle state by removing GUI components and unregistering input listeners.
* Also handles cleanup of resources, such as the opponent's map view.
*/
@Override
protected void disableState() {
getApp().getGuiNode().detachChild(battleNode);
getApp().getInputManager().removeListener(clickListener);
if (opponentMapView != null) {
opponentMapView.unregister();
opponentMapView = null;
}
}
/**
* Initializes the GUI components used in the battle state.
* Creates the opponent's map view and adds a grid overlay to it.
*/
private void initializeGuiComponents() {
opponentMapView = new MapView(getGameLogic().getOpponentMap(), getApp());
opponentMapView.addGrid();
}
/**
* Adds the initialized GUI components to the battle node.
* Currently, it attaches the opponent's map view to the node.
*/
private void addGuiComponents() {
battleNode.attachChild(opponentMapView.getNode());
}
/**
* Lays out the GUI components within the window, positioning them appropriately.
* The opponent's map view is positioned based on the window's dimensions and a specified gap.
*/
private void layoutGuiComponents() {
final AppSettings s = getApp().getContext().getSettings();
final float mapWidth = opponentMapView.getWidth();
final float mapHeight = opponentMapView.getHeight();
final float windowWidth = s.getWidth();
final float windowHeight = s.getHeight();
opponentMapView.getNode().setLocalTranslation(windowWidth - mapWidth - GAP,
windowHeight - mapHeight - GAP,
DEPTH);
}
/**
* Handles click events in the battle interface. If the event indicates a click (not a release),
* it translates the cursor position to the model's coordinate system and triggers the game logic
* for interacting with the opponent's map.
*
* @param isPressed whether the mouse button is currently pressed (true) or released (false)
*/
private void click(boolean isPressed) {
if (!isPressed || showsDialog())
return;
final IntPoint cursorPos = opponentMapView.mouseToModel(getApp().getInputManager().getCursorPosition());
LOGGER.log(Level.DEBUG, "click: {0}", cursorPos); //NON-NLS
getGameLogic().clickOpponentMap(cursorPos);
}
}

View File

@@ -0,0 +1,173 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.client.gui;
import com.jme3.input.controls.ActionListener;
import com.jme3.math.Vector2f;
import com.jme3.scene.Node;
import com.jme3.system.AppSettings;
import com.simsilica.lemur.Button;
import com.simsilica.lemur.Container;
import pp.battleship.client.BattleshipAppState;
import java.lang.System.Logger;
import java.lang.System.Logger.Level;
import static pp.battleship.Resources.lookup;
import static pp.battleship.client.BattleshipApp.CLICK;
/**
* EditorState manages the editor mode in the Battleship game,
* allowing players to place and rotate ships.
*/
public class EditorAppState extends BattleshipAppState {
private static final Logger LOGGER = System.getLogger(EditorAppState.class.getName());
private static final float DEPTH = 0f;
private static final float GAP = 20f;
private final ActionListener clickListener = (name, isPressed, tpf) -> click(isPressed);
private final Node editorNode = new Node("Editor"); //NON-NLS
private Container buttonContainer;
private Button rotateButton;
private Button readyButton;
private MapView ownMapView;
private MapView harborView;
private Vector2f oldCursorPosition;
/**
* Enables the editor state by attaching necessary nodes and listeners.
*/
@Override
protected void enableState() {
editorNode.detachAllChildren();
initializeGuiComponents();
addGuiComponents();
layoutGuiComponents();
getApp().getGuiNode().attachChild(editorNode);
getApp().getInputManager().addListener(clickListener, CLICK);
}
/**
* Disables the editor state by detaching nodes and removing listeners.
*/
@Override
protected void disableState() {
getApp().getGuiNode().detachChild(editorNode);
getApp().getInputManager().removeListener(clickListener);
if (ownMapView != null) {
ownMapView.unregister();
ownMapView = null;
}
if (harborView != null) {
harborView.unregister();
harborView = null;
}
}
/**
* Updates the editor state, handling cursor movement and enabling buttons.
*
* @param tpf Time per frame
*/
@Override
public void update(float tpf) {
super.update(tpf);
cursorMovement();
enableButtons();
}
/**
* Enables or disables buttons based on the logic state.
*/
private void enableButtons() {
readyButton.setEnabled(getGameLogic().isMapComplete());
rotateButton.setEnabled(getGameLogic().movingShip());
}
/**
* Handles cursor movement for previewing ship placement.
*/
private void cursorMovement() {
if (!getGameLogic().movingShip() || ownMapView == null || showsDialog())
return;
final Vector2f cursorPosition = getApp().getInputManager().getCursorPosition();
if (!cursorPosition.equals(oldCursorPosition)) {
oldCursorPosition = new Vector2f(cursorPosition);
getGameLogic().movePreview(ownMapView.mouseToModel(cursorPosition));
}
}
/**
* Initializes the GUI components for the editor.
*/
private void initializeGuiComponents() {
ownMapView = new MapView(getGameLogic().getOwnMap(), getApp());
harborView = new MapView(getGameLogic().getHarbor(), getApp());
ownMapView.addGrid();
rotateButton = new Button(lookup("button.rotate"));
readyButton = new Button(lookup("button.ready"));
rotateButton.addClickCommands(e -> {
if (!showsDialog())
getGameLogic().rotateShip();
});
readyButton.addClickCommands(e -> {
if (!showsDialog())
getGameLogic().mapFinished();
});
buttonContainer = new Container();
}
/**
* Adds the GUI components to the editor node.
*/
private void addGuiComponents() {
buttonContainer.addChild(rotateButton, 0, 0);
buttonContainer.addChild(readyButton, 0, 1);
editorNode.attachChild(ownMapView.getNode());
editorNode.attachChild(harborView.getNode());
editorNode.attachChild(buttonContainer);
}
/**
* Lays out the GUI components on the screen.
*/
private void layoutGuiComponents() {
final AppSettings s = getApp().getContext().getSettings();
final float harborWidth = harborView.getWidth();
final float harborHeight = harborView.getHeight();
final float ownMapWidth = ownMapView.getWidth();
final float ownMapHeight = ownMapView.getHeight();
final float windowWidth = s.getWidth();
final float windowHeight = s.getHeight();
ownMapView.getNode()
.setLocalTranslation(0.5f * (windowWidth - harborWidth - ownMapWidth - GAP),
0.5f * (windowHeight - ownMapHeight),
DEPTH);
harborView.getNode()
.setLocalTranslation(0.5f * (windowWidth - harborWidth + ownMapWidth + GAP),
0.5f * (windowHeight - harborHeight),
DEPTH);
buttonContainer.setLocalTranslation(0.5f * (windowWidth - harborWidth - ownMapWidth - GAP),
0.5f * (windowHeight - ownMapHeight - GAP),
DEPTH);
}
/**
* Handles click events to place or rotate ships on the maps.
*/
private void click(boolean isPressed) {
if (!isPressed || showsDialog()) return;
final Vector2f cursorPos = getApp().getInputManager().getCursorPosition();
LOGGER.log(Level.DEBUG, "click: {0}", cursorPos); //NON-NLS
getGameLogic().clickHarbor(harborView.mouseToModel(cursorPos));
getGameLogic().clickOwnMap(ownMapView.mouseToModel(cursorPos));
}
}

View File

@@ -0,0 +1,191 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.client.gui;
import com.jme3.material.Material;
import com.jme3.material.RenderState.BlendMode;
import com.jme3.math.ColorRGBA;
import com.jme3.math.Vector2f;
import com.jme3.math.Vector3f;
import com.jme3.scene.Geometry;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial.CullHint;
import com.jme3.scene.shape.Quad;
import pp.battleship.client.BattleshipApp;
import pp.battleship.model.IntPoint;
import pp.battleship.model.ShipMap;
import pp.util.FloatPoint;
import pp.util.Position;
/**
* Represents the visual view of a {@link ShipMap}, used to display the map structure such as the player's map, harbor,
* and opponent's map. This class handles the graphical representation of the map, including background setup, grid lines,
* and interaction between the model and the view.
*/
class MapView {
private static final float FIELD_SIZE = 40f;
private static final float GRID_LINE_WIDTH = 2f;
private static final float BACKGROUND_DEPTH = -4f;
private static final float GRID_DEPTH = -1f;
private static final ColorRGBA BACKGROUND_COLOR = new ColorRGBA(0, 0.05f, 0.05f, 0.5f);
private static final ColorRGBA GRID_COLOR = ColorRGBA.Green;
// Reference to the main application and the ship map being visualized
private final BattleshipApp app;
private final Node mapNode = new Node("map"); // NON-NLS
private final ShipMap map;
private final MapViewSynchronizer synchronizer;
/**
* Constructs a new MapView for a given {@link ShipMap} and {@link BattleshipApp}.
* Initializes the view by setting up the background and registering a synchronizer to listen to changes in the map.
*
* @param map the ship map to visualize
* @param app the main application instance
*/
MapView(ShipMap map, BattleshipApp app) {
this.map = map;
this.app = app;
this.synchronizer = new MapViewSynchronizer(this);
setupBackground();
app.getGameLogic().addListener(synchronizer);
}
/**
* Unregisters the {@link MapViewSynchronizer} from the listener list of the ClientGameLogic,
* stopping the view from receiving updates when the underlying {@link ShipMap} changes.
* After calling this method, this MapView instance should no longer be used.
*/
void unregister() {
app.getGameLogic().removeListener(synchronizer);
}
/**
* Gets the {@link ShipMap} associated with this view.
*
* @return the ship map
*/
public ShipMap getMap() {
return map;
}
/**
* Gets the {@link BattleshipApp} instance associated with this view.
*
* @return the main application instance
*/
public BattleshipApp getApp() {
return app;
}
/**
* Sets up the background of the map view using a quad geometry.
* The background is configured with a semi-transparent color and placed at a specific depth.
*/
private void setupBackground() {
final Material mat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md"); // NON-NLS
mat.setColor("Color", BACKGROUND_COLOR); // NON-NLS
mat.getAdditionalRenderState().setBlendMode(BlendMode.Alpha);
final Position corner = modelToView(map.getWidth(), map.getHeight());
final Geometry background = new Geometry("MapBackground", new Quad(corner.getX(), corner.getY()));
background.setMaterial(mat);
background.setLocalTranslation(0f, 0f, BACKGROUND_DEPTH);
background.setCullHint(CullHint.Never);
mapNode.attachChild(background);
}
/**
* Adds grid lines to the map view to visually separate the fields within the map.
* The grid lines are drawn based on the dimensions of the ship map.
*/
public void addGrid() {
for (int x = 0; x <= map.getWidth(); x++) {
final Position f = modelToView(x, 0);
final Position t = modelToView(x, map.getHeight());
mapNode.attachChild(gridLine(f, t));
}
for (int y = 0; y <= map.getHeight(); y++) {
final Position f = modelToView(0, y);
final Position t = modelToView(map.getWidth(), y);
mapNode.attachChild(gridLine(f, t));
}
}
/**
* Gets the root node containing all visual elements in this map view.
*
* @return the root node for the map view
*/
public Node getNode() {
return mapNode;
}
/**
* Gets the total width of the map in view coordinates.
*
* @return the width of the map in view coordinates
*/
public float getWidth() {
return FIELD_SIZE * map.getWidth();
}
/**
* Gets the total height of the map in view coordinates.
*
* @return the height of the map in view coordinates
*/
public float getHeight() {
return FIELD_SIZE * map.getHeight();
}
/**
* Converts coordinates from view coordinates to model coordinates.
*
* @param x the x-coordinate in view space
* @param y the y-coordinate in view space
* @return the corresponding model coordinates as an {@link IntPoint}
*/
public IntPoint viewToModel(float x, float y) {
return new IntPoint((int) Math.floor(x / FIELD_SIZE), (int) Math.floor(y / FIELD_SIZE));
}
/**
* Converts coordinates from model coordinates to view coordinates.
*
* @param x the x-coordinate in model space
* @param y the y-coordinate in model space
* @return the corresponding view coordinates as a {@link Position}
*/
public Position modelToView(float x, float y) {
return new FloatPoint(x * FIELD_SIZE, y * FIELD_SIZE);
}
/**
* Converts the mouse position to model coordinates.
* This method takes into account the map's transformation in the 3D scene.
*
* @param pos the 2D vector representing the mouse position in the view
* @return the corresponding model coordinates as an {@link IntPoint}
*/
public IntPoint mouseToModel(Vector2f pos) {
final Vector3f world = new Vector3f(pos.getX(), pos.getY(), 0f);
final Vector3f view = mapNode.getWorldTransform().transformInverseVector(world, null);
return viewToModel(view.getX(), view.getY());
}
/**
* Creates a visual representation of a grid line between two positions.
*
* @param p1 the start position of the grid line
* @param p2 the end position of the grid line
* @return a {@link Geometry} representing the grid line
*/
private Geometry gridLine(Position p1, Position p2) {
return app.getDraw().makeFatLine(p1, p2, GRID_DEPTH, GRID_COLOR, GRID_LINE_WIDTH);
}
}

View File

@@ -0,0 +1,125 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.client.gui;
import com.jme3.math.ColorRGBA;
import com.jme3.scene.Geometry;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;
import pp.battleship.model.Battleship;
import pp.battleship.model.Shot;
import pp.util.Position;
/**
* Synchronizes the visual representation of the ship map with the game model.
* It handles the rendering of ships and shots on the map view, updating the view
* whenever changes occur in the model.
*/
class MapViewSynchronizer extends ShipMapSynchronizer {
// Constants for rendering properties
private static final float SHIP_LINE_WIDTH = 6f;
private static final float SHOT_DEPTH = -2f;
private static final float SHIP_DEPTH = 0f;
private static final float INDENT = 4f;
// Colors used for different visual elements
private static final ColorRGBA HIT_COLOR = ColorRGBA.Red;
private static final ColorRGBA MISS_COLOR = ColorRGBA.Blue;
private static final ColorRGBA SHIP_BORDER_COLOR = ColorRGBA.White;
private static final ColorRGBA PREVIEW_COLOR = ColorRGBA.Gray;
private static final ColorRGBA ERROR_COLOR = ColorRGBA.Red;
// The MapView associated with this synchronizer
private final MapView view;
/**
* Constructs a new MapViewSynchronizer for the given MapView.
* Initializes the synchronizer and adds existing elements from the model to the view.
*
* @param view the MapView to synchronize with the game model
*/
public MapViewSynchronizer(MapView view) {
super(view.getMap(), view.getNode());
this.view = view;
addExisting();
}
/**
* Creates a visual representation of a shot on the map.
* A hit shot is represented in red, while a miss is represented in blue.
*
* @param shot the Shot object representing the shot in the model
* @return a Spatial representing the shot on the map
*/
@Override
public Spatial visit(Shot shot) {
// Convert the shot's model coordinates to view coordinates
final Position p1 = view.modelToView(shot.getX(), shot.getY());
final Position p2 = view.modelToView(shot.getX() + 1, shot.getY() + 1);
final ColorRGBA color = shot.isHit() ? HIT_COLOR : MISS_COLOR;
// Create and return a rectangle representing the shot
return view.getApp().getDraw().makeRectangle(p1.getX(), p1.getY(),
SHOT_DEPTH,
p2.getX() - p1.getX(), p2.getY() - p1.getY(),
color);
}
/**
* Creates a visual representation of a battleship on the map.
* The ship's border color depends on its status: normal, valid preview, or invalid preview.
*
* @param ship the Battleship object representing the ship in the model
* @return a Spatial representing the ship on the map
*/
@Override
public Spatial visit(Battleship ship) {
// Create a node to represent the ship
final Node shipNode = new Node("ship"); //NON-NLS
// Convert the ship's model coordinates to view coordinates
final Position p1 = view.modelToView(ship.getMinX(), ship.getMinY());
final Position p2 = view.modelToView(ship.getMaxX() + 1, ship.getMaxY() + 1);
// Calculate the coordinates for the ship's bounding box
final float x1 = p1.getX() + INDENT;
final float y1 = p1.getY() + INDENT;
final float x2 = p2.getX() - INDENT;
final float y2 = p2.getY() - INDENT;
// Determine the color based on the ship's status
final ColorRGBA color = switch (ship.getStatus()) {
case NORMAL -> SHIP_BORDER_COLOR;
case VALID_PREVIEW -> PREVIEW_COLOR;
case INVALID_PREVIEW -> ERROR_COLOR;
};
// Add the ship's borders to the node
shipNode.attachChild(shipLine(x1, y1, x2, y1, color));
shipNode.attachChild(shipLine(x1, y2, x2, y2, color));
shipNode.attachChild(shipLine(x1, y1, x1, y2, color));
shipNode.attachChild(shipLine(x2, y1, x2, y2, color));
// Return the complete ship representation
return shipNode;
}
/**
* Creates a line geometry representing part of the ship's border.
*
* @param x1 the starting x-coordinate of the line
* @param y1 the starting y-coordinate of the line
* @param x2 the ending x-coordinate of the line
* @param y2 the ending y-coordinate of the line
* @param color the color of the line
* @return a Geometry representing the line
*/
private Geometry shipLine(float x1, float y1, float x2, float y2, ColorRGBA color) {
return view.getApp().getDraw().makeFatLine(x1, y1, x2, y2, SHIP_DEPTH, color, SHIP_LINE_WIDTH);
}
}

View File

@@ -0,0 +1,203 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.client.gui;
import com.jme3.app.Application;
import com.jme3.app.state.AppStateManager;
import com.jme3.asset.AssetManager;
import com.jme3.light.AmbientLight;
import com.jme3.light.DirectionalLight;
import com.jme3.material.Material;
import com.jme3.math.ColorRGBA;
import com.jme3.math.Vector2f;
import com.jme3.math.Vector3f;
import com.jme3.renderer.Camera;
import com.jme3.renderer.queue.RenderQueue.ShadowMode;
import com.jme3.scene.Geometry;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;
import com.jme3.scene.shape.Box;
import com.jme3.shadow.DirectionalLightShadowRenderer;
import com.jme3.shadow.EdgeFilteringMode;
import com.jme3.texture.Texture;
import com.jme3.util.SkyFactory;
import com.jme3.util.TangentBinormalGenerator;
import pp.battleship.client.BattleshipAppState;
import pp.battleship.model.ShipMap;
import pp.util.FloatMath;
import static pp.util.FloatMath.TWO_PI;
import static pp.util.FloatMath.cos;
import static pp.util.FloatMath.sin;
import static pp.util.FloatMath.sqrt;
/**
* Manages the rendering and visual aspects of the sea and sky in the Battleship game.
* This state is responsible for setting up and updating the sea, sky, and lighting
* conditions, and controls the camera to create a dynamic view of the game environment.
*/
public class SeaAppState extends BattleshipAppState {
/**
* The path to the sea texture material.
*/
private static final String SEA_TEXTURE = "Textures/Terrain/Water/Water.j3m"; //NON-NLS
private static final float ABOVE_SEA_LEVEL = 4f;
private static final float INCLINATION = 2.5f;
/**
* The root node for all visual elements in this state.
*/
private final Node viewNode = new Node("view"); //NON-NLS
/**
* The node containing the scene elements, such as the sea surface.
*/
private final Node sceneNode = new Node("scene"); //NON-NLS
/**
* Synchronizes the sea's visual representation with the game logic.
*/
private SeaSynchronizer synchronizer;
/**
* The current angle of the camera around the center of the map.
*/
private float cameraAngle;
/**
* Initializes the state by setting up the sky, lights, and other visual components.
* This method is called when the state is first attached to the state manager.
*
* @param stateManager the state manager
* @param application the application
*/
@Override
public void initialize(AppStateManager stateManager, Application application) {
super.initialize(stateManager, application);
viewNode.attachChild(sceneNode);
setupLights();
setupSky();
}
/**
* Enables the sea and sky state, setting up the scene and registering any necessary listeners.
* This method is called when the state is set to active.
*/
@Override
protected void enableState() {
sceneNode.detachAllChildren();
setupScene();
if (synchronizer == null) {
synchronizer = new SeaSynchronizer(getApp(), sceneNode, getGameLogic().getOwnMap());
getGameLogic().addListener(synchronizer);
}
getApp().getRootNode().attachChild(viewNode);
}
/**
* Disables the sea and sky state, removing visual elements from the scene and unregistering listeners.
* This method is called when the state is set to inactive.
*/
@Override
protected void disableState() {
getApp().getRootNode().detachChild(viewNode);
if (synchronizer != null) {
getGameLogic().removeListener(synchronizer);
synchronizer = null;
}
}
/**
* Updates the state each frame, moving the camera to simulate it circling around the map.
*
* @param tpf the time per frame (seconds)
*/
@Override
public void update(float tpf) {
super.update(tpf);
cameraAngle += TWO_PI * 0.05f * tpf;
adjustCamera();
}
/**
* Sets up the lighting for the scene, including directional and ambient lights.
* Also configures shadows to enhance the visual depth of the scene.
*/
private void setupLights() {
final AssetManager assetManager = getApp().getAssetManager();
final DirectionalLightShadowRenderer shRend = new DirectionalLightShadowRenderer(assetManager, 2048, 3);
shRend.setLambda(0.55f);
shRend.setShadowIntensity(0.6f);
shRend.setEdgeFilteringMode(EdgeFilteringMode.Bilinear);
getApp().getViewPort().addProcessor(shRend);
final DirectionalLight sun = new DirectionalLight();
sun.setDirection(new Vector3f(-1f, -0.7f, -1f).normalizeLocal());
viewNode.addLight(sun);
shRend.setLight(sun);
final AmbientLight ambientLight = new AmbientLight(new ColorRGBA(0.3f, 0.3f, 0.3f, 0f));
viewNode.addLight(ambientLight);
}
/**
* Sets up the sky in the scene using a skybox with textures for all six directions.
* This creates a realistic and immersive environment for the sea.
*/
private void setupSky() {
final AssetManager assetManager = getApp().getAssetManager();
final Texture west = assetManager.loadTexture("Textures/Sky/Lagoon/lagoon_west.jpg"); //NON-NLS
final Texture east = assetManager.loadTexture("Textures/Sky/Lagoon/lagoon_east.jpg"); //NON-NLS
final Texture north = assetManager.loadTexture("Textures/Sky/Lagoon/lagoon_north.jpg"); //NON-NLS
final Texture south = assetManager.loadTexture("Textures/Sky/Lagoon/lagoon_south.jpg"); //NON-NLS
final Texture up = assetManager.loadTexture("Textures/Sky/Lagoon/lagoon_up.jpg"); //NON-NLS
final Texture down = assetManager.loadTexture("Textures/Sky/Lagoon/lagoon_down.jpg"); //NON-NLS
final Spatial sky = SkyFactory.createSky(assetManager, west, east, north, south, up, down);
sky.rotate(0f, FloatMath.PI, 0f);
viewNode.attachChild(sky);
}
/**
* Sets up the sea surface in the scene. This includes creating the sea mesh,
* applying textures, and enabling shadows.
*/
private void setupScene() {
final ShipMap ownMap = getGameLogic().getOwnMap();
final float x = 0.5f * ownMap.getWidth();
final float y = 0.5f * ownMap.getHeight();
final Box seaMesh = new Box(y + 0.5f, 0.1f, x + 0.5f);
final Geometry seaGeo = new Geometry("sea", seaMesh); //NON-NLS
seaGeo.setLocalTranslation(new Vector3f(y, -0.1f, x));
seaMesh.scaleTextureCoordinates(new Vector2f(4f, 4f));
final Material seaMat = getApp().getAssetManager().loadMaterial(SEA_TEXTURE);
seaGeo.setMaterial(seaMat);
seaGeo.setShadowMode(ShadowMode.CastAndReceive);
TangentBinormalGenerator.generate(seaGeo);
sceneNode.attachChild(seaGeo);
}
/**
* Adjusts the camera position and orientation to create a circular motion around
* the center of the map. This provides a dynamic view of the sea and surrounding environment.
*/
private void adjustCamera() {
final ShipMap ownMap = getGameLogic().getOwnMap();
final float mx = 0.5f * ownMap.getWidth();
final float my = 0.5f * ownMap.getHeight();
final float radius = 2f * sqrt(mx * mx + my + my);
final float cos = radius * cos(cameraAngle);
final float sin = radius * sin(cameraAngle);
final float x = mx - cos;
final float y = my - sin;
final Camera camera = getApp().getCamera();
camera.setLocation(new Vector3f(y, ABOVE_SEA_LEVEL, x));
camera.getRotation().lookAt(new Vector3f(sin, -INCLINATION, cos),
Vector3f.UNIT_Y);
camera.update();
}
}

View File

@@ -0,0 +1,213 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.client.gui;
import com.jme3.material.Material;
import com.jme3.material.RenderState.BlendMode;
import com.jme3.math.ColorRGBA;
import com.jme3.renderer.queue.RenderQueue.ShadowMode;
import com.jme3.scene.Geometry;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;
import com.jme3.scene.shape.Box;
import com.jme3.scene.shape.Cylinder;
import pp.battleship.client.BattleshipApp;
import pp.battleship.model.Battleship;
import pp.battleship.model.Rotation;
import pp.battleship.model.ShipMap;
import pp.battleship.model.Shot;
import static java.util.Objects.requireNonNull;
import static pp.util.FloatMath.HALF_PI;
import static pp.util.FloatMath.PI;
/**
* The {@code SeaSynchronizer} class is responsible for synchronizing the graphical
* representation of the ships and shots on the sea map with the underlying data model.
* It extends the {@link ShipMapSynchronizer} to provide specific synchronization
* logic for the sea map.
*/
class SeaSynchronizer extends ShipMapSynchronizer {
private static final String UNSHADED = "Common/MatDefs/Misc/Unshaded.j3md"; //NON-NLS
private static final String KING_GEORGE_V_MODEL = "Models/KingGeorgeV/KingGeorgeV.j3o"; //NON-NLS
private static final String COLOR = "Color"; //NON-NLS
private static final String SHIP = "ship"; //NON-NLS
private static final String SHOT = "shot"; //NON-NLS
private static final ColorRGBA BOX_COLOR = ColorRGBA.Gray;
private static final ColorRGBA SPLASH_COLOR = new ColorRGBA(0f, 0f, 1f, 0.4f);
private static final ColorRGBA HIT_COLOR = new ColorRGBA(1f, 0f, 0f, 0.4f);
private final ShipMap map;
private final BattleshipApp app;
/**
* Constructs a {@code SeaSynchronizer} object with the specified application, root node, and ship map.
*
* @param app the Battleship application
* @param root the root node to which graphical elements will be attached
* @param map the ship map containing the ships and shots
*/
public SeaSynchronizer(BattleshipApp app, Node root, ShipMap map) {
super(app.getGameLogic().getOwnMap(), root);
this.app = app;
this.map = map;
addExisting();
}
/**
* Visits a {@link Shot} and creates a graphical representation of it.
* If the shot is a hit, it attaches the representation to the ship node.
*
* @param shot the shot to be represented
* @return the graphical representation of the shot, or null if the shot is a hit
* and the representation has been attached to the ship node
*/
@Override
public Spatial visit(Shot shot) {
return shot.isHit() ? handleHit(shot) : createCylinder(shot);
}
/**
* Handles a hit by attaching its representation to the node that
* contains the ship model as a child so that it moves with the ship.
*
* @param shot a hit
* @return always null to prevent the representation from being attached
* to the items node as well
*/
private Spatial handleHit(Shot shot) {
final Battleship ship = requireNonNull(map.findShipAt(shot), "Missing ship");
final Node shipNode = requireNonNull((Node) getSpatial(ship), "Missing ship node");
final Geometry representation = createCylinder(shot);
representation.getLocalTranslation().subtractLocal(shipNode.getLocalTranslation());
shipNode.attachChild(representation);
return null;
}
/**
* Creates a cylinder geometry representing the specified shot.
* The appearance of the cylinder depends on whether the shot is a hit or a miss.
*
* @param shot the shot to be represented
* @return the geometry representing the shot
*/
private Geometry createCylinder(Shot shot) {
final ColorRGBA color = shot.isHit() ? HIT_COLOR : SPLASH_COLOR;
final float height = shot.isHit() ? 1.2f : 0.1f;
final Cylinder cylinder = new Cylinder(2, 20, 0.45f, height, true);
final Geometry geometry = new Geometry(SHOT, cylinder);
geometry.setMaterial(createColoredMaterial(color));
geometry.rotate(HALF_PI, 0f, 0f);
// compute the center of the shot in world coordinates
geometry.setLocalTranslation(shot.getY() + 0.5f, 0f, shot.getX() + 0.5f);
return geometry;
}
/**
* Visits a {@link Battleship} and creates a graphical representation of it.
* The representation is either a 3D model or a simple box depending on the
* type of battleship.
*
* @param ship the battleship to be represented
* @return the node containing the graphical representation of the battleship
*/
@Override
public Spatial visit(Battleship ship) {
final Node node = new Node(SHIP);
node.attachChild(createShip(ship));
// compute the center of the ship in world coordinates
final float x = 0.5f * (ship.getMinY() + ship.getMaxY() + 1f);
final float z = 0.5f * (ship.getMinX() + ship.getMaxX() + 1f);
node.setLocalTranslation(x, 0f, z);
node.addControl(new ShipControl(ship));
return node;
}
/**
* Creates the appropriate graphical representation of the specified battleship.
* The representation is either a detailed model or a simple box based on the length of the ship.
*
* @param ship the battleship to be represented
* @return the spatial representing the battleship
*/
private Spatial createShip(Battleship ship) {
return ship.getLength() == 4 ? createBattleship(ship) : createBox(ship);
}
/**
* Creates a simple box to represent a battleship that is not of the "King George V" type.
*
* @param ship the battleship to be represented
* @return the geometry representing the battleship as a box
*/
private Spatial createBox(Battleship ship) {
final Box box = new Box(0.5f * (ship.getMaxY() - ship.getMinY()) + 0.3f,
0.3f,
0.5f * (ship.getMaxX() - ship.getMinX()) + 0.3f);
final Geometry geometry = new Geometry(SHIP, box);
geometry.setMaterial(createColoredMaterial(BOX_COLOR));
geometry.setShadowMode(ShadowMode.CastAndReceive);
return geometry;
}
/**
* Creates a new {@link Material} with the specified color.
* If the color includes transparency (i.e., alpha value less than 1),
* the material's render state is set to use alpha blending, allowing for
* semi-transparent rendering.
*
* @param color the {@link ColorRGBA} to be applied to the material. If the alpha value
* of the color is less than 1, the material will support transparency.
* @return a {@link Material} instance configured with the specified color and,
* if necessary, alpha blending enabled.
*/
private Material createColoredMaterial(ColorRGBA color) {
final Material material = new Material(app.getAssetManager(), UNSHADED);
if (color.getAlpha() < 1f)
material.getAdditionalRenderState().setBlendMode(BlendMode.Alpha);
material.setColor(COLOR, color);
return material;
}
/**
* Creates a detailed 3D model to represent a "King George V" battleship.
*
* @param ship the battleship to be represented
* @return the spatial representing the "King George V" battleship
*/
private Spatial createBattleship(Battleship ship) {
final Spatial model = app.getAssetManager().loadModel(KING_GEORGE_V_MODEL);
model.rotate(-HALF_PI, calculateRotationAngle(ship.getRot()), 0f);
model.scale(1.48f);
model.setShadowMode(ShadowMode.CastAndReceive);
return model;
}
/**
* Calculates the rotation angle for the specified rotation.
*
* @param rot the rotation of the battleship
* @return the rotation angle in radians
*/
private static float calculateRotationAngle(Rotation rot) {
return switch (rot) {
case RIGHT -> HALF_PI;
case DOWN -> 0f;
case LEFT -> -HALF_PI;
case UP -> PI;
};
}
}

View File

@@ -0,0 +1,106 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.client.gui;
import com.jme3.math.Quaternion;
import com.jme3.math.Vector3f;
import com.jme3.renderer.RenderManager;
import com.jme3.renderer.ViewPort;
import com.jme3.scene.control.AbstractControl;
import pp.battleship.model.Battleship;
import static pp.util.FloatMath.DEG_TO_RAD;
import static pp.util.FloatMath.TWO_PI;
import static pp.util.FloatMath.sin;
/**
* Controls the oscillating pitch motion of a battleship model in the game.
* The ship oscillates to simulate a realistic movement on water, based on its orientation and length.
*/
class ShipControl extends AbstractControl {
/**
* The axis of rotation for the ship's pitch (tilting forward and backward).
*/
private final Vector3f axis;
/**
* The duration of one complete oscillation cycle in seconds.
*/
private final float cycle;
/**
* The amplitude of the pitch oscillation in radians, determining how much the ship tilts.
*/
private final float amplitude;
/**
* A quaternion representing the ship's current pitch rotation.
*/
private final Quaternion pitch = new Quaternion();
/**
* The current time within the oscillation cycle, used to calculate the ship's pitch angle.
*/
private float time;
/**
* Constructs a new ShipControl instance for the specified Battleship.
* The ship's orientation determines the axis of rotation, while its length influences
* the cycle duration and amplitude of the oscillation.
*
* @param ship the Battleship object to control
*/
public ShipControl(Battleship ship) {
// Determine the axis of rotation based on the ship's orientation
axis = switch (ship.getRot()) {
case LEFT, RIGHT -> Vector3f.UNIT_X;
case UP, DOWN -> Vector3f.UNIT_Z;
};
// Set the cycle duration and amplitude based on the ship's length
cycle = ship.getLength() * 2f;
amplitude = 5f * DEG_TO_RAD / ship.getLength();
}
/**
* Updates the ship's pitch oscillation each frame. The ship's pitch is adjusted
* to create a continuous tilting motion, simulating the effect of waves.
*
* @param tpf time per frame (in seconds), used to calculate the new pitch angle
*/
@Override
protected void controlUpdate(float tpf) {
// If spatial is null, do nothing
if (spatial == null) return;
// Update the time within the oscillation cycle
time = (time + tpf) % cycle;
// Calculate the current angle of the oscillation
final float angle = amplitude * sin(time * TWO_PI / cycle);
// Update the pitch Quaternion with the new angle
pitch.fromAngleAxis(angle, axis);
// Apply the pitch rotation to the spatial
spatial.setLocalRotation(pitch);
}
/**
* This method is called during the rendering phase, but it does not perform any
* operations in this implementation as the control only influences the spatial's
* transformation, not its rendering process.
*
* @param rm the RenderManager rendering the controlled Spatial (not null)
* @param vp the ViewPort being rendered (not null)
*/
@Override
protected void controlRender(RenderManager rm, ViewPort vp) {
// No rendering logic is needed for this control
}
}

View File

@@ -0,0 +1,89 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.client.gui;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;
import pp.battleship.model.Item;
import pp.battleship.model.ShipMap;
import pp.battleship.model.Visitor;
import pp.battleship.notification.GameEventListener;
import pp.battleship.notification.ItemAddedEvent;
import pp.battleship.notification.ItemRemovedEvent;
import pp.view.ModelViewSynchronizer;
/**
* Abstract base class for synchronizing the visual representation of a {@link ShipMap} with its model state.
* This class handles the addition and removal of items from the ship map, ensuring that changes in the model
* are accurately reflected in the view.
* <p>
* Subclasses are responsible for providing the specific implementation of how each item in the map
* is represented visually by implementing the {@link Visitor} interface.
* </p>
*/
abstract class ShipMapSynchronizer extends ModelViewSynchronizer<Item> implements Visitor<Spatial>, GameEventListener {
// The ship map that this synchronizer is responsible for
private final ShipMap shipMap;
/**
* Constructs a new ShipMapSynchronizer.
* Initializes the synchronizer with the provided ship map and the root node for attaching view representations.
*
* @param map the ship map to be synchronized
* @param root the root node to which the view representations of the ship map items are attached
*/
protected ShipMapSynchronizer(ShipMap map, Node root) {
super(root);
this.shipMap = map;
}
/**
* Translates a model item into its corresponding visual representation.
* The specific visual representation is determined by the concrete implementation of the {@link Visitor} interface.
*
* @param item the item from the model to be translated
* @return the visual representation of the item as a {@link Spatial}
*/
@Override
protected Spatial translate(Item item) {
return item.accept(this);
}
/**
* Adds the existing items from the ship map to the view.
* This method should be called during initialization to ensure that all current items in the ship map
* are visually represented.
*/
protected void addExisting() {
shipMap.getItems().forEach(this::add);
}
/**
* Handles the event when an item is removed from the ship map.
* Removes the visual representation of the item from the view if it belongs to the synchronized ship map.
*
* @param event the event indicating that an item has been removed from the ship map
*/
@Override
public void receivedEvent(ItemRemovedEvent event) {
if (shipMap == event.map())
delete(event.item());
}
/**
* Handles the event when an item is added to the ship map.
* Adds the visual representation of the new item to the view if it belongs to the synchronized ship map.
*
* @param event the event indicating that an item has been added to the ship map
*/
@Override
public void receivedEvent(ItemAddedEvent event) {
if (shipMap == event.map())
add(event.item());
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 235 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

View File

@@ -0,0 +1,3 @@
based on:
https://free3d.com/3d-model/wwii-ship-uk-king-george-v-class-battleship-v1--185381.html
License: Free Personal Use Only

View File

@@ -0,0 +1,4 @@
based on:
water 002
by Katsukagi on 29/11/2018
https://3dtextures.me/2018/11/29/water-002/

View File

@@ -0,0 +1,14 @@
Material Water : Common/MatDefs/Light/Lighting.j3md {
MaterialParameters {
Shininess : 64
DiffuseMap : Repeat Textures/Terrain/Water/Water_002_COLOR.jpg
NormalMap : Repeat Textures/Terrain/Water/Water_002_NORM.jpg
SpecularMap : Repeat Textures/Terrain/Water/Water_002_ROUGH.jpg
ParallaxMap : Repeat Textures/Terrain/Water/Water_002_DISP.png
// PackedNormalParallax : true
// UseMaterialColors : true
Ambient : 0.5 0.5 0.5 1.0
Diffuse : 1.0 1.0 1.0 1.0
Specular : 1.0 1.0 1.0 1.0
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 207 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

View File

@@ -0,0 +1,16 @@
plugins {
id 'buildlogic.jme-application-conventions'
}
description = 'Battleship converter for resources'
dependencies {
implementation libs.jme3.core
runtimeOnly libs.jme3.desktop
runtimeOnly libs.jme3.plugins
}
application {
mainClass = 'pp.battleship.exporter.ModelExporter'
}

View File

@@ -0,0 +1,69 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.exporter;
import com.jme3.app.SimpleApplication;
import com.jme3.export.JmeExporter;
import com.jme3.export.binary.BinaryExporter;
import com.jme3.scene.Spatial;
import com.jme3.system.JmeContext;
import com.jme3.util.TangentBinormalGenerator;
import java.io.File;
import java.io.IOException;
import java.lang.System.Logger;
import java.lang.System.Logger.Level;
/**
* This class transforms models into j3o format.
*/
public class ModelExporter extends SimpleApplication {
private static final Logger LOGGER = System.getLogger(ModelExporter.class.getName());
/**
* The main method of the converter
*
* @param args input args
*/
public static void main(String[] args) {
ModelExporter application = new ModelExporter();
application.start(JmeContext.Type.Headless);
}
/**
* Overrides {@link com.jme3.app.SimpleApplication#simpleInitApp()}.
* It initializes a simple app by exporting robots and rocks.
*/
@Override
public void simpleInitApp() {
export("Models/KingGeorgeV/King_George_V.obj", "KingGeorgeV.j3o"); //NON-NLS
stop();
}
/**
* Exports spatial into a file
*
* @param fileName the file name of the model
* @param exportFileName the name of the file where the .j3o file is going to be stored
*/
private void export(String fileName, String exportFileName) {
final File file = new File(exportFileName);
JmeExporter exporter = BinaryExporter.getInstance();
try {
final Spatial model = getAssetManager().loadModel(fileName);
TangentBinormalGenerator.generate(model);
exporter.save(model, file);
}
catch (IOException exception) {
LOGGER.log(Level.ERROR, "write to {0} failed", file); //NON-NLS
throw new RuntimeException(exception);
}
LOGGER.log(Level.INFO, "wrote file {0}", file); //NON-NLS
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 235 KiB

View File

@@ -0,0 +1,18 @@
# 3ds Max Wavefront OBJ Exporter v0.97b - (c)2007 guruware
# File Created: 16.03.2012 14:15:53
newmtl _King_George_V
Ns 60.0000
Ni 1.5000
d 1.0000
Tr 0.0000
Tf 1.0000 1.0000 1.0000
illum 2
Ka 1.0000 1.0000 1.0000
Kd 1.0000 1.0000 1.0000
Ks 0.4500 0.4500 0.4500
Ke 0.0000 0.0000 0.0000
map_Ka King_George_V.jpg
map_Kd King_George_V.jpg
map_bump King_George_V_bump.jpg
bump King_George_V_bump.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

View File

@@ -0,0 +1,3 @@
based on:
https://free3d.com/3d-model/wwii-ship-uk-king-george-v-class-battleship-v1--185381.html
License: Free Personal Use Only

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Some files were not shown because too many files have changed in this diff Show More