Compare commits
	
		
			1 Commits
		
	
	
		
			f8fe0b9877
			...
			revert-71a
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 9abf7350e4 | 
							
								
								
									
										26
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -1,26 +0,0 @@ | ||||
| # 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
									
									
								
							
							
						
						| @@ -1 +0,0 @@ | ||||
| # | ||||
							
								
								
									
										3
									
								
								Projekte/.gitattributes
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -1,3 +0,0 @@ | ||||
| *.bat           text eol=crlf | ||||
| gradlew         text eol=lf | ||||
|  | ||||
| @@ -1,18 +0,0 @@ | ||||
| <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> | ||||
| @@ -1,18 +0,0 @@ | ||||
| <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> | ||||
| @@ -1,17 +0,0 @@ | ||||
| <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> | ||||
| @@ -1,24 +0,0 @@ | ||||
| <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> | ||||
| @@ -1,189 +0,0 @@ | ||||
| # 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 | ||||
| @@ -1,22 +0,0 @@ | ||||
| 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' | ||||
| } | ||||
| @@ -1,73 +0,0 @@ | ||||
| ######################################## | ||||
| ## 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 | ||||
| @@ -1,8 +0,0 @@ | ||||
| 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 | ||||
| @@ -1,66 +0,0 @@ | ||||
| { | ||||
|   "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" | ||||
|     } | ||||
|   ] | ||||
| } | ||||
| @@ -1,66 +0,0 @@ | ||||
| { | ||||
|   "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" | ||||
|     } | ||||
|   ] | ||||
| } | ||||
| @@ -1,429 +0,0 @@ | ||||
| //////////////////////////////////////// | ||||
| // 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(); | ||||
|     } | ||||
| } | ||||
| @@ -1,196 +0,0 @@ | ||||
| //////////////////////////////////////// | ||||
| // 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; | ||||
|     } | ||||
| } | ||||
| @@ -1,102 +0,0 @@ | ||||
| //////////////////////////////////////// | ||||
| // 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(); | ||||
| } | ||||
| @@ -1,135 +0,0 @@ | ||||
| //////////////////////////////////////// | ||||
| // 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(); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,143 +0,0 @@ | ||||
| //////////////////////////////////////// | ||||
| // 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")); | ||||
|     } | ||||
| } | ||||
| @@ -1,153 +0,0 @@ | ||||
| //////////////////////////////////////// | ||||
| // 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()); | ||||
|     } | ||||
| } | ||||
| @@ -1,152 +0,0 @@ | ||||
| //////////////////////////////////////// | ||||
| // 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); | ||||
|     } | ||||
| } | ||||
| @@ -1,122 +0,0 @@ | ||||
| //////////////////////////////////////// | ||||
| // 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); | ||||
|     } | ||||
| } | ||||
| @@ -1,173 +0,0 @@ | ||||
| //////////////////////////////////////// | ||||
| // 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)); | ||||
|     } | ||||
| } | ||||
| @@ -1,191 +0,0 @@ | ||||
| //////////////////////////////////////// | ||||
| // 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); | ||||
|     } | ||||
| } | ||||
| @@ -1,125 +0,0 @@ | ||||
| //////////////////////////////////////// | ||||
| // 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); | ||||
|     } | ||||
| } | ||||
| @@ -1,203 +0,0 @@ | ||||
| //////////////////////////////////////// | ||||
| // 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(); | ||||
|     } | ||||
| } | ||||
| @@ -1,213 +0,0 @@ | ||||
| //////////////////////////////////////// | ||||
| // 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; | ||||
|         }; | ||||
|     } | ||||
| } | ||||
| @@ -1,106 +0,0 @@ | ||||
| //////////////////////////////////////// | ||||
| // 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 | ||||
|     } | ||||
| } | ||||
| @@ -1,89 +0,0 @@ | ||||
| //////////////////////////////////////// | ||||
| // 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()); | ||||
|     } | ||||
| } | ||||
| Before Width: | Height: | Size: 235 KiB | 
| Before Width: | Height: | Size: 94 KiB | 
| @@ -1,3 +0,0 @@ | ||||
| based on: | ||||
| https://free3d.com/3d-model/wwii-ship-uk-king-george-v-class-battleship-v1--185381.html | ||||
| License: Free Personal Use Only | ||||
| @@ -1,4 +0,0 @@ | ||||
| based on: | ||||
| water 002 | ||||
| by Katsukagi on 29/11/2018 | ||||
| https://3dtextures.me/2018/11/29/water-002/ | ||||
| @@ -1,14 +0,0 @@ | ||||
| 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 | ||||
|      } | ||||
| } | ||||
| Before Width: | Height: | Size: 15 KiB | 
| Before Width: | Height: | Size: 207 KiB | 
| Before Width: | Height: | Size: 88 KiB | 
| Before Width: | Height: | Size: 31 KiB | 
| Before Width: | Height: | Size: 70 KiB | 
| @@ -1,16 +0,0 @@ | ||||
| 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' | ||||
| } | ||||
| @@ -1,69 +0,0 @@ | ||||
| //////////////////////////////////////// | ||||
| // 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 | ||||
|     } | ||||
| } | ||||
| Before Width: | Height: | Size: 235 KiB | 
| @@ -1,18 +0,0 @@ | ||||
| # 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 | ||||
| Before Width: | Height: | Size: 94 KiB | 
| @@ -1,3 +0,0 @@ | ||||
| based on: | ||||
| https://free3d.com/3d-model/wwii-ship-uk-king-george-v-class-battleship-v1--185381.html | ||||
| License: Free Personal Use Only | ||||
| @@ -1,12 +0,0 @@ | ||||
| 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 | ||||
| } | ||||
| @@ -1,103 +0,0 @@ | ||||
| //////////////////////////////////////// | ||||
| // 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; | ||||
|     } | ||||
| } | ||||
| @@ -1,40 +0,0 @@ | ||||
| //////////////////////////////////////// | ||||
| // 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 */ } | ||||
| } | ||||
| @@ -1,102 +0,0 @@ | ||||
| //////////////////////////////////////// | ||||
| // 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); | ||||
|     } | ||||
| } | ||||
| @@ -1,38 +0,0 @@ | ||||
| //////////////////////////////////////// | ||||
| // 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); | ||||
| } | ||||
| @@ -1,355 +0,0 @@ | ||||
| //////////////////////////////////////// | ||||
| // 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); | ||||
|     } | ||||
| } | ||||
| @@ -1,22 +0,0 @@ | ||||
| //////////////////////////////////////// | ||||
| // 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); | ||||
| } | ||||
| @@ -1,202 +0,0 @@ | ||||
| //////////////////////////////////////// | ||||
| // 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 */ } | ||||
| } | ||||
| @@ -1,267 +0,0 @@ | ||||
| //////////////////////////////////////// | ||||
| // 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(); | ||||
|     } | ||||
| } | ||||
| @@ -1,32 +0,0 @@ | ||||
| //////////////////////////////////////// | ||||
| // 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; | ||||
|     } | ||||
| } | ||||
| @@ -1,65 +0,0 @@ | ||||
| //////////////////////////////////////// | ||||
| // 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; | ||||
|     } | ||||
| } | ||||
| @@ -1,32 +0,0 @@ | ||||
| //////////////////////////////////////// | ||||
| // 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(); | ||||
| } | ||||
| @@ -1,41 +0,0 @@ | ||||
| //////////////////////////////////////// | ||||
| // 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())); | ||||
|     } | ||||
| } | ||||
| @@ -1,54 +0,0 @@ | ||||
| //////////////////////////////////////// | ||||
| // 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; | ||||
|     } | ||||
| } | ||||
| @@ -1,220 +0,0 @@ | ||||
| //////////////////////////////////////// | ||||
| // 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)); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,23 +0,0 @@ | ||||
| //////////////////////////////////////// | ||||
| // 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); | ||||
| } | ||||
| @@ -1,33 +0,0 @@ | ||||
| //////////////////////////////////////// | ||||
| // 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 | ||||
| } | ||||
| @@ -1,114 +0,0 @@ | ||||
| //////////////////////////////////////// | ||||
| // 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; | ||||
|     } | ||||
| } | ||||
| @@ -1,75 +0,0 @@ | ||||
| //////////////////////////////////////// | ||||
| // 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()); | ||||
|     } | ||||
| } | ||||
| @@ -1,93 +0,0 @@ | ||||
| //////////////////////////////////////// | ||||
| // 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())); | ||||
|     } | ||||
| } | ||||
| @@ -1,127 +0,0 @@ | ||||
| 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(); | ||||
|     } | ||||
| } | ||||
| @@ -1,145 +0,0 @@ | ||||
| //////////////////////////////////////// | ||||
| // 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); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,29 +0,0 @@ | ||||
| //////////////////////////////////////// | ||||
| // 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); | ||||
| } | ||||
| @@ -1,32 +0,0 @@ | ||||
| //////////////////////////////////////// | ||||
| // 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); | ||||
| } | ||||
| @@ -1,66 +0,0 @@ | ||||
| //////////////////////////////////////// | ||||
| // 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); | ||||
|     } | ||||
| } | ||||
| @@ -1,63 +0,0 @@ | ||||
| //////////////////////////////////////// | ||||
| // 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); | ||||
|     } | ||||
| } | ||||
| @@ -1,211 +0,0 @@ | ||||
| //////////////////////////////////////// | ||||
| // 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"; | ||||
|     } | ||||
| } | ||||
| @@ -1,97 +0,0 @@ | ||||
| //////////////////////////////////////// | ||||
| // 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"; | ||||
|     } | ||||
| } | ||||
| @@ -1,36 +0,0 @@ | ||||
| //////////////////////////////////////// | ||||
| // 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); | ||||
| } | ||||
| @@ -1,39 +0,0 @@ | ||||
| //////////////////////////////////////// | ||||
| // 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(); | ||||
| } | ||||
| @@ -1,71 +0,0 @@ | ||||
| //////////////////////////////////////// | ||||
| // 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"; | ||||
|     } | ||||
| } | ||||
| @@ -1,324 +0,0 @@ | ||||
| //////////////////////////////////////// | ||||
| // 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); | ||||
|     } | ||||
| } | ||||
| @@ -1,94 +0,0 @@ | ||||
| //////////////////////////////////////// | ||||
| // 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 | ||||
|     } | ||||
| } | ||||
| @@ -1,28 +0,0 @@ | ||||
| //////////////////////////////////////// | ||||
| // 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(); | ||||
| } | ||||
| @@ -1,31 +0,0 @@ | ||||
| //////////////////////////////////////// | ||||
| // 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); | ||||
| } | ||||
| @@ -1,67 +0,0 @@ | ||||
| //////////////////////////////////////// | ||||
| // 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]; | ||||
|     } | ||||
| } | ||||
| @@ -1,251 +0,0 @@ | ||||
| //////////////////////////////////////// | ||||
| // 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); | ||||
|     } | ||||
| } | ||||
| @@ -1,140 +0,0 @@ | ||||
| //////////////////////////////////////// | ||||
| // 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); | ||||
|     } | ||||
| } | ||||
| @@ -1,31 +0,0 @@ | ||||
| //////////////////////////////////////// | ||||
| // 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); | ||||
| } | ||||
| @@ -1,28 +0,0 @@ | ||||
| //////////////////////////////////////// | ||||
| // 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); | ||||
| } | ||||
| @@ -1,51 +0,0 @@ | ||||
| //////////////////////////////////////// | ||||
| // 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); | ||||
|     } | ||||
| } | ||||
| @@ -1,117 +0,0 @@ | ||||
| //////////////////////////////////////// | ||||
| // 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()); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,23 +0,0 @@ | ||||
| //////////////////////////////////////// | ||||
| // 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); | ||||
|     } | ||||
| } | ||||
| @@ -1,20 +0,0 @@ | ||||
| //////////////////////////////////////// | ||||
| // 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); | ||||
| } | ||||
| @@ -1,20 +0,0 @@ | ||||
| //////////////////////////////////////// | ||||
| // 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); | ||||
| } | ||||
| @@ -1,48 +0,0 @@ | ||||
| //////////////////////////////////////// | ||||
| // 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 */ } | ||||
| } | ||||
| @@ -1,25 +0,0 @@ | ||||
| //////////////////////////////////////// | ||||
| // 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); | ||||
|     } | ||||
| } | ||||
| @@ -1,29 +0,0 @@ | ||||
| //////////////////////////////////////// | ||||
| // 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); | ||||
|     } | ||||
| } | ||||
| @@ -1,28 +0,0 @@ | ||||
| //////////////////////////////////////// | ||||
| // 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); | ||||
|     } | ||||
| } | ||||
| @@ -1,26 +0,0 @@ | ||||
| //////////////////////////////////////// | ||||
| // 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 | ||||
| } | ||||
| @@ -1,26 +0,0 @@ | ||||
| //////////////////////////////////////// | ||||
| // 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); | ||||
|     } | ||||
| } | ||||