added contents

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

26
.gitignore vendored Normal file
View File

@ -0,0 +1,26 @@
# Gradle
.gradle
build
# VSC
bin
# IntelliJ
*.iml
.idea
out
# Eclipse
.classpath
.project
# Libraries
*.so
*.dylib
*.dll
*.jar
*.class
.DS_Store
!Projekte/gradle/wrapper/gradle-wrapper.jar

1
Dokumente/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
#

3
Projekte/.gitattributes vendored Normal file
View File

@ -0,0 +1,3 @@
*.bat text eol=crlf
gradlew text eol=lf

View File

@ -0,0 +1,18 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="BattleshipApp (Mac)" type="Application" factoryName="Application"
singleton="false">
<option name="MAIN_CLASS_NAME" value="pp.battleship.client.BattleshipApp"/>
<module name="Projekte.battleship.client.main"/>
<option name="VM_PARAMETERS" value="-XstartOnFirstThread"/>
<option name="WORKING_DIRECTORY" value="$MODULE_WORKING_DIR$"/>
<extension name="coverage">
<pattern>
<option name="PATTERN" value="pp.battleship.client.*"/>
<option name="ENABLED" value="true"/>
</pattern>
</extension>
<method v="2">
<option name="Make" enabled="true"/>
</method>
</configuration>
</component>

View File

@ -0,0 +1,18 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="BattleshipApp" type="Application" factoryName="Application" singleton="false"
nameIsGenerated="true">
<option name="MAIN_CLASS_NAME" value="pp.battleship.client.BattleshipApp"/>
<module name="Projekte.battleship.client.main"/>
<option name="VM_PARAMETERS" value="-Djava.util.logging.config.file=logging.properties"/>
<option name="WORKING_DIRECTORY" value="$MODULE_WORKING_DIR$"/>
<extension name="coverage">
<pattern>
<option name="PATTERN" value="pp.battleship.client.*"/>
<option name="ENABLED" value="true"/>
</pattern>
</extension>
<method v="2">
<option name="Make" enabled="true"/>
</method>
</configuration>
</component>

View File

@ -0,0 +1,17 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="BattleshipServer" type="Application" factoryName="Application"
nameIsGenerated="true">
<option name="MAIN_CLASS_NAME" value="pp.battleship.server.BattleshipServer"/>
<module name="Projekte.battleship.server.main"/>
<option name="WORKING_DIRECTORY" value="$MODULE_WORKING_DIR$"/>
<extension name="coverage">
<pattern>
<option name="PATTERN" value="pp.battleship.server.*"/>
<option name="ENABLED" value="true"/>
</pattern>
</extension>
<method v="2">
<option name="Make" enabled="true"/>
</method>
</configuration>
</component>

View File

@ -0,0 +1,24 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Projekte [test]" type="GradleRunConfiguration" factoryName="Gradle">
<ExternalSystemSettings>
<option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="externalSystemIdString" value="GRADLE" />
<option name="scriptParameters" value="--continue" />
<option name="taskDescriptions">
<list />
</option>
<option name="taskNames">
<list>
<option value="test" />
</list>
</option>
<option name="vmOptions" />
</ExternalSystemSettings>
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
<DebugAllEnabled>false</DebugAllEnabled>
<RunAsTest>false</RunAsTest>
<method v="2" />
</configuration>
</component>

189
Projekte/README.md Normal file
View File

@ -0,0 +1,189 @@
# Beispielprogramme des Programmierprojekts
Hier ist der Quellcode für das in der Einarbeitungsphase genutzte Spiel
_Battleships_ zu finden. Die Quellen bestehen aus den folgenden
Gradle-Unterprojekten:
* _:battleship:server_
* _:battleship:client_
* _:battleship:model_
* _:battleship:converter_
* _:common_
* _:jme-common_
_Battleships_ ist ein netzwerkbasiertes Spiel und besteht aus einem Server- und
einem Clientanteil, die in den Unterprojekten _:battleship:server_ und
_:battleship:client_ realisiert sind. Beide nutzen das Unterprojekt
_:battleship:model_, das den gemeinsamen Modellanteil enthält.
Die Unterprojekte _:common_ und _:jme-common_ enthalten Hilfsklassen.
Das Unterprojekt _:battleship:converter_ wird für _Battleships_ selbst nicht
benötigt, sondern enthält lediglich den Code, um ein im Spiel verwendetes
3d-Modell eines Schlachtschiffs in eine _J3O_-Datei umzuwandeln, die von jME
einfacher geladen werden kann.
## 1 Vorbereitung
Für das Programmierprojekt empfehlen wir die Verwendung von Java 20. Unter Linux
sollte [_Eclipse Temurin_](https://adoptium.net/temurin/releases/?version=20)
als JDK verwendet werden, andere JDKs können unter Linux Probleme verursachen.
Auf anderen Betriebssystemen empfehlen wir aber ebenfalls Temurin. Im Folgenden
ist beschrieben, wie Sie Temurin installieren und die Umgebungsvariable
**JAVA_HOME** richtig setzen, damit Sie Gradle (siehe unten) verwenden können.
### 1.1 Installation von Temurin
Laden Sie [_Eclipse Temurin_](https://adoptium.net/temurin/releases/?version=20)
entsprechend Ihrem Betriebssystem und Ihrer Prozessorarchitektur herunter und
entpacken Sie das Archiv in einem Verzeichnis Ihrer Wahl auf Ihrem Rechner.
### 1.2 Setzen von JAVA_HOME
Zur Verwendung mit Gradle muss die Umgebungsvariable **JAVA_HOME** richtig
gesetzt werden. Folgen Sie dazu den nachfolgenden Anweisungen entsprechend Ihrem
Betriebssystem:
* **Windows:**
Öffnen Sie ihre Powershell (Core) bzw. ihr Windows Terminal mit Powershell
(Core). Überprüfen Sie, ob die Umgebungsvariable korrekt gesetzt ist:
`Get-ChildItem -Path Env:JAVA_HOME`
Falls kein oder ein falscher Pfad gesetzt ist, setzen Sie diesen mit dem
folgenden Kommando (in einer Zeile):
`[System.Environment]::SetEnvironmentVariable('JAVA_HOME','<Pfad zum SDK>',[System.EnvironmentVariableTarget]::User)`
Alternativ können Sie die GUI verwenden. Unter Windows 10 klicken Sie die
Windows-Taste und dann das Zahnrad um die Einstellungen zu öffnen. Dort wählen
Sie "System", dann "Info" (links unten) und nun
"Erweiterte Systemeinstellungen" (rechts) um den Dialog "Systemeigenschaften"
zu starten. Im Reiter "Erweitert" klicken Sie
"Umgebungsvariablen..." und klicken dann unter "Benutzervariablen" den Knopf
"Neu..." um JAVA_HOME anzulegen oder "Bearbeiten" um ihn zu ändern. Geben Sie
als Name `JAVA_HOME` und als Wert den Pfad ein. Schließen Sie mit "OK".
> **(!) Beachten Sie, dass Sie die jeweilige Applikation neu starten müssen**,
> um von der gesetzten Umgebungsvariablen Notiz zu nehmen.
> Dies betrifft auch die Shell, die Sie gerade verwenden.
* **UNIX (Linux/MacOS):**
Öffnen oder erstellen Sie die Datei `~/.profile` (wenn Sie die Bash verwenden;
bei anderen Shells sind es andere Dateien) und ergänzen Sie am Ende der Datei
die Zeile:
`export JAVA_HOME="<Pfad zum entpackten Archiv>"`
Ersetzen Sie dabei `<Pfad zum entpackten Archiv>` mit dem entsprechenden Pfad.
Zum Beispiel:
`export JAVA_HOME="/home/user/jdk-20.0.2"`
Fügen Sie dann die folgende Zeile hinzu:
`export PATH="$JAVA_HOME/bin:$PATH"`
## 2 Programmstart
Grundsätzlich kann man das gesamte Projekt einfach in IntelliJ öffnen. Details
dazu sind im Aufgabenblatt zur Einarbeitungsaufgabe zu finden. Im Folgenden ist
beschrieben, wie die _Batttleships_ unmittelbar von der Kommandozeile gestartet
werden können.
Um _Battleships_ spielen zu können, muss man zuerst das Server-Programm auf
einem Rechner und dann zweimal das Client-Programm auf beliebigen Rechnern
starten, die TCP/IP-Verbindungen zum Server erlauben. Natürlich ist es auch
möglich, alle drei Programme auf demselben Rechner zu starten.
Es empfiehlt sich der Start von der Kommandozeile. Will man alle drei Programme
auf demselben Rechner starten, sollte man dazu drei Shell-Instanzen öffnen und
in jeder eines der Programme starten. Auf diese Weise können die
Logging-Ausgaben der drei Programme voneinander unterschieden werden.
Das Server-Programm startet man unmittelbar mit Gradle mit
`./gradlew :battleship:server:run`
Unter Windows kann es je nach Shell (Eingabeaufforderung cmd) erforderlich sein,
`/` jeweils durch `\ ` zu ersetzen.
Im Verzeichnis `battleship/server` befindet sich die Datei `config.propeties`,
worüber sich der Server konfigurieren lässt. Mit der Zeile `port=1234` lässt
sich der verwendete Server-Port (hier 1234) einstellen. Außerdem befindet sich
dort die Datei `logging.properties`, womit das Logging des Servers konfiguriert
wird.
Das Client-Programm startet man unmittelbar mit Gradle mit
`./gradlew :battleship:client:run`
Die Datei `logging.properties` im Verzeichnis `battleship/client` konfiguriert
das Logging des Clients.
Alternativ kann man auch die Start-Skripte
* `./battleship/server/build/install/battleship-server/bin/battleship-server`
* `./battleship/client/build/install/battleship/bin/battleship`
direkt in der Kommandozeile starten. Allerdings müssen sie zuvor mittels
`./gradlew installDist`
erzeugt worden sein. Beachten Sie aber, dass nur im **aktuellen
Arbeitsverzeichnis** nach den Dateien `config.properties` und
`logging.properties` gesucht wird und diese geladen werden. Das heißt, dass die
vordefinierten Dateien in den Verzeichnissen `battleship/server` und
`battleship/client` nur dann gelesen werden, wenn Sie diese Verzeichnisse als
aktuelle Arbeitsverzeichnisse nutzen. Wie üblich müssen Sie dazu in der
Kommandozeile
`cd battleship/server`
bzw.
`cd battleship/client`
eingeben.
## 3 Hinweise zu _Battleships_
Der _Battleships_-Client hat ein Menü, in das man immer mit der
Esc-Taste kommt. Aus dem Menü heraus lässt sich das Programm auch schließen.
Beachte, dass sich beim Laden und Speichern eines Spiels kein Dateidialog
öffnet. Vielmehr muss man den Dateipfad in das Dialogfeld eingeben. Da
JSON-Dateien geschrieben werden, empfiehlt sich das Datei-Suffix _.json_.
## 4 Allgemeine Gradle-Tasks:
- `./gradlew clean`
Entfernt alle `build`-Verzeichnisse und alle erzeugten Dateien.
- `./gradlew classes`
Übersetzt den Quellcode und legt unter build den Bytecode sowie
Ressourcen ab.
- `./gradlew javadoc`
Erzeugt die Dokumentation aus den JavaDoc-Kommentaren im Verzeichnis
`build/docs/javadoc` des jeweiligen Unterprojekts.
- `./gradlew test`
Führt die JUnit-Tests durch. Ergebnisse sind im Verzeichnis
`build/reports/tests` des jeweiligen Unterprojekts zu finden.
- `./gradlew build`
Führt die JUnit-Tests durch und erstellt in `build/distributions`
gepackte Distributionsdateien
- `./gradlew installDist`
Erstellt unter `battleship/client/build/install` und
`battleship/server/build/install` Verzeichnisse, die jeweils eine ausführbare
Distribution samt Start-Skripten enthält (siehe oben).
---
Juli 2024

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 235 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 207 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 235 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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