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 + +![PlantUML diagramm of all components outlined in this document](/documents/images/docs/networking/server-architecure/networking_components.svg) + +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 @@ +NetworkManager+start(port: int)Session-id: SessionId+Session(id: SessionId)+getId(): SessionId+run()+send(response: String)SessionId-value: UUID+SessionId()+SessionId(value: UUID)SessionManager-sessions: Map<UUID, Session>+add(session: Session)+getById(id: UUID): Session+remove(id: UUID)RawRequest-session: Session-rawMessage: StringResponse+encode(): StringTokenizer+tokenize(input: String): List<String>ProtocolParser+parse(raw: RawRequest): PrimitiveRequestPrimitiveRequest-command: String-arguments: Map<String, String>«per command»CommandParser+parse(primitive: PrimitiveRequest): Request«per command»Request-session: SessionCommandRouter+route(request: Request)«per command»CommandHandler+handle(request: Request)SuccessResponseErrorResponsecreatesadds session toidentified bycreatessendsmanagespassed tousesproducespassed toproducesdispatches toreceives \ 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) (Consumer) handler); + } + + /** + * Publishes an event to all subscribed handlers. + * + * @param event the event to publish + */ + public void publish(T event) { + Objects.requireNonNull(event, "event must not be null"); + List> subscribers = handlers.get(event.getClass()); + if (subscribers != null) { + subscribers.forEach(h -> h.accept(event)); + } + } +} diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/sessions/Session.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/sessions/Session.java new file mode 100644 index 0000000..d2464f1 --- /dev/null +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/sessions/Session.java @@ -0,0 +1,85 @@ +package ch.unibas.dmi.dbis.cs108.casono.server.network.sessions; + +import java.io.EOFException; +import java.io.IOException; +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.transport.TransportLayer; + +/** + * Represents a client session in the network server. + */ +public class Session implements Runnable { + private SessionId id; + private Thread thread; + private TransportLayer transport; + private Logger logger; + private Boolean running; + private EventBus eventBus; + + /** + * Creates a new Session with the given transport and event bus. + * + * @param transport the transport layer for communication + * @param eventBus the event bus for publishing events + * @throws IOException if an I/O error occurs during initialization + */ + public Session(TransportLayer transport, EventBus eventBus) throws IOException { + this.id = new SessionId(); + this.thread = new Thread(this, "session-" + this.id.value()); + this.transport = transport; + this.running = true; + this.eventBus = eventBus; + + this.logger = LogManager.getLogger(Session.class.toString() + id.value()); + this.logger.info("Created new session"); + } + + /** + * Returns the ID of this session. + * + * @return the session ID + */ + public SessionId getId() { + return this.id; + } + + /** + * Starts the session thread. + */ + public void start() { + thread.start(); + } + + /** + * Closes the session and its transport. + * + * @throws IOException if an I/O error occurs + */ + public void close() throws IOException { + transport.close(); + this.running = false; + } + + /** + * Runs the session loop, reading from the transport. + */ + @Override + public void run() { + while (running) { + try { + System.out.println("Recieved: " + transport.read()); + } catch (EOFException e) { + logger.info("Client disconnected"); + eventBus.publish(new DisconnectEvent(id)); + break; + } catch (IOException e) { + e.printStackTrace(); + break; + } + } + } +} diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/sessions/SessionId.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/sessions/SessionId.java new file mode 100644 index 0000000..431ebdc --- /dev/null +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/sessions/SessionId.java @@ -0,0 +1,35 @@ +package ch.unibas.dmi.dbis.cs108.casono.server.network.sessions; + +import java.util.UUID; + +/** + * Represents a unique identifier for a session. + */ +public class SessionId { + private final UUID value; + + /** + * Creates a new SessionId with a randomly generated UUID. + */ + public SessionId() { + this.value = UUID.randomUUID(); + } + + /** + * Creates a new SessionId with the specified UUID. + * + * @param UUID to use for this SessionId + */ + public SessionId(UUID value) { + this.value = value; + } + + /** + * Returns the UUID value of this SessionId. + * + * @return the UUID value + */ + public UUID value() { + return value; + } +} diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/sessions/SessionManager.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/sessions/SessionManager.java new file mode 100644 index 0000000..42bc4ac --- /dev/null +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/sessions/SessionManager.java @@ -0,0 +1,59 @@ +package ch.unibas.dmi.dbis.cs108.casono.server.network.sessions; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Manages active sessions in the server. + */ +public class SessionManager { + private Map sessions; + + /** + * Constructs a new SessionManager. + */ + public SessionManager() { + this.sessions = new ConcurrentHashMap<>(); + } + + /** + * Adds a session to the manager. + * + * @param session the session to add + */ + public void addSession(Session session) { + sessions.put(session.getId(), session); + System.out.println("Added session " + session.getId().value() + " to session manager"); + } + + /** + * Removes a session by its ID. + * + * @param id the ID of the session to remove + * @return the removed session, or null if not found + */ + public Session removeSession(SessionId id) { + System.out.println("Removed session " + id.value() + " from session manager"); + return sessions.remove(id); + } + + /** + * Removes the specified session. + * + * @param session the session to remove + * @return the removed session, or null if not found + */ + public Session removeSession(Session session) { + return sessions.remove(session.getId()); + } + + /** + * Retrieves a session by its ID. + * + * @param id the ID of the session to retrieve + * @return the session with the specified ID, or null if not found + */ + public Session getSessionById(SessionId id) { + return sessions.get(id); + } +} diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/transport/TcpTransport.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/transport/TcpTransport.java new file mode 100644 index 0000000..3f80d6c --- /dev/null +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/transport/TcpTransport.java @@ -0,0 +1,63 @@ +package ch.unibas.dmi.dbis.cs108.casono.server.network.transport; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.net.Socket; +import java.nio.charset.StandardCharsets; + +/** + * Implements TCP-based transport layer for network communication. + */ +public class TcpTransport implements TransportLayer { + private Socket socket; + private DataInputStream in; + private DataOutputStream out; + + /** + * Creates a new TcpTransport with the given socket. + * + * @param socket the socket to use for communication + * @throws IOException if an I/O error occurs + */ + public TcpTransport(Socket socket) throws IOException { + this.socket = socket; + this.in = new DataInputStream(socket.getInputStream()); + this.out = new DataOutputStream(socket.getOutputStream()); + } + + /** + * Reads a string from the socket. + * + * @return the read string + * @throws IOException if an I/O error occurs + */ + public String read() throws IOException { + int length = in.readInt(); + byte[] payload = new byte[length]; + in.readFully(payload); + return new String(payload, StandardCharsets.UTF_8); + } + + /** + * Writes a string to the socket. + * + * @param payload the string to write + * @throws IOException if an I/O error occurs + */ + public void write(String payload) throws IOException { + byte[] rawPayload = payload.getBytes(StandardCharsets.UTF_8); + out.writeInt(rawPayload.length); + out.write(rawPayload); + out.flush(); + } + + /** + * Closes the socket. + * + * @throws IOException if an I/O error occurs + */ + public void close() throws IOException { + socket.close(); + } +} diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/transport/TransportLayer.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/transport/TransportLayer.java new file mode 100644 index 0000000..f487a9c --- /dev/null +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/transport/TransportLayer.java @@ -0,0 +1,31 @@ +package ch.unibas.dmi.dbis.cs108.casono.server.network.transport; + +import java.io.IOException; + +/** + * Interface for transport layer implementations. + */ +public interface TransportLayer { + /** + * Reads data from the transport layer. + * + * @return the read data as a string + * @throws IOException if an I/O error occurs + */ + String read() throws IOException; + + /** + * Writes data to the transport layer. + * + * @param data the data to write + * @throws IOException if an I/O error occurs + */ + void write(String data) throws IOException; + + /** + * Closes the transport layer. + * + * @throws IOException if an I/O error occurs + */ + void close() throws IOException; +} diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/example/HelloWorld.java b/src/main/java/ch/unibas/dmi/dbis/cs108/example/HelloWorld.java deleted file mode 100644 index e7ab515..0000000 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/example/HelloWorld.java +++ /dev/null @@ -1,12 +0,0 @@ -package ch.unibas.dmi.dbis.cs108.example; - -/** - * A simple HelloWorld class. - */ -public class HelloWorld { - - public static void main(String[] args) { - System.out.println("Hello World"); - } - -} diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/example/gui/javafx/GUI.java b/src/main/java/ch/unibas/dmi/dbis/cs108/example/gui/javafx/GUI.java deleted file mode 100644 index 3746cae..0000000 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/example/gui/javafx/GUI.java +++ /dev/null @@ -1,32 +0,0 @@ -package ch.unibas.dmi.dbis.cs108.example.gui.javafx; - -import javafx.application.Application; -import javafx.scene.Scene; -import javafx.scene.control.Label; -import javafx.scene.layout.StackPane; -import javafx.stage.Stage; - -/** - * This is an example JavaFX-Application. - */ -public class GUI extends Application { - - /** - * Launching this method will not work on some platforms. - * What you should do is to create a separate main class and launch the GUI class from there as is done in {@link Main} - */ - public static void main(String[] args) { - launch(args); - } - - @Override - public void start(Stage stage) { - String javaVersion = System.getProperty("java.version"); - String javafxVersion = System.getProperty("javafx.version"); - Label l = new Label("Hello, JavaFX " + javafxVersion + ", running on Java " + javaVersion + "."); - Scene scene = new Scene(new StackPane(l), 640, 480); - stage.setScene(scene); - stage.show(); - } - -} diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/example/gui/javafx/Main.java b/src/main/java/ch/unibas/dmi/dbis/cs108/example/gui/javafx/Main.java deleted file mode 100644 index aac1f4d..0000000 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/example/gui/javafx/Main.java +++ /dev/null @@ -1,14 +0,0 @@ -package ch.unibas.dmi.dbis.cs108.example.gui.javafx; - -import javafx.application.Application; - -public class Main { - - /** - * This is simply a wrapper to launch the {@link GUI} class. - * The reason this class exists is documented in {@link GUI#main(String[])} - */ - public static void main(String[] args) { - Application.launch(GUI.class, args); - } -} diff --git a/src/main/resources/log4j2.xml b/src/main/resources/log4j2.xml new file mode 100644 index 0000000..01d74ca --- /dev/null +++ b/src/main/resources/log4j2.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/ui-structure/Casinomainui.css b/src/main/resources/ui-structure/Casinomainui.css index 129c457..8a9dc97 100644 --- a/src/main/resources/ui-structure/Casinomainui.css +++ b/src/main/resources/ui-structure/Casinomainui.css @@ -120,6 +120,7 @@ -fx-padding: 0 0 40 0; } +/* EXIT BUTTON */ .button-exit { -fx-background-radius: 12; -fx-border-radius: 12; @@ -128,17 +129,17 @@ -fx-border-width: 2; -fx-padding: 6 15; -fx-cursor: hand; + -fx-background-color: #333333; + -fx-border-color: #666666; + -fx-text-fill: #CCCCCC; } - .button-exit:hover { -fx-translate-y: -3; -fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0, 0.8), 0, 0, 3, 3); } -.button-exit { - -fx-background-color: #333333; - -fx-border-color: #666666; - -fx-text-fill: #CCCCCC; +/* LOBBY ERSTELLEN BUTTON */ +.button-create-lobby { -fx-background-radius: 12; -fx-border-radius: 12; -fx-font-family: "Courier New"; @@ -146,6 +147,14 @@ -fx-border-width: 2; -fx-padding: 6 15; -fx-cursor: hand; + -fx-background-color: #1a7f2e; + -fx-border-color: #2ecc71; + -fx-text-fill: #ffffff; +} +.button-create-lobby:hover { + -fx-translate-y: -3; + -fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0, 0.8), 0, 0, 3, 3); + -fx-background-color: #27ae60; } diff --git a/src/main/resources/ui-structure/Casinomainui.fxml b/src/main/resources/ui-structure/Casinomainui.fxml index 3b8bee7..2d18a48 100644 --- a/src/main/resources/ui-structure/Casinomainui.fxml +++ b/src/main/resources/ui-structure/Casinomainui.fxml @@ -68,6 +68,18 @@ + 0); + } + + @Test + void testJoinLobbyPlaceholder() { + // check if exception is thrown + assertDoesNotThrow(() -> gridManager.joinLobby(123)); + } +} diff --git a/src/test/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/lobbyui/LobbyButtonTranslationManagerTest.java b/src/test/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/lobbyui/LobbyButtonTranslationManagerTest.java new file mode 100644 index 0000000..4c1e526 --- /dev/null +++ b/src/test/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/lobbyui/LobbyButtonTranslationManagerTest.java @@ -0,0 +1,47 @@ +package ch.unibas.dmi.dbis.cs108.casono.client.ui.lobbyui; + +import org.junit.jupiter.api.*; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +class LobbyButtonTranslationManagerTest { + LobbyButtonTranslationManager manager; + + @BeforeEach + void setUp() { + manager = new LobbyButtonTranslationManager(); + manager.getButtonIdToLobbyId().clear(); + } + + @Test + void testAddLobbyButton() throws Exception { + manager.addLobbyButton(1, 100); + assertEquals(100, manager.getLobbyIdForButton(1)); + } + + @Test + void testRemoveLobbyButton() throws Exception { + manager.addLobbyButton(2, 200); + manager.removeLobbyButton(2); + assertNull(manager.getLobbyIdForButton(2)); + } + + @Test + void testIsFull() throws Exception { + for (int i = 1; i <= 8; i++) { + manager.addLobbyButton(i, 100 + i); + } + assertTrue(manager.isFull()); + Exception ex = assertThrows(Exception.class, () -> manager.addLobbyButton(9, 109)); + assertEquals("Grid is full!", ex.getMessage()); + } + + @Test + void testGetButtonIdToLobbyId() throws Exception { + manager.addLobbyButton(4, 400); + Map map = manager.getButtonIdToLobbyId(); + assertTrue(map.containsKey(4)); + assertEquals(400, map.get(4)); + } +} diff --git a/src/test/java/ch/unibas/dmi/dbis/cs108/example/HelloWorldTest.java b/src/test/java/ch/unibas/dmi/dbis/cs108/example/HelloWorldTest.java deleted file mode 100644 index a91b85d..0000000 --- a/src/test/java/ch/unibas/dmi/dbis/cs108/example/HelloWorldTest.java +++ /dev/null @@ -1,67 +0,0 @@ -package ch.unibas.dmi.dbis.cs108.example; - -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import java.io.ByteArrayOutputStream; -import java.io.PrintStream; - -import static org.junit.jupiter.api.Assertions.assertTrue; - -/** - * An example test class. - * Checks the output of the {@link HelloWorld} class and makes sure it contains "Hello World" - */ -public class HelloWorldTest { - - /* - * Streams to store system.out and system.err content - */ - private ByteArrayOutputStream outStream = new ByteArrayOutputStream(); - private ByteArrayOutputStream errStream = new ByteArrayOutputStream(); - - /* - * Here we store the previous pointers to system.out / system.err - */ - private PrintStream outBackup; - private PrintStream errBackup; - - /** - * This method is executed before each test. - * It redirects System.out and System.err to our variables {@link #outStream} and {@link #errStream}. - * This allows us to test their content later. - */ - @BeforeEach - public void redirectStdOutStdErr() { - outBackup = System.out; - errBackup = System.err; - System.setOut(new PrintStream(outStream)); - System.setErr(new PrintStream(errStream)); - } - - /** - * This method is run after each test. - * It redirects System.out / System.err back to the normal streams. - */ - @AfterEach - public void reestablishStdOutStdErr() { - System.setOut(outBackup); - System.setErr(errBackup); - } - - /** - * This is a normal JUnit-Test. It executes the HelloWorld-Method and verifies that it actually wrote "Hello World" to stdout - */ - @Test - public void testMain() { - HelloWorld.main(new String[0]); - String toTest = outStream.toString(); - toTest = removeNewline(toTest); - assertTrue(toTest.contains("Hello World")); - } - - private static String removeNewline(String str) { - return str.replace("\n", "").replace("\r", ""); - } -}