Compare commits
	
		
			1 Commits
		
	
	
		
			6b110c81c8
			...
			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); |  | ||||||
|     } |  | ||||||
| } |  | ||||||