diff --git a/.gitignore b/.gitignore
index aa33988..c29286e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -118,4 +118,7 @@ $RECYCLE.BIN/
*.drawio.pdf
## IntelliJ
-.idea
\ No newline at end of file
+.idea
+
+## bin
+bin/
\ No newline at end of file
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
new file mode 100644
index 0000000..04bb8bd
--- /dev/null
+++ b/.gitlab-ci.yml
@@ -0,0 +1,110 @@
+stages:
+ - lint
+ - report
+ - build
+ - test
+
+# Reusable definitions
+.gradle-cache: &gradle-cache
+ cache:
+ key: '$CI_PROJECT_ID-gradle'
+ paths:
+ - .gradle/
+ - ~/.gradle/caches/
+
+.on-commit: &on-commit
+ rules:
+ - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
+ when: never
+ - if: '$CI_COMMIT_BRANCH'
+ allow_failure: true
+
+.on-mr: &on-mr
+ rules:
+ - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
+ allow_failure: true # deactivate after all issues (shown by checkstyle) have been resolved
+
+# Jobs
+checkstyle:
+ <<: [*gradle-cache, *on-commit]
+ stage: lint
+ image: gradle:9.3.1-jdk25
+ script:
+ - gradle checkstyleMain checkstyleTest
+ allow_failure: true
+ artifacts:
+ when: always
+ paths:
+ - build/reports/checkstyle/main.html
+ - build/reports/checkstyle/main.xml
+ - build/reports/checkstyle/test.html
+ - build/reports/checkstyle/test.xml
+ expose_as: 'Checkstyle Report'
+ expire_in: 1 week
+
+checkstyle-mr:
+ <<: [*gradle-cache, *on-mr]
+ stage: lint
+ image: gradle:9.3.1-jdk25
+ script:
+ - gradle checkstyleMain checkstyleTest
+ allow_failure: true
+ artifacts:
+ when: always
+ paths:
+ - build/reports/checkstyle/main.html
+ - build/reports/checkstyle/main.xml
+ - build/reports/checkstyle/test.html
+ - build/reports/checkstyle/test.xml
+ expose_as: 'Checkstyle Report'
+ expire_in: 1 week
+
+checkstyle-report:
+ stage: report
+ image: python:3.14
+ needs:
+ - job: checkstyle-mr
+ artifacts: true
+ optional: true
+ rules:
+ - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
+ script:
+ - python3 scripts/convert-checkstyle-gl-report.py
+ artifacts:
+ when: always
+ reports:
+ codequality: gl-code-quality-report.json
+ expire_in: 1 week
+
+compile-check:
+ <<: *gradle-cache
+ stage: build
+ image: gradle:9.3.1-jdk25
+ script:
+ - gradle compileTestJava
+ needs: []
+ rules:
+ - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
+ allow_failure: false
+ - if: '$CI_COMMIT_BRANCH'
+ allow_failure: false
+
+test:
+ <<: *gradle-cache
+ stage: test
+ image: gradle:9.3.1-jdk25
+ script:
+ - gradle test
+ artifacts:
+ when: always
+ reports:
+ junit: build/test-results/test/*.xml
+ paths:
+ - build/reports/tests/test/
+ expose_as: 'Test Report'
+ expire_in: 1 week
+ rules:
+ - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
+ allow_failure: false
+ - if: '$CI_COMMIT_BRANCH'
+ allow_failure: true
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..e0f15db
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,3 @@
+{
+ "java.configuration.updateBuildConfiguration": "automatic"
+}
\ No newline at end of file
diff --git a/bin/main/images/backround.png b/bin/main/images/backround.png
new file mode 100644
index 0000000..e5e1841
Binary files /dev/null and b/bin/main/images/backround.png differ
diff --git a/bin/main/images/logo.png b/bin/main/images/logo.png
new file mode 100644
index 0000000..e906bd4
Binary files /dev/null and b/bin/main/images/logo.png differ
diff --git a/bin/main/images/logoinverted.png b/bin/main/images/logoinverted.png
new file mode 100644
index 0000000..73053b6
Binary files /dev/null and b/bin/main/images/logoinverted.png differ
diff --git a/build.gradle b/build.gradle
index 99a402a..e126d6d 100644
--- a/build.gradle
+++ b/build.gradle
@@ -2,6 +2,8 @@ plugins {
id 'application'
id 'java'
id 'org.openjfx.javafxplugin' version '0.1.0'
+ id 'checkstyle'
+ id 'com.diffplug.spotless' version '7.0.2'
}
group = 'ch.unibas.dmi.dbis'
@@ -27,14 +29,40 @@ javafx {
}
dependencies {
+ // Source: https://mvnrepository.com/artifact/org.apache.logging.log4j
+ implementation("org.apache.logging.log4j:log4j-api:2.25.3")
+ runtimeOnly("org.apache.logging.log4j:log4j-core:2.25.3")
+
+ // Source: https://mvnrepository.com/artifact/org.fusesource.jansi/jansi
+ runtimeOnly("org.fusesource.jansi:jansi:2.4.2")
+
testImplementation("org.junit.jupiter:junit-jupiter:5.10.0")
testRuntimeOnly("org.junit.platform:junit-platform-launcher:1.10.0")
+ implementation 'org.json:json:20240303'
}
test {
useJUnitPlatform()
}
+checkstyle {
+ toolVersion = '10.21.0'
+ configFile = file('config/checkstyle/checkstyle.xml')
+ configProperties = [
+ 'suppressionFile': file('config/checkstyle/suppressions.xml').absolutePath
+ ]
+}
+
+spotless {
+ java {
+ googleJavaFormat('1.25.2').aosp()
+ importOrder()
+ removeUnusedImports()
+ trimTrailingWhitespace()
+ endWithNewline()
+ }
+}
+
tasks.named('jar', Jar) {
manifest {
attributes('Main-Class': application.mainClass.get())
@@ -42,6 +70,22 @@ tasks.named('jar', Jar) {
}
+// Dieses JVM-Argument erlaubt JavaFX und anderen Libraries den Zugriff auf native Methoden.
+// Ohne diese Einstellung erscheinen Warnungen und zukünftige Java-Versionen könnten den Zugriff blockieren.
+// Siehe: https://openjdk.org/jeps/472
+tasks.withType(JavaExec) {
+ jvmArgs += '--enable-native-access=ALL-UNNAMED'
+}
+
+
+
+tasks.register('cruntest', JavaExec) {
+ classpath = sourceSets.main.runtimeClasspath
+ mainClass = 'ch.unibas.dmi.dbis.cs108.casono.Main'
+ jvmArgs '--enable-native-access=ALL-UNNAMED'
+ args 'client', '0.0.0.0:1234'
+}
+
tasks.register('fatJar', Jar) {
group = 'build'
description = 'Assembles a runnable fat JAR including runtime dependencies.'
diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml
new file mode 100644
index 0000000..746bba4
--- /dev/null
+++ b/config/checkstyle/checkstyle.xml
@@ -0,0 +1,174 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/config/checkstyle/suppressions.xml b/config/checkstyle/suppressions.xml
new file mode 100644
index 0000000..d532e42
--- /dev/null
+++ b/config/checkstyle/suppressions.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/documents/docs/networking/server-architecture.md b/documents/docs/networking/server-architecture.md
new file mode 100644
index 0000000..8537382
--- /dev/null
+++ b/documents/docs/networking/server-architecture.md
@@ -0,0 +1,86 @@
+# Server-Side Networking — How It All Fits Together
+
+This doc is meant to give a solid mixed-level understanding of how our server-side networking works. No deep dives — just a clear picture of what's happening under the hood when a client connects and sends a command.
+
+> **Note:** This is v1. Annotations and Reflection-based dispatching are on the roadmap but not covered here yet.
+
+> **Disclamer:** This document has been written by Claude. I modified certain parts and verified its contents for correctness.
+
+## Our Protocol at a Glance
+
+Our protocol is inspired by **POP3** - a classic, text-based protocol that communicates over a raw TCP connection. The idea is simple: the client sends a command as a plain-text string, and the server responds with either a success or an error.
+
+A typical exchange looks something like this:
+
+```
+Client → GET_DELTA SINCE=42
+Server → +OK
+ PLAYER_FOLDED playerId=3
+ POT 240
+ NEXT_TURN playerId=1
+ .
+```
+
+Responses start with `+OK` on success or `-ERR` when something goes wrong. Commands are short, uppercase strings - sometimes followed by arguments.
+
+We don't use HTTP, there's no JSON body, no headers. Just a raw socket, a text stream, and a clearly defined set of commands.
+
+## Core Components & Their Roles
+
+
+
+Here's a quick rundown of the main building blocks:
+
+### `NetworkManager`
+This is the entry point. It binds to a specific port and listens for incoming TCP connections.
+Once a client connects, it creates a `Session` adds it to the `SessionManager` and goes back to waiting. Its not the responsibility of the `NetworkManager` to recieve and send data from and to each connected client.
+
+### `Session`
+Every client that connects has its own `Session` instance running on its own thread. This component owns the lifecycle of that connection: it reads incoming data, passes it along for processing, and writes responses back.
+
+### `SessionId`
+The `SessionId` is used to uniquely identify each session. Essentially its a wrapper arround the UUID type.
+
+### `SessionManager`
+The `SessionManager` stores all currently active sessions. It allows for the retrieval of a specific session by its id.
+
+### `ProtocolParser`
+Raw text (encapsulated in a `RawRequest`) coming off the socket isn't immediately useful — the `ProtocolParser` turns it with the help of the `Tokenizer` into a `PrimitiveRequest`.
+
+### `RawRequest`
+The `RawRequest` object is created after a message has been recieved.
+It holds both a reference to the `Session` that recieved the message and the message itself.
+
+### `PrimitiveRequest`
+The `PrimitiveRequest` object is created by the `ProtocolParser` after the content of the `RawRequest` has been tokenized by the `Tokenizer`.
+The command is stored in its own attribute and the arguments are accessible as a dictionary.
+
+### `Tokenizer`
+The job of the `tokenizer` is it, to take the raw string and turn it into tokens.
+For example, the command `CHAT_LOBBY GAME=12 MESSAGE='All-in?'` will be decoded as `"CHAT_LOBBY", "GAME", "=", "12", "MESSAGE", "=", "All-in?"`.
+It is unaware about the meaning.
+
+### `CommandParser` (per command)
+Takes in the `PrimitiveRequest` and checks if all required fields are provided with the correct value.
+Creates a command specific `Request` object containing command arguments in a structured manner.
+
+### `Request` (per command)
+Has fields common along each implementation such as a reference to the `Session` that recieved the request.
+Furthermore each implementation has fields unique to each command. For example, the `ChatLobbyRequest` has fields for the game and the message.
+
+### `CommandRouter`
+By identifying the type of class of the `Request`, the `CommandRouter` routes the request to the matching `CommandHandler`.
+
+### `CommandHandler` (per command)
+Each supported command has its own handler — a small, focused class that contains the logic for that specific command. They have access to the domain, containing inner parts of the game itself.
+
+### `Response` (per response)
+The interface has two sub-interfaces for either failed `ErrorResponse` or successfull `SuccessResponse` execution of commands.
+
+Each step has exactly one responsibility. Increasing the ability to test and extend different components later on.
+
+## Concurrency - Handling Multiple Clients
+
+Every incoming connection spawns a new **Thread**. This means multiple clients can be served simultaneously without blocking each other.
+
+As each `Session` runs entirely on its own thread, there's no shared mutable state between sessions, with exception of the `SessionManager` and other key components explained later.
diff --git a/documents/images/docs/networking/server-architecure/networking_components.puml b/documents/images/docs/networking/server-architecure/networking_components.puml
new file mode 100644
index 0000000..647654a
--- /dev/null
+++ b/documents/images/docs/networking/server-architecure/networking_components.puml
@@ -0,0 +1,96 @@
+@startuml
+skinparam classAttributeIconSize 0
+skinparam packageStyle rectangle
+skinparam linetype ortho
+skinparam backgroundColor transparent
+
+' Network Layer
+class NetworkManager {
+ + start(port: int)
+}
+
+class Session {
+ - id: SessionId
+ + Session(id: SessionId)
+ + getId(): SessionId
+ + run()
+ + send(response: String)
+}
+
+class SessionId {
+ - value: UUID
+ + SessionId()
+ + SessionId(value: UUID)
+}
+
+class SessionManager {
+ - sessions: Map
+ + add(session: Session)
+ + getById(id: UUID): Session
+ + remove(id: UUID)
+}
+
+NetworkManager --> Session : creates
+NetworkManager --> SessionManager : adds session to
+Session --> SessionId : identified by
+Session --> RawRequest : creates
+Session ..> Response : sends
+SessionManager o-- Session : manages
+
+' Protocol Layer
+class RawRequest {
+ - session: Session
+ - rawMessage: String
+}
+
+class Tokenizer {
+ + tokenize(input: String): List
+}
+
+class ProtocolParser {
+ + parse(raw: RawRequest): PrimitiveRequest
+}
+
+class PrimitiveRequest {
+ - command: String
+ - arguments: Map
+}
+
+RawRequest --> ProtocolParser : passed to
+ProtocolParser --> Tokenizer : uses
+ProtocolParser --> PrimitiveRequest : produces
+
+' Command Layer
+interface CommandParser <> {
+ + parse(primitive: PrimitiveRequest): Request
+}
+
+interface Request <> {
+ - session: Session
+}
+
+class CommandRouter {
+ + route(request: Request)
+}
+
+interface CommandHandler <> {
+ + handle(request: Request)
+}
+
+PrimitiveRequest --> CommandParser : passed to
+CommandParser --> Request : produces
+CommandRouter --> CommandHandler : dispatches to
+CommandRouter ..> Request : receives
+
+' Response Layer
+interface Response {
+ + encode(): String
+}
+
+interface SuccessResponse
+
+interface ErrorResponse
+
+Response <|-- SuccessResponse
+Response <|-- ErrorResponse
+@enduml
diff --git a/documents/images/docs/networking/server-architecure/networking_components.svg b/documents/images/docs/networking/server-architecure/networking_components.svg
new file mode 100644
index 0000000..9db9f6d
--- /dev/null
+++ b/documents/images/docs/networking/server-architecure/networking_components.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/scripts/convert-checkstyle-gl-report.py b/scripts/convert-checkstyle-gl-report.py
new file mode 100644
index 0000000..990cff6
--- /dev/null
+++ b/scripts/convert-checkstyle-gl-report.py
@@ -0,0 +1,29 @@
+# Skript to convert findings of checkstyle into code quality report for gitlab
+import xml.etree.ElementTree as ET, json, hashlib
+
+findings = []
+for source in ["main", "test"]:
+ try:
+ tree = ET.parse(f"build/reports/checkstyle/{source}.xml")
+ for file in tree.findall("file"):
+ path = file.get("name")
+ for error in file.findall("error"):
+ line = int(error.get("line", 1))
+ msg = error.get("message", "")
+ fingerprint = hashlib.md5(f"{path}{line}{msg}".encode()).hexdigest()
+ findings.append({
+ "type": "issue",
+ "check_name": error.get("source", "checkstyle"),
+ "description": msg,
+ "severity": "minor",
+ "fingerprint": fingerprint,
+ "location": {
+ "path": "src/" + path.split("/src/")[1] if "/src/" in path else path,
+ "lines": {"begin": line}
+ }
+ })
+ except FileNotFoundError:
+ pass
+
+with open("gl-code-quality-report.json", "w") as f:
+ json.dump(findings, f)
diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/Main.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/Main.java
index d1890db..f7d2273 100644
--- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/Main.java
+++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/Main.java
@@ -3,8 +3,25 @@ package ch.unibas.dmi.dbis.cs108.casono;
import ch.unibas.dmi.dbis.cs108.casono.client.ClientApp;
import ch.unibas.dmi.dbis.cs108.casono.server.ServerApp;
+/**
+ * Main entry point for Casono application.
+ * Handles client and server startup.
+ *
+ * Standardkonstruktor für die Anwendung.
+ */
public final class Main {
+ /**
+ * Standardkonstruktor.
+ */
+ public Main() {
+ // Standardkonstruktor
+ }
+
+ /**
+ * Main entry point for Casono.
+ * @param args Command line arguments
+ */
public static void main(String[] args) {
if (!isValid(args)) {
printUsage();
diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ClientApp.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ClientApp.java
index 8c491ea..76dc06d 100644
--- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ClientApp.java
+++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ClientApp.java
@@ -1,6 +1,29 @@
package ch.unibas.dmi.dbis.cs108.casono.client;
+
+/**
+ * Entry point for the Casono client application.
+ * Handles client startup and connection parameters.
+ */
import ch.unibas.dmi.dbis.cs108.casono.client.ui.Launcher;
+/**
+ * Entry point for the Casono client application.
+ * Handles client startup and connection parameters.
+ *
+ * Standardkonstruktor für die Anwendung.
+ */
public class ClientApp {
+
+ /**
+ * Standardkonstruktor.
+ */
+ public ClientApp() {
+ // Standardkonstruktor
+ }
+ /**
+ * Starts the client application with the given address.
+ * @param arg Address in the format "ip:port".
+ * @throws IllegalArgumentException if the address format is invalid.
+ */
public static void start(String arg) {
String[] parts = arg.split(":", 2);
if (parts.length != 2) {
diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/Launcher.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/Launcher.java
index 2ee2696..c687abf 100644
--- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/Launcher.java
+++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/Launcher.java
@@ -1,22 +1,26 @@
package ch.unibas.dmi.dbis.cs108.casono.client.ui;
-import ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.CasinoGameUI;
import javafx.application.Application;
/**
- * Launcher-Klasse für die Casino-Anwendung.
- *
- * Startet die JavaFX-Anwendung. Während der Entwicklung kann hier direkt
- * `Casinogameui` für Tests aufgerufen werden. Im fertigen Spiel erfolgt
- * der Aufruf von `Casinogameui` über die `Casinomainui`.
- *
- * Starte den Client über das Terminal mit:
- * {@code ./gradlew run --args="client 0.0.0.0:1234"}
+ * Launcher for the Casono main UI.
+ *
+ * Standardkonstruktor für die Anwendung.
*/
public class Launcher {
+
+ /**
+ * Standardkonstruktor.
+ */
+ public Launcher() {
+ // Standardkonstruktor
+ }
+ /**
+ * Main entry point for launching the UI.
+ * @param args Command line arguments
+ */
public static void main(String[] args) {
- // Application.launch(ch.unibas.dmi.dbis.cs108.casono.client.ui.lobbyui.Casinomainui.class, args);
- Application.launch(CasinoGameUI.class, args);
+ Application.launch(ch.unibas.dmi.dbis.cs108.casono.client.ui.lobbyui.Casinomainui.class, args);
}
}
\ No newline at end of file
diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/lobbyui/Casinomainui.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/lobbyui/Casinomainui.java
index 20cc74a..169da1d 100644
--- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/lobbyui/Casinomainui.java
+++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/lobbyui/Casinomainui.java
@@ -1,5 +1,9 @@
package ch.unibas.dmi.dbis.cs108.casono.client.ui.lobbyui;
+/**
+ * Main UI application for Casono.
+ * Loads the main FXML layout and sets up the stage.
+ */
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
@@ -7,8 +11,25 @@ import javafx.stage.Stage;
import java.io.IOException;
+/**
+ * JavaFX Application class for the Casono main UI.
+ *
+ * Standardkonstruktor für die Anwendung.
+ */
public class Casinomainui extends Application {
+
+ /**
+ * Standardkonstruktor.
+ */
+ public Casinomainui() {
+ // Standardkonstruktor
+ }
@Override
+ /**
+ * Starts the JavaFX application and loads the main UI.
+ * @param stage The primary stage for this application.
+ * @throws IOException If loading the FXML fails.
+ */
public void start(Stage stage) throws IOException {
FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("/ui-structure/Casinomainui.fxml"));
Scene scene = new Scene(fxmlLoader.load(), 1200, 800);
@@ -20,6 +41,10 @@ public class Casinomainui extends Application {
stage.show();
}
+ /**
+ * Main entry point for launching the application.
+ * @param args Command line arguments
+ */
public static void main(String[] args) {
launch();
}
diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/lobbyui/CasinomainuiController.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/lobbyui/CasinomainuiController.java
index c34990f..5c235cf 100644
--- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/lobbyui/CasinomainuiController.java
+++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/lobbyui/CasinomainuiController.java
@@ -1,22 +1,21 @@
package ch.unibas.dmi.dbis.cs108.casono.client.ui.lobbyui;
-import javafx.application.Platform;
-import javafx.fxml.FXML;
-import javafx.scene.control.Label;
-import javafx.scene.layout.AnchorPane;
-import javafx.scene.control.Button;
-import javafx.scene.image.ImageView;
-import javafx.scene.shape.Rectangle;
-import javafx.application.Platform;
-import javafx.fxml.FXML;
-import javafx.scene.control.Label;
-import javafx.scene.control.TextField;
-import javafx.scene.input.KeyCode;
-import javafx.scene.input.KeyEvent;
-import javafx.scene.input.MouseEvent;
-import javafx.scene.layout.HBox;
-import javafx.scene.layout.AnchorPane;
+import javafx.application.Platform;
+import javafx.fxml.FXML;
+import javafx.scene.control.Label;
+import javafx.scene.control.Button;
+import javafx.scene.image.Image;
+import javafx.scene.image.ImageView;
+import javafx.scene.layout.AnchorPane;
+import javafx.scene.layout.VBox;
+import javafx.scene.shape.Rectangle;
+
+/**
+ * Controller for the Casono main UI lobby.
+ * Handles UI initialization and user actions.
+ */
public class CasinomainuiController {
+
@FXML
private AnchorPane rootPane;
@FXML
@@ -24,26 +23,63 @@ public class CasinomainuiController {
@FXML
private Label subtitleLabel;
@FXML
- private javafx.scene.image.ImageView logoView;
+ private ImageView logoView;
@FXML
- private javafx.scene.shape.Rectangle greenBox;
+ private Rectangle greenBox;
@FXML
private Button exitbutton;
-
-
+ @FXML
+ private VBox casinoTable;
+ private LobbyButtonTranslationManager translationManager;
+ private LobbyButtonGridManager gridManager;
+ private int nextButtonId = 1;
+ public CasinomainuiController() {
+ // Default constructor
+ }
+
+ /**
+ * Initializes the UI components and sets default values.
+ */
+ @FXML
public void initialize() {
titleLabel.setText("Casono");
subtitleLabel.setText("Texas Hold'em Poker");
- // Logo laden
- logoView.setImage(new javafx.scene.image.Image(getClass().getResource("/images/logo.png").toExternalForm()));
+ logoView.setImage(new Image(getClass().getResource("/images/logo.png").toExternalForm()));
+
+ translationManager = new LobbyButtonTranslationManager();
+ gridManager = new LobbyButtonGridManager(new javafx.scene.layout.GridPane(), translationManager);
+ casinoTable.getChildren().clear();
+ casinoTable.getChildren().add(gridManager.getGridPane());
+ gridManager.renderLobbyButtons();
}
+ /**
+ * Handles the exit button action to close the application.
+ */
@FXML
public void handleexitbutton() {
Platform.exit();
}
+ /**
+ * Handles creation of a new lobby button.
+ */
+ @FXML
+ public void handleCreateLobbyButton() {
+ if (translationManager.isFull()) {
+ System.out.println("Grid voll! Keine weiteren Lobbys moeglich.");
+ return;
+ }
+ int buttonId = nextButtonId++;
+ int lobbyId = gridManager.createLobby();
+ try {
+ translationManager.addLobbyButton(buttonId, lobbyId);
+ System.out.println("ButtonID: " + buttonId + ", LobbyID: " + lobbyId);
+ gridManager.renderLobbyButtons();
+ } catch (Exception e) {
+ System.out.println("Fehler beim Hinzufügen: " + e.getMessage());
+ }
+ }
}
-
diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/lobbyui/LobbyButtonGridManager.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/lobbyui/LobbyButtonGridManager.java
new file mode 100644
index 0000000..6add82d
--- /dev/null
+++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/lobbyui/LobbyButtonGridManager.java
@@ -0,0 +1,101 @@
+package ch.unibas.dmi.dbis.cs108.casono.client.ui.lobbyui;
+
+/**
+ * Manages the grid for lobby buttons and rendering.
+ * Uses LobbyButtonTranslationManager for mapping ButtonID to LobbyID.
+ */
+
+import javafx.scene.control.Button;
+import javafx.scene.image.Image;
+import javafx.scene.image.ImageView;
+import javafx.scene.layout.GridPane;
+import java.util.Map;
+
+/**
+ * Manages the grid for lobby buttons and rendering.
+ * Uses LobbyButtonTranslationManager for mapping ButtonID to LobbyID.
+ */
+public class LobbyButtonGridManager {
+
+ /** GridPane for the button grid. */
+ private final GridPane gridPane;
+ /** Manager for mapping ButtonID to LobbyID. */
+ private final LobbyButtonTranslationManager translationManager;
+ /** Number of rows in the grid. */
+ private final int rows = 2;
+ /** Number of columns in the grid. */
+ private final int cols = 4;
+ /** Path to the button image. */
+ private final String buttonImagePath = "/images/logo.png";
+
+ /**
+ * Constructor for the GridManager.
+ *
+ * @param gridPane the GridPane for rendering
+ * @param translationManager the manager for mapping ButtonID to LobbyID
+ */
+ public LobbyButtonGridManager(GridPane gridPane, LobbyButtonTranslationManager translationManager) {
+ this.gridPane = gridPane;
+ this.translationManager = translationManager;
+ }
+
+ /**
+ * Renders all lobby buttons in the grid.
+ * Creates a button for each mapping with image and event handler.
+ */
+ public void renderLobbyButtons() {
+ gridPane.getChildren().clear();
+ int index = 0;
+ Map mapping = translationManager.getButtonIdToLobbyId();
+ if (mapping.isEmpty()) {
+ // No buttons to render
+ return;
+ }
+ for (Map.Entry entry : mapping.entrySet()) {
+ int buttonId = entry.getKey();
+ Button btn = new Button();
+ btn.setId("lobbyBtn-" + buttonId);
+ btn.setGraphic(new ImageView(new Image(getClass().getResourceAsStream(buttonImagePath))));
+ btn.setOnAction(e -> {
+ Integer lobbyId = translationManager.getLobbyIdForButton(buttonId);
+ if (lobbyId != null)
+ joinLobby(lobbyId);
+ });
+ int row = index / cols;
+ int col = index % cols;
+ gridPane.add(btn, col, row);
+ index++;
+ }
+ }
+
+ /**
+ * Placeholder for lobby creation logic. Returns a generated lobbyId.
+ *
+ * @return The generated lobbyId
+ */
+ public int createLobby() {
+ // TODO: Replace with actual lobby creation logic
+ int lobbyId = (int) (Math.random() * 10000 + 1);
+ System.out.println("Lobby created: " + lobbyId);
+ return lobbyId;
+ }
+
+ /**
+ * Placeholder for joining a lobby.
+ *
+ * @param lobbyId The lobbyId to join
+ */
+ public void joinLobby(int lobbyId) {
+ // TODO: Replace with actual join logic
+ System.out.println("Joining lobby: " + lobbyId);
+ }
+
+ /**
+ * Getter for the GridPane.
+ *
+ * @return The GridPane for the button grid
+ */
+ public javafx.scene.layout.GridPane getGridPane() {
+ return gridPane;
+ }
+}
diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/lobbyui/LobbyButtonTranslationManager.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/lobbyui/LobbyButtonTranslationManager.java
new file mode 100644
index 0000000..e3dd72e
--- /dev/null
+++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/lobbyui/LobbyButtonTranslationManager.java
@@ -0,0 +1,66 @@
+package ch.unibas.dmi.dbis.cs108.casono.client.ui.lobbyui;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Verwaltet das Mapping zwischen Button-IDs und Lobby-IDs rein im Speicher.
+ * Keine Dateioperationen, nur Laufzeitdatenstruktur.
+ */
+public class LobbyButtonTranslationManager {
+ /** Maximale Anzahl an Buttons/Lobbys */
+ private static final int MAX_BUTTONS = 8;
+ /** Zuordnung ButtonID → LobbyID */
+ private final Map buttonIdToLobbyId = new HashMap<>();
+
+ /**
+ * Konstruktor: initialisiert die Zuordnung leer.
+ */
+ public LobbyButtonTranslationManager() {
+ // Zuordnung bleibt leer beim Start
+ }
+
+ /**
+ * Prüft, ob das Grid voll ist (MAX_BUTTONS erreicht).
+ * @return true, wenn Grid voll; sonst false
+ */
+ public boolean isFull() {
+ return buttonIdToLobbyId.size() >= MAX_BUTTONS;
+ }
+
+ /**
+ * Fügt eine Zuordnung ButtonID → LobbyID hinzu.
+ * @param buttonId Die ID des Buttons
+ * @param lobbyId Die ID der Lobby
+ * @throws Exception wenn das Grid voll ist
+ */
+ public void addLobbyButton(int buttonId, int lobbyId) throws Exception {
+ if (isFull()) throw new Exception("Grid is full!");
+ buttonIdToLobbyId.put(buttonId, lobbyId);
+ }
+
+ /**
+ * Entfernt eine Zuordnung für die gegebene ButtonID.
+ * @param buttonId Die ID des zu entfernenden Buttons
+ */
+ public void removeLobbyButton(int buttonId) {
+ buttonIdToLobbyId.remove(buttonId);
+ }
+
+ /**
+ * Gibt die LobbyID für eine gegebene ButtonID zurück.
+ * @param buttonId Die ButtonID
+ * @return Die zugehoerige LobbyID oder null, falls nicht vorhanden
+ */
+ public Integer getLobbyIdForButton(int buttonId) {
+ return buttonIdToLobbyId.get(buttonId);
+ }
+
+ /**
+ * Gibt die gesamte Zuordnung ButtonID → LobbyID zurück.
+ * @return Map aller Zuordnungen
+ */
+ public Map getButtonIdToLobbyId() {
+ return buttonIdToLobbyId;
+ }
+}
diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/ServerApp.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/ServerApp.java
index 520dc6b..a7814b0 100644
--- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/ServerApp.java
+++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/ServerApp.java
@@ -1,8 +1,22 @@
package ch.unibas.dmi.dbis.cs108.casono.server;
+import ch.unibas.dmi.dbis.cs108.casono.server.network.NetworkManager;
+import ch.unibas.dmi.dbis.cs108.casono.server.network.events.DisconnectEvent;
+import ch.unibas.dmi.dbis.cs108.casono.server.network.events.EventBus;
+import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionManager;
+
+/**
+ * Application class for starting the server.
+ */
public class ServerApp {
public static void start(String arg) {
int port = Integer.parseInt(arg);
System.out.println("You've selected the server. It will accept connections at port " + port);
+
+ EventBus eventBus = new EventBus();
+ SessionManager sessionManager = new SessionManager();
+ eventBus.subscribe(DisconnectEvent.class, event -> sessionManager.removeSession(event.sessionId()));
+ NetworkManager networkManager = new NetworkManager(port, sessionManager, eventBus);
+ networkManager.start();
}
}
diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/NetworkManager.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/NetworkManager.java
new file mode 100644
index 0000000..50b53d9
--- /dev/null
+++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/NetworkManager.java
@@ -0,0 +1,81 @@
+package ch.unibas.dmi.dbis.cs108.casono.server.network;
+
+import java.io.IOException;
+import java.net.ServerSocket;
+import java.net.Socket;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import ch.unibas.dmi.dbis.cs108.casono.server.network.events.DisconnectEvent;
+import ch.unibas.dmi.dbis.cs108.casono.server.network.events.EventBus;
+import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.Session;
+import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionManager;
+import ch.unibas.dmi.dbis.cs108.casono.server.network.transport.TcpTransport;
+
+/**
+ * Creates and manages the server socket. Accepts new incoming connections and creates sessions.
+ */
+public class NetworkManager implements Runnable {
+ private Integer port;
+ private Logger logger;
+ private Thread thread;
+ private Boolean running;
+ private SessionManager sessionManager;
+ private EventBus eventBus;
+
+ /**
+ * Creates a new NetworkManager with the given port, session manager, and event bus.
+ *
+ * @param port the port to listen on
+ * @param sessionManager the session manager to use
+ * @param eventBus the event bus for events
+ */
+ public NetworkManager(Integer port, SessionManager sessionManager, EventBus eventBus) {
+ this.port = port;
+ this.logger = LogManager.getLogger(NetworkManager.class);
+ this.thread = new Thread(this, "networkManager");
+ this.running = true;
+ this.sessionManager = sessionManager;
+ this.eventBus = eventBus;
+ this.eventBus.subscribe(DisconnectEvent.class, event -> clientDisconnected(event));
+ }
+
+ /**
+ * Starts the internal thread to accept new connections.
+ */
+ public void start() {
+ logger.debug("Starting server at port " + port);
+ thread.start();
+ }
+
+ /**
+ * Handles client disconnection events.
+ *
+ * @param event the disconnect event
+ */
+ public void clientDisconnected(DisconnectEvent event) {
+ logger.info("Session " + event.sessionId().value() + " disconnected adhasghd");
+ }
+
+ /**
+ * Runs the network manager loop, accepting connections.
+ */
+ @Override
+ public void run() {
+ try (ServerSocket serverSocket = new ServerSocket(port)) {
+ while (running) {
+ Socket clientSocket = serverSocket.accept();
+
+ System.out.println("Accepted connection from " + clientSocket.getRemoteSocketAddress());
+
+ Session session = new Session(new TcpTransport(clientSocket), eventBus);
+ sessionManager.addSession(session);
+ session.start();
+ }
+
+ } catch (IOException e) {
+ logger.fatal(e);
+ }
+ }
+}
diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/events/DisconnectEvent.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/events/DisconnectEvent.java
new file mode 100644
index 0000000..b6b0d78
--- /dev/null
+++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/events/DisconnectEvent.java
@@ -0,0 +1,8 @@
+package ch.unibas.dmi.dbis.cs108.casono.server.network.events;
+
+import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionId;
+
+/**
+ * Represents a disconnect event for a session.
+ */
+public record DisconnectEvent(SessionId sessionId) implements Event {}
diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/events/Event.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/events/Event.java
new file mode 100644
index 0000000..75c0954
--- /dev/null
+++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/events/Event.java
@@ -0,0 +1,6 @@
+package ch.unibas.dmi.dbis.cs108.casono.server.network.events;
+
+/**
+ * Marker interface for events in the event bus system.
+ */
+interface Event {}
diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/events/EventBus.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/events/EventBus.java
new file mode 100644
index 0000000..4141ab8
--- /dev/null
+++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/events/EventBus.java
@@ -0,0 +1,40 @@
+package ch.unibas.dmi.dbis.cs108.casono.server.network.events;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.function.Consumer;
+
+/**
+ * A simple event bus for publishing and subscribing to events.
+ */
+public class EventBus {
+ private final Map, List>> handlers = new ConcurrentHashMap<>();
+
+ /**
+ * Subscribes a handler to a specific event type.
+ *
+ * @param eventType the class of the event to subscribe to
+ * @param handler the consumer to handle the event
+ */
+ @SuppressWarnings("unchecked") // This cast is safe, because handlers only get passed the type they subscribed to
+ public void subscribe(Class eventType, Consumer handler) {
+ handlers.computeIfAbsent(eventType, k -> new CopyOnWriteArrayList<>())
+ .add((Consumer