diff --git a/Projekte/monopoly/client/build.gradle b/Projekte/monopoly/client/build.gradle new file mode 100644 index 0000000..7582682 --- /dev/null +++ b/Projekte/monopoly/client/build.gradle @@ -0,0 +1,22 @@ +plugins { + id 'buildlogic.jme-application-conventions' +} + +description = 'Monopoly Client' + +dependencies { + implementation project(":jme-common") + implementation project(":monopoly: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.monopoly.client.MonopolyApp' + applicationName = 'monopoly' +} diff --git a/Projekte/monopoly/client/client.properties b/Projekte/monopoly/client/client.properties new file mode 100644 index 0000000..4403fa3 --- /dev/null +++ b/Projekte/monopoly/client/client.properties @@ -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 diff --git a/Projekte/monopoly/client/logging.properties b/Projekte/monopoly/client/logging.properties new file mode 100644 index 0000000..cbbaec0 --- /dev/null +++ b/Projekte/monopoly/client/logging.properties @@ -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 diff --git a/Projekte/monopoly/client/src/main/java/pp/monopoly/client/GameSound.java b/Projekte/monopoly/client/src/main/java/pp/monopoly/client/GameSound.java new file mode 100644 index 0000000..d74df40 --- /dev/null +++ b/Projekte/monopoly/client/src/main/java/pp/monopoly/client/GameSound.java @@ -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.monopoly.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.monopoly.notification.GameEventListener; +import pp.monopoly.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(); + } + } +} diff --git a/Projekte/monopoly/client/src/main/java/pp/monopoly/client/Menu.java b/Projekte/monopoly/client/src/main/java/pp/monopoly/client/Menu.java new file mode 100644 index 0000000..34a281f --- /dev/null +++ b/Projekte/monopoly/client/src/main/java/pp/monopoly/client/Menu.java @@ -0,0 +1,59 @@ +//////////////////////////////////////// +// Programming project code +// UniBw M, 2022, 2023, 2024 +// www.unibw.de/inf2 +// (c) Mark Minas (mark.minas@unibw.de) +//////////////////////////////////////// + +package pp.monopoly.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.util.prefs.Preferences; + +import static pp.monopoly.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 MonopolyApp app; + + + /** + * Constructs the Menu dialog for the Battleship application. + * + * @param app the BattleshipApp instance + */ + public Menu(MonopolyApp app) { + super(app.getDialogManager()); + this.app = app; + } + + /** + * Updates the state of the load and save buttons based on the game logic. + */ + @Override + public void update() { + + } + + /** + * As an escape action, this method closes the menu if it is the top dialog. + */ + @Override + public void escape() { + close(); + } +} diff --git a/Projekte/monopoly/client/src/main/java/pp/monopoly/client/MonopolyApp.java b/Projekte/monopoly/client/src/main/java/pp/monopoly/client/MonopolyApp.java new file mode 100644 index 0000000..618d42b --- /dev/null +++ b/Projekte/monopoly/client/src/main/java/pp/monopoly/client/MonopolyApp.java @@ -0,0 +1,423 @@ +//////////////////////////////////////// +// Programming project code +// UniBw M, 2022, 2023, 2024 +// www.unibw.de/inf2 +// (c) Mark Minas (mark.minas@unibw.de) +//////////////////////////////////////// + +package pp.monopoly.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.monopoly.game.client.MonopolyClient; +import pp.monopoly.game.client.ClientGameLogic; +import pp.monopoly.game.client.ServerConnection; +import pp.monopoly.notification.ClientStateEvent; +import pp.monopoly.notification.GameEventListener; +import pp.monopoly.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.monopoly.Resources.lookup; + +/** + * The main class for the Monopoly client application. + * It manages the initialization, input setup, GUI setup, and game states for the client. + */ +public class MonopolyApp extends SimpleApplication implements MonopolyClient, GameEventListener { + + /** + * Logger for logging messages within the application. + */ + private static final Logger LOGGER = System.getLogger(MonopolyApp.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 Monopoly client application. + */ + private final MonopolyAppConfig 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 Monopoly application. + * + * @param args Command-line arguments for launching the application. + */ + public static void main(String[] args) { + new MonopolyApp().start(); + } + + /** + * Constructs a new {@code MonopolyApp} instance. + * Initializes the configuration, server connection, and game logic listeners. + */ + private MonopolyApp() { + config = new MonopolyAppConfig(); + 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("Monopoly.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() { + 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 Monopoly client. + * + * @return The {@link MonopolyClientConfig} instance. + */ + @Override + public MonopolyAppConfig 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(); + + //TODO add states + stateManager.attachAll(); + } + + /** + * 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) { + //TODO + throw new UnsupportedOperationException("unimplemented method"); + } + + /** + * 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(); + } +} diff --git a/Projekte/monopoly/client/src/main/java/pp/monopoly/client/MonopolyAppConfig.java b/Projekte/monopoly/client/src/main/java/pp/monopoly/client/MonopolyAppConfig.java new file mode 100644 index 0000000..95070d2 --- /dev/null +++ b/Projekte/monopoly/client/src/main/java/pp/monopoly/client/MonopolyAppConfig.java @@ -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.monopoly.client; + +import com.jme3.math.ColorRGBA; +import pp.monopoly.game.client.MonopolyClientConfig; + +/** + * Provides access to the Monopoly application configuration. + * Extends {@link MonopolyClientConfig} to include additional properties specific to the client, + * particularly those related to screen settings and visual customization. + *

+ * Note: Attributes of this class should not be marked as {@code final} + * to ensure proper functionality when reading from a properties file. + *

+ */ +public class MonopolyAppConfig extends MonopolyClientConfig { + + /** + * 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. + *

+ * Requires a GPU that supports GL_ARB_framebuffer_sRGB; otherwise, this setting will be ignored. + *

+ */ + @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 MonopolyAppConfig} with predefined values. + */ + public MonopolyAppConfig() { + // 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; + } +} diff --git a/Projekte/monopoly/client/src/main/java/pp/monopoly/client/MonopolyAppState.java b/Projekte/monopoly/client/src/main/java/pp/monopoly/client/MonopolyAppState.java new file mode 100644 index 0000000..6342436 --- /dev/null +++ b/Projekte/monopoly/client/src/main/java/pp/monopoly/client/MonopolyAppState.java @@ -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.monopoly.client; + +import com.jme3.app.Application; +import com.jme3.app.state.AbstractAppState; +import com.jme3.app.state.AppStateManager; +import pp.monopoly.game.client.ClientGameLogic; + +/** + * Abstract class representing a state in the Battleship game. + * Extends the AbstractAppState from jMonkeyEngine to manage state behavior. + */ +public abstract class MonopolyAppState extends AbstractAppState { + private MonopolyApp app; + + /** + * Creates a new MonopolyAppState that is initially disabled. + * + * @see #setEnabled(boolean) + */ + protected MonopolyAppState() { + 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 = (MonopolyApp) application; + if (isEnabled()) enableState(); + } + + /** + * Returns the MonopolyApp instance associated with this MonopolyAppState. + * + * @return The MonopolyApp instance. + */ + public MonopolyApp 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 MonopolyAppState. + * 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(); +} diff --git a/Projekte/monopoly/client/src/main/java/pp/monopoly/client/NetworkDialog.java b/Projekte/monopoly/client/src/main/java/pp/monopoly/client/NetworkDialog.java new file mode 100644 index 0000000..6b6ea98 --- /dev/null +++ b/Projekte/monopoly/client/src/main/java/pp/monopoly/client/NetworkDialog.java @@ -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.monopoly.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.monopoly.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 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 MonopolyApp 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()); + } +} diff --git a/Projekte/monopoly/client/src/main/java/pp/monopoly/client/NetworkSupport.java b/Projekte/monopoly/client/src/main/java/pp/monopoly/client/NetworkSupport.java new file mode 100644 index 0000000..f6270bf --- /dev/null +++ b/Projekte/monopoly/client/src/main/java/pp/monopoly/client/NetworkSupport.java @@ -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.monopoly.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.monopoly.game.client.ServerConnection; +import pp.monopoly.message.client.ClientMessage; +import pp.monopoly.message.server.ServerMessage; + +import java.io.IOException; +import java.lang.System.Logger; +import java.lang.System.Logger.Level; + +import static pp.monopoly.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, ClientStateListener, ServerConnection { + private static final Logger LOGGER = System.getLogger(NetworkSupport.class.getName()); + private final MonopolyApp app; + private Client client; + + /** + * Constructs a NetworkSupport instance for the given Battleship application. + * + * @param app The Battleship application instance. + */ + public NetworkSupport(MonopolyApp app) { + this.app = app; + } + + /** + * Returns the Battleship application instance. + * + * @return Battleship application instance + */ + MonopolyApp 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); + } +} diff --git a/Projekte/monopoly/client/src/main/java/pp/monopoly/client/gui/BoardSynchronizer.java b/Projekte/monopoly/client/src/main/java/pp/monopoly/client/gui/BoardSynchronizer.java new file mode 100644 index 0000000..cfca711 --- /dev/null +++ b/Projekte/monopoly/client/src/main/java/pp/monopoly/client/gui/BoardSynchronizer.java @@ -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.monopoly.client.gui; + +import com.jme3.scene.Node; +import com.jme3.scene.Spatial; +import pp.monopoly.model.Item; +import pp.monopoly.model.Board; +import pp.monopoly.model.Visitor; +import pp.monopoly.notification.GameEventListener; +import pp.monopoly.notification.ItemAddedEvent; +import pp.monopoly.notification.ItemRemovedEvent; +import pp.view.ModelViewSynchronizer; + +/** + * Abstract base class for synchronizing the visual representation of a {@link Board} with its model state. + * This class handles the addition and removal of items from the map, ensuring that changes in the model + * are accurately reflected in the view. + *

+ * Subclasses are responsible for providing the specific implementation of how each item in the map + * is represented visually by implementing the {@link Visitor} interface. + *

+ */ +abstract class BoardSynchronizer extends ModelViewSynchronizer implements Visitor, GameEventListener { + // The map that this synchronizer is responsible for + private final Board board; + + /** + * Constructs a new BoardSynchronizer. + * Initializes the synchronizer with the provided map and the root node for attaching view representations. + * + * @param map the map to be synchronized + * @param root the root node to which the view representations of the map items are attached + */ + protected BoardSynchronizer(Board map, Node root) { + super(root); + this.board = 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 map to the view. + * This method should be called during initialization to ensure that all current items in the map + * are visually represented. + */ + protected void addExisting() { + board.getItems().forEach(this::add); + } + + /** + * Handles the event when an item is removed from the map. + * Removes the visual representation of the item from the view if it belongs to the synchronized map. + * + * @param event the event indicating that an item has been removed from the map + */ + @Override + public void receivedEvent(ItemRemovedEvent event) { + if (board == event.map()) + delete(event.item()); + } + + /** + * Handles the event when an item is added to the map. + * Adds the visual representation of the new item to the view if it belongs to the synchronized map. + * + * @param event the event indicating that an item has been added to the map + */ + @Override + public void receivedEvent(ItemAddedEvent event) { + if (board == event.map()) + add(event.item()); + } +} diff --git a/Projekte/monopoly/client/src/main/java/pp/monopoly/client/gui/MapView.java b/Projekte/monopoly/client/src/main/java/pp/monopoly/client/gui/MapView.java new file mode 100644 index 0000000..0a20a66 --- /dev/null +++ b/Projekte/monopoly/client/src/main/java/pp/monopoly/client/gui/MapView.java @@ -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.monopoly.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.monopoly.client.MonopolyApp; +import pp.monopoly.model.IntPoint; +import pp.monopoly.model.Board; +import pp.util.FloatPoint; +import pp.util.Position; + +/** + * Represents the visual view of a {@link Board}, 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 MonopolyApp app; + private final Node mapNode = new Node("map"); // NON-NLS + private final Board map; + private final MapViewSynchronizer synchronizer; + + /** + * Constructs a new MapView for a given {@link Board} and {@link MonopolyApp}. + * 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(Board map, MonopolyApp 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 Board} changes. + * After calling this method, this MapView instance should no longer be used. + */ + void unregister() { + app.getGameLogic().removeListener(synchronizer); + } + + /** + * Gets the {@link Board} associated with this view. + * + * @return the ship map + */ + public Board getMap() { + return map; + } + + /** + * Gets the {@link MonopolyApp} instance associated with this view. + * + * @return the main application instance + */ + public MonopolyApp 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); + } +} diff --git a/Projekte/monopoly/client/src/main/java/pp/monopoly/client/gui/MapViewSynchronizer.java b/Projekte/monopoly/client/src/main/java/pp/monopoly/client/gui/MapViewSynchronizer.java new file mode 100644 index 0000000..d88dd05 --- /dev/null +++ b/Projekte/monopoly/client/src/main/java/pp/monopoly/client/gui/MapViewSynchronizer.java @@ -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.monopoly.client.gui; + +import com.jme3.math.ColorRGBA; +import com.jme3.scene.Geometry; +import com.jme3.scene.Node; +import com.jme3.scene.Spatial; +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 BoardSynchronizer { + // 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 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); + } +} diff --git a/Projekte/monopoly/model/build.gradle b/Projekte/monopoly/model/build.gradle new file mode 100644 index 0000000..69dc09f --- /dev/null +++ b/Projekte/monopoly/model/build.gradle @@ -0,0 +1,10 @@ +plugins { + id 'buildlogic.java-library-conventions' +} + +description = 'Monopoly common model' + +dependencies { + api project(":common") + api libs.jme3.networking +} diff --git a/Projekte/monopoly/model/src/main/java/pp/monopoly/MonopolyConfig.java b/Projekte/monopoly/model/src/main/java/pp/monopoly/MonopolyConfig.java new file mode 100644 index 0000000..b65c514 --- /dev/null +++ b/Projekte/monopoly/model/src/main/java/pp/monopoly/MonopolyConfig.java @@ -0,0 +1,82 @@ +//////////////////////////////////////// +// Programming project code +// UniBw M, 2022, 2023, 2024 +// www.unibw.de/inf2 +// (c) Mark Minas (mark.minas@unibw.de) +//////////////////////////////////////// + +package pp.monopoly; + +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 Monopoly game. + *

+ * This class allows for loading configuration settings from a properties file, + * including the server port, map dimensions + *

+ *

+ * Note: Attributes of this class are not marked as {@code final} to allow + * for proper initialization when reading from a properties file. + *

+ */ +public class MonopolyConfig extends Config { + + /** + * The default port number for the Monopoly 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; + + /** + * Creates an instance of {@code MonopolyConfig} with default settings. + */ + public MonopolyConfig() { + // Default constructor + } + + /** + * Returns the port number configured for the Monopoly 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); + } + +} diff --git a/Projekte/monopoly/model/src/main/java/pp/monopoly/Resources.java b/Projekte/monopoly/model/src/main/java/pp/monopoly/Resources.java new file mode 100644 index 0000000..e833157 --- /dev/null +++ b/Projekte/monopoly/model/src/main/java/pp/monopoly/Resources.java @@ -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.monopoly; + +import java.util.ResourceBundle; + +/** + * Provides access to the resource bundle of the game. + * + * @see #BUNDLE + */ +public class Resources { + /** + * The resource bundle for the Monopoly game. + */ + public static final ResourceBundle BUNDLE = ResourceBundle.getBundle("monopoly"); //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 */ } +} diff --git a/Projekte/monopoly/model/src/main/java/pp/monopoly/game/client/ClientGameLogic.java b/Projekte/monopoly/model/src/main/java/pp/monopoly/game/client/ClientGameLogic.java new file mode 100644 index 0000000..4801307 --- /dev/null +++ b/Projekte/monopoly/model/src/main/java/pp/monopoly/game/client/ClientGameLogic.java @@ -0,0 +1,171 @@ +//////////////////////////////////////// +// Programming project code +// UniBw M, 2022, 2023, 2024 +// www.unibw.de/inf2 +// (c) Mark Minas (mark.minas@unibw.de) +//////////////////////////////////////// + +package pp.monopoly.game.client; + +import pp.monopoly.message.client.ClientMessage; +import pp.monopoly.message.server.ServerInterpreter; +import pp.monopoly.model.IntPoint; +import pp.monopoly.model.Board; +import pp.monopoly.notification.ClientStateEvent; +import pp.monopoly.notification.GameEvent; +import pp.monopoly.notification.GameEventBroker; +import pp.monopoly.notification.GameEventListener; +import pp.monopoly.notification.InfoTextEvent; +import pp.monopoly.notification.Sound; +import pp.monopoly.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 Monopoly. + * Manages the player's 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 listeners = new ArrayList<>(); + private Board ownMap; + private Board harbor; + private Board opponentMap; + private ClientState state = null; + + /** + * 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 player's own map. + * + * @return the player's own map + */ + public Board getMap() { + return ownMap; + } + + /** + * Moves the preview figure to the specified position. + * + * @param pos the new position for the preview figure + */ + public void movePreview(IntPoint pos) { + state.movePreview(pos); + } + + /** + * 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); + } + + /** + * 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 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); + } +} diff --git a/Projekte/monopoly/model/src/main/java/pp/monopoly/game/client/ClientSender.java b/Projekte/monopoly/model/src/main/java/pp/monopoly/game/client/ClientSender.java new file mode 100644 index 0000000..b50c6ab --- /dev/null +++ b/Projekte/monopoly/model/src/main/java/pp/monopoly/game/client/ClientSender.java @@ -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.monopoly.game.client; + +import pp.monopoly.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); +} diff --git a/Projekte/monopoly/model/src/main/java/pp/monopoly/game/client/ClientState.java b/Projekte/monopoly/model/src/main/java/pp/monopoly/game/client/ClientState.java new file mode 100644 index 0000000..4bd64db --- /dev/null +++ b/Projekte/monopoly/model/src/main/java/pp/monopoly/game/client/ClientState.java @@ -0,0 +1,85 @@ +//////////////////////////////////////// +// Programming project code +// UniBw M, 2022, 2023, 2024 +// www.unibw.de/inf2 +// (c) Mark Minas (mark.minas@unibw.de) +//////////////////////////////////////// + +package pp.monopoly.game.client; +import pp.monopoly.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 battle state should be shown. + * + * @return true if the battle state should be shown, false otherwise + */ + boolean showTurn() { + return false; + } + + /** + * Moves the preview figure to the specified position. + * + * @param pos the new position for the preview figure + */ + void movePreview(IntPoint pos) { + ClientGameLogic.LOGGER.log(Level.DEBUG, "movePreview has no effect 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"); + } + + /** + * 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 */ } +} diff --git a/Projekte/monopoly/model/src/main/java/pp/monopoly/game/client/MonopolyClient.java b/Projekte/monopoly/model/src/main/java/pp/monopoly/game/client/MonopolyClient.java new file mode 100644 index 0000000..7ced9da --- /dev/null +++ b/Projekte/monopoly/model/src/main/java/pp/monopoly/game/client/MonopolyClient.java @@ -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.monopoly.game.client; + +/** + * Interface representing a Monopoly client. + * Provides methods to access game logic, configuration, and to enqueue tasks. + */ +public interface MonopolyClient { + + /** + * Returns the game logic associated with this client. + * + * @return the ClientGameLogic instance + */ + ClientGameLogic getGameLogic(); + + /** + * Returns the configuration associated with this client. + * + * @return the MonopolyConfig instance + */ + MonopolyClientConfig getConfig(); + + /** + * Enqueues a task to be executed by the client. + * + * @param runnable the task to be executed + */ + void enqueue(Runnable runnable); +} diff --git a/Projekte/monopoly/model/src/main/java/pp/monopoly/game/client/MonopolyClientConfig.java b/Projekte/monopoly/model/src/main/java/pp/monopoly/game/client/MonopolyClientConfig.java new file mode 100644 index 0000000..b92a4b6 --- /dev/null +++ b/Projekte/monopoly/model/src/main/java/pp/monopoly/game/client/MonopolyClientConfig.java @@ -0,0 +1,52 @@ +//////////////////////////////////////// +// Programming project code +// UniBw M, 2022, 2023, 2024 +// www.unibw.de/inf2 +// (c) Mark Minas (mark.minas@unibw.de) +//////////////////////////////////////// + +package pp.monopoly.game.client; + +import pp.monopoly.MonopolyConfig; +import pp.monopoly.model.IntPoint; + +import java.io.File; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +/** + * Class providing access to the Monopoly client configuration. + * Extends {@link MonopolyConfig} 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. + *

+ * Note: Attributes of this class should not be marked as {@code final} + * to ensure proper functionality when reading from a properties file. + *

+ */ +public class MonopolyClientConfig extends MonopolyConfig { + + /** + * Path to the file representing the map. + */ + @Property("map") + private String map; + + + /** + * Creates a default {@code MonopolyClientConfig} with predefined values. + */ + public MonopolyClientConfig() { + // Default constructor + } + + /** + * Returns the file representing the opponent's map. + * + * @return the opponent's map file, or {@code null} if not set. + */ + public File getMap() { + return map == null ? null : new File(map); + } +} diff --git a/Projekte/monopoly/model/src/main/java/pp/monopoly/game/client/ServerConnection.java b/Projekte/monopoly/model/src/main/java/pp/monopoly/game/client/ServerConnection.java new file mode 100644 index 0000000..0c2cfe9 --- /dev/null +++ b/Projekte/monopoly/model/src/main/java/pp/monopoly/game/client/ServerConnection.java @@ -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.monopoly.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(); +} diff --git a/Projekte/monopoly/model/src/main/java/pp/monopoly/game/server/Player.java b/Projekte/monopoly/model/src/main/java/pp/monopoly/game/server/Player.java new file mode 100644 index 0000000..67b6791 --- /dev/null +++ b/Projekte/monopoly/model/src/main/java/pp/monopoly/game/server/Player.java @@ -0,0 +1,15 @@ +//////////////////////////////////////// +// Programming project code +// UniBw M, 2022, 2023, 2024 +// www.unibw.de/inf2 +// (c) Mark Minas (mark.minas@unibw.de) +//////////////////////////////////////// + +package pp.monopoly.game.server; + +/** + * Class representing a player + */ +public class Player { + +} diff --git a/Projekte/monopoly/model/src/main/java/pp/monopoly/game/server/ServerGameLogic.java b/Projekte/monopoly/model/src/main/java/pp/monopoly/game/server/ServerGameLogic.java new file mode 100644 index 0000000..35279b6 --- /dev/null +++ b/Projekte/monopoly/model/src/main/java/pp/monopoly/game/server/ServerGameLogic.java @@ -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.monopoly.game.server; + +import pp.monopoly.MonopolyConfig; +import pp.monopoly.message.client.ClientInterpreter; +import pp.monopoly.message.server.ServerMessage; + +import pp.monopoly.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 Monopoly. + * 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 MonopolyConfig config; + private final List players = new ArrayList<>(2); + private final Set 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, MonopolyConfig 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) { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method"); + } + + /** + * 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) { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method"); + } + + /** + * 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) { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method"); + } + + /** + * Marks the player as ready + * Transitions the state to PLAY if both players are ready. + * + * @param player the player who is ready + */ + void playerReady(Player player) { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method"); + } + +} diff --git a/Projekte/monopoly/model/src/main/java/pp/monopoly/game/server/ServerSender.java b/Projekte/monopoly/model/src/main/java/pp/monopoly/game/server/ServerSender.java new file mode 100644 index 0000000..f14ce46 --- /dev/null +++ b/Projekte/monopoly/model/src/main/java/pp/monopoly/game/server/ServerSender.java @@ -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.monopoly.game.server; + +import pp.monopoly.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); +} diff --git a/Projekte/monopoly/model/src/main/java/pp/monopoly/game/server/ServerState.java b/Projekte/monopoly/model/src/main/java/pp/monopoly/game/server/ServerState.java new file mode 100644 index 0000000..d6185d9 --- /dev/null +++ b/Projekte/monopoly/model/src/main/java/pp/monopoly/game/server/ServerState.java @@ -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.monopoly.game.server; + +/** + * Represents the different states of the Monopoly 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 + */ + BATTLE, + + /** + * The game has ended because all the players went bankrott + */ + GAME_OVER +} diff --git a/Projekte/monopoly/model/src/main/java/pp/monopoly/message/client/ClientInterpreter.java b/Projekte/monopoly/model/src/main/java/pp/monopoly/message/client/ClientInterpreter.java new file mode 100644 index 0000000..91c274b --- /dev/null +++ b/Projekte/monopoly/model/src/main/java/pp/monopoly/message/client/ClientInterpreter.java @@ -0,0 +1,15 @@ +//////////////////////////////////////// +// Programming project code +// UniBw M, 2022, 2023, 2024 +// www.unibw.de/inf2 +// (c) Mark Minas (mark.minas@unibw.de) +//////////////////////////////////////// + +package pp.monopoly.message.client; + +/** + * Visitor interface for processing all client messages. + */ +public interface ClientInterpreter { + +} diff --git a/Projekte/monopoly/model/src/main/java/pp/monopoly/message/client/ClientMessage.java b/Projekte/monopoly/model/src/main/java/pp/monopoly/message/client/ClientMessage.java new file mode 100644 index 0000000..de0a3a5 --- /dev/null +++ b/Projekte/monopoly/model/src/main/java/pp/monopoly/message/client/ClientMessage.java @@ -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.monopoly.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); +} diff --git a/Projekte/monopoly/model/src/main/java/pp/monopoly/message/server/ServerInterpreter.java b/Projekte/monopoly/model/src/main/java/pp/monopoly/message/server/ServerInterpreter.java new file mode 100644 index 0000000..73a3359 --- /dev/null +++ b/Projekte/monopoly/model/src/main/java/pp/monopoly/message/server/ServerInterpreter.java @@ -0,0 +1,16 @@ +//////////////////////////////////////// +// Programming project code +// UniBw M, 2022, 2023, 2024 +// www.unibw.de/inf2 +// (c) Mark Minas (mark.minas@unibw.de) +//////////////////////////////////////// + +package pp.monopoly.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 { + +} diff --git a/Projekte/monopoly/model/src/main/java/pp/monopoly/message/server/ServerMessage.java b/Projekte/monopoly/model/src/main/java/pp/monopoly/message/server/ServerMessage.java new file mode 100644 index 0000000..49d7bab --- /dev/null +++ b/Projekte/monopoly/model/src/main/java/pp/monopoly/message/server/ServerMessage.java @@ -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.monopoly.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(); +} diff --git a/Projekte/monopoly/model/src/main/java/pp/monopoly/model/Board.java b/Projekte/monopoly/model/src/main/java/pp/monopoly/model/Board.java new file mode 100644 index 0000000..b230fdb --- /dev/null +++ b/Projekte/monopoly/model/src/main/java/pp/monopoly/model/Board.java @@ -0,0 +1,167 @@ +//////////////////////////////////////// +// Programming project code +// UniBw M, 2022, 2023, 2024 +// www.unibw.de/inf2 +// (c) Mark Minas (mark.minas@unibw.de) +//////////////////////////////////////// + +package pp.monopoly.model; + +import pp.monopoly.notification.GameEvent; +import pp.monopoly.notification.GameEventBroker; +import pp.monopoly.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 figures and registers houses, hotels + * 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 Board { + /** + * A list of items (figure, house, etc.) placed on the map. + */ + private final List 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 Board(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 figure or a house) 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)); + } + + /** + * 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 the type of items to return + * @return a stream of items matching the specified class type + */ + private Stream getItems(Class clazz) { + return items.stream().filter(clazz::isInstance).map(clazz::cast); + } + + /** + * Returns an unmodifiable list of all items currently on the map. + * + * @return an unmodifiable list of all items + */ + public List 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; + } + + /** + * 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 map. + * + * @return a string representation of the map + */ + @Override + public String toString() { + return "Board:{" + 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); + } +} diff --git a/Projekte/monopoly/model/src/main/java/pp/monopoly/model/IntPoint.java b/Projekte/monopoly/model/src/main/java/pp/monopoly/model/IntPoint.java new file mode 100644 index 0000000..1ffa23b --- /dev/null +++ b/Projekte/monopoly/model/src/main/java/pp/monopoly/model/IntPoint.java @@ -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.monopoly.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 + } +} diff --git a/Projekte/monopoly/model/src/main/java/pp/monopoly/model/IntPosition.java b/Projekte/monopoly/model/src/main/java/pp/monopoly/model/IntPosition.java new file mode 100644 index 0000000..2fe55b8 --- /dev/null +++ b/Projekte/monopoly/model/src/main/java/pp/monopoly/model/IntPosition.java @@ -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.monopoly.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(); +} diff --git a/Projekte/monopoly/model/src/main/java/pp/monopoly/model/Item.java b/Projekte/monopoly/model/src/main/java/pp/monopoly/model/Item.java new file mode 100644 index 0000000..c3e2a5e --- /dev/null +++ b/Projekte/monopoly/model/src/main/java/pp/monopoly/model/Item.java @@ -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.monopoly.model; + +/** + * An interface representing any item on a board + * 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 the type of result returned by the visitor + * @return the result of the visitor's operation on the item + */ + T accept(Visitor 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); +} diff --git a/Projekte/monopoly/model/src/main/java/pp/monopoly/model/Rotation.java b/Projekte/monopoly/model/src/main/java/pp/monopoly/model/Rotation.java new file mode 100644 index 0000000..37bfab0 --- /dev/null +++ b/Projekte/monopoly/model/src/main/java/pp/monopoly/model/Rotation.java @@ -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.monopoly.model; + +import java.io.Serializable; + +/** + * Represents the rotation of a Item and provides functionality related to rotation. + */ +public enum Rotation implements Serializable { + /** + * Represents the item facing upwards. + */ + UP, + /** + * Represents the item facing rightwards. + */ + RIGHT, + /** + * Represents the item facing downwards. + */ + DOWN, + /** + * Represents the item 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]; + } +} diff --git a/Projekte/monopoly/model/src/main/java/pp/monopoly/model/Visitor.java b/Projekte/monopoly/model/src/main/java/pp/monopoly/model/Visitor.java new file mode 100644 index 0000000..ed5deec --- /dev/null +++ b/Projekte/monopoly/model/src/main/java/pp/monopoly/model/Visitor.java @@ -0,0 +1,17 @@ +//////////////////////////////////////// +// Programming project code +// UniBw M, 2022, 2023, 2024 +// www.unibw.de/inf2 +// (c) Mark Minas (mark.minas@unibw.de) +//////////////////////////////////////// + +package pp.monopoly.model; + +/** + * An interface for implementing the Visitor pattern for different types of elements in the Monopoly model. + * + * @param the type of result returned by the visit methods + */ +public interface Visitor { + +} diff --git a/Projekte/monopoly/model/src/main/java/pp/monopoly/model/VoidVisitor.java b/Projekte/monopoly/model/src/main/java/pp/monopoly/model/VoidVisitor.java new file mode 100644 index 0000000..471421d --- /dev/null +++ b/Projekte/monopoly/model/src/main/java/pp/monopoly/model/VoidVisitor.java @@ -0,0 +1,16 @@ +//////////////////////////////////////// +// Programming project code +// UniBw M, 2022, 2023, 2024 +// www.unibw.de/inf2 +// (c) Mark Minas (mark.minas@unibw.de) +//////////////////////////////////////// + +package pp.monopoly.model; + +/** + * An interface for implementing the Visitor pattern for different types of elements in the Monopoly model + * without returning any result. + */ +public interface VoidVisitor { + +} diff --git a/Projekte/monopoly/model/src/main/java/pp/monopoly/notification/ClientStateEvent.java b/Projekte/monopoly/model/src/main/java/pp/monopoly/notification/ClientStateEvent.java new file mode 100644 index 0000000..d30cca7 --- /dev/null +++ b/Projekte/monopoly/model/src/main/java/pp/monopoly/notification/ClientStateEvent.java @@ -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.monopoly.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); + } +} diff --git a/Projekte/monopoly/model/src/main/java/pp/monopoly/notification/GameEvent.java b/Projekte/monopoly/model/src/main/java/pp/monopoly/notification/GameEvent.java new file mode 100644 index 0000000..a4eac55 --- /dev/null +++ b/Projekte/monopoly/model/src/main/java/pp/monopoly/notification/GameEvent.java @@ -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.monopoly.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); +} diff --git a/Projekte/monopoly/model/src/main/java/pp/monopoly/notification/GameEventBroker.java b/Projekte/monopoly/model/src/main/java/pp/monopoly/notification/GameEventBroker.java new file mode 100644 index 0000000..c29e004 --- /dev/null +++ b/Projekte/monopoly/model/src/main/java/pp/monopoly/notification/GameEventBroker.java @@ -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.monopoly.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); +} diff --git a/Projekte/monopoly/model/src/main/java/pp/monopoly/notification/GameEventListener.java b/Projekte/monopoly/model/src/main/java/pp/monopoly/notification/GameEventListener.java new file mode 100644 index 0000000..3dec0c6 --- /dev/null +++ b/Projekte/monopoly/model/src/main/java/pp/monopoly/notification/GameEventListener.java @@ -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.monopoly.notification; + +/** + * Listener interface for all events implemented by subclasses of {@linkplain pp.monopoly.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 */ } +} diff --git a/Projekte/monopoly/model/src/main/java/pp/monopoly/notification/InfoTextEvent.java b/Projekte/monopoly/model/src/main/java/pp/monopoly/notification/InfoTextEvent.java new file mode 100644 index 0000000..6084e80 --- /dev/null +++ b/Projekte/monopoly/model/src/main/java/pp/monopoly/notification/InfoTextEvent.java @@ -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.monopoly.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); + } +} diff --git a/Projekte/monopoly/model/src/main/java/pp/monopoly/notification/ItemAddedEvent.java b/Projekte/monopoly/model/src/main/java/pp/monopoly/notification/ItemAddedEvent.java new file mode 100644 index 0000000..f2e2749 --- /dev/null +++ b/Projekte/monopoly/model/src/main/java/pp/monopoly/notification/ItemAddedEvent.java @@ -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.monopoly.notification; + +import pp.monopoly.model.Item; +import pp.monopoly.model.Board; + +/** + * 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, Board 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); + } +} diff --git a/Projekte/monopoly/model/src/main/java/pp/monopoly/notification/ItemRemovedEvent.java b/Projekte/monopoly/model/src/main/java/pp/monopoly/notification/ItemRemovedEvent.java new file mode 100644 index 0000000..76a19dc --- /dev/null +++ b/Projekte/monopoly/model/src/main/java/pp/monopoly/notification/ItemRemovedEvent.java @@ -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.monopoly.notification; + +import pp.monopoly.model.Item; +import pp.monopoly.model.Board; + +/** + * Event when an item gets removed. + * + * @param item the destroyed item + */ +public record ItemRemovedEvent(Item item, Board 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); + } +} diff --git a/Projekte/monopoly/model/src/main/java/pp/monopoly/notification/Sound.java b/Projekte/monopoly/model/src/main/java/pp/monopoly/notification/Sound.java new file mode 100644 index 0000000..280ad4f --- /dev/null +++ b/Projekte/monopoly/model/src/main/java/pp/monopoly/notification/Sound.java @@ -0,0 +1,8 @@ +package pp.monopoly.notification; + +/** + * Enumeration representing different types of sounds used in the game. + */ +public enum Sound { + +} diff --git a/Projekte/monopoly/model/src/main/java/pp/monopoly/notification/SoundEvent.java b/Projekte/monopoly/model/src/main/java/pp/monopoly/notification/SoundEvent.java new file mode 100644 index 0000000..2eaaefe --- /dev/null +++ b/Projekte/monopoly/model/src/main/java/pp/monopoly/notification/SoundEvent.java @@ -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.monopoly.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); + } +} diff --git a/Projekte/monopoly/model/src/main/resources/monopoly.properties b/Projekte/monopoly/model/src/main/resources/monopoly.properties new file mode 100644 index 0000000..1fdd723 --- /dev/null +++ b/Projekte/monopoly/model/src/main/resources/monopoly.properties @@ -0,0 +1,9 @@ +######################################## +## 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 diff --git a/Projekte/monopoly/model/src/main/resources/monopoly_de.properties b/Projekte/monopoly/model/src/main/resources/monopoly_de.properties new file mode 100644 index 0000000..868e433 --- /dev/null +++ b/Projekte/monopoly/model/src/main/resources/monopoly_de.properties @@ -0,0 +1,9 @@ +######################################## +## Programming project code +## UniBw M, 2022, 2023, 2024 +## www.unibw.de/inf2 +## (c) Mark Minas (mark.minas@unibw.de) +######################################## +# +monopoly.name=Monopoly +button.ready=Bereit diff --git a/Projekte/monopoly/model/src/test/java/pp/monopoly/client/ClientLogicTest.java b/Projekte/monopoly/model/src/test/java/pp/monopoly/client/ClientLogicTest.java new file mode 100644 index 0000000..2d19489 --- /dev/null +++ b/Projekte/monopoly/model/src/test/java/pp/monopoly/client/ClientLogicTest.java @@ -0,0 +1,7 @@ +package pp.monopoly.client; + +import org.junit.Test; + +public class ClientLogicTest { + +} diff --git a/Projekte/monopoly/model/src/test/java/pp/monopoly/game/client/ClientGameLogicTest.java b/Projekte/monopoly/model/src/test/java/pp/monopoly/game/client/ClientGameLogicTest.java new file mode 100644 index 0000000..18c636f --- /dev/null +++ b/Projekte/monopoly/model/src/test/java/pp/monopoly/game/client/ClientGameLogicTest.java @@ -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.monopoly.game.client; + +import org.junit.Before; +import org.junit.Test; + + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class ClientGameLogicTest { + +} \ No newline at end of file diff --git a/Projekte/monopoly/model/src/test/java/pp/monopoly/game/server/ServerGameLogicTest.java b/Projekte/monopoly/model/src/test/java/pp/monopoly/game/server/ServerGameLogicTest.java new file mode 100644 index 0000000..8422cf1 --- /dev/null +++ b/Projekte/monopoly/model/src/test/java/pp/monopoly/game/server/ServerGameLogicTest.java @@ -0,0 +1,12 @@ +package pp.monopoly.game.server; + +import org.junit.Before; +import org.junit.Test; + + +import static org.junit.Assert.assertEquals; + + +public class ServerGameLogicTest { + +} \ No newline at end of file diff --git a/Projekte/monopoly/model/src/test/java/pp/monopoly/model/RandomPositionIteratorTest.java b/Projekte/monopoly/model/src/test/java/pp/monopoly/model/RandomPositionIteratorTest.java new file mode 100644 index 0000000..b154cad --- /dev/null +++ b/Projekte/monopoly/model/src/test/java/pp/monopoly/model/RandomPositionIteratorTest.java @@ -0,0 +1,9 @@ +package pp.monopoly.model; + +import org.junit.Test; + +import static org.junit.Assert.fail; + +public class RandomPositionIteratorTest { + +} diff --git a/Projekte/monopoly/run-mac.sh b/Projekte/monopoly/run-mac.sh new file mode 100644 index 0000000..b4e9b50 --- /dev/null +++ b/Projekte/monopoly/run-mac.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +# Starts the server and two clients in different terminal windows on MacOS + +# export JAVA_OPTS="-Duser.language=fr -Duser.region=FR -Djava.util.logging.config.file=logging.properties" +# export JAVA_OPTS="-Duser.language=en -Duser.region=US -Djava.util.logging.config.file=logging.properties" +export JAVA_OPTS="-Djava.util.logging.config.file=logging.properties" + +export CWD=`pwd`/`dirname $0` +cd $CWD/.. +./gradlew installDist +osascript -e "tell app \"Terminal\" to do script \"cd $CWD/server; build/install/monopoly-server/bin/monopoly-server\"" +sleep 2 +osascript -e "tell app \"Terminal\" to do script \"cd $CWD/client; build/install/monopoly/bin/monopoly\"" +sleep 2 +osascript -e "tell app \"Terminal\" to do script \"cd $CWD/client; build/install/monopoly/bin/monopoly\"" diff --git a/Projekte/monopoly/run.sh b/Projekte/monopoly/run.sh new file mode 100644 index 0000000..3cd8c5d --- /dev/null +++ b/Projekte/monopoly/run.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +# Starts two clients and the server + +# export JAVA_OPTS="-Duser.language=fr -Duser.region=FR -Djava.util.logging.config.file=logging.properties" +# export JAVA_OPTS="-Duser.language=en -Duser.region=US -Djava.util.logging.config.file=logging.properties" +export JAVA_OPTS="-Djava.util.logging.config.file=logging.properties" + +export CWD=`pwd`/`dirname $0` +cd $CWD/.. + +./gradlew installDist + +cd monopoly/client +build/install/monopoly/bin/monopoly & +sleep 3 +build/install/monopoly/bin/monopoly & +cd ../server +build/install/monopoly-server/bin/monopoly-server diff --git a/Projekte/monopoly/server/build.gradle b/Projekte/monopoly/server/build.gradle new file mode 100644 index 0000000..72f2584 --- /dev/null +++ b/Projekte/monopoly/server/build.gradle @@ -0,0 +1,14 @@ +plugins { + id 'buildlogic.java-application-conventions' +} + +description = 'Monopoly Server' + +dependencies { + implementation project(":monopoly:model") +} + +application { + mainClass = 'pp.monopoly.server.MonopolyServer' + applicationName = 'monopoly-server' +} diff --git a/Projekte/monopoly/server/logging.properties b/Projekte/monopoly/server/logging.properties new file mode 100644 index 0000000..0656ca0 --- /dev/null +++ b/Projekte/monopoly/server/logging.properties @@ -0,0 +1,7 @@ +handlers=java.util.logging.ConsoleHandler +.level=INFO +pp.level=FINE +com.jme3.network.level=INFO +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 diff --git a/Projekte/monopoly/server/server.properties b/Projekte/monopoly/server/server.properties new file mode 100644 index 0000000..2439842 --- /dev/null +++ b/Projekte/monopoly/server/server.properties @@ -0,0 +1,14 @@ +######################################## +## Programming project code +## UniBw M, 2022, 2023, 2024 +## www.unibw.de/inf2 +## (c) Mark Minas (mark.minas@unibw.de) +######################################## +# +# Battleship server configuration file +# +# This file defines the configuration settings for the Battleship server. +# +# The port number on which the server will listen for incoming connections. +port=1234 + diff --git a/Projekte/monopoly/server/src/main/java/pp/monopoly/server/MonopolyServer.java b/Projekte/monopoly/server/src/main/java/pp/monopoly/server/MonopolyServer.java new file mode 100644 index 0000000..73aaa17 --- /dev/null +++ b/Projekte/monopoly/server/src/main/java/pp/monopoly/server/MonopolyServer.java @@ -0,0 +1,163 @@ +//////////////////////////////////////// +// Programming project code +// UniBw M, 2022, 2023, 2024 +// www.unibw.de/inf2 +// (c) Mark Minas (mark.minas@unibw.de) +//////////////////////////////////////// + +package pp.monopoly.server; + +import com.jme3.network.ConnectionListener; +import com.jme3.network.HostedConnection; +import com.jme3.network.Message; +import com.jme3.network.MessageListener; +import com.jme3.network.Network; +import com.jme3.network.Server; +import com.jme3.network.serializing.Serializer; +import pp.monopoly.MonopolyConfig; +import pp.monopoly.game.server.Player; +import pp.monopoly.game.server.ServerGameLogic; +import pp.monopoly.game.server.ServerSender; +import pp.monopoly.message.client.ClientMessage; +import pp.monopoly.message.server.ServerMessage; +import pp.monopoly.model.IntPoint; + +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.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.logging.LogManager; + +/** + * Server implementing the visitor pattern as MessageReceiver for ClientMessages + */ +public class MonopolyServer implements MessageListener, ConnectionListener, ServerSender { + private static final Logger LOGGER = System.getLogger(MonopolyServer.class.getName()); + private static final File CONFIG_FILE = new File("server.properties"); + + private final MonopolyConfig config = new MonopolyConfig(); + private Server myServer; + private final ServerGameLogic logic; + private final BlockingQueue pendingMessages = new LinkedBlockingQueue<>(); + + 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 Monopolys server. + */ + public static void main(String[] args) { + new MonopolyServer().run(); + } + + /** + * Creates the server. + */ + MonopolyServer() { + config.readFromIfExists(CONFIG_FILE); + LOGGER.log(Level.INFO, "Configuration: {0}", config); //NON-NLS + logic = new ServerGameLogic(this, config); + } + + public void run() { + startServer(); + while (true) + processNextMessage(); + } + + private void startServer() { + try { + LOGGER.log(Level.INFO, "Starting server..."); //NON-NLS + myServer = Network.createServer(config.getPort()); + initializeSerializables(); + myServer.start(); + registerListeners(); + LOGGER.log(Level.INFO, "Server started: {0}", myServer.isRunning()); //NON-NLS + } + catch (IOException e) { + LOGGER.log(Level.ERROR, "Couldn't start server: {0}", e.getMessage()); //NON-NLS + exit(1); + } + } + + private void processNextMessage() { + try { + pendingMessages.take().process(logic); + } + catch (InterruptedException ex) { + LOGGER.log(Level.INFO, "Interrupted while waiting for messages"); //NON-NLS + Thread.currentThread().interrupt(); + } + } + + private void initializeSerializables() { + Serializer.registerClass(IntPoint.class); + } + + private void registerListeners() { + myServer.addConnectionListener(this); + } + + @Override + public void messageReceived(HostedConnection source, Message message) { + LOGGER.log(Level.INFO, "message received from {0}: {1}", source.getId(), message); //NON-NLS + if (message instanceof ClientMessage clientMessage) + pendingMessages.add(new ReceivedMessage(clientMessage, source.getId())); + } + + @Override + public void connectionAdded(Server server, HostedConnection hostedConnection) { + LOGGER.log(Level.INFO, "new connection {0}", hostedConnection); //NON-NLS + logic.addPlayer(hostedConnection.getId()); + } + + @Override + public void connectionRemoved(Server server, HostedConnection hostedConnection) { + LOGGER.log(Level.INFO, "connection closed: {0}", hostedConnection); //NON-NLS + final Player player = logic.getPlayerById(hostedConnection.getId()); + if (player == null) + LOGGER.log(Level.INFO, "closed connection does not belong to an active player"); //NON-NLS + else { //NON-NLS + LOGGER.log(Level.INFO, "closed connection belongs to {0}", player); //NON-NLS + exit(0); + } + } + + private void exit(int exitValue) { //NON-NLS + LOGGER.log(Level.INFO, "close request"); //NON-NLS + if (myServer != null) + for (HostedConnection client : myServer.getConnections()) //NON-NLS + if (client != null) client.close("Game over"); //NON-NLS + System.exit(exitValue); + } + + /** + * Send the specified message to the specified connection. + * + * @param id the connection id + * @param message the message + */ + public void send(int id, ServerMessage message) { + if (myServer == null || !myServer.isRunning()) { + LOGGER.log(Level.ERROR, "no server running when trying to send {0}", message); //NON-NLS + return; + } + final HostedConnection connection = myServer.getConnection(id); + if (connection != null) + connection.send(message); + else + LOGGER.log(Level.ERROR, "there is no connection with id={0}", id); //NON-NLS + } +} \ No newline at end of file diff --git a/Projekte/monopoly/server/src/main/java/pp/monopoly/server/ReceivedMessage.java b/Projekte/monopoly/server/src/main/java/pp/monopoly/server/ReceivedMessage.java new file mode 100644 index 0000000..59c38cc --- /dev/null +++ b/Projekte/monopoly/server/src/main/java/pp/monopoly/server/ReceivedMessage.java @@ -0,0 +1,17 @@ +//////////////////////////////////////// +// Programming project code +// UniBw M, 2022, 2023, 2024 +// www.unibw.de/inf2 +// (c) Mark Minas (mark.minas@unibw.de) +//////////////////////////////////////// + +package pp.monopoly.server; + +import pp.monopoly.message.client.ClientInterpreter; +import pp.monopoly.message.client.ClientMessage; + +record ReceivedMessage(ClientMessage message, int from) { + void process(ClientInterpreter interpreter) { + message.accept(interpreter, from); + } +} diff --git a/Projekte/settings.gradle b/Projekte/settings.gradle index 7440607..e5f6b48 100644 --- a/Projekte/settings.gradle +++ b/Projekte/settings.gradle @@ -2,6 +2,9 @@ include ':battleship:client' include ':battleship:server' include ':battleship:model' include ':battleship:converter' +include ':monopoly:client' +include ':monopoly:server' +include ':monopoly:model' include ':common' include ':jme-common'