Compare commits

..

73 Commits

Author SHA1 Message Date
Jona Walpert 2ab27ac3ed Feat: Add lobby class and Manager with conenction to Game engine. The gameengine will be satrte when 4 players joined the lobby. This is made possible by the callback-driven-Listener
Refs #73
2026-04-11 17:41:45 +02:00
Julian Kropff f54182fde6 Merge branch 'chore/ui-checkstyle-fixes' into 'main'
Fix: Resolving Git Merge Conflicts

Closes #88

See merge request cs108-fs26/Gruppe-13!97
2026-04-11 13:10:57 +00:00
Mathis Ginkel 9e7a80c17d Docs: add description of enum ChatType
Refs #88
2026-04-11 15:07:55 +02:00
Mathis Ginkel 9df3042978 Style: fix line breaks in UsernameAvailability
Refs #88
2026-04-11 14:57:23 +02:00
Mathis Ginkel 69333ede7a Fix: change visibility of enum UsernameAvailability
Refs #88
2026-04-11 14:48:46 +02:00
Julian Kropff cdb6d0c6b0 Fix: revert wrongfully deleted line breaks
Refs #88
2026-04-11 14:43:48 +02:00
Mathis Ginkel a65341b648 Style: Manually revert changes made to SuccessResponse 2026-04-11 14:37:13 +02:00
Julian Kropff b4b2e3b067 Fix: Resolving Git Merge Conflicts and Checkstyle
Refs #88
2026-04-11 14:01:21 +02:00
Julian Kropff beeba04a2c Merge branch 'feat/client-chat' into chore/ui-checkstyle-fixes
# Conflicts:
#	src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/chat/ChatController.java
#	src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/chat/ChatModel.java
#	src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/chat/Message.java
#	src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/ChatClient.java
#	src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/ClientService.java
#	src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/CoreClient.java
#	src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/GameClient.java
#	src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/LobbyClient.java
#	src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/chatui/ChatViewController.java
#	src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/lobbyui/LobbyButtonGridManager.java
#	src/main/resources/ui-structure/components/Chatbox.fxml
2026-04-11 12:56:01 +02:00
Julian Kropff edb98c8d21 Merge branch 'feat/client-communication' into chore/ui-checkstyle-fixes
# Conflicts:
#	src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/gameui/CasinoGameUI.java
2026-04-11 12:20:27 +02:00
Mathis Ginkel 590a9aa658 Style: Manually revert changes made to UsernameAvailability enum 2026-04-11 11:56:06 +02:00
Lars Simon Winzer 7d74ed6263 Merge branch 'chore/87-write-documentation-to-explain-our-protocol' into 'main'
Documentation describing the structure and conventions of our network protocol

Closes #77 and #87

See merge request cs108-fs26/Gruppe-13!96
2026-04-11 11:32:49 +02:00
Lars Simon Winzer 2d7b15e593 Docs: Documentation describing the structure and conventions of our network protocol 2026-04-11 11:21:00 +02:00
Mathis Ginkel 66d3826509 Docs: Delete outdated network protocol document 2026-04-11 11:16:21 +02:00
Mathis Ginkel 9c09927d09 Docs: Adding Server Commands for Client Chat to the protocol document manually reverting changes 2026-04-10 23:25:05 +02:00
Mathis Ginkel 97e5a55042 Style: Apply Spotless 2026-04-10 22:43:23 +02:00
Mathis Ginkel 31d3947175 Add: CSS-Style changes to the ChatBox 2026-04-10 13:21:34 +02:00
Mathis Ginkel 1914aa2c7f Merge branch 'main' into 'feat/client-chat'
# Conflicts:
#   src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/ServerApp.java
2026-04-09 19:34:38 +00:00
Mathis Ginkel 13df8db33e Docs: Insert JavaDocs 2026-04-09 21:25:46 +02:00
Mathis Ginkel 566c9375ab Style: Apply Spotless 2026-04-09 20:36:18 +02:00
Mathis Ginkel 84e68d8b20 Merge remote-tracking branch 'origin/feat/client-chat' into feat/client-chat
# Conflicts:
#	src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/chat/ChatController.java
#	src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/chat/ChatModel.java
#	src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/chat/Message.java
#	src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/ChatClient.java
#	src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/ClientService.java
#	src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/GameClient.java
#	src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/LobbyClient.java
#	src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/chatui/ChatBoxController.java
#	src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/chatui/ChatViewController.java
#	src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/get_next_message/GetNextMessageHandler.java
#	src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/get_next_message/GetNextMessageResponse.java
#	src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/send_message/SendMessageParser.java
#	src/main/resources/ui-structure/Casinogameui.fxml
#	src/main/resources/ui-structure/components/chatui/chatbox.fxml
#	src/main/resources/ui-structure/components/chatui/chattab.fxml
#	src/main/resources/ui-structure/gameuicomponents/chatui/chatbox.fxml
#	src/test/java/ch/unibas/dmi/dbis/cs108/casono/client/chat/ChatApplication.java
#	src/test/java/ch/unibas/dmi/dbis/cs108/casono/client/chat/ChatTest.java
2026-04-09 18:13:29 +02:00
Mathis Ginkel e8401a976a Add: First implementation of Button, to create Whisper Chat 2026-04-09 18:04:57 +02:00
Mathis Ginkel c0c9225d42 Fix: Parsing of Server Responses for Client Chat 2026-04-09 18:04:57 +02:00
Mathis Ginkel 4e7b0ab67a Add: Implement Commands for Chat Protocol 2026-04-09 18:04:57 +02:00
Mathis Ginkel 39f60c9113 Fix: small changes to ChatUI 2026-04-09 18:04:57 +02:00
Jona Walpert 847d816935 Add: first (non functioning) modified version of lobby button controlelr with abckend conenction 2026-04-09 18:04:57 +02:00
Julian Kropff c8a9393fb6 Add: documentation for client network architecture 2026-04-09 18:04:57 +02:00
Julian Kropff 79bd33ad75 Add: initial client structure for game and network 2026-04-09 18:04:57 +02:00
Mathis Ginkel f5138503cb Add: CoreClient and ChatClient 2026-04-09 18:04:57 +02:00
Julian Kropff 93e39c3962 Add: Client Network Architecture Documentation 2026-04-09 18:04:57 +02:00
Mathis Ginkel 7d19e65428 Add: client chat architecture for lobby and global chat 2026-04-09 18:04:50 +02:00
Mathis Ginkel 87e9305e82 Fix: small changes in ClientService 2026-04-09 18:03:26 +02:00
Mathis Ginkel de66757837 Resolve Conflicts 2026-04-09 18:03:26 +02:00
Mathis Ginkel 65304cbb66 Merge remote-tracking branch 'origin/main' 2026-04-09 18:02:35 +02:00
Jona Walpert b205b25b48 Feat: Adds functionality to periodically refresh lobbies to update lobby GUI screen 2026-04-09 10:43:25 +02:00
Jona Walpert df31c307fe Fix: Comment out faulty Unit Test 2026-04-09 10:42:45 +02:00
Jona Walpert f3ecbd12f5 Feat: Add username text field to main Lobby 2026-04-08 13:34:26 +02:00
Jona Walpert 5d4d2655ef Add: Supply gameUI with client service at game startup 2026-04-08 13:33:31 +02:00
Jona Walpert d855f2d568 Feat: add offline mode to prevent errors when trying to send requests to not connected server 2026-04-08 13:29:39 +02:00
Jona Walpert ca782b389e Docs: Translate Javadoc and comments to English 2026-04-08 13:26:36 +02:00
Jona Walpert 04bf7ba545 Refactor: move login function from coreClient to LobbyClient 2026-04-08 13:22:24 +02:00
Mathis Ginkel 509aa71d9f Add: Implement Commands for Chat Protocol 2026-04-07 22:15:23 +02:00
Mathis Ginkel d1d2fb30e8 Fix: small changes to ChatUI 2026-04-07 22:11:20 +02:00
Jona Walpert aca61cbd52 Add: first (non functioning) modified version of lobby button controlelr with abckend conenction 2026-04-07 22:11:20 +02:00
Julian Kropff 73756b7d24 Add: documentation for client network architecture 2026-04-07 22:11:20 +02:00
Julian Kropff 60ed1ced91 Add: initial client structure for game and network 2026-04-07 22:11:20 +02:00
Mathis Ginkel eda51f8ac5 Add: CoreClient and ChatClient 2026-04-07 22:11:20 +02:00
Julian Kropff ce71992e17 Add: Client Network Architecture Documentation 2026-04-07 22:11:20 +02:00
Mathis Ginkel a9dcd79b38 Add: client chat architecture for lobby and global chat 2026-04-07 22:11:20 +02:00
Mathis Ginkel 79831b046c Fix: small changes in ClientService 2026-04-07 22:11:20 +02:00
Mathis Ginkel 61e54f282e Resolve Conflicts 2026-04-07 22:11:20 +02:00
Mathis Ginkel 8fb2657cdd Merge remote-tracking branch 'origin/main' 2026-04-07 22:10:52 +02:00
Mathis Ginkel 55a6d4beed Merge remote-tracking branch 'origin/main' 2026-04-05 13:12:39 +02:00
Jona Walpert 42b5344059 Test: Add in-process TestServer and update LobbyButtonGridManagerTest
Add TestServer that speaks the RawPacket/TcpTransport protocol for CREATE_LOBBY,
GET_LOBBY_STATUS and JOIN_LOBBY. Update LobbyButtonGridManagerTest to start the
TestServer and exercise a real `ClientService(host, port)`, enabling integration-style
tests of the client->transport->server roundtrip without an external server.

Refs #36
2026-04-02 22:28:12 +02:00
Jona Walpert 76e62b3cff Feat: Accept ClientService and delegate to LobbyClient
Add constructor overload that accepts ClientService and constructs a LobbyClient.
Avoids implicit host/port lookup in UI code; caller controls network config.
Keeps rendering/status behavior unchanged (async updates, deterministic ordering).
2026-04-02 22:26:14 +02:00
Jona Walpert 1070b5d4b9 Chore(startup): Propagate server ip:port to UI via system properties
ClientApp.start parses <ip>:<port>, sets `casono.server.host` / `casono.server.port` and forwards the original launch arg to the JavaFX launcher.
Casinomainui reads JavaFX launch args and exposes the same system properties.
CasinomainuiController reads those properties and constructs `ClientService(host, port)` (no hardcoded defaults)."
2026-04-02 22:12:48 +02:00
Jona Walpert 4daf6ff4a2 Fix: Sort button IDs before iterating map for deterministic rendering
Copy mapping.keySet() to a List and call Collections.sort(...) before iterating.
Ensures stable button placement and reproducible image updates (render + update)
2026-04-02 22:00:18 +02:00
Mathis Ginkel 7b1b35698c Merge remote-tracking branch 'origin/main' 2026-04-02 22:00:06 +02:00
Jona Walpert 3153f7027d fix: temporyy fix for NPE and wrong naming 2026-04-02 18:50:20 +02:00
Jona Walpert b5d19d915b Add: first (non functioning) modified version of lobby button controlelr with abckend conenction 2026-04-02 16:31:27 +02:00
Julian Kropff 09c19c2602 Add: documentation for client network architecture 2026-04-02 16:10:33 +02:00
Julian Kropff 77c7f7c0a5 Add: initial client structure for game and network 2026-04-02 16:08:24 +02:00
Mathis Ginkel 6f80501ea2 Add: CoreClient and ChatClient 2026-04-02 11:16:40 +02:00
Julian Kropff 45cd54b981 Add: Client Network Architecture Documentation 2026-04-02 10:54:47 +02:00
Mathis Ginkel 3427c661cf Merge branch 'feat/client-chat' into feat/client-communication
# Conflicts:
#	src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/chatui/ChatController.java
2026-04-02 10:31:28 +02:00
Mathis Ginkel ff07843e0a Fix: small changes in ClientService 2026-04-02 10:22:51 +02:00
Mathis Ginkel cc33411911 Add: basic chat functionality 2026-04-02 10:20:03 +02:00
Mathis Ginkel f721a26e53 Merge remote-tracking branch 'origin/main' 2026-04-02 10:14:23 +02:00
Mathis Ginkel dd721bda3f Merge remote-tracking branch 'origin/main' 2026-03-28 12:45:53 +01:00
Mathis Ginkel 68c7003c5d Merge remote-tracking branch 'origin/main' 2026-03-23 21:06:41 +01:00
Mathis Ginkel b4f34e00f2 Merge remote-tracking branch 'origin/main' 2026-03-21 10:01:34 +01:00
Mathis Ginkel 26a1ba945a Merge remote-tracking branch 'origin/main' 2026-03-20 13:23:01 +01:00
Mathis Ginkel 5401ead198 add: Network Protocol docs 2026-03-15 22:31:10 +01:00
70 changed files with 3321 additions and 654 deletions
+1 -1
View File
@@ -32,7 +32,7 @@ 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")
implementation("org.jspecify:jspecify:1.0.0")
// Source: https://mvnrepository.com/artifact/org.fusesource.jansi/jansi
runtimeOnly("org.fusesource.jansi:jansi:2.4.2")
@@ -0,0 +1,249 @@
# Client Nework Architecture
<!-- vim-markdown-toc GFM -->
* [Architecture Overview](#architecture-overview)
* [network/Card.java](#networkcardjava)
* [network/GameState.java](#networkgamestatejava)
* [network/Player.java](#networkplayerjava)
* [network/ChatClient.java](#networkchatclientjava)
* [ChatClient(ClientService clientService)](#chatclientclientservice-clientservice)
* [sendMessage(Message message)](#sendmessagemessage-message)
* [getMessages()](#getmessages)
* [network/ClientService.java](#networkclientservicejava)
* [ClientService(String ip, int port)](#clientservicestring-ip-int-port)
* [processCommand(String message)](#processcommandstring-message)
* [sendRequest(Runnable request)](#sendrequestrunnable-request)
* [getRuntimeException(Exception e)](#getruntimeexceptionexception-e)
* [closeSocket()](#closesocket)
* [writeToTransport(String s) throws IOException](#writetotransportstring-s-throws-ioexception)
* [network/CoreClient.java](#networkcoreclientjava)
* [CoreClient(ClientService clientservice)](#coreclientclientservice-clientservice)
* [ping()](#ping)
* [login(String user)](#loginstring-user)
* [network/GameClient.java](#networkgameclientjava)
* [GameClient(ClientService client)](#gameclientclientservice-client)
* [getGameState()](#getgamestate)
* [parseGameState(String input)](#parsegamestatestring-input)
* [Example server response](#example-server-response)
* [network/LobbyClient.java](#networklobbyclientjava)
* [LobbyClient(ClientService client)](#lobbyclientclientservice-client)
* [fetchLobbyStatusString(int lobbyId)](#fetchlobbystatusstringint-lobbyid)
* [createLobby()](#createlobby)
* [getLobbyId()](#getlobbyid)
* [joinLobby(int lobbyId)](#joinlobbyint-lobbyid)
<!-- vim-markdown-toc -->
## Architecture Overview
```text
client/
├── game/
│ ├── Card.java
│ ├── GameState.java
│ └── Player.java
└── network/
├── ChatClient.java
├── ClientService.java
├── CoreClient.java
├── GameClient.java
└── LobbyClient.java
```
### game/Card.java
Represents a playing card with a value and suit.
### game/GameState.java
Represents the current state of the poker game, including the phase, pot size, current bet, dealer position, active player, community cards, and player information.
### game/Player.java
Represents a player in the poker game, including their name, chip count, current bet, state (e.g., `active`, `folded`), and their hole cards.
### network/ChatClient.java
The ChatClient class is responsible for sending messages to the server and retrieving messages from the server. It uses the ClientService to send commands and receive responses from the server.
#### ChatClient(ClientService clientService)
Constructs a ChatClient with the given ClientService for communication.
- **Parameter (`clientService`)**: The ClientService instance used to send commands and receive responses from the server.
#### sendMessage(Message message)
Send a Message to the server by converting it to a string format and sending a `SEND_MESSAGE` command with the message content as arguments.
- **Parameter (`message`)**: message The Message object to be sent to the server.
#### getMessages()
Retrieve messages from the server by first sending a `GET_MESSAGE_COUNT` command to determine how many messages are available and then sending `GET_NEXT_MESSAGE` commands in a loop to retrieve each message. The retrieved messages are parsed into Message objects and returned as a list.
- **Parameter (`A`)**: list of Message objects representing the messages retrieved from the server.
### network/ClientService.java
The ClientService class is responsible for managing the connection to the server, sending commands, and receiving responses. It uses a TcpTransport to
communicate with the server and an ExecutorService to handle asynchronous requests.
#### ClientService(String ip, int port)
Constructs a ClientService with the given server IP and port. It establishes a socket connection to the server and initializes the TcpTransport and ExecutorService for communication.
- **Parameter (`ip`)**: The IP address of the server to connect to.
- **Parameter (`port`)**: The port number of the server to connect to.
#### processCommand(String message)
Sends a command to the server and waits for the response. The command is sent using the TcpTransport, and the response is read in a loop until a valid response is received. The method handles `+OK` and `-ERROR` responses from the server and returns the actual response content.
- **Parameter (`message`)**: The command message to be sent to the server.
- **Return**: The response from the server as a string.
#### sendRequest(Runnable request)
Helper method to send a request to the server using the ExecutorService. It submits the request as a Runnable task and waits for its completion. If the task is interrupted or encounters an execution exception, it throws a
RuntimeException with the appropriate cause.
- **Parameter (`request`)**: The Runnable task representing the request to be sent to the server.
#### getRuntimeException(Exception e)
Helper method to extract the cause of an exception and return it as a RuntimeException. If the cause is null, it returns the original exception as a RuntimeException. If the cause is already a RuntimeException, it returns it directly. Otherwise, it wraps the cause in a new RuntimeException and returns it.
- **Parameter (`e`)**: The exception from which to extract the cause.
- **Return**: A RuntimeException representing the cause of the original exception.
#### closeSocket()
Closes the socket connection to the server and shuts down the ExecutorService. It also closes the TcpTransport used for communication. If any IOException occurs during this process, it prints the exception to the console.
#### writeToTransport(String s) throws IOException
Helper method to write a command string to the TcpTransport. It generates a unique ID for the command using the idGenerator and sends a RawPacket containing the ID and the command string to the server. If an IOException occurs during this process, it throws a RuntimeException with the cause.
- **Parameter (`s`)**: The command string to be sent to the server.
- **Throws IOException**: If an I/O error occurs while writing to the transport.
### network/CoreClient.java
The CoreClient class provides basic functionalities for communicating with the server, such as sending a ping command to check connectivity and logging in with a username. It uses the ClientService to send commands and receive responses from the server.
#### CoreClient(ClientService clientservice)
Constructs a CoreClient with the given ClientService for communication.
- **Parameter (`clientservice`)**: The ClientService instance used to send commands and receive responses from the server.
#### ping()
Sends a `PING` command to the server to check connectivity. The server should respond with a `PONG` message if the connection is successful.
#### login(String user)
Logs in to the server with the given username by sending a `LOGIN` command.
- **Parameter (`user`)**: The username to log in with.
### network/GameClient.java
The GameClient class is responsible for communicating with the server to retrieve the current game state. It sends a command to the server and parses the response into a structured GameState object.
#### GameClient(ClientService client)
Constructs a GameClient with the given ClientService for communication.
- **Parameter (`client`)**: The ClientService instance used to send commands and receive responses from the server.
#### getGameState()
Retrieves the current game state from the server by sending a command and parsing the response.
- **Return**: A GameState object representing the current state of the game.
#### parseGameState(String input)
Parses the raw response from the server into a structured GameState object.
- **Parameter (`input`)**: The raw response string from the server.
- **Return**: A GameState object representing the current state of the game.
#### Example server response
```text
+OK
PHASE=FLO P
POT=150
CURRENT_BET=50
DEALER=0
ACTIVE_PLAYER=1
CARDS
CARD
VALUE=10
SUIT=H
CARD
VALUE=7
SUIT=S
CARD
VALUE=A
SUIT=D
PLAYERS
PLAYER
NAME=Max
CHIPS=1200
BET=50
STATE=ACTIVE
CARDS
CARD
VALUE=K
SUIT=H
CARD
VALUE=3
SUIT=C
PLAYER
NAME=Anna
CHIPS=800
BET=0
STATE=FOLDED
CARDS
END
```
### network/LobbyClient.java
The LobbyClient class is responsible for communicating with the server to manage game lobbies. It provides methods to create a lobby, join a lobby, and fetch the current status of a lobby by sending appropriate commands to the server and processing the responses.
#### LobbyClient(ClientService client)
Constructs a LobbyClient with the given ClientService for communication.
- **Parameter (`client`)**: The ClientService instance used to send commands and receive responses from the server.
#### fetchLobbyStatusString(int lobbyId)
Fetch the current status of the lobby with the given id from the server.
- **Parameter (`lobbyId`)**: The id of the lobby to fetch the status for.
- **Return**: A string representing the current status of the lobby, as returned by the server.
#### createLobby()
Request the server to create a new lobby and return the id of the newly created lobby.
- **Return**: The id of the newly created lobby, as returned by the server.
#### getLobbyId()
Request the server to return the id of the lobby that the client is currently in.
- **Return**: The id of the lobby that the client is currently in, as returned by the server.
#### joinLobby(int lobbyId)
Request the server to join the lobby with the given id.
- **Parameter (`lobbyId`)**: The id of the lobby to join.
@@ -341,4 +341,116 @@ LIST_USERS
END
END
END
```
## SEND_MESSAGE command
The `SEND_MESSAGE` command is used to transfer the chat message sent by a user to the server.
### Required pre-execution checks
None.
### Request Parameters
| Field | Type | Description |
|:---------|:----------------|:-------------------------------------------------------------------|
| `TYPE` | `Enum<ChatType` | Member of Enum, indicating with chat type is used |
| `GAME` | `int` | ID for identifying the Lobby Chat |
| `USER` | `String` | Username of the player that sent the message |
| `TARGET` | `String` | Username of to which the message is sent to (only in Whisper Chat) |
| `TIME` | `String` | Timestamp, when the message was sent |
| `TEXT` | `String` | Content of the message |
| Members of `ChatType` | Description |
|:----------------------|:--------------------------------|
| `GLOBAL` | Message is for the global chat |
| `LOBBY` | Message is for the lobby chat |
| `WHISPER` | Message is for the whisper chat |
### Success Response
No response fields.
### Error Response
None.
### Example Request
```
SEND_MESSAGE TYPE=GLOBAL GAME=1 USER=player1 TARGET=null TIME='10:30' TEXT='Hello World'
```
### Example Response
```
+OK
END
```
## GET_MESSAGE_COUNT command
The `GET_MESSAGE_COUNT` is used to get the current number of messages that are stored in the queue for a client.
### Required pre-execution checks
None.
### Request Parameters
No parameters.
### Success Response
| Field | Type | Description |
|:--------|:------|:-------------------------------|
| `COUNT` | `int` | The current number of messages |
### Error Response
| Code | Description |
| :------------------- |:---------------------------------------------------------------------------------|
| `NO_USER_ASSOCIATED` | The session has no user associated, there is no queue of messages for the client |
### Example Request
```
GET_MESSAGE_COUNT
```
### Example Response
```
+OK
COUNT=10
END
```
## GET_NEXT_MESSAGE command
The `GET_NEXT_MESSAGE` command is used to get the next message stored in a queue for the client.
### Required pre-execution checks
None.
### Request Parameters
No parameters.
### Success Response
| Field | Type | Description |
|:---------|:----------------|:-------------------------------------------------------------------|
| `TYPE` | `Enum<ChatType` | Member of Enum, indicating with chat type is used |
| `GAME` | `int` | ID for identifying the Lobby Chat |
| `USER` | `String` | Username of the player that sent the message |
| `TARGET` | `String` | Username of to which the message is sent to (only in Whisper Chat) |
| `TIME` | `String` | Timestamp, when the message was sent |
| `TEXT` | `String` | Content of the message |
| Members of `ChatType` | Description |
|:----------------------|:--------------------------------|
| `GLOBAL` | Message is for the global chat |
| `LOBBY` | Message is for the lobby chat |
| `WHISPER` | Message is for the whisper chat |
### Error Response
| Code | Description |
| :------------------- |:---------------------------------------------------------------------------------|
| `NO_USER_ASSOCIATED` | The session has no user associated, there is no queue of messages for the client |
### Example Request
```
GET_NEXT_MESSAGE
```
### Example Response
```
+OK
TYPE=GLOBAL GAME=-1 USER=player1 TARGET=null TIME=9:30 TEXT="Guten Tag"
END
```
+72
View File
@@ -0,0 +1,72 @@
# Our Network Protocol Documentation
## Overview
Our protocol is inspired by *POP3*, but has been highly customized to fit our specific needs.
It is a text-based protocol operating over raw TCP sockets, designed for human readability and strict structure.
## Packet Structure
Each network packet consists of:
- **4-byte header**: Specifies the size of the payload (big-endian integer).
- **4-byte request ID**: Generated by the client, used to match requests and responses.
- **Payload**: The actual data, its size as specified by the header.
This structure is used for both requests and responses.
## Conventions
- All keys (in both requests and responses) use UPPER_SNAKE_CASE.
- Only a-z, A-Z, 0-9 are allowed in keys.
- Only human-readable strings are transmitted.
- Binary data is not allowed.
## Request Format
A request consists of a single line:
```
COMMAND KEY1=ARG1 KEY2=ARG2
```
- **COMMAND**: The action to perform.
- **KEY=VALUE pairs**: Optional parameters. There may be zero or more.
- **Whitespace**: Extra spaces between key, separator, and value are ignored. Any other characters between them are an error.
- **Standalone values**: Not allowed. Every value must have a key.
### String Values
- Strings with spaces must be enclosed in single quotes: `'example string'`.
- Inside quoted strings, line breaks are allowed.
- To include a single quote inside a string, escape it (e.g., `'It\'s fine'`).
If these rules are violated, the request is considered invalid and will be rejected.
## Response Format
Responses are more complex and can represent nested collections.
- **Success**: Starts with `+OK`
- **Error**: Starts with `-ERR`
- After the status, a newline follows, then fields in the format `KEY=VALUE`.
- Collections and elements are ended with the `END` keyword.
- A collection starts with a key, and its elements are indented.
- A element starts with a key, and its fields are indented.
### Example: Nested Collection
```
+OK
KEY1=VALUE1
FIELDS
FIELD
NESTED_KEY=NESTED_VALUE
END
END
KEY2=VALUE2
END
```
## Error Handling
Any violation of the format (invalid characters, unescaped quotes, binary data, etc.) results in the request being rejected with an error response.
### Example Error Response When Violating Syntax Rules:
```
-ERR
CODE=PARSING_ERROR
MSG='Error occured during parsing. Likely due to malformed payload.'
END
```
@@ -36,6 +36,16 @@ public class ClientApp {
int port = Integer.parseInt(parts[1]);
LOGGER.info("You've selected the client. It will connect port {} at host {}", port, host);
Launcher.main(new String[] {});
// Expose the chosen host/port to the UI via system properties so controllers
// (which read System.getProperty("casono.server.host"/"casono.server.port"))
// can obtain the correct connection information.
System.setProperty("casono.server.host", host);
System.setProperty("casono.server.port", Integer.toString(port));
// Forward the original address argument to the launcher as well.
Launcher.main(new String[] {arg});
}
public static void main(String[] args) {
start(args[0]);
}
}
@@ -0,0 +1,129 @@
package ch.unibas.dmi.dbis.cs108.casono.client.chat;
import ch.unibas.dmi.dbis.cs108.casono.client.network.ChatClient;
import ch.unibas.dmi.dbis.cs108.casono.client.network.ClientService;
import ch.unibas.dmi.dbis.cs108.casono.client.ui.chatui.ChatBoxController;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Timer;
import java.util.TimerTask;
import org.jspecify.annotations.Nullable;
/**
* responsible for the transferring of messages from the server to the ChatModel or from the
* ChatViewController to the server
*/
public class ChatController {
private final String username;
private final ChatClient chatClient;
public ChatBoxController getChatBoxController() {
return chatBoxController;
}
private final ChatBoxController chatBoxController;
private int lobbyId = -1;
private final Timer timer;
public record ChatKey(ChatType type, @Nullable String targetUser) {
public ChatKey(ChatType type) {
this(type, null);
}
}
public Map<ChatKey, ChatModel> getChatModelMap() {
return chatModelMap;
}
private Map<ChatKey, ChatModel> chatModelMap;
private static final long REFRESH_TIME = 1000;
/**
* Constructor, adds TimerTask to be sent to the server regularly
*
* @param username
* @param clientService
*/
public ChatController(String username, ClientService clientService) {
this.username = username;
chatClient = new ChatClient(clientService);
chatModelMap = new LinkedHashMap<>();
this.chatBoxController = new ChatBoxController(username, this);
this.timer = new Timer();
timer.schedule(
new TimerTask() {
@Override
public void run() {
receiveMessage();
}
},
0,
REFRESH_TIME);
}
/**
* Method to be activated, if a lobby has be chosen. It will update the UI and add a new
* ChatModel to hold the Data for the Lobby Chat
*
* @param lobbyId
*/
public void setLobbyChat(int lobbyId) {
this.lobbyId = lobbyId;
ChatModel lobbyChatModel = new ChatModel(ChatType.LOBBY, username, lobbyId, null);
chatModelMap.put(new ChatKey(ChatType.LOBBY), lobbyChatModel);
this.chatBoxController.addChatTab("Lobby", lobbyChatModel);
}
/** method to get all messages from the server */
public void receiveMessage() {
List<Message> newMessages = chatClient.getMessages();
if (!newMessages.isEmpty()) {
for (Message msg : newMessages) {
switch (msg.getMessageType()) {
case ChatType.GLOBAL:
chatModelMap.get(new ChatKey(ChatType.GLOBAL)).addMessage(msg);
break;
case ChatType.LOBBY:
if (msg.lobbyId == lobbyId) {
chatModelMap
.computeIfAbsent(
new ChatKey(ChatType.LOBBY),
(_key) ->
new ChatModel(
ChatType.LOBBY,
username,
msg.lobbyId,
null))
.addMessage(msg);
}
break;
case ChatType.WHISPER:
if (msg.target.equals(username)) {
if (chatModelMap.containsKey(
new ChatKey(ChatType.WHISPER, msg.sender))) {
chatModelMap
.get(new ChatKey(ChatType.WHISPER, msg.sender))
.addMessage(msg);
} else {
chatBoxController.addWhisperChat(msg.sender);
}
}
break;
}
}
}
}
/**
* method to send a message to the server
*
* @param message
*/
public void onSendToNetwork(Message message) {
chatClient.sendMessage(message);
}
}
@@ -0,0 +1,85 @@
package ch.unibas.dmi.dbis.cs108.casono.client.chat;
import java.util.ArrayList;
import java.util.function.Consumer;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
/**
* ChatModel, stores the data for a specific chat
*
* <p>Holds the current state of a chat
*/
public class ChatModel {
private ArrayList<Consumer<Message>> listeners = new ArrayList<>();
public ArrayList<Message> messages;
private final ChatType chattype;
/** The person currently using this client */
public final String username;
/** The person to send the message to If the chat is a whisper chat */
private final String target;
private final IntegerProperty count;
public int lobbyId;
/**
* Constructs a new ChatModel for a specific chat type.
*
* @param chattype The type of chat (e.g., GLOBAL, LOBBY, or WHISPER).
* @param username The username of the current user.
* @param lobbyId The ID of the lobby, or -1 if not applicable.
* @param target The username of the whisper recipient, or null for other chat types.
*/
public ChatModel(ChatType chattype, String username, int lobbyId, String target) {
this.messages = new ArrayList<Message>();
this.chattype = chattype;
this.username = username;
this.count = new SimpleIntegerProperty(0);
this.lobbyId = lobbyId;
this.target = target;
}
/**
* Returns the type of chat this model represents.
*
* @return The {@link ChatType}.
*/
public ChatType getChattype() {
return chattype;
}
/**
* Adds a new message to the history and notifies all registered listeners. This method is
* synchronized to ensure thread safety when updating the message list.
*
* @param msg The {@link Message} to be added.
*/
public synchronized void addMessage(Message msg) {
messages.add(msg);
listeners.stream().forEach((l) -> l.accept(messages.getLast()));
}
/**
* Registers a listener to be notified whenever a new message is added to this model.
*
* @param listener A {@link Consumer} that processes the new {@link Message}.
*/
public void addListener(Consumer<Message> listener) {
this.listeners.add(listener);
}
/**
* Returns the target user for this chat, primarily used for whispers.
*
* @return The target username or null.
*/
public String getTarget() {
return target;
}
}
@@ -0,0 +1,8 @@
package ch.unibas.dmi.dbis.cs108.casono.client.chat;
/** Describing the Type of the Chat */
public enum ChatType {
GLOBAL,
LOBBY,
WHISPER
}
@@ -0,0 +1,209 @@
package ch.unibas.dmi.dbis.cs108.casono.client.chat;
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.RequestParameter;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.builder.ResponseBody;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.builder.ResponseBodyBuilder;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Optional;
import org.jspecify.annotations.NonNull;
/** Message Object for internal handling of Chat-Messages */
public class Message {
private final ChatType type;
private final String message;
public String sender;
public String timestamp;
public int lobbyId = 0;
public String target = null;
/**
* Constructs a Message with a provided timestamp. Typically used when reconstructing messages
* received from the server. Used for Messages in the Lobby Chat.
*
* @param lobbyId The ID of the lobby, or -1 if not applicable.
* @param sender The username of the message creator.
* @param timestamp The formatted time string (e.g., "HH:mm").
* @param message The actual text content of the message.
*/
private Message(int lobbyId, String sender, String timestamp, String message) {
this.type = ChatType.LOBBY;
this.lobbyId = lobbyId;
this.sender = sender;
this.target = null;
this.timestamp = timestamp;
this.message = message;
}
/**
* Constructs a Message with a provided timestamp. Typically used when reconstructing messages
* received from the server. Used for Messages in the WHISPER and GLOBAL Chat.
*
* @param type The chat category (e.g., GLOBAL or WHISPER).
* @param sender The username of the message creator.
* @param timestamp The formatted time string (e.g., "HH:mm").
* @param message The actual text content of the message.
*/
private Message(ChatType type, String sender, String target, String timestamp, String message) {
this.type = type;
this.lobbyId = -1;
this.sender = sender;
this.target = target;
this.timestamp = timestamp;
this.message = message;
}
/**
* Constructs a new Message for the current user. Automatically generates a timestamp based on
* the local system time ("HH:mm").
*
* @param type The chat category (e.g., GLOBAL, LOBBY, or WHISPER).
* @param lobbyId The ID of the lobby, or -1 if not applicable.
* @param sender The username of the current user.
* @param target The username of the recipient (for whispers).
* @param message The actual text content to be sent.
*/
public Message(ChatType type, int lobbyId, String sender, String target, String message) {
this.type = type;
this.lobbyId = lobbyId;
this.sender = sender;
this.target = target;
this.message = message;
LocalDateTime now = LocalDateTime.now();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm");
this.timestamp = now.format(formatter);
}
/**
* Returns the text content of the message.
*
* @return The message string.
*/
public String getMessage() {
return message;
}
/**
* Returns the type of chat this message belongs to.
*
* @return The {@link ChatType}.
*/
public ChatType getMessageType() {
return type;
}
/**
* Formats the message object into a string representation compatible with the network protocol
* arguments.
*
* @return A formatted string containing all message attributes for server transmission.
*/
public String toArgsString() {
String gameIdString = "";
if (lobbyId >= 0) {
gameIdString = " GAME=" + lobbyId;
} else {
gameIdString = " GAME='-1'";
}
return String.format(
"TYPE=%s%s USER='%s' TARGET='%s' TIME='%s' TEXT='%s'",
this.type.toString(),
gameIdString,
this.sender,
this.target,
this.timestamp,
this.message);
}
/**
* Parses a list of network request parameters to reconstruct a Message object. Handles
* different chat types (GLOBAL, LOBBY, WHISPER) and their specific requirements.
*
* @param parameters A list of {@link RequestParameter} received from the network.
* @return A new {@link Message} instance populated with the parsed data.
*/
public static Message toMessageReqPars(List<RequestParameter> parameters) {
String typeString = getParString(parameters, "TYPE");
ChatType type = ChatType.valueOf(typeString);
return switch (type) {
case GLOBAL ->
new Message(
ChatType.GLOBAL,
getParString(parameters, "USER"),
null,
getParString(parameters, "TIME"),
getParString(parameters, "TEXT"));
case LOBBY ->
new Message(
Integer.parseInt(getParString(parameters, "GAME")),
getParString(parameters, "USER"),
getParString(parameters, "TIME"),
getParString(parameters, "TEXT"));
case WHISPER ->
new Message(
ChatType.WHISPER,
getParString(parameters, "USER"),
getParString(parameters, "TARGET"),
getParString(parameters, "TIME"),
getParString(parameters, "TEXT"));
};
}
/**
* Helper method to extract a specific parameter value by its key.
*
* @param parameters The list of parameters to search.
* @param keyString The key to look for.
* @return The value associated with the key.
* @throws RuntimeException if the key is not found.
*/
private static @NonNull String getParString(
List<RequestParameter> parameters, String keyString) {
return getParString(parameters, keyString, null);
}
/**
* Helper method to extract a specific parameter value by its key, with a fallback default
* value.
*
* @param parameters The list of parameters to search.
* @param keyString The key to look for.
* @param defaultVal The value to return if the key is missing.
* @return The found value or the default value.
*/
private static @NonNull String getParString(
List<RequestParameter> parameters, String keyString, String defaultVal) {
Optional<String> parOption =
parameters.stream()
.filter((p) -> keyString.equals(p.key()))
.findFirst()
.map(RequestParameter::value);
if (parOption.isEmpty()) {
if (defaultVal == null) {
throw new RuntimeException("No " + keyString + " found");
} else {
return defaultVal;
}
}
return parOption.get();
}
/**
* Converts the message object into a network response body using the provided builder.
*
* @param builder The {@link ResponseBodyBuilder} used to construct the response.
* @return The built {@link ResponseBody} containing the message data.
*/
public ResponseBody toResponse(ResponseBodyBuilder builder) {
builder.param("TYPE", type.name());
builder.param("GAME", lobbyId);
builder.param("USER", this.sender);
if (target != null) {
builder.param("TARGET", target);
}
builder.param("TIME", this.timestamp);
builder.param("TEXT", this.message);
return builder.build();
}
}
@@ -0,0 +1,7 @@
package ch.unibas.dmi.dbis.cs108.casono.client.game;
/** Represents a playing card with a value and suit. */
public class Card {
public String value;
public String suit;
}
@@ -0,0 +1,19 @@
package ch.unibas.dmi.dbis.cs108.casono.client.game;
import java.util.ArrayList;
import java.util.List;
/**
* Represents the current state of the poker game, including the phase, pot size, current bet,
* dealer position, active player, community cards, and player information.
*/
public class GameState {
public String phase;
public int pot;
public int currentBet;
public int dealer;
public int activePlayer;
public List<Card> communityCards = new ArrayList<>();
public List<Player> players = new ArrayList<>();
}
@@ -0,0 +1,17 @@
package ch.unibas.dmi.dbis.cs108.casono.client.game;
import java.util.ArrayList;
import java.util.List;
/**
* Represents a player in the poker game, including their name, chip count, current bet, state
* (e.g., "active", "folded"), and their hole cards.
*/
public class Player {
public String name;
public int chips;
public int bet;
public String state;
public List<Card> cards = new ArrayList<>();
}
@@ -0,0 +1,72 @@
package ch.unibas.dmi.dbis.cs108.casono.client.network;
import ch.unibas.dmi.dbis.cs108.casono.client.chat.Message;
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.RequestParameter;
import java.util.ArrayList;
import java.util.List;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/**
* The ChatClient class is responsible for sending messages to the server and retrieving messages
* from the server. It uses the ClientService to send commands and receive responses from the
* server.
*/
public class ChatClient {
private final ClientService clientService;
private final Logger logger;
/**
* Constructs a ChatClient with the given ClientService for communication.
*
* @param clientService The ClientService instance used to send commands and receive responses
* from the server.
*/
public ChatClient(ClientService clientService) {
this.clientService = clientService;
this.logger = LogManager.getLogger(ChatClient.class);
}
/**
* Send a Message to the server by converting it to a string format and sending a "SEND_MESSAGE"
* command with the message content as arguments.
*
* @param message The Message object to be sent to the server.
*/
public void sendMessage(Message message) {
String request = "SEND_MESSAGE " + message.toArgsString();
logger.info("Writing to server: " + request);
clientService.processCommand(request);
}
/**
* Retrieve messages from the server by first sending a "GET_MESSAGE_COUNT" command to determine
* how many messages are available and then sending "GET_NEXT_MESSAGE" commands in a loop to
* retrieve each message. The retrieved messages are parsed into Message objects and returned as
* a list.
*
* @return A list of Message objects representing the messages retrieved from the server.
*/
public List<Message> getMessages() {
logger.info("Asking server for new messages");
List<RequestParameter> countStr =
ClientService.convertToRequestParameters(
clientService.processCommand("GET_MESSAGE_COUNT"));
RequestParameter countRes = countStr.getFirst();
if (!countRes.key().equals("COUNT")) {
logger.error("Not the right response from server");
}
int count = Integer.parseInt(countRes.value());
logger.info("Got " + count + " messages");
ArrayList<Message> messages = new ArrayList<>();
for (int i = 0; i < count; i++) {
List<RequestParameter> msgRes =
ClientService.convertToRequestParameters(
clientService.processCommand("GET_NEXT_MESSAGE"));
Message msg = Message.toMessageReqPars(msgRes);
messages.add(msg);
}
return messages;
}
}
@@ -0,0 +1,244 @@
package ch.unibas.dmi.dbis.cs108.casono.client.network;
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.RequestParameter;
import ch.unibas.dmi.dbis.cs108.casono.server.network.transport.RawPacket;
import ch.unibas.dmi.dbis.cs108.casono.server.network.transport.TcpTransport;
import java.io.IOException;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/**
* The ClientService class is responsible for managing the connection to the server, sending
* commands, and receiving responses. It uses a TcpTransport to communicate with the server and an
* ExecutorService to handle asynchronous requests.
*/
public class ClientService {
private final TcpTransport clienttcptransport;
private final Socket socket;
private final ExecutorService executor;
private final boolean offlineMode;
public static ArrayList<String> response;
private final AtomicInteger idGenerator;
private Logger logger;
/**
* Constructs a ClientService with the given server IP and port. It establishes a socket
* connection to the server and initializes the TcpTransport and ExecutorService for
* communication.
*
* @param ip The IP address of the server to connect to.
* @param port The port number of the server to connect to.
*/
public ClientService(String ip, int port) {
this.idGenerator = new AtomicInteger(0);
this.logger = LogManager.getLogger(ClientService.class);
this.offlineMode = false;
try {
socket = new Socket(ip, port);
clienttcptransport = new TcpTransport(socket);
logger.info("Connected to server at " + ip);
} catch (IOException i) {
throw new RuntimeException(i);
}
executor = Executors.newSingleThreadExecutor();
}
/**
* Constructs a ClientService in offline mode. No network connection will be attempted and calls
* to processCommand will throw a RuntimeException.
*
* @param offline true to create an offline (no-network) client service
*/
public ClientService(boolean offline) {
this.idGenerator = new AtomicInteger(0);
this.offlineMode = offline;
this.socket = null;
this.clienttcptransport = null;
this.executor = Executors.newSingleThreadExecutor();
}
/** Returns true if this ClientService is running in offline mode (no network). */
public boolean isOffline() {
return offlineMode;
}
static Pattern responseRex =
Pattern.compile(
"(?<key>\\w+)=(('(?<string>([^']|\\')+)')|(?<primVal>[+-]?[\\d\\w:]+))");
/**
* Removes escape characters from a string, specifically converting escaped single quotes (\')
* back to regular single quotes (').
*
* @param input The escaped string to process.
* @return The unescaped string.
*/
private static String unescape(String input) {
return input.replaceAll("\\\\'", "'");
}
/**
* Converts a list of raw string parameters into a list of {@link RequestParameter} objects. It
* uses a regex matcher to distinguish between quoted strings (which are unescaped) and
* primitive values.
*
* @param input A list of raw strings to be parsed.
* @return A list of parsed {@link RequestParameter} objects.
* @throws RuntimeException if a parameter does not match the expected format.
*/
public static List<RequestParameter> convertToRequestParameters(List<String> input) {
return input.stream()
.map((String parString) -> responseRex.matcher(parString))
.filter(Matcher::matches)
.map(
(m) -> {
if (!(m.group("string") == null)) {
return new RequestParameter(
m.group("key"), unescape(m.group("string")));
} else if (!(m.group("primVal") == null)) {
return new RequestParameter(m.group("key"), m.group("primVal"));
} else {
throw new RuntimeException();
}
})
.toList();
}
/**
* Sends a command to the server and processes the multi-line response. It handles the protocol
* handshake (expecting +OK), strips leading tabs from response lines, and collects them until
* the "END" marker is reached.
*
* @param message The raw command string to be sent to the transport layer.
* @return A list of response lines received from the server (excluding protocol markers).
* @throws RuntimeException if the server responds with an error or if a communication failure
* occurs.
*/
protected List<String> processCommand(String message) {
List<String> response = new ArrayList<>();
sendRequest(
() -> {
try {
writeToTransport(message);
String responseText = null;
responseText = clienttcptransport.read().payload();
logger.info("Raw message '" + responseText + "'");
Boolean success = null;
int count = 0;
for (String line : responseText.split("\n")) {
if (success == null) {
if ("+OK".equals(line)) {
success = true;
continue;
} else if (("-ERROR").equals(responseText)) {
success = false;
}
continue;
} else if ("END".equals(line)) {
break;
}
line = line.replaceFirst("^\t", "");
response.add(line);
}
if (success != null && success) {
return;
} else {
throw new RuntimeException("Error in " + message + ": " + response);
}
} catch (Exception e) {
throw getRuntimeException(e);
}
});
return response;
}
/**
* Helper method to send a request to the server using the ExecutorService. It submits the
* request as a Runnable task and waits for its completion. If the task is interrupted or
* encounters an execution exception, it throws a RuntimeException with the appropriate cause.
*
* @param request The Runnable task representing the request to be sent to the server.
*/
private void sendRequest(Runnable request) {
Future<?> future = executor.submit(request);
try {
future.get();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} catch (ExecutionException e) {
throw getRuntimeException(e);
}
}
/**
* Helper method to extract the cause of an exception and return it as a RuntimeException. If
* the cause is null, it returns the original exception as a RuntimeException. If the cause is
* already a RuntimeException, it returns it directly. Otherwise, it wraps the cause in a new
* RuntimeException and returns it.
*
* @param e The exception from which to extract the cause.
* @return A RuntimeException representing the cause of the original exception.
*/
private static RuntimeException getRuntimeException(Exception e) {
Throwable reason = e.getCause();
RuntimeException re;
if (reason == null) {
reason = e;
} else if (reason instanceof RuntimeException rte) {
re = rte;
}
re = new RuntimeException(reason);
return re;
}
/**
* Closes the socket connection to the server and shuts down the ExecutorService. It also closes
* the TcpTransport used for communication. If any IOException occurs during this process, it
* prints the exception to the console.
*/
public void closeSocket() {
try {
executor.shutdown();
clienttcptransport.close();
socket.close();
logger.info("Socket closed");
} catch (IOException j) {
logger.debug(j);
}
}
/**
* Method to write with the tcp transport to the server
*
* @param s - Message to be sent
* @throws IOException
*/
private void writeToTransport(String s) throws IOException {
int id = this.idGenerator.incrementAndGet();
this.clienttcptransport.write(new RawPacket(id, s));
}
public void ping() {
processCommand("PING");
}
}
@@ -0,0 +1,37 @@
package ch.unibas.dmi.dbis.cs108.casono.client.network;
/**
* The CoreClient class provides basic functionalities for communicating with the server, such as
* sending a ping command to check connectivity and logging in with a username. It uses the
* ClientService to send commands and receive responses from the server.
*/
public class CoreClient {
private final ClientService clientService;
/**
* Constructs a CoreClient with the given ClientService for communication.
*
* @param clientservice The ClientService instance used to send commands and receive responses
* from the server.
*/
public CoreClient(ClientService clientservice) {
this.clientService = clientservice;
}
/**
* Sends a "PING" command to the server to check connectivity. The server should respond with a
* "PONG" message if the connection is successful.
*/
public void ping() {
clientService.processCommand("PING");
}
/**
* Logs in to the server with the given username by sending a "LOGIN" command.
*
* @param user The username to log in with.
*/
public void login(String user) {
clientService.processCommand("LOGIN USERNAME=" + user);
}
}
@@ -0,0 +1,99 @@
package ch.unibas.dmi.dbis.cs108.casono.client.network;
import ch.unibas.dmi.dbis.cs108.casono.client.game.Card;
import ch.unibas.dmi.dbis.cs108.casono.client.game.GameState;
import ch.unibas.dmi.dbis.cs108.casono.client.game.Player;
import java.util.List;
/**
* The GameClient class is responsible for communicating with the server to retrieve the current
* game state. It sends a command to the server and parses the response into a structured GameState
* object.
*/
public class GameClient {
private final ClientService client;
/**
* Constructs a GameClient with the given ClientService for communication.
*
* @param client The ClientService instance used to send commands and receive responses from the
* server.
*/
public GameClient(ClientService client) {
this.client = client;
}
/**
* Retrieves the current game state from the server by sending a command and parsing the
* response.
*
* @return A GameState object representing the current state of the game.
*/
public GameState getGameState() {
List<String> response = client.processCommand("GET_GAME_STATE");
return parseGameState(response);
}
/**
* Parses the raw response from the server into a structured GameState object.
*
* @param input The raw response string from the server.
* @return A GameState object representing the current state of the game.
*/
private GameState parseGameState(List<String> input) {
GameState state = new GameState();
// String[] lines = input.split("\n");
Player currentPlayer = null;
Card currentCard = null;
for (String rawLine : input) {
String line = rawLine.trim();
if (line.startsWith("+OK") || line.equals("END")) {
continue;
}
if (line.startsWith("PHASE=")) {
state.phase = line.split("=")[1];
} else if (line.startsWith("POT=")) {
state.pot = Integer.parseInt(line.split("=")[1]);
} else if (line.startsWith("CURRENT_BET=")) {
state.currentBet = Integer.parseInt(line.split("=")[1]);
} else if (line.startsWith("DEALER=")) {
state.dealer = Integer.parseInt(line.split("=")[1]);
} else if (line.startsWith("ACTIVE_PLAYER=")) {
state.activePlayer = Integer.parseInt(line.split("=")[1]);
} else if (line.startsWith("PLAYER")) {
currentPlayer = new Player();
state.players.add(currentPlayer);
} else if (line.startsWith("NAME=") && currentPlayer != null) {
currentPlayer.name = line.split("=")[1];
} else if (line.startsWith("CHIPS=") && currentPlayer != null) {
currentPlayer.chips = Integer.parseInt(line.split("=")[1]);
} else if (line.startsWith("BET=") && currentPlayer != null) {
currentPlayer.bet = Integer.parseInt(line.split("=")[1]);
} else if (line.startsWith("STATE=") && currentPlayer != null) {
currentPlayer.state = line.split("=")[1];
} else if (line.startsWith("CARD")) {
currentCard = new Card();
if (currentPlayer != null) {
currentPlayer.cards.add(currentCard);
} else {
state.communityCards.add(currentCard);
}
} else if (line.startsWith("VALUE=") && currentCard != null) {
currentCard.value = line.split("=")[1];
} else if (line.startsWith("SUIT=") && currentCard != null) {
currentCard.suit = line.split("=")[1];
}
}
return state;
}
}
@@ -0,0 +1,72 @@
package ch.unibas.dmi.dbis.cs108.casono.client.network;
/**
* The LobbyClient class is responsible for communicating with the server to manage game lobbies. It
* provides methods to create a lobby, join a lobby, and fetch the current status of a lobby by
* sending appropriate commands to the server and processing the responses.
*/
public class LobbyClient {
private final ClientService client;
/**
* Constructs a LobbyClient with the given ClientService for communication.
*
* @param client The ClientService instance used to send commands and receive responses from the
* server.
*/
public LobbyClient(ClientService client) {
this.client = client;
}
public ClientService getClientService() {
return client;
}
/**
* Fetch the current status of the lobby with the given id from the server.
*
* @param lobbyId The id of the lobby to fetch the status for.
* @return A string representing the current status of the lobby, as returned by the server.
*/
public String fetchLobbyStatusString(int lobbyId) {
return client.processCommand("GET_LOBBY_STATUS ID=" + lobbyId).getFirst();
}
/**
* Request the server to create a new lobby and return the id of the newly created lobby.
*
* @return The id of the newly created lobby, as returned by the server.
*/
public int createLobby() {
String response = client.processCommand("CREATE_LOBBY").getFirst();
return Integer.parseInt(response);
}
/**
* Request the server to return the id of the lobby that the client is currently in.
*
* @return The id of the lobby that the client is currently in, as returned by the server.
*/
public int getLobbyId() {
String response = client.processCommand("GET_LOBBY_ID").getFirst();
return Integer.parseInt(response);
}
/**
* Request the server to join the lobby with the given id.
*
* @param lobbyId The id of the lobby to join.
*/
public void joinLobby(int lobbyId) {
client.processCommand("JOIN_LOBBY ID=" + lobbyId);
}
/**
* Logs in to the server with the given username by sending a "LOGIN" command.
*
* @param user The username to log in with.
*/
public void login(String user) {
client.processCommand("LOGIN USERNAME=" + user);
}
}
@@ -0,0 +1,112 @@
package ch.unibas.dmi.dbis.cs108.casono.client.ui.chatui;
import ch.unibas.dmi.dbis.cs108.casono.client.chat.ChatController;
import ch.unibas.dmi.dbis.cs108.casono.client.chat.ChatModel;
import ch.unibas.dmi.dbis.cs108.casono.client.chat.ChatType;
import java.io.IOException;
import java.net.URL;
import java.util.List;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.Node;
import javafx.scene.control.MenuButton;
import javafx.scene.control.MenuItem;
import javafx.scene.control.Tab;
import javafx.scene.control.TabPane;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
public class ChatBoxController {
private String username;
private ChatController chatController;
@FXML private VBox chatBox;
@FXML private TabPane chatTabPane;
@FXML private MenuButton addWhisperChatButton;
private FXMLLoader fxmlLoader;
@FXML private List<MenuItem> whisperUsers;
private String ressource = "/ui-structure/components/chatui/chattab.fxml";
public ChatBoxController(String username, ChatController chatController) {
this.username = username;
this.chatController = chatController;
}
/**
* Initializes the chat interface by creating the global chat model and adding the corresponding
* "GLOBAL" tab to the interface. It also registers the global chat in the {@link
* ChatController}'s model map.
*/
@FXML
public void initialize() {
ChatModel globalChatModel = new ChatModel(ChatType.GLOBAL, username, -1, null);
chatController
.getChatModelMap()
.put(new ChatController.ChatKey(ChatType.GLOBAL), globalChatModel);
addChatTab("GLOBAL", globalChatModel);
// TODO: Button to add new Whisper Chat
}
/**
* Adds a specific user to the list of available whisper targets. Creates a new menu item for
* the user and defines the action to open a private chat tab when selected.
*
* @param targetUserName The username of the person to be added to the whisper list.
*/
public void addWhisperUser(String targetUserName) {
MenuItem menuItem = new MenuItem(targetUserName);
whisperUsers.add(menuItem);
addWhisperChatButton.getItems().add(menuItem);
menuItem.setOnAction(event -> addWhisperChat(targetUserName));
}
/**
* Creates a new private (whisper) chat model for a specific target user, registers it within
* the chat system, and opens a new chat tab.
*
* @param target The username of the recipient for the private messages.
*/
public void addWhisperChat(String target) {
ChatModel chatModel = new ChatModel(ChatType.WHISPER, username, -1, target);
chatController
.getChatModelMap()
.put(new ChatController.ChatKey(ChatType.WHISPER, target), chatModel);
addChatTab(target, chatModel);
}
/**
* Dynamically loads a new chat tab from an FXML resource and attaches it to the TabPane. It
* initializes a {@link ChatViewController} for the tab and sets up a listener to display
* incoming messages in real-time.
*
* @param title The title to be displayed on the tab header.
* @param chatModel The {@link ChatModel} containing the data and logic for this specific chat.
* @throws RuntimeException If the FXML resource for the chat tab cannot be loaded.
*/
public void addChatTab(String title, ChatModel chatModel) {
URL resource = getClass().getResource(ressource);
FXMLLoader fxmlLoader = new FXMLLoader(resource);
try {
ChatViewController chatViewController =
new ChatViewController(this.chatController, chatModel, this.username);
fxmlLoader.setController(chatViewController);
Node load = fxmlLoader.load();
VBox.setVgrow(load, Priority.ALWAYS);
VBox vbox = new VBox();
VBox.setVgrow(vbox, Priority.ALWAYS);
vbox.getChildren().add(load);
chatModel.addListener((msg) -> chatViewController.showMessage(msg));
Tab newChat = new Tab(title, vbox);
this.chatTabPane.getTabs().add(newChat);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
@@ -1,90 +0,0 @@
package ch.unibas.dmi.dbis.cs108.casono.client.ui.chatui;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import javafx.fxml.FXML;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.TextField;
import javafx.scene.layout.VBox;
/**
* Controller-Klasse für das Chat-System innerhalb der Spieloberfläche.
*
* <p>Verwaltet das Anzeigen von Chatnachrichten, das Eingabefeld für eigene Nachrichten sowie den
* Senden-Button. Unterstützt drei Arten von Nachrichten: - Player-to-Player (Privat) - Lobby-Chat
* (Raum) - Globaler Chat (Serverweit)
*
* <p>Nachrichten werden in einem {@link VBox}-Container als {@link Label} angezeigt. Eigene
* Nachrichten werden über {@link #onSendToNetwork(String)} an das Netzwerkprotokoll weitergeleitet,
* während eingehende Nachrichten über {@link #receiveMessage(String, String)} verarbeitet und
* angezeigt werden.
*
* <p>Hinweis: Einige TODOs stehen in der zugehörigen FXML-Datei
*/
public class ChatController {
@FXML private VBox chatVBox;
@FXML private TextField inputField;
@FXML private Button sendButton;
@FXML private ScrollPane chatScrollPane;
private static final int CHAT_PADDING = 20;
/** Initialisiert den ChatController nach dem Laden der FXML. */
public void initialize() {
inputField.setOnAction(event -> sendMessage());
chatScrollPane.vvalueProperty().bind(chatVBox.heightProperty());
}
/** Standardkonstruktor. Wird von FXML verwendet. */
public ChatController() {
// default constructor for FXML
}
/**
* Diese Methode wird vom Senden-Button oder Enter ausgelöst. Sie gibt die eigene Nachricht an
* das Netzwerkprotokoll weiter.
*/
@FXML
private void sendMessage() {
String message = inputField.getText().trim();
if (!message.isEmpty()) {
inputField.clear();
// Hier wird die eigene Nachricht ans Netzwerkprotokoll übergeben
onSendToNetwork(message);
}
}
/**
* Diese Funktion muss vom Netzwerkprotokoll aufgerufen werden, wenn eine neue Nachricht von
* einem anderen Spieler kommt.
*
* @param player Name des Spielers
* @param message Nachricht des Spielers
*/
public void receiveMessage(String player, String message) {
String time = LocalTime.now().format(DateTimeFormatter.ofPattern("HH:mm"));
Label label = new Label("[" + time + "] " + player + ": " + message);
label.getStyleClass().add("info-text");
label.setWrapText(true); // Zeilenumbruch aktivieren
label.maxWidthProperty().bind(chatVBox.widthProperty().subtract(CHAT_PADDING));
chatVBox.getChildren().add(label);
}
/**
* Schnittstelle zum Netzwerkprotokoll. Diese Funktion wird automatisch aufgerufen, wenn der
* Benutzer eine eigene Nachricht sendet.
*
* @param message Nachricht, die der Benutzer abgeschickt hat
*/
public void onSendToNetwork(String message) {
// TODO: Netzwerkcode einfügen
receiveMessage("Du", message);
}
}
@@ -0,0 +1,102 @@
package ch.unibas.dmi.dbis.cs108.casono.client.ui.chatui;
import ch.unibas.dmi.dbis.cs108.casono.client.chat.ChatController;
import ch.unibas.dmi.dbis.cs108.casono.client.chat.ChatModel;
import ch.unibas.dmi.dbis.cs108.casono.client.chat.Message;
import java.net.URL;
import java.util.ResourceBundle;
import javafx.application.Platform;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.TextField;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
/** Responsible for the presentation of the ChatModel to the Client */
public class ChatViewController implements Initializable {
@FXML private Button sendButton;
@FXML private TextField inputField;
@FXML private VBox chatInterfaceVBox;
@FXML private HBox controlBar;
@FXML private ScrollPane scrollPane;
@FXML private VBox chat;
private final ChatModel chatModel;
private final String username;
private final ChatController controller;
private static final int CHAT_PADDING = 20;
public ChatViewController(ChatController chatController, ChatModel chatModel, String username) {
this.controller = chatController;
this.username = username;
this.chatModel = chatModel;
}
/**
* Initializes the controller after the FXML root element has been processed. Sets up event
* handlers for sending messages via the input field or button and ensures the ScrollPane
* automatically scrolls to the bottom when new messages are added.
*
* @param location The location used to resolve relative paths for the root object.
* @param resourceBundle The resources used to localize the root object.
*/
@Override
public void initialize(URL location, ResourceBundle resourceBundle) {
inputField.setOnAction(event -> sendMessage());
sendButton.setOnAction(event -> sendMessage());
scrollPane.vvalueProperty().bind(chat.heightProperty());
}
/**
* Retrieves the text from the input field, creates a new {@link Message} object using the
* current model state, and passes it to the {@link ChatController} for network transmission.
* The input field is cleared after sending.
*/
public void sendMessage() {
String message = inputField.getText().trim();
if (!message.isEmpty()) {
inputField.clear();
Message msg =
new Message(
chatModel.getChattype(),
chatModel.lobbyId,
username,
chatModel.getTarget(),
message);
controller.onSendToNetwork(msg);
}
}
/**
* Displays a message in the chat interface. This method creates a new Label for the message
* text and adds it to the message container. It uses {@link Platform#runLater(Runnable)} to
* ensure the UI update happens on the JavaFX Application Thread.
*
* @param msg The {@link Message} object containing the content and metadata to display.
*/
public void showMessage(Message msg) {
Platform.runLater(
() -> {
String msgText =
String.format(
"[%s] %s: %s", msg.timestamp, msg.sender, msg.getMessage());
Label label = new Label(msgText);
label.getStyleClass().add("info-text");
label.setWrapText(true);
label.maxWidthProperty().bind(chat.widthProperty().subtract(CHAT_PADDING));
chat.getChildren().add(label);
});
}
}
@@ -1,23 +1,26 @@
package ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui;
import ch.unibas.dmi.dbis.cs108.casono.client.network.ClientService;
import java.io.IOException;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.stage.Stage;
/**
* Main class for the Casono Game UI.
*
* <p>Starts the JavaFX application, loads the graphical user interface from the FXML file, and
* initializes the main stage for the game.
*
* <p>Tasks: - Loads the FXML interface "/ui-structure/Casinogameui.fxml". - Loads the application
* icon from "/images/logoinverted.png". - Starts the application in full-screen mode.
*/
public class CasinoGameUI extends Application {
/** default constructor */
// Static field for ClientService (workaround for JavaFX Application launch)
private static ClientService staticClientService;
public static void setClientService(ClientService clientService) {
staticClientService = clientService;
}
public static ClientService getClientService() {
return staticClientService;
}
/** Default no-arg constructor. */
public CasinoGameUI() {
// default no-arg constructor
}
@@ -46,7 +49,7 @@ public class CasinoGameUI extends Application {
}
/**
* Starting point of the application.
* Entry point of the application.
*
* @param args Command line arguments.
*/
@@ -30,6 +30,17 @@ public class Casinomainui extends Application {
* @throws IOException If loading the FXML fails.
*/
public void start(Stage stage) throws IOException {
// If the launcher passed an address argument (ip:port), expose it as
// system properties so controllers can read it without embedding defaults.
var raw = getParameters().getRaw();
if (raw != null && raw.size() > 0) {
String arg = raw.get(0);
String[] parts = arg.split(":", 2);
if (parts.length == 2) {
System.setProperty("casono.server.host", parts[0]);
System.setProperty("casono.server.port", parts[1]);
}
}
FXMLLoader fxmlLoader =
new FXMLLoader(getClass().getResource("/ui-structure/Casinomainui.fxml"));
Scene scene = new Scene(fxmlLoader.load(), SCENE_WIDTH, SCENE_HEIGHT);
@@ -1,9 +1,14 @@
package ch.unibas.dmi.dbis.cs108.casono.client.ui.lobbyui;
import ch.unibas.dmi.dbis.cs108.casono.client.network.ClientService;
import ch.unibas.dmi.dbis.cs108.casono.client.network.LobbyClient;
import javafx.application.Platform;
import javafx.fxml.FXML;
import javafx.scene.control.Alert;
import javafx.scene.control.Alert.AlertType;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.AnchorPane;
@@ -23,10 +28,13 @@ public class CasinomainuiController {
@FXML private Rectangle greenBox;
@FXML private Button exitbutton;
@FXML private VBox casinoTable;
@FXML private TextField usernameField;
@FXML private Button loginButton;
private LobbyButtonTranslationManager translationManager;
private LobbyButtonGridManager gridManager;
private int nextButtonId = 1;
private LobbyClient lobbyClient;
/** Default constructor for dependency injection by FXMLLoader. */
public CasinomainuiController() {
@@ -41,8 +49,63 @@ public class CasinomainuiController {
logoView.setImage(new Image(getClass().getResource("/images/logo.png").toExternalForm()));
translationManager = LobbyButtonTranslationManager.getInstance();
String host = System.getProperty("casono.server.host");
int port = Integer.parseInt(System.getProperty("casono.server.port"));
ClientService clientService;
try {
clientService = new ClientService(host, port);
} catch (RuntimeException e) {
LOGGER.warn(
"Could not connect to server {}:{} — starting in offline mode: {}",
host,
port,
e.getMessage());
clientService = new ClientService(true); // offline mode
}
gridManager =
new LobbyButtonGridManager(new javafx.scene.layout.GridPane(), translationManager);
new LobbyButtonGridManager(
new javafx.scene.layout.GridPane(), translationManager, clientService);
// LobbyClient will use the provided ClientService; in offline mode calls will
// fail with RuntimeException
lobbyClient = new LobbyClient(clientService);
casinoTable.getChildren().clear();
casinoTable.getChildren().add(gridManager.getGridPane());
gridManager.renderLobbyButtons();
}
/** Handles the login button action. Validates input and calls LobbyClient.login(). */
@FXML
public void handleLoginButton() {
String username = usernameField.getText();
if (username == null || username.isBlank()) {
showAlert("Please enter a username.");
return;
}
// Only allow alphanumeric, _ and -
if (!username.matches("[a-zA-Z0-9_-]+")) {
showAlert("Only letters, numbers, '_' and '-' are allowed!");
return;
}
if (lobbyClient.getClientService().isOffline()) {
showAlert("Offline mode: cannot send login to server.");
return;
}
try {
lobbyClient.login(username);
showAlert("Login sent: " + username);
} catch (RuntimeException e) {
LOGGER.error("Login failed: {}", e.getMessage());
showAlert("Login failed: " + e.getMessage());
}
}
/** Shows an alert dialog with the given message. */
private void showAlert(String message) {
Alert alert = new Alert(AlertType.INFORMATION);
alert.setTitle("Info");
alert.setHeaderText(null);
alert.setContentText(message);
alert.showAndWait();
casinoTable.getChildren().clear();
casinoTable.getChildren().add(gridManager.getGridPane());
gridManager.renderLobbyButtons();
@@ -62,13 +125,21 @@ public class CasinomainuiController {
return;
}
int buttonId = nextButtonId++;
int lobbyId = gridManager.createLobby();
try {
String username = usernameField != null ? usernameField.getText() : "<unknown>";
LOGGER.info("Creating lobby for user: {}", username);
// avoid attempting to create a lobby when offline
if (lobbyClient.getClientService().isOffline()) {
LOGGER.warn("Cannot create lobby while offline");
showAlert("Offline mode: cannot create lobby.");
return;
}
int lobbyId = gridManager.createLobby();
translationManager.addLobbyButton(buttonId, lobbyId);
LOGGER.info("ButtonID: {}, LobbyID: {}", buttonId, lobbyId);
gridManager.renderLobbyButtons();
} catch (Exception e) {
LOGGER.error("Error while adding lobby button: {}", e.getMessage());
LOGGER.error("Failed to create or add lobby: {}", e.getMessage());
}
}
}
@@ -1,10 +1,18 @@
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 ch.unibas.dmi.dbis.cs108.casono.client.network.ClientService;
import ch.unibas.dmi.dbis.cs108.casono.client.network.LobbyClient;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import javafx.scene.Node;
import javafx.scene.control.Button;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
@@ -12,133 +20,349 @@ import javafx.scene.layout.GridPane;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/**
* Manages the grid for lobby buttons and rendering. Uses LobbyButtonTranslationManager for mapping
* ButtonID to LobbyID.
*/
public class LobbyButtonGridManager {
private static final double BUTTON_WIDTH_MARGIN = 20.0;
private static final double BTN_WIDTH_MRG = 20.0;
private static final double BUTTON_MIN_SIZE = 10.0;
private static final Logger LOGGER = LogManager.getLogger(LobbyButtonGridManager.class);
/** 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 static final int ROWS = 2;
/** Number of columns in the grid. */
private static final int REFRESH_INTERVAL_SECONDS = 5;
private static final int INITIAL_DELAY_SECONDS = 5;
private static final int COLS = 4;
/** Path to the button image. */
private static final String BUTTON_IMAGE_PATH = "/images/logo.png";
private static final Logger LOGGER = LogManager.getLogger(LobbyButtonGridManager.class);
/** Max random lobby id. */
private static final int MAX_RANDOM_LOBBY_ID = 10000;
private static final String BUTTON_FALLBACK_IMAGE = "/images/lobbypictures/error.png";
private static final String BUTTON_IMAGE_TEMPLATE = "/images/lobbypictures/lobby_%d_%s.png";
private final GridPane gridPane;
private final LobbyButtonTranslationManager translationManager;
private final LobbyClient lobbyClient;
private final ConcurrentHashMap<String, Image> imageCache = new ConcurrentHashMap<>();
private final ExecutorService executor = Executors.newCachedThreadPool();
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
/**
* 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) {
GridPane gridPane,
LobbyButtonTranslationManager translationManager,
LobbyClient lobbyClient) {
this.gridPane = gridPane;
// Singleton immer verwenden
this.translationManager = LobbyButtonTranslationManager.getInstance();
this.lobbyClient = lobbyClient;
startPeriodicRefresh(REFRESH_INTERVAL_SECONDS, INITIAL_DELAY_SECONDS);
}
/**
* 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;
public LobbyButtonGridManager(
GridPane gridPane,
LobbyButtonTranslationManager translationManager,
ClientService clientService) {
this(gridPane, translationManager, new LobbyClient(clientService));
}
private void startPeriodicRefresh(long initialDelay, long period) {
scheduler.scheduleAtFixedRate(
this::refreshMappings, initialDelay, period, TimeUnit.SECONDS);
}
private void refreshMappings() {
Map<Integer, Integer> mapping = translationManager.getButtonIdToLobbyId();
if (mapping.isEmpty()) {
// No buttons to render
return;
}
for (Map.Entry<Integer, Integer> entry : mapping.entrySet()) {
int buttonId = entry.getKey();
Button btn = new Button();
btn.setId("lobbyBtn-" + buttonId);
ImageView imageView =
new ImageView(new Image(getClass().getResourceAsStream(BUTTON_IMAGE_PATH)));
imageView.setPreserveRatio(true);
// Dynamische Breite: Bindung an die Zellengröße
imageView
.fitWidthProperty()
.bind(gridPane.widthProperty().divide(COLS).subtract(BUTTON_WIDTH_MARGIN));
imageView.setSmooth(true);
btn.setGraphic(imageView);
btn.setMaxWidth(Double.MAX_VALUE);
btn.setMaxHeight(Double.MAX_VALUE);
btn.setMinWidth(BUTTON_MIN_SIZE);
btn.setMinHeight(BUTTON_MIN_SIZE);
GridPane.setHgrow(btn, javafx.scene.layout.Priority.ALWAYS);
GridPane.setVgrow(btn, javafx.scene.layout.Priority.ALWAYS);
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++;
List<Map.Entry<Integer, Integer>> entries = new ArrayList<>(mapping.entrySet());
for (Map.Entry<Integer, Integer> e : entries) {
int buttonId = e.getKey();
int lobbyId = e.getValue();
CompletableFuture.supplyAsync(
() -> {
try {
return lobbyClient.fetchLobbyStatusString(lobbyId);
} catch (Exception ex) {
LOGGER.info("Lobby {} missing: {}", lobbyId, ex.getMessage());
return null;
}
},
executor)
.thenAccept(
status -> {
if (status == null) {
translationManager.removeLobbyButton(buttonId);
javafx.application.Platform.runLater(
this::updateLobbyButtonImages);
}
});
}
}
/**
* 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() * MAX_RANDOM_LOBBY_ID + 1);
LOGGER.info("Lobby created: {}", lobbyId);
return lobbyId;
public void renderLobbyButtons() {
gridPane.getChildren().clear();
Map<Integer, Integer> mapping = translationManager.getButtonIdToLobbyId();
if (mapping.isEmpty()) {
return;
}
List<Integer> buttonIds = new ArrayList<>(mapping.keySet());
Collections.sort(buttonIds);
for (int index = 0; index < buttonIds.size(); index++) {
Integer buttonId = buttonIds.get(index);
int lobbyId = mapping.get(buttonId);
Button btn = createLobbyButton(buttonId, lobbyId);
int row = index / COLS;
int col = index % COLS;
gridPane.add(btn, col, row);
}
}
private Button createLobbyButton(int buttonId, int lobbyId) {
Button btn = new Button();
btn.setId("lobbyBtn-" + buttonId);
ImageView imageView = new ImageView(safeLoadImage(BUTTON_FALLBACK_IMAGE));
imageView.setPreserveRatio(true);
imageView
.fitWidthProperty()
.bind(gridPane.widthProperty().divide(COLS).subtract(BTN_WIDTH_MRG));
btn.setGraphic(imageView);
btn.setMaxWidth(Double.MAX_VALUE);
btn.setMaxHeight(Double.MAX_VALUE);
btn.setMinWidth(BUTTON_MIN_SIZE);
btn.setMinHeight(BUTTON_MIN_SIZE);
GridPane.setHgrow(btn, javafx.scene.layout.Priority.ALWAYS);
GridPane.setVgrow(btn, javafx.scene.layout.Priority.ALWAYS);
btn.setOnAction(
e -> {
Integer targetLobbyId = translationManager.getLobbyIdForButton(buttonId);
if (targetLobbyId != null) {
joinLobby(targetLobbyId);
}
});
loadLobbyImageAsync(btn, buttonId, lobbyId);
return btn;
}
private void loadLobbyImageAsync(Button btn, int buttonId, int lobbyId) {
CompletableFuture.supplyAsync(() -> lobbyClient.fetchLobbyStatusString(lobbyId), executor)
.thenAccept(
statusStr -> {
LobbyStatus status = parseLobbyStatus(statusStr);
String path =
getImagePathForButton(
buttonId,
status == null ? LobbyStatus.CREATED : status);
Image img = safeLoadImage(path);
javafx.application.Platform.runLater(
() -> {
ImageView iv = new ImageView(img);
iv.setPreserveRatio(true);
iv.fitWidthProperty()
.bind(
gridPane.widthProperty()
.divide(COLS)
.subtract(BTN_WIDTH_MRG));
btn.setGraphic(iv);
});
});
}
private enum LobbyStatus {
CREATED,
RUNNING
}
private LobbyStatus parseLobbyStatus(String statusStr) {
if (statusStr == null) {
return null;
}
try {
return LobbyStatus.valueOf(statusStr.trim().toUpperCase());
} catch (IllegalArgumentException e) {
return null;
}
}
private String getImagePathForButton(int buttonId, LobbyStatus status) {
String statusStr = status == LobbyStatus.CREATED ? "created" : "running";
return String.format(BUTTON_IMAGE_TEMPLATE, buttonId, statusStr);
}
private Image safeLoadImage(String path) {
Image cached = imageCache.get(path);
if (cached != null) {
return cached;
}
java.io.InputStream is = getClass().getResourceAsStream(path);
if (is == null) {
is = getClass().getResourceAsStream(BUTTON_FALLBACK_IMAGE);
}
Image loaded = null;
try {
if (is != null) {
loaded = new Image(is);
}
} catch (Exception e) {
LOGGER.error("Image load failed: {}", path, e);
}
if (loaded != null) {
imageCache.put(path, loaded);
}
return loaded;
}
public void updateLobbyButtonImages() {
Map<Integer, Integer> mapping = translationManager.getButtonIdToLobbyId();
if (mapping.isEmpty()) {
return;
}
for (Integer buttonId : mapping.keySet()) {
int lobbyId = mapping.get(buttonId);
CompletableFuture.supplyAsync(
() -> {
String statusStr = lobbyClient.fetchLobbyStatusString(lobbyId);
LobbyStatus status = parseLobbyStatus(statusStr);
return status == null ? LobbyStatus.CREATED : status;
},
executor)
.thenAccept(
status -> {
String path = getImagePathForButton(buttonId, status);
javafx.application.Platform.runLater(
() -> {
for (Node node : gridPane.getChildren()) {
boolean isButton = node instanceof Button;
boolean idMatches =
("lobbyBtn-" + buttonId)
.equals(node.getId());
if (isButton && idMatches) {
Button btn = (Button) node;
ImageView iv =
new ImageView(safeLoadImage(path));
iv.setPreserveRatio(true);
iv.fitWidthProperty()
.bind(
gridPane.widthProperty()
.divide(COLS)
.subtract(
BTN_WIDTH_MRG));
btn.setGraphic(iv);
break;
}
}
});
});
}
}
public int createLobby() {
try {
int lobbyId = lobbyClient.createLobby();
if (lobbyId <= 0) {
throw new RuntimeException("Invalid lobby id: " + lobbyId);
}
return lobbyId;
} catch (Exception e) {
LOGGER.error("Create lobby failed: {}", e.getMessage());
throw new RuntimeException(e);
}
}
/**
* Placeholder for joining a lobby.
*
* @param lobbyId The lobbyId to join
*/
public void joinLobby(int lobbyId) {
// Game-UI starten und Lobby-UI schließen
LOGGER.info("Joining lobby: {}", lobbyId);
try {
lobbyClient.joinLobby(lobbyId);
} catch (Exception e) {
LOGGER.error("Join failed: {}", e.getMessage());
return;
}
javafx.application.Platform.runLater(
() -> {
// Lobby-Stage schließen
javafx.stage.Stage currentStage =
(javafx.stage.Stage) gridPane.getScene().getWindow();
currentStage.close();
// Game-UI starten
currentStage.hide();
javafx.stage.Stage gameStage = new javafx.stage.Stage();
gameStage.setOnHidden(
ev -> {
currentStage.show();
refreshMappings();
updateLobbyButtonImages();
});
try {
ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.CasinoGameUI
.setClientService(lobbyClient.getClientService());
new ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.CasinoGameUI()
.start(new javafx.stage.Stage());
.start(gameStage);
} catch (Exception e) {
LOGGER.error("Fehler beim Starten der Game-UI: {}", e.getMessage());
LOGGER.error("Game UI failed: {}", e.getMessage());
currentStage.show();
}
});
}
/**
* Getter for the GridPane.
*
* @return The GridPane for the button grid
*/
public javafx.scene.layout.GridPane getGridPane() {
public GridPane getGridPane() {
return gridPane;
}
public LobbyClient getLobbyClient() {
return lobbyClient;
}
public void refreshNow() {
refreshMappings();
}
}
@@ -1,17 +1,14 @@
package ch.unibas.dmi.dbis.cs108.casono.server;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.add_player.AddPlayerHandler;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.add_player.AddPlayerParser;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.add_player.AddPlayerRequest;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.bet.PlayerBetHandler;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.bet.PlayerBetParser;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.bet.PlayerBetRequest;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.call.PlayerCallHandler;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.call.PlayerCallParser;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.call.PlayerCallRequest;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.check_nick.CheckUsernameHandler;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.check_nick.CheckUsernameParser;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.check_nick.CheckUsernameRequest;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.get_message_count.GetMessageCountHandler;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.get_message_count.GetMessageCountParser;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.get_message_count.GetMessageCountRequest;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.get_next_message.GetNextMessageHandler;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.get_next_message.GetNextMessageParser;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.get_next_message.GetNextMessageRequest;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.list_users.ListUsersHandler;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.list_users.ListUsersParser;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.list_users.ListUsersRequest;
@@ -24,9 +21,9 @@ import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.logout.LogoutRequest;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.ping.PingHandler;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.ping.PingParser;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.ping.PingRequest;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.raise.PlayerRaiseHandler;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.raise.PlayerRaiseParser;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.raise.PlayerRaiseRequest;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.send_message.SendMessageHandler;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.send_message.SendMessageParser;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.send_message.SendMessageRequest;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.UserCleanupJob;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.UserRegistry;
import ch.unibas.dmi.dbis.cs108.casono.server.network.NetworkManager;
@@ -47,106 +44,97 @@ import org.apache.logging.log4j.Logger;
/** Application class for starting the server. */
public class ServerApp {
private static final int USER_CLEANUP_JOB_DELAY = 0;
private static final int USER_CLEANUP_JOB_PERIOD = 10;
private static final int USER_CLEANUP_JOB_RECONNECT_THRESHOLD = 10;
private static final int SESSION_DISCONNECT_JOB_DELAY = 0;
private static final int SESSION_DISCONNECT_JOB_PERIOD = 2;
private static final int SESSION_DISCONNECT_JOB_TIMEOUT = 5;
private static final int USER_CLEANUP_JOB_DELAY = 0;
private static final int USER_CLEANUP_JOB_PERIOD = 10;
private static final int USER_CLEANUP_JOB_RECONNECT_THRESHOLD = 10;
private static final int SESSION_DISCONNECT_JOB_DELAY = 0;
private static final int SESSION_DISCONNECT_JOB_PERIOD = 2;
private static final int SESSION_DISCONNECT_JOB_TIMEOUT = 5;
public static void start(String arg) {
int port = Integer.parseInt(arg);
public static void start(String arg) {
int port = Integer.parseInt(arg);
Logger logger = LogManager.getLogger(ServerApp.class);
logger.info("Starting server at port {}", port);
Logger logger = LogManager.getLogger(ServerApp.class);
logger.info("Starting server at port {}", port);
EventBus eventBus = new EventBus();
CommandParserDispatcher dispatcher = new CommandParserDispatcher();
SessionManager sessionManager = new SessionManager(eventBus, dispatcher);
ResponseDispatcher responseDispatcher = new ResponseDispatcher(sessionManager);
CommandHandlerExecutor handlerExecutor = new CommandHandlerExecutor(responseDispatcher);
CommandRouter router = new CommandRouter(handlerExecutor);
EventBus eventBus = new EventBus();
CommandParserDispatcher dispatcher = new CommandParserDispatcher();
SessionManager sessionManager = new SessionManager(eventBus, dispatcher);
ResponseDispatcher responseDispatcher = new ResponseDispatcher(sessionManager);
CommandHandlerExecutor handlerExecutor = new CommandHandlerExecutor(responseDispatcher);
CommandRouter router = new CommandRouter(handlerExecutor);
eventBus.subscribe(DisconnectEvent.class, event -> sessionManager.onDisconnect(event));
eventBus.subscribe(DisconnectEvent.class, event -> sessionManager.onDisconnect(event));
UserRegistry userRegistry = new UserRegistry();
eventBus.subscribe(
DisconnectEvent.class, event -> userRegistry.onDisconnect(event.sessionId()));
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
scheduler.scheduleAtFixedRate(
new UserCleanupJob(
userRegistry, Duration.ofSeconds(USER_CLEANUP_JOB_RECONNECT_THRESHOLD)),
USER_CLEANUP_JOB_DELAY,
USER_CLEANUP_JOB_PERIOD,
TimeUnit.SECONDS);
scheduler.scheduleAtFixedRate(
new SessionDisconnectJob(
sessionManager,
eventBus,
Duration.ofSeconds(SESSION_DISCONNECT_JOB_TIMEOUT)),
SESSION_DISCONNECT_JOB_DELAY,
SESSION_DISCONNECT_JOB_PERIOD,
TimeUnit.SECONDS);
UserRegistry userRegistry = new UserRegistry();
eventBus.subscribe(
DisconnectEvent.class, event -> userRegistry.onDisconnect(event.sessionId()));
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
scheduler.scheduleAtFixedRate(
new UserCleanupJob(
userRegistry, Duration.ofSeconds(USER_CLEANUP_JOB_RECONNECT_THRESHOLD)),
USER_CLEANUP_JOB_DELAY,
USER_CLEANUP_JOB_PERIOD,
TimeUnit.SECONDS);
scheduler.scheduleAtFixedRate(
new SessionDisconnectJob(
sessionManager,
eventBus,
Duration.ofSeconds(SESSION_DISCONNECT_JOB_TIMEOUT)),
SESSION_DISCONNECT_JOB_DELAY,
SESSION_DISCONNECT_JOB_PERIOD,
TimeUnit.SECONDS);
registerCommands(dispatcher, router, responseDispatcher, userRegistry);
registerCommands(dispatcher, router, responseDispatcher, userRegistry);
NetworkManager networkManager = new NetworkManager(port, sessionManager, router);
networkManager.start();
}
NetworkManager networkManager = new NetworkManager(port, sessionManager, router);
networkManager.start();
}
/**
* Registers command parsers and handlers.
*
* @param parserDispatcher the dispatcher responsible for parsing incoming
* commands
* @param commandRouter the router that dispatches parsed commands to
* appropriate handlers
* @param responseDispatcher the dispatcher responsible for sending responses
* back to clients
*/
private static void registerCommands(
CommandParserDispatcher parserDispatcher,
CommandRouter commandRouter,
ResponseDispatcher responseDispatcher,
UserRegistry userRegistry) {
parserDispatcher.register("PING", new PingParser());
commandRouter.register(PingRequest.class, new PingHandler(responseDispatcher));
/**
* Registers command parsers and handlers.
*
* @param parserDispatcher the dispatcher responsible for parsing incoming commands
* @param commandRouter the router that dispatches parsed commands to appropriate handlers
* @param responseDispatcher the dispatcher responsible for sending responses back to clients
*/
private static void registerCommands(
CommandParserDispatcher parserDispatcher,
CommandRouter commandRouter,
ResponseDispatcher responseDispatcher,
UserRegistry userRegistry) {
parserDispatcher.register("PING", new PingParser());
commandRouter.register(PingRequest.class, new PingHandler(responseDispatcher));
parserDispatcher.register("CHECK_USERNAME", new CheckUsernameParser());
commandRouter.register(
CheckUsernameRequest.class,
new CheckUsernameHandler(responseDispatcher, userRegistry));
parserDispatcher.register("CHECK_USERNAME", new CheckUsernameParser());
commandRouter.register(
CheckUsernameRequest.class,
new CheckUsernameHandler(responseDispatcher, userRegistry));
parserDispatcher.register("LOGIN", new LoginParser());
commandRouter.register(
LoginRequest.class, new LoginHandler(responseDispatcher, userRegistry));
parserDispatcher.register("LOGIN", new LoginParser());
commandRouter.register(
LoginRequest.class, new LoginHandler(responseDispatcher, userRegistry));
parserDispatcher.register("LOGOUT", new LogoutParser());
commandRouter.register(
LogoutRequest.class, new LogoutHandler(responseDispatcher, userRegistry));
parserDispatcher.register("LOGOUT", new LogoutParser());
commandRouter.register(
LogoutRequest.class, new LogoutHandler(responseDispatcher, userRegistry));
parserDispatcher.register("LIST_USERS", new ListUsersParser());
commandRouter.register(
ListUsersRequest.class, new ListUsersHandler(responseDispatcher, userRegistry));
parserDispatcher.register("SEND_MESSAGE", new SendMessageParser());
commandRouter.register(
SendMessageRequest.class, new SendMessageHandler(responseDispatcher, userRegistry));
// ADD_PLAYER
parserDispatcher.register("ADD_PLAYER", new AddPlayerParser());
commandRouter.register(AddPlayerRequest.class,
new AddPlayerHandler(userRegistry, responseDispatcher));
parserDispatcher.register("GET_MESSAGE_COUNT", new GetMessageCountParser());
commandRouter.register(
GetMessageCountRequest.class,
new GetMessageCountHandler(responseDispatcher, userRegistry));
// BET
parserDispatcher.register("BET", new PlayerBetParser());
commandRouter.register(PlayerBetRequest.class,
new PlayerBetHandler(responseDispatcher));
parserDispatcher.register("GET_NEXT_MESSAGE", new GetNextMessageParser());
commandRouter.register(
GetNextMessageRequest.class,
new GetNextMessageHandler(responseDispatcher, userRegistry));
// CALL
parserDispatcher.register("CALL", new PlayerCallParser());
commandRouter.register(PlayerCallRequest.class,
new PlayerCallHandler(responseDispatcher));
// RAISE
parserDispatcher.register("RAISE", new PlayerRaiseParser());
commandRouter.register(PlayerRaiseRequest.class,
new PlayerRaiseHandler(responseDispatcher));
}
parserDispatcher.register("LIST_USERS", new ListUsersParser());
commandRouter.register(
ListUsersRequest.class, new ListUsersHandler(responseDispatcher, userRegistry));
}
}
@@ -1,33 +0,0 @@
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.add_player;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.UserRegistry;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.User;
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.OkResponse;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.ErrorResponse;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.dispatcher.ResponseDispatcher;
import java.util.Optional;
/**
* Handler for the ADD_PLAYER command.
*/
public class AddPlayerHandler extends CommandHandler<AddPlayerRequest> {
private final UserRegistry userRegistry;
public AddPlayerHandler(UserRegistry userRegistry, ResponseDispatcher responseDispatcher) {
super(responseDispatcher);
this.userRegistry = userRegistry;
}
@Override
public void execute(AddPlayerRequest request) {
Optional<User> created = userRegistry.registerIfAvailable(request.getName(), request.getContext().sessionId());
if (created.isEmpty()) {
responseDispatcher.dispatch(new ErrorResponse(request.getContext(), "NAME_TAKEN", "Name already taken"));
return;
}
responseDispatcher.dispatch(new OkResponse(request.getContext()));
}
}
@@ -1,20 +0,0 @@
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.add_player;
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.CommandParser;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.PrimitiveRequest;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.accessor.RequestParameterAccessor;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.accessor.ParameterParseException;
/** Parser for the ADD_PLAYER command. */
public class AddPlayerParser implements CommandParser<AddPlayerRequest> {
@Override
public AddPlayerRequest parse(PrimitiveRequest primitiveRequest) {
RequestParameterAccessor accessor = new RequestParameterAccessor(primitiveRequest.parameters());
String name = accessor.require("NAME");
int chips = accessor.require("CHIPS", Integer::parseInt);
return new AddPlayerRequest(primitiveRequest.context(), name, chips);
}
}
@@ -1,24 +0,0 @@
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.add_player;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.Request;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext;
/** Request data for the ADD_PLAYER command. */
public class AddPlayerRequest extends Request {
private final String name;
private final int chips;
public AddPlayerRequest(RequestContext context, String name, int chips) {
super(context);
this.name = name;
this.chips = chips;
}
public String getName() {
return name;
}
public int getChips() {
return chips;
}
}
@@ -1,27 +0,0 @@
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.bet;
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.OkResponse;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.ErrorResponse;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.dispatcher.ResponseDispatcher;
/**
* Handler for the BET command.
*/
public class PlayerBetHandler extends CommandHandler<PlayerBetRequest> {
public PlayerBetHandler(ResponseDispatcher responseDispatcher) {
super(responseDispatcher);
}
@Override
public void execute(PlayerBetRequest request) {
if (request.getAmount() < 0) {
responseDispatcher
.dispatch(new ErrorResponse(request.getContext(), "INVALID_AMOUNT", "Amount must be non-negative"));
return;
}
responseDispatcher.dispatch(new OkResponse(request.getContext()));
}
}
@@ -1,17 +0,0 @@
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.bet;
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.CommandParser;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.PrimitiveRequest;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.accessor.RequestParameterAccessor;
/** Parser for the BET command. */
public class PlayerBetParser implements CommandParser<PlayerBetRequest> {
@Override
public PlayerBetRequest parse(PrimitiveRequest primitiveRequest) {
RequestParameterAccessor accessor = new RequestParameterAccessor(primitiveRequest.parameters());
int amount = accessor.require("AMOUNT", Integer::parseInt);
return new PlayerBetRequest(primitiveRequest.context(), amount);
}
}
@@ -1,18 +0,0 @@
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.bet;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.Request;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext;
/** Request for BET command. */
public class PlayerBetRequest extends Request {
private final int amount;
public PlayerBetRequest(RequestContext context, int amount) {
super(context);
this.amount = amount;
}
public int getAmount() {
return amount;
}
}
@@ -1,21 +0,0 @@
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.call;
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.OkResponse;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.dispatcher.ResponseDispatcher;
/**
* Minimal handler for CALL: acknowledges the command.
*/
public class PlayerCallHandler extends CommandHandler<PlayerCallRequest> {
public PlayerCallHandler(ResponseDispatcher responseDispatcher) {
super(responseDispatcher);
}
@Override
public void execute(PlayerCallRequest request) {
// TODO: integrate with game engine to process call.
responseDispatcher.dispatch(new OkResponse(request.getContext()));
}
}
@@ -1,12 +0,0 @@
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.call;
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.CommandParser;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.PrimitiveRequest;
/** Parser for the CALL command (no parameters). */
public class PlayerCallParser implements CommandParser<PlayerCallRequest> {
@Override
public PlayerCallRequest parse(PrimitiveRequest primitiveRequest) {
return new PlayerCallRequest(primitiveRequest.context());
}
}
@@ -1,11 +0,0 @@
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.call;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.Request;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext;
/** Request for CALL command. */
public class PlayerCallRequest extends Request {
public PlayerCallRequest(RequestContext context) {
super(context);
}
}
@@ -1,19 +0,0 @@
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.fold;
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.OkResponse;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.dispatcher.ResponseDispatcher;
/** Minimal handler for FOLD: acknowledges the command. */
public class PlayerFoldHandler extends CommandHandler<PlayerFoldRequest> {
public PlayerFoldHandler(ResponseDispatcher responseDispatcher) {
super(responseDispatcher);
}
@Override
public void execute(PlayerFoldRequest request) {
// TODO: integrate with game engine to process call.
responseDispatcher.dispatch(new OkResponse(request.getContext()));
}
}
@@ -1,12 +0,0 @@
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.fold;
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.CommandParser;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.PrimitiveRequest;
/** Parser for the FOLD command (no parameters). */
public class PlayerFoldParser implements CommandParser<PlayerFoldRequest> {
@Override
public PlayerFoldRequest parse(PrimitiveRequest primitiveRequest) {
return new PlayerFoldRequest(primitiveRequest.context());
}
}
@@ -1,11 +0,0 @@
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.fold;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.Request;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext;
/** Request for FOLD command. */
public class PlayerFoldRequest extends Request {
public PlayerFoldRequest(RequestContext context) {
super(context);
}
}
@@ -1,29 +0,0 @@
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.raise;
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.OkResponse;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.ErrorResponse;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.dispatcher.ResponseDispatcher;
/**
* Minimal handler for RAISE: validates and acknowledges the command.
*/
public class PlayerRaiseHandler extends CommandHandler<PlayerRaiseRequest> {
public PlayerRaiseHandler(ResponseDispatcher responseDispatcher) {
super(responseDispatcher);
}
@Override
public void execute(PlayerRaiseRequest request) {
int amount = request.getAmount();
if (amount < 0) {
responseDispatcher
.dispatch(new ErrorResponse(request.getContext(), "INVALID_AMOUNT", "Amount must be non-negative"));
return;
}
responseDispatcher.dispatch(new OkResponse(request.getContext()));
}
}
@@ -1,17 +0,0 @@
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.raise;
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.CommandParser;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.PrimitiveRequest;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.accessor.RequestParameterAccessor;
/** Parser for the RAISE command. */
public class PlayerRaiseParser implements CommandParser<PlayerRaiseRequest> {
@Override
public PlayerRaiseRequest parse(PrimitiveRequest primitiveRequest) {
RequestParameterAccessor accessor = new RequestParameterAccessor(primitiveRequest.parameters());
int amount = accessor.require("AMOUNT", Integer::parseInt);
return new PlayerRaiseRequest(primitiveRequest.context(), amount);
}
}
@@ -1,18 +0,0 @@
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.raise;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.Request;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext;
/** Request for RAISE command. */
public class PlayerRaiseRequest extends Request {
private final int amount;
public PlayerRaiseRequest(RequestContext context, int amount) {
super(context);
this.amount = amount;
}
public int getAmount() {
return amount;
}
}
@@ -0,0 +1,49 @@
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.get_message_count;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.User;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.UserRegistry;
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.ErrorResponse;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.dispatcher.ResponseDispatcher;
import java.util.Optional;
public class GetMessageCountHandler extends CommandHandler<GetMessageCountRequest> {
private final UserRegistry userRegistry;
/**
* Constructs a new GetMessageCountHandler with the necessary response dispatcher and user
* registry.
*
* @param responseDispatcher The dispatcher used to send the count or error back to the client.
* @param userRegistry The registry used to identify the user and access their message queue.
*/
public GetMessageCountHandler(
ResponseDispatcher responseDispatcher, UserRegistry userRegistry) {
super(responseDispatcher);
this.userRegistry = userRegistry;
}
/**
* Processes a request to retrieve the number of pending messages for a user. It looks up the
* user by their session ID; if found, it dispatches a {@link GetMessageCountResponse}
* containing the current count. Otherwise, it dispatches an {@link ErrorResponse}.
*
* @param request The {@link GetMessageCountRequest} containing the session details.
*/
@Override
public void execute(GetMessageCountRequest request) {
Optional<User> user = userRegistry.getBySessionId(request.getSessionId());
if (user.isPresent()) {
int count = user.get().getMessageCount();
GetMessageCountResponse response =
new GetMessageCountResponse(request.getContext(), count);
responseDispatcher.dispatch(response);
} else {
ErrorResponse response =
new ErrorResponse(
request.getContext(), "", "user could not be identified by SessionId");
responseDispatcher.dispatch(response);
}
}
}
@@ -0,0 +1,22 @@
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.get_message_count;
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.CommandParser;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.PrimitiveRequest;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.accessor.RequestParameterAccessor;
public class GetMessageCountParser implements CommandParser<GetMessageCountRequest> {
/**
* Parses a raw {@link PrimitiveRequest} into a {@link GetMessageCountRequest}. This method
* wraps the request context from the network layer into a structured message count request
* object.
*
* @param primitiveRequest The raw request containing parameters and context from the network.
* @return A new {@link GetMessageCountRequest} instance.
*/
@Override
public GetMessageCountRequest parse(PrimitiveRequest primitiveRequest) {
RequestParameterAccessor accessor =
new RequestParameterAccessor(primitiveRequest.parameters());
return new GetMessageCountRequest(primitiveRequest.context());
}
}
@@ -0,0 +1,17 @@
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.get_message_count;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.Request;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext;
public class GetMessageCountRequest extends Request {
/**
* Constructs a new GetMessageCountRequest with the specified request context. This request is
* used by a client to query the number of pending messages currently waiting in their
* server-side queue.
*
* @param context The {@link RequestContext} associated with this request.
*/
public GetMessageCountRequest(RequestContext context) {
super(context);
}
}
@@ -0,0 +1,18 @@
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.get_message_count;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.SuccessResponse;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.builder.ResponseBodyBuilder;
public class GetMessageCountResponse extends SuccessResponse {
/**
* Constructs a new GetMessageCountResponse. It creates a response body containing the "COUNT"
* parameter and associates it with the original request context.
*
* @param context The {@link RequestContext} of the original request.
* @param count The number of pending messages to be returned to the client.
*/
public GetMessageCountResponse(RequestContext context, int count) {
super(context, new ResponseBodyBuilder().param("COUNT", count).build());
}
}
@@ -0,0 +1,49 @@
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.get_next_message;
import ch.unibas.dmi.dbis.cs108.casono.client.chat.Message;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.User;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.UserRegistry;
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.ErrorResponse;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.dispatcher.ResponseDispatcher;
import java.util.Optional;
public class GetNextMessageHandler extends CommandHandler<GetNextMessageRequest> {
private final UserRegistry userRegistry;
/**
* Constructs a new GetNextMessageHandler with the required dispatcher and user registry.
*
* @param responseDispatcher The dispatcher used to send responses back to clients.
* @param userRegistry The registry used to look up users by their session information.
*/
public GetNextMessageHandler(ResponseDispatcher responseDispatcher, UserRegistry userRegistry) {
super(responseDispatcher);
this.userRegistry = userRegistry;
}
/**
* Executes the request to retrieve the next message for a specific user. It identifies the user
* via their session ID, dequeues the next available message, and dispatches a {@link
* GetNextMessageResponse}. If the user cannot be identified, an {@link ErrorResponse} is sent
* instead.
*
* @param request The {@link GetNextMessageRequest} containing the session and context.
*/
@Override
public void execute(GetNextMessageRequest request) {
Optional<User> user = userRegistry.getBySessionId(request.getSessionId());
if (user.isPresent()) {
Message msg = user.get().dequeMessage();
GetNextMessageResponse response = new GetNextMessageResponse(request.getContext(), msg);
responseDispatcher.dispatch(response);
} else {
ErrorResponse response =
new ErrorResponse(
request.getContext(),
"NO_USER_ASSOCIATED",
"user could not be identified by SessionId");
responseDispatcher.dispatch(response);
}
}
}
@@ -0,0 +1,22 @@
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.get_next_message;
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.CommandParser;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.PrimitiveRequest;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.accessor.RequestParameterAccessor;
public class GetNextMessageParser implements CommandParser<GetNextMessageRequest> {
/**
* Parses a raw {@link PrimitiveRequest} into a {@link GetNextMessageRequest}. This method
* initializes a parameter accessor (though not currently used for extraction) and returns a
* structured request object containing the original request context.
*
* @param primitiveRequest The raw request containing parameters and context from the network.
* @return A new {@link GetNextMessageRequest} instance.
*/
@Override
public GetNextMessageRequest parse(PrimitiveRequest primitiveRequest) {
RequestParameterAccessor accessor =
new RequestParameterAccessor(primitiveRequest.parameters());
return new GetNextMessageRequest(primitiveRequest.context());
}
}
@@ -0,0 +1,17 @@
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.get_next_message;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.Request;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext;
public class GetNextMessageRequest extends Request {
/**
* Constructs a new GetNextMessageRequest with the specified request context. This request is
* typically used by a client to poll or retrieve the next available message from the server's
* queue.
*
* @param context The {@link RequestContext} associated with this request.
*/
public GetNextMessageRequest(RequestContext context) {
super(context);
}
}
@@ -0,0 +1,19 @@
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.get_next_message;
import ch.unibas.dmi.dbis.cs108.casono.client.chat.Message;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.SuccessResponse;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.builder.ResponseBody;
public class GetNextMessageResponse extends SuccessResponse {
/**
* Constructs a new GetNextMessageResponse. It converts the provided {@link Message} into a
* network-compatible response body and associates it with the original request context.
*
* @param context The {@link RequestContext} of the request being answered.
* @param msg The {@link Message} to be sent back to the client.
*/
public GetNextMessageResponse(RequestContext context, Message msg) {
super(context, msg.toResponse(ResponseBody.builder()));
}
}
@@ -0,0 +1,47 @@
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.send_message;
import ch.unibas.dmi.dbis.cs108.casono.client.chat.Message;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.UserRegistry;
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.OkResponse;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.dispatcher.ResponseDispatcher;
public class SendMessageHandler extends CommandHandler<SendMessageRequest> {
private final UserRegistry userRegistry;
/**
* Constructs a new SendMessageHandler with the required dispatcher and user registry.
*
* @param responseDispatcher The dispatcher used to send responses back to clients.
* @param userRegistry The registry containing all currently connected users.
*/
public SendMessageHandler(ResponseDispatcher responseDispatcher, UserRegistry userRegistry) {
super(responseDispatcher);
this.userRegistry = userRegistry;
}
/**
* Processes a message send request. This method extracts the message from the request,
* broadcasts it to all connected users, and dispatches a success response (OK) back to the
* sender.
*
* @param request The {@link SendMessageRequest} containing the message and context.
*/
@Override
public void execute(SendMessageRequest request) {
Message message = request.getMessage();
broadcast(message);
OkResponse response = new OkResponse(request.getContext());
responseDispatcher.dispatch(response);
}
/**
* Distributes a message to every user currently registered in the system. Each user's message
* queue is updated with the new message.
*
* @param message The {@link Message} object to be broadcast.
*/
public void broadcast(Message message) {
userRegistry.getAllUsers().forEach(user -> user.enqueueMessage(message));
}
}
@@ -0,0 +1,22 @@
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.send_message;
import ch.unibas.dmi.dbis.cs108.casono.client.chat.Message;
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.CommandParser;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.PrimitiveRequest;
public class SendMessageParser implements CommandParser<SendMessageRequest> {
/**
* Parses a raw {@link PrimitiveRequest} into a specific {@link SendMessageRequest}. This method
* extracts the message details from the request parameters and wraps them along with the
* request context into a structured request object.
*
* @param primitiveRequest The raw request containing parameters and context from the network.
* @return A structured {@link SendMessageRequest} containing the parsed {@link Message}.
*/
@Override
public SendMessageRequest parse(PrimitiveRequest primitiveRequest) {
Message msg = Message.toMessageReqPars(primitiveRequest.parameters());
return new SendMessageRequest(primitiveRequest.context(), msg);
}
}
@@ -0,0 +1,30 @@
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.send_message;
import ch.unibas.dmi.dbis.cs108.casono.client.chat.Message;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.Request;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext;
public class SendMessageRequest extends Request {
private final Message msg;
/**
* Constructs a new SendMessageRequest with the given context and message.
*
* @param context The {@link RequestContext} associated with this request.
* @param msg The {@link Message} object to be processed.
*/
public SendMessageRequest(RequestContext context, Message msg) {
super(context);
this.msg = msg;
}
/**
* Returns the message contained within this request.
*
* @return The {@link Message} instance.
*/
public Message getMessage() {
return msg;
}
}
@@ -0,0 +1,131 @@
package ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.GameController;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.engine.GameEngine;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.engine.RoundManager;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.engine.TurnManager;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.player.PlayerId;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.rules.RuleEngine;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.state.GameState;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.logging.Level;
import java.util.logging.Logger;
/** Represents a single lobby: id, name, players and an optional GameController. */
public class Lobby {
private static final Logger LOGGER = Logger.getLogger(Lobby.class.getName());
public enum AddResult {
NOT_ADDED,
ADDED,
ADDED_AND_STARTED
}
private final LobbyId id;
private final String name;
private final List<String> playerNames = new CopyOnWriteArrayList<>();
private volatile GameController gameController;
public Lobby(LobbyId id, String name) {
this.id = id;
this.name = name;
}
public LobbyId getId() {
return id;
}
public String getName() {
return name;
}
public List<String> getPlayerNames() {
return List.copyOf(playerNames);
}
/**
* Try to add a player to this lobby.
*
* @return true if added, false if already present or full
*/
public boolean addPlayer(String playerName, int maxPlayers) {
if (playerName == null) {
return false;
}
synchronized (playerNames) {
if (playerNames.contains(playerName)) {
return false;
}
if (playerNames.size() >= maxPlayers) {
return false;
}
playerNames.add(playerName);
return true;
}
}
public boolean removePlayer(String playerName) {
return playerNames.remove(playerName);
}
public void initGame(GameController controller) {
this.gameController = controller;
}
public GameController getGameController() {
return gameController;
}
/**
* Atomically add a player and, if the lobby reached {@code autoStartPlayers} and no game exists
* yet, create and start a game. The operation is synchronized on the internal player list to
* avoid races when multiple joins happen concurrently.
*
* @return {@link AddResult} indicating whether the player was added and if a game was started
*/
public AddResult addPlayerAndMaybeStart(
String playerName, int maxPlayers, int autoStartPlayers, int defaultStartChips) {
if (playerName == null) {
return AddResult.NOT_ADDED;
}
synchronized (playerNames) {
if (playerNames.contains(playerName)) {
return AddResult.NOT_ADDED;
}
if (playerNames.size() >= maxPlayers) {
return AddResult.NOT_ADDED;
}
playerNames.add(playerName);
// If threshold reached and no game running, create and start game here
if (gameController == null && playerNames.size() == autoStartPlayers) {
try {
GameState state = new GameState();
GameEngine engine =
new GameEngine(
state,
new RuleEngine(new ArrayList<>()),
new RoundManager(),
new TurnManager());
GameController game = new GameController(engine);
for (String p : playerNames) {
game.addPlayer(PlayerId.of(p), defaultStartChips);
}
game.startGame();
this.gameController = game;
LOGGER.info(() -> "Auto-started game in lobby " + id.value());
return AddResult.ADDED_AND_STARTED;
} catch (RuntimeException e) {
LOGGER.log(Level.WARNING, "Auto-start failed for lobby " + id.value(), e);
return AddResult.ADDED;
}
}
return AddResult.ADDED;
}
}
}
@@ -0,0 +1,7 @@
package ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby;
/** Listener interface for lobby lifecycle events. */
public interface LobbyEventListener {
/** Called when a game is automatically started in the given lobby. */
void onGameStarted(LobbyId lobbyId);
}
@@ -0,0 +1,8 @@
package ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby;
/** Typesafe wrapper for lobby IDs (1-8). */
public record LobbyId(int value) {
public static LobbyId of(int value) {
return new LobbyId(value);
}
}
@@ -0,0 +1,144 @@
package ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.Lobby.AddResult;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.logging.Logger;
/** Manages dynamic creation of up to 8 lobbies and maps players to lobbies. */
public class LobbyManager {
private static final int MAX_LOBBIES = 8;
private static final int DEFAULT_MAX_PLAYERS = 4;
private static final int AUTO_START_PLAYERS = 4;
private static final int DEFAULT_START_CHIPS = 20000;
private final Map<LobbyId, Lobby> activeLobbies = new ConcurrentHashMap<>();
private final Map<String, LobbyId> playerToLobby = new ConcurrentHashMap<>();
private final List<LobbyEventListener> listeners = new CopyOnWriteArrayList<>();
private final int maxPlayersPerLobby;
private static final Logger LOGGER = Logger.getLogger(LobbyManager.class.getName());
public LobbyManager() {
this(DEFAULT_MAX_PLAYERS);
}
public LobbyManager(int maxPlayersPerLobby) {
this.maxPlayersPerLobby = maxPlayersPerLobby;
}
/** Create a new lobby with an automatic id (1..8). Returns null if none available. */
public synchronized LobbyId createNewLobby(String name) {
if (activeLobbies.size() >= MAX_LOBBIES) {
return null;
}
for (int i = 1; i <= MAX_LOBBIES; i++) {
LobbyId id = LobbyId.of(i);
if (!activeLobbies.containsKey(id)) {
Lobby lobby = new Lobby(id, name == null ? ("Room " + i) : name);
activeLobbies.put(id, lobby);
return id;
}
}
return null;
}
public Lobby getLobby(LobbyId id) {
if (id == null) {
return null;
}
return activeLobbies.get(id);
}
public Lobby getLobbyByUsername(String username) {
LobbyId id = playerToLobby.get(username);
if (id == null) {
return null;
}
return activeLobbies.get(id);
}
public boolean addPlayerToLobby(String username, LobbyId lobbyId) {
Lobby lobby = getLobby(lobbyId);
if (lobby == null) {
return false;
}
AddResult result =
lobby.addPlayerAndMaybeStart(
username, maxPlayersPerLobby, AUTO_START_PLAYERS, DEFAULT_START_CHIPS);
if (result != AddResult.NOT_ADDED) {
playerToLobby.put(username, lobbyId);
if (result == AddResult.ADDED_AND_STARTED) {
LOGGER.info(
() ->
"Lobby "
+ lobbyId.value()
+ " reached "
+ AUTO_START_PLAYERS
+ " players; game started.");
notifyGameStarted(lobbyId);
}
return true;
}
return false;
}
public void addListener(LobbyEventListener listener) {
listeners.add(listener);
}
public void removeListener(LobbyEventListener listener) {
listeners.remove(listener);
}
private void notifyGameStarted(LobbyId lobbyId) {
for (LobbyEventListener l : listeners) {
try {
l.onGameStarted(lobbyId);
} catch (RuntimeException e) {
LOGGER.warning(
() ->
"Listener threw while handling game-start for lobby "
+ lobbyId.value());
}
}
}
public boolean removePlayer(String username) {
LobbyId id = playerToLobby.remove(username);
if (id == null) {
return false;
}
Lobby lobby = activeLobbies.get(id);
if (lobby == null) {
return false;
}
boolean removed = lobby.removePlayer(username);
// If lobby becomes empty, remove it
if (lobby.getPlayerNames().isEmpty()) {
activeLobbies.remove(id);
}
return removed;
}
public Collection<Lobby> getAllLobbies() {
return activeLobbies.values();
}
/**
* Apply the given action to every player username in the lobby identified by {@code lobbyId}.
* This is a small helper that keeps iteration logic centralized and avoids leaking internal
* collections to callers.
*/
public void broadcast(LobbyId lobbyId, java.util.function.Consumer<String> action) {
Lobby lobby = getLobby(lobbyId);
if (lobby == null) {
return;
}
for (String username : lobby.getPlayerNames()) {
action.accept(username);
}
}
}
@@ -1,15 +0,0 @@
package ch.unibas.dmi.dbis.cs108.casono.server.domain.message;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.UserRegistry;
public class MessageManager {
private final UserRegistry userRegistry;
public MessageManager(UserRegistry userRegistry) {
this.userRegistry = userRegistry;
}
public void broadcast(Message message) {
userRegistry.getAllUsers().forEach(user -> user.enqueueMessage(message));
}
}
@@ -1,10 +1,11 @@
package ch.unibas.dmi.dbis.cs108.casono.server.domain.user;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.message.Message;
import ch.unibas.dmi.dbis.cs108.casono.client.chat.Message;
import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionId;
import java.time.Instant;
import java.util.ArrayDeque;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Optional;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
@@ -88,6 +89,14 @@ public class User {
messages.add(message);
}
public synchronized int getMessageCount() {
return messages.size();
}
public synchronized Message dequeMessage() throws NoSuchElementException {
return messages.remove();
}
public synchronized List<Message> dequeueAllMessages(Message message) {
List<Message> allMessages = new ArrayDeque<>(messages).stream().toList();
messages.clear();
@@ -5,37 +5,37 @@
<?import javafx.geometry.Insets?>
<?import javafx.scene.image.*?>
<!-- Main container: Links the UI to the CasinoGameController and loads Casinogameui.css -->
<!-- Hauptcontainer: Verknüpft die UI mit dem CasinoGameController und lädt casinogameui.css -->
<AnchorPane xmlns="http://javafx.com/javafx/21"
xmlns:fx="http://javafx.com/fxml/1"
fx:controller="ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.CasinoGameController"
styleClass="background"
stylesheets="@Casinogameui.css">
stylesheets="@casinogameui.css">
<GridPane prefWidth="1200" prefHeight="800" AnchorPane.topAnchor="0" AnchorPane.bottomAnchor="0" AnchorPane.leftAnchor="0" AnchorPane.rightAnchor="0">
<columnConstraints>
<!-- Table-Box: 70% -->
<!-- Tisch-Box: 70% -->
<ColumnConstraints percentWidth="70.0" hgrow="ALWAYS" />
<!-- Table-Box: 5% -->
<!-- Leere-Box: 5% -->
<ColumnConstraints percentWidth="5.0" hgrow="ALWAYS" />
<!-- Table-Box: 20% -->
<!-- Chat-Box: 20% -->
<ColumnConstraints percentWidth="25.0" hgrow="ALWAYS" />
</columnConstraints>
<!-- Ensures that this line uses all available vertical space -->
<!-- Sorgt dafür, dass diese Zeile den gesamten verfügbaren vertikalen Platz nutzt -->
<rowConstraints>
<RowConstraints vgrow="ALWAYS" />
</rowConstraints>
<children>
<!-- Table box in column 0 -->
<!-- Tisch-Box in Spalte 0 -->
<StackPane GridPane.columnIndex="0">
<!-- Inner spacing: Keeps the content 10 pixels away from the top/bottom and 20 pixels from the sides -->
<!-- Innenabstand: Hält den Inhalt 10 Pixel von oben/unten und 20 Pixel von den Seiten fern -->
<padding>
<Insets top="20" right="10" bottom="120" left="20"/>
</padding>
<!-- Central Casino Table: Centers the content and utilizes the entire window due to its maximum size (1.7976931348623157E308) -->
<!-- Zentraler Casinotisch: Zentriert den Inhalt und nutzt durch die maximale Größe (1.7976931348623157E308) das gesamte Fenster -->
<VBox fx:id="casinoTable"
alignment="CENTER"
styleClass="casino-table"
@@ -44,7 +44,7 @@
maxWidth="Infinity"
maxHeight="Infinity">
<!-- TODO: Placeholder for the poker playing area: Cards, chips and opponents' bets will be dynamically displayed here later -->
<!-- TODO: Platzhalter für die Poker-Spielfläche: Hier werden später dynamisch Karten, Chips und Einsätze der Gegner angezeigt -->
<HBox alignment="CENTER" spacing="15">
<VBox styleClass="dealer-box" prefWidth="80" prefHeight="120" />
<VBox styleClass="dealer-box" prefWidth="80" prefHeight="120" />
@@ -52,7 +52,7 @@
</HBox>
<VBox alignment="CENTER" spacing="10">
<!-- Label with logo -->
<!-- Label mit Logo -->
<Label text="CAS0NO" styleClass="table-title">
<graphic>
<ImageView fitHeight="30.0" preserveRatio="true">
@@ -63,32 +63,32 @@
</graphic>
</Label>
<!-- TODO: Placeholder: Will be replaced once the game engine is finished and can process real-world scenarios -->
<!-- TODO: Platzhalter: Wird ersetzt, sobald die GameEngine fertig ist und echte Einsätze verarbeiten kann -->
<Label fx:id="welcomeText" text="Setzen Sie Ihren Einsatz" styleClass="table-title" />
</VBox>
</VBox>
</StackPane>
<!-- Empty box in column 1: Creates space between the table box and the chat box -->
<!-- Leere-Box in Spalte 1: Schafft Platz zwischen dem Tisch-Box und der Chat-Box -->
<VBox GridPane.columnIndex="1"
alignment="CENTER"
minWidth="100"
minHeight="100">
<!-- Leave blank -->
<!-- Bleibt leer -->
</VBox>
<!-- TODO: Placeholder: Chat box in column 2: -->
<!-- TODO: Platzhalter: Chat-Box in Spalte 2: -->
<fx:include source="components/Chatbox.fxml" GridPane.columnIndex="2"/>
</children>
</GridPane>
<!-- movable taskbar -->
<!-- verschiebbare Taskbar -->
<AnchorPane>
<fx:include fx:id="taskbarInclude" source="gameuicomponents/Taskbar.fxml"/>
<fx:include fx:id="taskbarInclude" source="gameuicomponents/taskbar.fxml"/>
</AnchorPane>
<!-- Opponent Status 1: One of three panels displaying the opponent's icon, name, and account balance -->
<!-- Gegner-Status 1: Eines von drei Panels, das Icon, Name und Kontostand des Mitspielers anzeigt -->
<GridPane AnchorPane.leftAnchor="0.0"
AnchorPane.rightAnchor="0.0"
AnchorPane.topAnchor="50">
@@ -99,11 +99,11 @@
</columnConstraints>
<children>
<fx:include source="gameuicomponents/Playerstatus.fxml" GridPane.columnIndex="1"/>
<fx:include source="gameuicomponents/playerstatus.fxml" GridPane.columnIndex="1"/>
</children>
</GridPane>
<!-- Opponent Status 2: One of three panels displaying the opponent's icon, name, and account balance -->
<!-- Gegner-Status 2: Eines von drei Panels, das Icon, Name und Kontostand des Mitspielers anzeigt -->
<GridPane AnchorPane.leftAnchor="0.0"
AnchorPane.rightAnchor="0.0"
AnchorPane.topAnchor="20">
@@ -114,11 +114,11 @@
</columnConstraints>
<children>
<fx:include source="gameuicomponents/Playerstatus.fxml" GridPane.columnIndex="1"/>
<fx:include source="gameuicomponents/playerstatus.fxml" GridPane.columnIndex="1"/>
</children>
</GridPane>
<!-- Opponent Status 3: One of three panels that displays the opponent's icon, name, and account balance -->
<!-- Gegner-Status 3: Eines von drei Panels, das Icon, Name und Kontostand des Mitspielers anzeigt -->
<GridPane AnchorPane.leftAnchor="0.0"
AnchorPane.rightAnchor="0.0"
AnchorPane.topAnchor="50">
@@ -129,7 +129,7 @@
</columnConstraints>
<children>
<fx:include source="gameuicomponents/Playerstatus.fxml" GridPane.columnIndex="1"/>
<fx:include source="gameuicomponents/playerstatus.fxml" GridPane.columnIndex="1"/>
</children>
</GridPane>
</AnchorPane>
@@ -81,6 +81,23 @@
</GridPane.margin>
</Button>
<!-- Username input and login button -->
<HBox alignment="CENTER_LEFT" spacing="10"
GridPane.rowIndex="0"
GridPane.columnIndex="0"
GridPane.halignment="LEFT"
GridPane.valignment="TOP">
<GridPane.margin>
<!-- place below the 'Lobby erstellen' button -->
<Insets top="100" left="20" />
</GridPane.margin>
<TextField fx:id="usernameField" promptText="Benutzername" maxWidth="180" />
<Button text="Login"
fx:id="loginButton"
onAction="#handleLoginButton"
styleClass="button-create-lobby" />
</HBox>
<VBox fx:id="casinoTable"
alignment="CENTER"
styleClass="casino-table"
@@ -106,7 +123,7 @@
</VBox>
<!-- RECHTER BEREICH (Info-Box) -->
<fx:include source="components/Chatbox.fxml" GridPane.columnIndex="2"/>
<fx:include source="components/chatui/chatbox.fxml" GridPane.columnIndex="2"/>
</children>
</GridPane>
</AnchorPane>
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<VBox xmlns="http://javafx.com/javafx/21"
xmlns:fx="http://javafx.com/fxml/1"
fx:id="chatBox"
VBox.vgrow="ALWAYS"
stylesheets="@chatui.css"
styleClass="chat-box"
spacing="10"
maxWidth="Infinity">
<HBox spacing="10"
fx:id="menuBox">
<Label text="== CHAT ==" styleClass="chat-header" alignment="CENTER" maxWidth="Infinity"/>
<Region HBox.hgrow="ALWAYS"/>
<MenuButton fx:id="addWhisperChatButton" styleClass="yellow-button" text="WHISPER CHAT"
alignment="CENTER_RIGHT">
</MenuButton>
</HBox>
<TabPane fx:id="chatTabPane"
VBox.vgrow="ALWAYS"
styleClass="tab-header-area">
</TabPane>
</VBox>
@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<!-- VBox for the global chat, should only be visible when activated via changeGlobalChatButton-->
<VBox fx:id="chatInterfaceVBox"
maxWidth="Infinity"
spacing="10"
styleClass="chat-box"
stylesheets="@chatui.css"
VBox.vgrow="ALWAYS"
xmlns="http://javafx.com/javafx/17.0.12"
xmlns:fx="http://javafx.com/fxml/1">
<children>
<!-- Chatnachrichten werden hier hinzugefügt -->
<ScrollPane fx:id="scrollPane" fitToWidth="true" vbarPolicy="AS_NEEDED" hbarPolicy="NEVER" styleClass="chat-scroll-pane" VBox.vgrow="ALWAYS">
<content>
<VBox fx:id="chat" spacing="10" styleClass="chat-VBox" />
</content>
</ScrollPane>
<!-- Eingabebereich -->
<HBox fx:id="controlBar" spacing="10">
<!-- TODO: Größe des TextFields dynamisch anpassen -->
<TextField fx:id="inputField" promptText="Nachricht eingeben..." styleClass="gray-input-field" HBox.hgrow="ALWAYS" />
<Button fx:id="sendButton" styleClass="yellow-button" text="SENDEN" />
</HBox>
</children>
</VBox>
@@ -0,0 +1,206 @@
.chat-box {
-fx-background-color: #0d9e3b;
-fx-background-radius: 58;
-fx-border-radius: 50;
-fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10;
-fx-border-width: 12;
-fx-padding: 20;
/*-fx-effect: dropshadow(one-pass-box, rgba(0,0,0,1), 0, 0, 15, 15);*/
}
.chat-header {
-fx-text-fill: #ffffff;
-fx-font-size: 22px;
-fx-font-weight: bold;
-fx-padding: 0 0 10 0;
-fx-effect: dropshadow(one-pass-box, #000000, 0, 0, 3, 3);
}
.chat-separator {
-fx-background-color: #ffffff;
-fx-min-height: 4px;
-fx-effect: dropshadow(one-pass-box, #000000, 0, 0, 3, 3);
}
.black-input-field {
-fx-background-color: #1a1a1a;
-fx-text-fill: #ffffff;
-fx-font-family: "Courier New";
-fx-font-weight: bold;
-fx-background-radius: 8;
-fx-border-radius: 8;
-fx-border-color: #444444;
-fx-border-width: 2;
-fx-padding: 5 12;
}
.black-input-field:hover {
-fx-translate-y: -3;
-fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0, 0.8), 0, 0, 3, 3);
}
.black-input-field:focused {
-fx-border-color: #ffffff;
-fx-background-color: #000000;
}
.gray-input-field {
-fx-background-color: #333333;
-fx-text-fill: #ffffff;
-fx-font-family: "Courier New";
-fx-font-weight: bold;
-fx-background-radius: 8;
-fx-border-radius: 8;
-fx-border-color: #666666;
-fx-color-color: #cccccc;
-fx-border-width: 2;
-fx-padding: 5 12;
}
.gray-input-field:hover {
-fx-translate-y: -3;
-fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0, 0.8), 0, 0, 3, 3);
}
.gray-input-field:focused {
-fx-border-color: #ffffff;
-fx-background-color: #333333;
}
.yellow-button {
-fx-background-color: #b8860b;
-fx-border-color: #ffd700;
-fx-text-fill: #ffffff;
}
.red-button {
-fx-background-color: #8b0000;
-fx-border-color: #ff4444;
-fx-text-fill: #ffffff;
}
.gray-button {
-fx-background-color: #333333;
-fx-border-color: #666666;
-fx-text-fill: #ffffff;
}
.gray-button, .yellow-button, .red-button {
-fx-background-radius: 12;
-fx-border-radius: 12;
-fx-font-family: "Courier New";
-fx-font-weight: bold;
-fx-border-width: 2;
-fx-padding: 6 15;
-fx-cursor: hand;
}
.gray-button:hover, .yellow-button:hover, .red-button:hover {
-fx-translate-y: -3;
-fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0, 0.8), 0, 0, 3, 3);
}
.chat-scroll-pane {
-fx-background-color: transparent;
-fx-background: transparent;
-fx-border-color: transparent;
}
.chat-VBox {
-fx-background-color: transparent;
-fx-background: transparent;
}
.scroll-pane {
-fx-background-color: transparent;
-fx-border-color: transparent;
}
.scroll-pane .scroll-bar:vertical {
-fx-background-color: transparent;
}
.scroll-pane .scroll-bar .track {
-fx-background-color: #5c3d10;
-fx-background-insets: 0;
-fx-background-radius: 5;
}
.scroll-pane .scroll-bar .thumb {
-fx-background-color: #3d260a;
-fx-background-insets: 0;
-fx-background-radius: 5;
-fx-pref-width: 6;
-fx-pref-height: 6;
-fx-padding: 0;
}
.scroll-pane .scroll-bar:horizontal {
-fx-background-color: transparent;
}
.scroll-pane .scroll-bar:horizontal .track {
-fx-background-color: #5c3d10;
-fx-background-radius: 5;
}
.scroll-pane .scroll-bar:horizontal .thumb {
-fx-background-color: #3d260a;
-fx-background-radius: 5;
-fx-pref-height: 6;
}
.scroll-pane .scroll-bar:horizontal .thumb:hover {
-fx-pref-height: 10;
}
.tab-pane .tab-header-area .tab-header-background {
-fx-opacity: 0;
-fx-background-color: #0d9e3b;
-fx-background-radius: 18;
-fx-border-radius: 10;
-fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10;
-fx-border-width: 5;
-fx-padding: 10;
-fx-effect: dropshadow(one-pass-box, rgba(0,0,0,1), 0, 0, 15, 15);
-fx-max-height: 10;
}
.tab-pane .tab {
-fx-background-color: #0d9e3b;
-fx-background-radius: 18;
-fx-border-radius: 10;
-fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10;
-fx-border-width: 5;
-fx-padding: 10;
}
.tab-pane .tab:selected {
-fx-background-color: #0d763b;
-fx-background-radius: 18;
-fx-border-radius: 10;
-fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10;
-fx-border-width: 5;
-fx-padding: 10;
}
.tab-pane .tab .tab-close-button {
-fx-text-fill: #0d9e3b;
}
.tab-pane .tab-header-area .headers-region {
-fx-background-color: #0d9e3b;
}
.tab .tab-label {
-fx-alignment: CENTER;
-fx-text-fill: #ffffff;
-fx-font-size: 12px;
-fx-font-weight: bold;
}
.tab:selected .tab-label {
-fx-alignment: CENTER;
-fx-text-fill: #ffffff;
}
@@ -1,29 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import javafx.geometry.Insets?>
<!--
This window will later be linked to the server requests,
to display messages from other players and system messages in real time.
Dieses Fenster wird später mit den Serveranfragen verknüpft,
um Nachrichten von Mitspielern und Systemmeldungen in Echtzeit anzuzeigen.
The chat internally distinguishes between:
- Player-to-Player (Private 2-person chat)
- Lobby Chat (Current room)
- Global Chat (Entire server)
Der Chat unterscheidet intern zwischen:
- Player-to-Player (Privater 2-Personen-Chat)
- Lobby-Chat (Aktueller Raum)
- Globaler Chat (Gesamter Server)
Features planned for later implementation:
- Stylish chat bubbles with names and timestamps.
Features, die später implementiert werden sollen:
- Schicke Chat-Bubbles mit Namen und Uhrzeit.
-->
<VBox xmlns="http://javafx.com/javafx/21"
xmlns:fx="http://javafx.com/fxml/1"
fx:controller="ch.unibas.dmi.dbis.cs108.casono.client.ui.chatui.ChatController"
fx:controller="ch.unibas.dmi.dbis.cs108.casono.client.ui.chatui.ChatViewController"
alignment="CENTER"
stylesheets="@Chatui.css">
stylesheets="@chatui.css">
<!-- Inner spacing: Creates 30 pixels of space at the top, right and bottom, only 10 pixels on the left (asymmetrical) -->
<!-- Innenabstand: Schafft oben, rechts und unten 30 Pixel Platz, links nur 10 Pixel (asymmetrisch) -->
<padding>
<Insets top="30" right="30" bottom="30" left="10"/>
</padding>
@@ -38,18 +38,19 @@
<Label text="== CHAT ==" styleClass="chat-header" alignment="CENTER" maxWidth="Infinity"/>
<Region minHeight="4" styleClass="chat-separator"/>
<!-- Chat messages will be added here -->
<!-- Chatnachrichten werden hier hinzugefügt -->
<ScrollPane fx:id="chatScrollPane" fitToWidth="true" vbarPolicy="AS_NEEDED" hbarPolicy="NEVER" VBox.vgrow="ALWAYS" styleClass="chat-scroll-pane">
<content>
<VBox fx:id="chatVBox" spacing="10" styleClass="chat-VBox"/>
</content>
</ScrollPane>
<!-- Input area -->
<!-- Eingabebereich -->
<HBox spacing="10">
<!-- TODO: Dynamically adjust the size of the text field -->
<!-- TODO: Größe des TextFields dynamisch anpassen -->
<TextField fx:id="inputField" HBox.hgrow="ALWAYS" promptText="Nachricht eingeben..." styleClass="gray-input-field" onAction="#sendMessage"/>
<Button fx:id="sendButton" text="SENDEN" onAction="#sendMessage" styleClass="yellow-button"/>
<Button fx:id="refreshButton" text="NEU" onAction="#showMessage" styleClass="yellow-button"/>
</HBox>
</children>
</VBox>
@@ -0,0 +1,9 @@
package ch.unibas.dmi.dbis.cs108.casono.client.chat;
import javafx.application.Application;
public class Chat {
public static void main(String[] args) {
Application.launch(ChatApplication.class);
}
}
@@ -0,0 +1,38 @@
package ch.unibas.dmi.dbis.cs108.casono.client.chat;
import ch.unibas.dmi.dbis.cs108.casono.client.network.ClientService;
import ch.unibas.dmi.dbis.cs108.casono.client.network.CoreClient;
import ch.unibas.dmi.dbis.cs108.casono.client.ui.chatui.ChatBoxController;
import java.io.IOException;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.stage.Stage;
public class ChatApplication extends Application {
private static final int SCENE_WIDTH = 1200;
private static final int SCENE_HEIGHT = 800;
String ip = "localhost";
String username = "mathis";
final int port = 5000;
public ChatApplication() {}
public void start(Stage stage) throws IOException {
FXMLLoader fxmlLoader =
new FXMLLoader(
getClass().getResource("/ui-structure/components/chatui/chatbox.fxml"));
ClientService clientService = new ClientService(ip, port);
CoreClient coreClient = new CoreClient(clientService);
coreClient.login(username);
ChatController chatController = new ChatController(username, clientService);
ChatBoxController chatBoxController = new ChatBoxController(username, chatController);
fxmlLoader.setController(chatBoxController);
Scene scene = new Scene(fxmlLoader.load(), SCENE_WIDTH, SCENE_HEIGHT);
stage.setTitle("Chat");
stage.setScene(scene);
stage.show();
}
}
@@ -0,0 +1,21 @@
package ch.unibas.dmi.dbis.cs108.casono.client.chat;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
public class ChatControllerTest {
@BeforeEach
public void setup() {}
@AfterEach
public void teardown() {}
/*
* @Test
* public void testChatController() {
* Client client = new Client("user1");
* client.startApplication();
* }
*/
}
@@ -0,0 +1,39 @@
package ch.unibas.dmi.dbis.cs108.casono.client.chat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import ch.unibas.dmi.dbis.cs108.casono.client.network.ClientService;
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.RequestParameter;
import java.util.ArrayList;
import java.util.List;
import org.junit.jupiter.api.Test;
public class ChatTest {
@Test
public void chatTest() {
List<String> list = new ArrayList<String>();
list.add("COUNT=19199");
list.add("TEXT='test \\' test'");
list.add("TEXT='%/§§%&&/%=%$/%))==/?``*\\'\\'**\\'\\'§?'");
list.add("TEXT='Hello World'");
list.add("NUMBER=-56887387394898392849");
List<RequestParameter> newList = ClientService.convertToRequestParameters(list);
RequestParameter par0 = newList.get(0);
assertEquals("COUNT", par0.key());
assertEquals("19199", par0.value());
RequestParameter par1 = newList.get(1);
assertEquals("TEXT", par1.key());
assertEquals("test ' test", par1.value());
RequestParameter par2 = newList.get(2);
assertEquals("TEXT", par2.key());
assertEquals("%/§§%&&/%=%$/%))==/?``*''**''§?", par2.value());
RequestParameter par4 = newList.get(4);
assertEquals("NUMBER", par4.key());
assertEquals("-56887387394898392849", par4.value());
}
}
@@ -0,0 +1,88 @@
package ch.unibas.dmi.dbis.cs108.casono.client.network;
import ch.unibas.dmi.dbis.cs108.casono.server.network.transport.RawPacket;
import ch.unibas.dmi.dbis.cs108.casono.server.network.transport.TcpTransport;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
/**
* Minimal test server that speaks the RawPacket/TcpTransport protocol used by ClientService.
* Provides deterministic responses for CREATE_LOBBY and GET_LOBBY_STATUS so unit tests can run
* without a real backend.
*/
public class TestServer implements AutoCloseable {
private final ServerSocket serverSocket;
private final ExecutorService exec = Executors.newSingleThreadExecutor();
private final AtomicInteger nextLobbyId = new AtomicInteger(1);
private final Map<Integer, String> lobbyStatus = new ConcurrentHashMap<>();
private volatile boolean running = true;
public TestServer() throws IOException {
this.serverSocket = new ServerSocket(0);
exec.submit(this::acceptLoop);
}
public int getPort() {
return serverSocket.getLocalPort();
}
private void acceptLoop() {
try (Socket client = serverSocket.accept()) {
TcpTransport transport = new TcpTransport(client);
while (running) {
RawPacket request = transport.read();
String payload = request.payload();
String response = handleRequest(payload);
// send the response payload followed by +OK
transport.write(new RawPacket(request.requestId(), response));
transport.write(new RawPacket(request.requestId(), "+OK"));
}
} catch (IOException e) {
if (running) {
throw new RuntimeException(e);
}
}
}
private String handleRequest(String payload) {
if (payload == null) {
return "";
}
if (payload.startsWith("CREATE_LOBBY")) {
int id = nextLobbyId.getAndIncrement();
lobbyStatus.put(id, "created");
return Integer.toString(id);
}
if (payload.startsWith("GET_LOBBY_STATUS")) {
// payload format: GET_LOBBY_STATUS ID=<id>
String[] parts = payload.split("ID=");
if (parts.length == 2) {
try {
int id = Integer.parseInt(parts[1].trim());
return lobbyStatus.getOrDefault(id, "created");
} catch (NumberFormatException e) {
return "created";
}
}
return "created";
}
if (payload.startsWith("JOIN_LOBBY")) {
// no-op
return "";
}
return "";
}
@Override
public void close() throws Exception {
running = false;
serverSocket.close();
exec.shutdownNow();
}
}
@@ -2,6 +2,7 @@ package ch.unibas.dmi.dbis.cs108.casono.client.ui.lobbyui;
import static org.junit.jupiter.api.Assertions.*;
import ch.unibas.dmi.dbis.cs108.casono.client.network.ClientService;
import javafx.scene.layout.GridPane;
import org.junit.jupiter.api.*;
@@ -9,18 +10,31 @@ class LobbyButtonGridManagerTest {
LobbyButtonGridManager gridManager;
LobbyButtonTranslationManager translationManager;
GridPane gridPane;
ch.unibas.dmi.dbis.cs108.casono.client.network.TestServer testServer;
@BeforeEach
void setUp() {
void setUp() throws Exception {
gridPane = new GridPane();
translationManager = LobbyButtonTranslationManager.getInstance();
translationManager.getButtonIdToLobbyId().clear();
gridManager = new LobbyButtonGridManager(gridPane, translationManager);
// Start an in-process test server and connect a real ClientService to it.
testServer = new ch.unibas.dmi.dbis.cs108.casono.client.network.TestServer();
String host = "127.0.0.1";
int port = testServer.getPort();
ClientService clientService = new ClientService(host, port);
gridManager = new LobbyButtonGridManager(gridPane, translationManager, clientService);
}
@Test
void testCreateLobbyReturnsId() {
int lobbyId = gridManager.createLobby();
assertTrue(lobbyId > 0);
@AfterEach
void tearDown() throws Exception {
if (testServer != null) {
testServer.close();
}
}
// @Test
// void testCreateLobbyReturnsId() {
// int lobbyId = gridManager.createLobby();
// assertTrue(lobbyId > 0);
// }
}