Compare commits

..

14 Commits

Author SHA1 Message Date
Jona Walpert cfd13b43fc Feat: register "Call", "Fold","Bet","Raise","AddPlayer" command and change import locatio nto match new folder structure 2026-04-10 13:52:57 +02:00
Jona Walpert 5916c26b86 Feat: add "Call" command 2026-04-10 13:51:48 +02:00
Jona Walpert d734f815e8 Feat: add "Fold" command 2026-04-10 13:51:16 +02:00
Jona Walpert c5518d64a3 Feat: add "Raise" command 2026-04-10 13:50:57 +02:00
Jona Walpert f1e09cb328 Feat: add "Bet" command 2026-04-10 13:50:36 +02:00
Jona Walpert b06cd571b0 Feat: add "AddPlayer" command 2026-04-10 13:50:09 +02:00
Lars Simon Winzer 3ee1e63577 Merge branch 'fix/81-incorrect-visibility-of-throwingparser' into 'main'
Change visibility of ThrowingParser from package-private to public

Closes #81

See merge request cs108-fs26/Gruppe-13!94
2026-04-10 11:30:22 +02:00
Lars Simon Winzer 8f4ce1db80 Fix: Change visibility of ThrowingParser from package-private to public 2026-04-10 11:25:09 +02:00
Lars Simon Winzer 066d4ee8c3 Merge branch 'chore/66-milestone-achievements-template-and-contributing' into 'main'
Add milestone achievements template and update contributing guidelines

Closes #66

See merge request cs108-fs26/Gruppe-13!93
2026-04-10 10:57:40 +02:00
Lars Simon Winzer 5ed455c20d Refactor: Cut header length 2026-04-10 10:48:52 +02:00
Lars Simon Winzer 938ef71c2a Revert "Refactor: Swap title and category header and cut header length"
This reverts commit 09e8822bef.
2026-04-10 10:35:25 +02:00
Lars Simon Winzer 09e8822bef Refactor: Swap title and category header and cut header length 2026-04-10 10:34:37 +02:00
Lars Simon Winzer 35297775dc Add: Milestone Achievements section to contribution guidelines 2026-04-10 10:28:10 +02:00
Lars Simon Winzer 77dad54c84 Add: Milestone achievement gitlab issue template 2026-04-10 10:14:03 +02:00
68 changed files with 651 additions and 2545 deletions
@@ -0,0 +1,16 @@
## Milestone Achievement
<!-- The recommended type is: Task -->
### Category
<!-- Category of the milestone (Process, Product, Presentation) -->
### Title
<!-- Title of the milestone (equal to the title in the milestone catalog (https://p9.dmi.unibas.ch/cs108/2026) -->
### Rewarded points on completion
<!-- Number of points rewarded on completion of the milestone -->
### Description of milestone
<!-- Description of the milestone (equal to the description in the milestone catalog (https://p9.dmi.unibas.ch/cs108/2026) -->
/label ~achievement
+15
View File
@@ -12,6 +12,7 @@ Since this project is part of a course at the University of Basel, the [Code of
- [Creating an issue](#creating-an-issue) - [Creating an issue](#creating-an-issue)
- [During implementation](#during-implementation) - [During implementation](#during-implementation)
- [Collaborative work](#collaborative-work) - [Collaborative work](#collaborative-work)
- [Milestone Achievements](#milestone-achievements)
- [Git Workflow](#git-workflow) - [Git Workflow](#git-workflow)
- [Creating a branch](#creating-a-branch) - [Creating a branch](#creating-a-branch)
- [Working on a branch](#working-on-a-branch) - [Working on a branch](#working-on-a-branch)
@@ -49,6 +50,20 @@ Issue or Task in GitLab **before** any implementation begins.
centrally visible and searchable. centrally visible and searchable.
- Before starting work that overlaps with an existing issue, check its comment thread first to avoid duplicating effort. - Before starting work that overlaps with an existing issue, check its comment thread first to avoid duplicating effort.
### Milestone Achievements
For every process and product-related milestone achievement, there is a dedicated milestone task.
- **Do not create a branch directly from a milestone task.**
- Instead, create a normal implementation task (using the regular task template) and reference the milestone task there.
- In the merge request, reference the milestone task again.
- If the milestone condition is fully met, you may use `Closing #<id>` to close the milestone task.
- If it is only partially addressed, use `Relates to #<id>` or `Contributes to #<id>` so the milestone task stays open.
Milestone tasks may, but do not have to, be assigned to a specific person.
- If multiple people are involved, contribution is tracked through linked tasks that reference the milestone task.
- For small topics, a Milestone Achievement may be assigned to one person. Others should only contribute on request and should not modify components introduced under that achievement without coordination.
## Git Workflow ## Git Workflow
We use a **feature branch -> main** strategy. The `main` branch is always in a releasable state. We use a **feature branch -> main** strategy. The `main` branch is always in a releasable state.
+1 -1
View File
@@ -32,7 +32,7 @@ dependencies {
// Source: https://mvnrepository.com/artifact/org.apache.logging.log4j // Source: https://mvnrepository.com/artifact/org.apache.logging.log4j
implementation("org.apache.logging.log4j:log4j-api:2.25.3") implementation("org.apache.logging.log4j:log4j-api:2.25.3")
runtimeOnly("org.apache.logging.log4j:log4j-core: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 // Source: https://mvnrepository.com/artifact/org.fusesource.jansi/jansi
runtimeOnly("org.fusesource.jansi:jansi:2.4.2") runtimeOnly("org.fusesource.jansi:jansi:2.4.2")
@@ -1,249 +0,0 @@
# 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,103 +341,4 @@ LIST_USERS
END END
END 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` | GLOBAL, LOBBY or WHISPER, specifying the Chat |
| `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 |
### 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` | GLOBAL, LOBBY or WHISPER, specifying the Chat |
| `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 |
### 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
``` ```
@@ -38,8 +38,4 @@ public class ClientApp {
LOGGER.info("You've selected the client. It will connect port {} at host {}", port, host); LOGGER.info("You've selected the client. It will connect port {} at host {}", port, host);
Launcher.main(new String[] {}); Launcher.main(new String[] {});
} }
public static void main(String[] args) {
start(args[0]);
}
} }
@@ -1,129 +0,0 @@
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);
}
}
@@ -1,85 +0,0 @@
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;
}
}
@@ -1,8 +0,0 @@
package ch.unibas.dmi.dbis.cs108.casono.client.chat;
/** Describing the Type of the Chat */
public enum ChatType {
GLOBAL,
LOBBY,
WHISPER
}
@@ -1,203 +0,0 @@
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.
*
* @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 message creator.
* @param target The username of the recipient (required for whispers, otherwise null).
* @param timestamp The formatted time string (e.g., "HH:mm").
* @param message The actual text content of the message.
*/
public Message(
ChatType type,
int lobbyId,
String sender,
String target,
String timestamp,
String message) {
this.type = type;
this.lobbyId = lobbyId;
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,
-1,
getParString(parameters, "USER"),
null,
getParString(parameters, "TIME"),
getParString(parameters, "TEXT"));
case LOBBY ->
new Message(
ChatType.LOBBY,
Integer.parseInt(getParString(parameters, "GAME")),
getParString(parameters, "USER"),
null,
getParString(parameters, "TIME"),
getParString(parameters, "TEXT"));
case WHISPER ->
new Message(
ChatType.WHISPER,
Integer.parseInt(getParString(parameters, "GAME", "-1")),
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();
}
}
@@ -1,9 +0,0 @@
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;
}
@@ -1,23 +0,0 @@
package ch.unibas.dmi.dbis.cs108.casono.client.game;
import ch.unibas.dmi.dbis.cs108.casono.client.game.Card;
import ch.unibas.dmi.dbis.cs108.casono.client.game.Player;
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<>();
}
@@ -1,17 +0,0 @@
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<>();
}
@@ -1,72 +0,0 @@
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;
}
}
@@ -1,222 +0,0 @@
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;
public static ArrayList<String> response;
private final AtomicInteger idGenerator;
private final 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);
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();
}
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");
}
}
@@ -1,37 +0,0 @@
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);
}
}
@@ -1,97 +0,0 @@
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();
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;
}
}
@@ -1,59 +0,0 @@
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;
}
/**
* 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);
}
}
@@ -1,112 +0,0 @@
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);
}
}
}
@@ -0,0 +1,90 @@
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);
}
}
@@ -1,102 +0,0 @@
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);
});
}
}
@@ -5,11 +5,6 @@ package ch.unibas.dmi.dbis.cs108.casono.client.ui.lobbyui;
* ButtonID to LobbyID. * ButtonID to LobbyID.
*/ */
import java.util.Map; import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ThreadLocalRandom;
import ch.unibas.dmi.dbis.cs108.casono.client.network.LobbyClient;
import ch.unibas.dmi.dbis.cs108.casono.client.network.ClientService;
import javafx.scene.Node;
import javafx.scene.control.Button; import javafx.scene.control.Button;
import javafx.scene.image.Image; import javafx.scene.image.Image;
import javafx.scene.image.ImageView; import javafx.scene.image.ImageView;
@@ -18,8 +13,7 @@ import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
/** /**
* Manages the grid for lobby buttons and rendering. Uses * Manages the grid for lobby buttons and rendering. Uses LobbyButtonTranslationManager for mapping
* LobbyButtonTranslationManager for mapping
* ButtonID to LobbyID. * ButtonID to LobbyID.
*/ */
public class LobbyButtonGridManager { public class LobbyButtonGridManager {
@@ -42,61 +36,24 @@ public class LobbyButtonGridManager {
/** Path to the button image. */ /** Path to the button image. */
private static final String BUTTON_IMAGE_PATH = "/images/logo.png"; private static final String BUTTON_IMAGE_PATH = "/images/logo.png";
/** Image for a lobby in CREATED state. */
/** Default fallback image. */
private static final String BUTTON_FALLBACK_IMAGE = "/images/lobbypictures/error.png";
/**
* Template for per-button images. Use: button index and status
* (created|running). Example:
* /images/lobby_1_created.png
*/
private static final String BUTTON_IMAGE_TEMPLATE = "/images/lobbypictures/lobby_%d_%s.png";
/** Cache for loaded Images keyed by resource path. */
private final ConcurrentHashMap<String, Image> imageCache = new ConcurrentHashMap<>();
/** Max random lobby id. */ /** Max random lobby id. */
private static final int MAX_RANDOM_LOBBY_ID = 10000; private static final int MAX_RANDOM_LOBBY_ID = 10000;
private final LobbyClient lobbyClient;
/** /**
* Constructor for the GridManager. * Constructor for the GridManager.
* *
* @param gridPane the GridPane for rendering * @param gridPane the GridPane for rendering
* @param translationManager the manager for mapping ButtonID to LobbyID * @param translationManager the manager for mapping ButtonID to LobbyID
*/ */
public LobbyButtonGridManager( public LobbyButtonGridManager(
GridPane gridPane, LobbyButtonTranslationManager translationManager) { GridPane gridPane, LobbyButtonTranslationManager translationManager) {
this(gridPane, translationManager, createDefaultLobbyClient());
}
/**
* Constructor that accepts a `LobbyClient` implementation.
*/
public LobbyButtonGridManager(
GridPane gridPane, LobbyButtonTranslationManager translationManager, LobbyClient lobbyClient) {
this.gridPane = gridPane; this.gridPane = gridPane;
// Always use the singleton // Singleton immer verwenden
this.translationManager = LobbyButtonTranslationManager.getInstance(); this.translationManager = LobbyButtonTranslationManager.getInstance();
this.lobbyClient = lobbyClient;
}
private static LobbyClient createDefaultLobbyClient() {
String host = System.getProperty("casono.server.host", "127.0.0.1");
int port = 1337;
try {
port = Integer.parseInt(System.getProperty("casono.server.port", "1337"));
} catch (NumberFormatException e) {
// use default port if parsing fails
}
return new LobbyClient(new ClientService(host, port));
} }
/** /**
* Renders all lobby buttons in the grid. Creates a button for each mapping with * Renders all lobby buttons in the grid. Creates a button for each mapping with image and event
* image and event
* handler. * handler.
*/ */
public void renderLobbyButtons() { public void renderLobbyButtons() {
@@ -109,15 +66,12 @@ public class LobbyButtonGridManager {
} }
for (Map.Entry<Integer, Integer> entry : mapping.entrySet()) { for (Map.Entry<Integer, Integer> entry : mapping.entrySet()) {
int buttonId = entry.getKey(); int buttonId = entry.getKey();
int lobbyId = entry.getValue();
Button btn = new Button(); Button btn = new Button();
btn.setId("lobbyBtn-" + buttonId); btn.setId("lobbyBtn-" + buttonId);
// Set image based on lobby status (initial) ImageView imageView =
LobbyStatus initialStatus = getLobbyStatus(lobbyId); new ImageView(new Image(getClass().getResourceAsStream(BUTTON_IMAGE_PATH)));
Image image = safeLoadImage(getImagePathForButton(buttonId, initialStatus));
ImageView imageView = new ImageView(image);
imageView.setPreserveRatio(true); imageView.setPreserveRatio(true);
// Dynamic width: bind to the cell size // Dynamische Breite: Bindung an die Zellengröße
imageView imageView
.fitWidthProperty() .fitWidthProperty()
.bind(gridPane.widthProperty().divide(COLS).subtract(BUTTON_WIDTH_MARGIN)); .bind(gridPane.widthProperty().divide(COLS).subtract(BUTTON_WIDTH_MARGIN));
@@ -131,9 +85,9 @@ public class LobbyButtonGridManager {
GridPane.setVgrow(btn, javafx.scene.layout.Priority.ALWAYS); GridPane.setVgrow(btn, javafx.scene.layout.Priority.ALWAYS);
btn.setOnAction( btn.setOnAction(
e -> { e -> {
Integer targetLobbyId = translationManager.getLobbyIdForButton(buttonId); Integer lobbyId = translationManager.getLobbyIdForButton(buttonId);
if (targetLobbyId != null) { if (lobbyId != null) {
joinLobby(targetLobbyId); joinLobby(lobbyId);
} }
}); });
int row = index / COLS; int row = index / COLS;
@@ -143,141 +97,16 @@ public class LobbyButtonGridManager {
} }
} }
/** Possible lobby statuses. */
private enum LobbyStatus {
CREATED,
RUNNING
}
/** /**
* Return the current status of a lobby. * Placeholder for lobby creation logic. Returns a generated lobbyId.
*
* @param lobbyId the lobby id to query
* @return the lobby status (mapped from server string; CREATED or RUNNING)
*/
public LobbyStatus getLobbyStatus(int lobbyId) {
String serverStatus = lobbyClient.fetchLobbyStatusString(lobbyId);
LobbyStatus parsed = parseLobbyStatus(serverStatus);
if (parsed == null) {
// Defensive fallback
LOGGER.error("Unrecognized lobby status '{}' for lobby {}. Defaulting to CREATED.", serverStatus, lobbyId);
return LobbyStatus.CREATED;
}
return parsed;
}
/**
* Parse a status string returned by the server into the local enum.
* Accepts case-insensitive values like "created" / "CREATED" / "running".
* Returns null if the string is not recognized.
*/
private LobbyStatus parseLobbyStatus(String statusStr) {
if (statusStr == null)
return null;
try {
return LobbyStatus.valueOf(statusStr.trim().toUpperCase());
} catch (IllegalArgumentException e) {
return null;
}
}
private String getImagePathForStatus(LobbyStatus status) {
return status == LobbyStatus.CREATED
? String.format(BUTTON_IMAGE_TEMPLATE, 0, "created")
: String.format(BUTTON_IMAGE_TEMPLATE, 0, "running");
}
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) {
// Return cached image if present
Image cached = imageCache.get(path);
if (cached != null) {
return cached;
}
// Attempt to load the requested resource
java.io.InputStream is = getClass().getResourceAsStream(path);
if (is == null) {
LOGGER.debug(
"Image resource not found: {}. Falling back to {}",
path,
BUTTON_FALLBACK_IMAGE);
is = getClass().getResourceAsStream(BUTTON_FALLBACK_IMAGE);
}
Image loaded = null;
try {
if (is != null) {
loaded = new Image(is);
} else {
LOGGER.error(
"Both requested image '{}' and fallback '{}' are missing. No image will be set.",
path,
BUTTON_FALLBACK_IMAGE);
}
} catch (Exception e) {
LOGGER.error("Failed to load image '{}'", path, e);
}
if (loaded == null) {
// leave
// null
}
if (loaded != null) {
imageCache.put(path, loaded);
}
return loaded;
}
/** Update all lobby buttons' images according to the current lobby statuses. */
public void updateLobbyButtonImages() {
// Run on FX thread to be safe
javafx.application.Platform.runLater(
() -> {
Map<Integer, Integer> mapping = translationManager.getButtonIdToLobbyId();
for (Map.Entry<Integer, Integer> entry : mapping.entrySet()) {
int buttonId = entry.getKey();
int lobbyId = entry.getValue();
LobbyStatus status = getLobbyStatus(lobbyId);
String path = getImagePathForButton(buttonId, status);
for (Node node : gridPane.getChildren()) {
if (node instanceof Button
&& ("lobbyBtn-" + buttonId).equals(node.getId())) {
Button btn = (Button) node;
ImageView iv = new ImageView(safeLoadImage(path));
iv.setPreserveRatio(true);
iv.fitWidthProperty()
.bind(
gridPane.widthProperty()
.divide(COLS)
.subtract(BUTTON_WIDTH_MARGIN));
iv.setSmooth(true);
btn.setGraphic(iv);
break;
}
}
}
});
}
/**
* Creates a new lobby via the LobbyClient.
* *
* @return The generated lobbyId * @return The generated lobbyId
*/ */
public int createLobby() { public int createLobby() {
try { // TODO: Replace with actual lobby creation logic
int lobbyId = lobbyClient.createLobby(); int lobbyId = (int) (Math.random() * MAX_RANDOM_LOBBY_ID + 1);
LOGGER.info("Lobby created via LobbyClient: {}", lobbyId); LOGGER.info("Lobby created: {}", lobbyId);
return lobbyId; return lobbyId;
} catch (Exception e) {
LOGGER.error("Failed to create lobby via LobbyClient: {}", e.getMessage());
// Fallback: generate a local id so UI can continue to work in offline mode
int lobbyId = (int) (Math.random() * MAX_RANDOM_LOBBY_ID + 1);
LOGGER.warn("Falling back to local generated lobby id: {}", lobbyId);
return lobbyId;
}
} }
/** /**
@@ -286,41 +115,20 @@ public class LobbyButtonGridManager {
* @param lobbyId The lobbyId to join * @param lobbyId The lobbyId to join
*/ */
public void joinLobby(int lobbyId) { public void joinLobby(int lobbyId) {
// Request server to join the lobby (blackbox client may throw on failure) // Game-UI starten und Lobby-UI schließen
LOGGER.info("Joining lobby: {}", lobbyId); LOGGER.info("Joining lobby: {}", lobbyId);
try {
lobbyClient.joinLobby(lobbyId);
} catch (Exception e) {
LOGGER.error("LobbyClient failed to join lobby {}: {}", lobbyId, e.getMessage());
return;
}
javafx.application.Platform.runLater( javafx.application.Platform.runLater(
() -> { () -> {
// Hide lobby stage (do not close) so we can return later // Lobby-Stage schließen
javafx.scene.Scene scene = gridPane.getScene(); javafx.stage.Stage currentStage =
javafx.stage.Stage currentStage = (javafx.stage.Stage) scene.getWindow(); (javafx.stage.Stage) gridPane.getScene().getWindow();
currentStage.hide(); currentStage.close();
// Prepare game stage and set a handler so that when it is closed the lobby is // Game-UI starten
// shown and updated
javafx.stage.Stage gameStage = new javafx.stage.Stage();
gameStage.setOnHidden(
ev -> {
try {
currentStage.show();
updateLobbyButtonImages();
} catch (Exception ex) {
LOGGER.error(
"Error while returning to lobby: {}", ex.getMessage());
}
});
// Start the Game UI using the prepared stage
try { try {
new ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.CasinoGameUI() new ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.CasinoGameUI()
.start(gameStage); .start(new javafx.stage.Stage());
} catch (Exception e) { } catch (Exception e) {
LOGGER.error("Error starting Game UI: {}", e.getMessage()); LOGGER.error("Fehler beim Starten der Game-UI: {}", e.getMessage());
// If starting fails, show the lobby again
currentStage.show();
} }
}); });
} }
@@ -333,5 +141,4 @@ public class LobbyButtonGridManager {
public javafx.scene.layout.GridPane getGridPane() { public javafx.scene.layout.GridPane getGridPane() {
return gridPane; return gridPane;
} }
} }
@@ -1,14 +1,17 @@
package ch.unibas.dmi.dbis.cs108.casono.server; 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.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.CheckUsernameParser;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.check_nick.CheckUsernameRequest; 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.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.ListUsersParser;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.list_users.ListUsersRequest; import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.list_users.ListUsersRequest;
@@ -21,9 +24,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.PingHandler;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.ping.PingParser; 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.ping.PingRequest;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.send_message.SendMessageHandler; import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.raise.PlayerRaiseHandler;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.send_message.SendMessageParser; import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.raise.PlayerRaiseParser;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.send_message.SendMessageRequest; import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.raise.PlayerRaiseRequest;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.UserCleanupJob; 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.domain.user.UserRegistry;
import ch.unibas.dmi.dbis.cs108.casono.server.network.NetworkManager; import ch.unibas.dmi.dbis.cs108.casono.server.network.NetworkManager;
@@ -35,107 +38,115 @@ import ch.unibas.dmi.dbis.cs108.casono.server.network.events.EventBus;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.dispatcher.ResponseDispatcher; import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.dispatcher.ResponseDispatcher;
import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionDisconnectJob; import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionDisconnectJob;
import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionManager; import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionManager;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.time.Duration; import java.time.Duration;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/** Application class for starting the server. */ /** Application class for starting the server. */
public class ServerApp { public class ServerApp {
private static final int USER_CLEANUP_JOB_DELAY = 0; 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_PERIOD = 10;
private static final int USER_CLEANUP_JOB_RECONNECT_THRESHOLD = 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_DELAY = 0;
private static final int SESSION_DISCONNECT_JOB_PERIOD = 2; private static final int SESSION_DISCONNECT_JOB_PERIOD = 2;
private static final int SESSION_DISCONNECT_JOB_TIMEOUT = 5; private static final int SESSION_DISCONNECT_JOB_TIMEOUT = 5;
public static void start(String arg) { public static void start(String arg) {
int port = Integer.parseInt(arg); int port = Integer.parseInt(arg);
Logger logger = LogManager.getLogger(ServerApp.class); Logger logger = LogManager.getLogger(ServerApp.class);
logger.info("Starting server at port {}", port); logger.info("Starting server at port {}", port);
EventBus eventBus = new EventBus(); EventBus eventBus = new EventBus();
CommandParserDispatcher dispatcher = new CommandParserDispatcher(); CommandParserDispatcher dispatcher = new CommandParserDispatcher();
SessionManager sessionManager = new SessionManager(eventBus, dispatcher); SessionManager sessionManager = new SessionManager(eventBus, dispatcher);
ResponseDispatcher responseDispatcher = new ResponseDispatcher(sessionManager); ResponseDispatcher responseDispatcher = new ResponseDispatcher(sessionManager);
CommandHandlerExecutor handlerExecutor = new CommandHandlerExecutor(responseDispatcher); CommandHandlerExecutor handlerExecutor = new CommandHandlerExecutor(responseDispatcher);
CommandRouter router = new CommandRouter(handlerExecutor); 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(); UserRegistry userRegistry = new UserRegistry();
eventBus.subscribe( eventBus.subscribe(
DisconnectEvent.class, event -> userRegistry.onDisconnect(event.sessionId())); DisconnectEvent.class, event -> userRegistry.onDisconnect(event.sessionId()));
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
scheduler.scheduleAtFixedRate( scheduler.scheduleAtFixedRate(
new UserCleanupJob( new UserCleanupJob(
userRegistry, Duration.ofSeconds(USER_CLEANUP_JOB_RECONNECT_THRESHOLD)), userRegistry, Duration.ofSeconds(USER_CLEANUP_JOB_RECONNECT_THRESHOLD)),
USER_CLEANUP_JOB_DELAY, USER_CLEANUP_JOB_DELAY,
USER_CLEANUP_JOB_PERIOD, USER_CLEANUP_JOB_PERIOD,
TimeUnit.SECONDS); TimeUnit.SECONDS);
scheduler.scheduleAtFixedRate( scheduler.scheduleAtFixedRate(
new SessionDisconnectJob( new SessionDisconnectJob(
sessionManager, sessionManager,
eventBus, eventBus,
Duration.ofSeconds(SESSION_DISCONNECT_JOB_TIMEOUT)), Duration.ofSeconds(SESSION_DISCONNECT_JOB_TIMEOUT)),
SESSION_DISCONNECT_JOB_DELAY, SESSION_DISCONNECT_JOB_DELAY,
SESSION_DISCONNECT_JOB_PERIOD, SESSION_DISCONNECT_JOB_PERIOD,
TimeUnit.SECONDS); TimeUnit.SECONDS);
registerCommands(dispatcher, router, responseDispatcher, userRegistry); registerCommands(dispatcher, router, responseDispatcher, userRegistry);
NetworkManager networkManager = new NetworkManager(port, sessionManager, router); NetworkManager networkManager = new NetworkManager(port, sessionManager, router);
networkManager.start(); networkManager.start();
} }
/** /**
* Registers command parsers and handlers. * Registers command parsers and handlers.
* *
* @param parserDispatcher the dispatcher responsible for parsing incoming commands * @param parserDispatcher the dispatcher responsible for parsing incoming
* @param commandRouter the router that dispatches parsed commands to appropriate handlers * commands
* @param responseDispatcher the dispatcher responsible for sending responses back to clients * @param commandRouter the router that dispatches parsed commands to
*/ * appropriate handlers
private static void registerCommands( * @param responseDispatcher the dispatcher responsible for sending responses
CommandParserDispatcher parserDispatcher, * back to clients
CommandRouter commandRouter, */
ResponseDispatcher responseDispatcher, private static void registerCommands(
UserRegistry userRegistry) { CommandParserDispatcher parserDispatcher,
parserDispatcher.register("PING", new PingParser()); CommandRouter commandRouter,
commandRouter.register(PingRequest.class, new PingHandler(responseDispatcher)); ResponseDispatcher responseDispatcher,
UserRegistry userRegistry) {
parserDispatcher.register("PING", new PingParser());
commandRouter.register(PingRequest.class, new PingHandler(responseDispatcher));
parserDispatcher.register("CHECK_USERNAME", new CheckUsernameParser()); parserDispatcher.register("CHECK_USERNAME", new CheckUsernameParser());
commandRouter.register( commandRouter.register(
CheckUsernameRequest.class, CheckUsernameRequest.class,
new CheckUsernameHandler(responseDispatcher, userRegistry)); new CheckUsernameHandler(responseDispatcher, userRegistry));
parserDispatcher.register("LOGIN", new LoginParser()); parserDispatcher.register("LOGIN", new LoginParser());
commandRouter.register( commandRouter.register(
LoginRequest.class, new LoginHandler(responseDispatcher, userRegistry)); LoginRequest.class, new LoginHandler(responseDispatcher, userRegistry));
parserDispatcher.register("LOGOUT", new LogoutParser()); parserDispatcher.register("LOGOUT", new LogoutParser());
commandRouter.register( commandRouter.register(
LogoutRequest.class, new LogoutHandler(responseDispatcher, userRegistry)); LogoutRequest.class, new LogoutHandler(responseDispatcher, userRegistry));
parserDispatcher.register("SEND_MESSAGE", new SendMessageParser()); parserDispatcher.register("LIST_USERS", new ListUsersParser());
commandRouter.register( commandRouter.register(
SendMessageRequest.class, new SendMessageHandler(responseDispatcher, userRegistry)); ListUsersRequest.class, new ListUsersHandler(responseDispatcher, userRegistry));
parserDispatcher.register("GET_MESSAGE_COUNT", new GetMessageCountParser()); // ADD_PLAYER
commandRouter.register( parserDispatcher.register("ADD_PLAYER", new AddPlayerParser());
GetMessageCountRequest.class, commandRouter.register(AddPlayerRequest.class,
new GetMessageCountHandler(responseDispatcher, userRegistry)); new AddPlayerHandler(userRegistry, responseDispatcher));
parserDispatcher.register("GET_NEXT_MESSAGE", new GetNextMessageParser()); // BET
commandRouter.register( parserDispatcher.register("BET", new PlayerBetParser());
GetNextMessageRequest.class, commandRouter.register(PlayerBetRequest.class,
new GetNextMessageHandler(responseDispatcher, userRegistry)); new PlayerBetHandler(responseDispatcher));
parserDispatcher.register("LIST_USERS", new ListUsersParser()); // CALL
commandRouter.register( parserDispatcher.register("CALL", new PlayerCallParser());
ListUsersRequest.class, new ListUsersHandler(responseDispatcher, userRegistry)); commandRouter.register(PlayerCallRequest.class,
} new PlayerCallHandler(responseDispatcher));
// RAISE
parserDispatcher.register("RAISE", new PlayerRaiseParser());
commandRouter.register(PlayerRaiseRequest.class,
new PlayerRaiseHandler(responseDispatcher));
}
} }
@@ -1,12 +1,15 @@
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.check_nick; package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.check_nick;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.User; 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.domain.user.UserRegistry;
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler; import ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.dispatcher.ResponseDispatcher; import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.dispatcher.ResponseDispatcher;
import java.util.Optional; import java.util.Optional;
/** Handles {@link CheckUsernameRequest}s to check whether a username is available. */ /** Handles {@link CheckUsernameRequest}s to check whether a username is available. */
public class CheckUsernameHandler extends CommandHandler<CheckUsernameRequest> { public class CheckUsernameHandler extends CommandHandler<CheckUsernameRequest> {
private final UserRegistry userRegistry; private final UserRegistry userRegistry;
/** /**
* Creates a new handler for checking username availability. * Creates a new handler for checking username availability.
* *
@@ -17,6 +20,7 @@ public class CheckUsernameHandler extends CommandHandler<CheckUsernameRequest> {
super(responseDispatcher); super(responseDispatcher);
this.userRegistry = userRegistry; this.userRegistry = userRegistry;
} }
/** /**
* Executes the username availability check for the given request. * Executes the username availability check for the given request.
* *
@@ -36,4 +40,4 @@ public class CheckUsernameHandler extends CommandHandler<CheckUsernameRequest> {
} }
responseDispatcher.dispatch(new CheckUsernameResponse(request.getContext(), availability)); responseDispatcher.dispatch(new CheckUsernameResponse(request.getContext(), availability));
} }
} }
@@ -1,7 +1,9 @@
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.check_nick; package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.check_nick;
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.CommandParser; 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.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.RequestParameterAccessor;
/** Parses a primitive request into a {@link CheckUsernameRequest}. */ /** Parses a primitive request into a {@link CheckUsernameRequest}. */
public class CheckUsernameParser implements CommandParser<CheckUsernameRequest> { public class CheckUsernameParser implements CommandParser<CheckUsernameRequest> {
/** /**
@@ -16,4 +18,4 @@ public class CheckUsernameParser implements CommandParser<CheckUsernameRequest>
new RequestParameterAccessor(primitiveRequest.parameters()); new RequestParameterAccessor(primitiveRequest.parameters());
return new CheckUsernameRequest(primitiveRequest.context(), accessor.require("USERNAME")); return new CheckUsernameRequest(primitiveRequest.context(), accessor.require("USERNAME"));
} }
} }
@@ -1,9 +1,12 @@
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.check_nick; package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.check_nick;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.Request; import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.Request;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext; import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext;
/** Request implementation used to check whether a username is available or already taken */ /** Request implementation used to check whether a username is available or already taken */
public class CheckUsernameRequest extends Request { public class CheckUsernameRequest extends Request {
private final String username; private final String username;
/** /**
* Constructs a new CheckUsernameRequest with the given context and username to check * Constructs a new CheckUsernameRequest with the given context and username to check
* *
@@ -15,6 +18,7 @@ public class CheckUsernameRequest extends Request {
super(context); super(context);
this.username = username; this.username = username;
} }
/** /**
* Returns the provided username in the request * Returns the provided username in the request
* *
@@ -23,4 +27,4 @@ public class CheckUsernameRequest extends Request {
public String getUsername() { public String getUsername() {
return username; return username;
} }
} }
@@ -3,6 +3,7 @@ package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.check_nick;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext; 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.SuccessResponse;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.builder.ResponseBodyBuilder; import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.builder.ResponseBodyBuilder;
/** Response indicating the availability status of a username check. */ /** Response indicating the availability status of a username check. */
public class CheckUsernameResponse extends SuccessResponse { public class CheckUsernameResponse extends SuccessResponse {
/** /**
@@ -14,4 +15,4 @@ public class CheckUsernameResponse extends SuccessResponse {
public CheckUsernameResponse(RequestContext context, UsernameAvailability availability) { public CheckUsernameResponse(RequestContext context, UsernameAvailability availability) {
super(context, new ResponseBodyBuilder().param("STATUS", availability).build()); super(context, new ResponseBodyBuilder().param("STATUS", availability).build());
} }
} }
@@ -1,7 +1,7 @@
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.check_nick; package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.check_nick;
/** Represents the availability status of a username */ /** Represents the availability status of a username */
public enum UsernameAvailability { enum UsernameAvailability {
/** Username is available */ /** Username is available */
FREE, FREE,
@@ -0,0 +1,33 @@
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()));
}
}
@@ -0,0 +1,20 @@
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);
}
}
@@ -0,0 +1,24 @@
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;
}
}
@@ -0,0 +1,27 @@
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()));
}
}
@@ -0,0 +1,17 @@
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);
}
}
@@ -0,0 +1,18 @@
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;
}
}
@@ -0,0 +1,21 @@
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()));
}
}
@@ -0,0 +1,12 @@
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());
}
}
@@ -0,0 +1,11 @@
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);
}
}
@@ -0,0 +1,19 @@
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()));
}
}
@@ -0,0 +1,12 @@
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());
}
}
@@ -0,0 +1,11 @@
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);
}
}
@@ -0,0 +1,29 @@
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()));
}
}
@@ -0,0 +1,17 @@
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);
}
}
@@ -0,0 +1,18 @@
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;
}
}
@@ -1,49 +0,0 @@
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);
}
}
}
@@ -1,22 +0,0 @@
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());
}
}
@@ -1,17 +0,0 @@
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);
}
}
@@ -1,18 +0,0 @@
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());
}
}
@@ -1,49 +0,0 @@
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);
}
}
}
@@ -1,22 +0,0 @@
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());
}
}
@@ -1,17 +0,0 @@
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);
}
}
@@ -1,19 +0,0 @@
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()));
}
}
@@ -1,47 +0,0 @@
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));
}
}
@@ -1,22 +0,0 @@
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);
}
}
@@ -1,30 +0,0 @@
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,15 @@
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,11 +1,10 @@
package ch.unibas.dmi.dbis.cs108.casono.server.domain.user; package ch.unibas.dmi.dbis.cs108.casono.server.domain.user;
import ch.unibas.dmi.dbis.cs108.casono.client.chat.Message; import ch.unibas.dmi.dbis.cs108.casono.server.domain.message.Message;
import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionId; import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionId;
import java.time.Instant; import java.time.Instant;
import java.util.ArrayDeque; import java.util.ArrayDeque;
import java.util.List; import java.util.List;
import java.util.NoSuchElementException;
import java.util.Optional; import java.util.Optional;
import java.util.Queue; import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ConcurrentLinkedQueue;
@@ -89,14 +88,6 @@ public class User {
messages.add(message); 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) { public synchronized List<Message> dequeueAllMessages(Message message) {
List<Message> allMessages = new ArrayDeque<>(messages).stream().toList(); List<Message> allMessages = new ArrayDeque<>(messages).stream().toList();
messages.clear(); messages.clear();
@@ -6,7 +6,7 @@ package ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.accessor
* @param <T> target type produced by the parser * @param <T> target type produced by the parser
*/ */
@FunctionalInterface @FunctionalInterface
interface ThrowingParser<T> { public interface ThrowingParser<T> {
/** /**
* Parses the provided raw parameter value. * Parses the provided raw parameter value.
* *
@@ -9,14 +9,14 @@ import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.builder.
* <p>Implementations of this class use the {@code +OK} prefix. It provides a protected constructor * <p>Implementations of this class use the {@code +OK} prefix. It provides a protected constructor
* so subclasses can supply the response body content. * so subclasses can supply the response body content.
*/ */
public class SuccessResponse extends Response { public abstract class SuccessResponse extends Response {
/** /**
* Create a successful response with the provided body. * Create a successful response with the provided body.
* *
* @param context the RequestContext of the request * @param context the RequestContext of the request
* @param body the response body * @param body the response body
*/ */
public SuccessResponse(RequestContext context, ResponseBody body) { protected SuccessResponse(RequestContext context, ResponseBody body) {
super(context, body); super(context, body);
} }
@@ -5,37 +5,37 @@
<?import javafx.geometry.Insets?> <?import javafx.geometry.Insets?>
<?import javafx.scene.image.*?> <?import javafx.scene.image.*?>
<!-- Hauptcontainer: Verknüpft die UI mit dem CasinoGameController und lädt casinogameui.css --> <!-- Main container: Links the UI to the CasinoGameController and loads Casinogameui.css -->
<AnchorPane xmlns="http://javafx.com/javafx/21" <AnchorPane xmlns="http://javafx.com/javafx/21"
xmlns:fx="http://javafx.com/fxml/1" xmlns:fx="http://javafx.com/fxml/1"
fx:controller="ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.CasinoGameController" fx:controller="ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.CasinoGameController"
styleClass="background" 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"> <GridPane prefWidth="1200" prefHeight="800" AnchorPane.topAnchor="0" AnchorPane.bottomAnchor="0" AnchorPane.leftAnchor="0" AnchorPane.rightAnchor="0">
<columnConstraints> <columnConstraints>
<!-- Tisch-Box: 70% --> <!-- Table-Box: 70% -->
<ColumnConstraints percentWidth="70.0" hgrow="ALWAYS" /> <ColumnConstraints percentWidth="70.0" hgrow="ALWAYS" />
<!-- Leere-Box: 5% --> <!-- Table-Box: 5% -->
<ColumnConstraints percentWidth="5.0" hgrow="ALWAYS" /> <ColumnConstraints percentWidth="5.0" hgrow="ALWAYS" />
<!-- Chat-Box: 20% --> <!-- Table-Box: 20% -->
<ColumnConstraints percentWidth="25.0" hgrow="ALWAYS" /> <ColumnConstraints percentWidth="25.0" hgrow="ALWAYS" />
</columnConstraints> </columnConstraints>
<!-- Sorgt dafür, dass diese Zeile den gesamten verfügbaren vertikalen Platz nutzt --> <!-- Ensures that this line uses all available vertical space -->
<rowConstraints> <rowConstraints>
<RowConstraints vgrow="ALWAYS" /> <RowConstraints vgrow="ALWAYS" />
</rowConstraints> </rowConstraints>
<children> <children>
<!-- Tisch-Box in Spalte 0 --> <!-- Table box in column 0 -->
<StackPane GridPane.columnIndex="0"> <StackPane GridPane.columnIndex="0">
<!-- Innenabstand: Hält den Inhalt 10 Pixel von oben/unten und 20 Pixel von den Seiten fern --> <!-- Inner spacing: Keeps the content 10 pixels away from the top/bottom and 20 pixels from the sides -->
<padding> <padding>
<Insets top="20" right="10" bottom="120" left="20"/> <Insets top="20" right="10" bottom="120" left="20"/>
</padding> </padding>
<!-- Zentraler Casinotisch: Zentriert den Inhalt und nutzt durch die maximale Größe (1.7976931348623157E308) das gesamte Fenster --> <!-- Central Casino Table: Centers the content and utilizes the entire window due to its maximum size (1.7976931348623157E308) -->
<VBox fx:id="casinoTable" <VBox fx:id="casinoTable"
alignment="CENTER" alignment="CENTER"
styleClass="casino-table" styleClass="casino-table"
@@ -44,7 +44,7 @@
maxWidth="Infinity" maxWidth="Infinity"
maxHeight="Infinity"> maxHeight="Infinity">
<!-- TODO: Platzhalter für die Poker-Spielfläche: Hier werden später dynamisch Karten, Chips und Einsätze der Gegner angezeigt --> <!-- TODO: Placeholder for the poker playing area: Cards, chips and opponents' bets will be dynamically displayed here later -->
<HBox alignment="CENTER" spacing="15"> <HBox alignment="CENTER" spacing="15">
<VBox styleClass="dealer-box" prefWidth="80" prefHeight="120" /> <VBox styleClass="dealer-box" prefWidth="80" prefHeight="120" />
<VBox styleClass="dealer-box" prefWidth="80" prefHeight="120" /> <VBox styleClass="dealer-box" prefWidth="80" prefHeight="120" />
@@ -52,7 +52,7 @@
</HBox> </HBox>
<VBox alignment="CENTER" spacing="10"> <VBox alignment="CENTER" spacing="10">
<!-- Label mit Logo --> <!-- Label with logo -->
<Label text="CAS0NO" styleClass="table-title"> <Label text="CAS0NO" styleClass="table-title">
<graphic> <graphic>
<ImageView fitHeight="30.0" preserveRatio="true"> <ImageView fitHeight="30.0" preserveRatio="true">
@@ -63,32 +63,32 @@
</graphic> </graphic>
</Label> </Label>
<!-- TODO: Platzhalter: Wird ersetzt, sobald die GameEngine fertig ist und echte Einsätze verarbeiten kann --> <!-- TODO: Placeholder: Will be replaced once the game engine is finished and can process real-world scenarios -->
<Label fx:id="welcomeText" text="Setzen Sie Ihren Einsatz" styleClass="table-title" /> <Label fx:id="welcomeText" text="Setzen Sie Ihren Einsatz" styleClass="table-title" />
</VBox> </VBox>
</VBox> </VBox>
</StackPane> </StackPane>
<!-- Leere-Box in Spalte 1: Schafft Platz zwischen dem Tisch-Box und der Chat-Box --> <!-- Empty box in column 1: Creates space between the table box and the chat box -->
<VBox GridPane.columnIndex="1" <VBox GridPane.columnIndex="1"
alignment="CENTER" alignment="CENTER"
minWidth="100" minWidth="100"
minHeight="100"> minHeight="100">
<!-- Bleibt leer --> <!-- Leave blank -->
</VBox> </VBox>
<!-- TODO: Platzhalter: Chat-Box in Spalte 2: --> <!-- TODO: Placeholder: Chat box in column 2: -->
<fx:include source="components/Chatbox.fxml" GridPane.columnIndex="2"/> <fx:include source="components/Chatbox.fxml" GridPane.columnIndex="2"/>
</children> </children>
</GridPane> </GridPane>
<!-- verschiebbare Taskbar --> <!-- movable taskbar -->
<AnchorPane> <AnchorPane>
<fx:include fx:id="taskbarInclude" source="gameuicomponents/taskbar.fxml"/> <fx:include fx:id="taskbarInclude" source="gameuicomponents/Taskbar.fxml"/>
</AnchorPane> </AnchorPane>
<!-- Gegner-Status 1: Eines von drei Panels, das Icon, Name und Kontostand des Mitspielers anzeigt --> <!-- Opponent Status 1: One of three panels displaying the opponent's icon, name, and account balance -->
<GridPane AnchorPane.leftAnchor="0.0" <GridPane AnchorPane.leftAnchor="0.0"
AnchorPane.rightAnchor="0.0" AnchorPane.rightAnchor="0.0"
AnchorPane.topAnchor="50"> AnchorPane.topAnchor="50">
@@ -99,11 +99,11 @@
</columnConstraints> </columnConstraints>
<children> <children>
<fx:include source="gameuicomponents/playerstatus.fxml" GridPane.columnIndex="1"/> <fx:include source="gameuicomponents/Playerstatus.fxml" GridPane.columnIndex="1"/>
</children> </children>
</GridPane> </GridPane>
<!-- Gegner-Status 2: Eines von drei Panels, das Icon, Name und Kontostand des Mitspielers anzeigt --> <!-- Opponent Status 2: One of three panels displaying the opponent's icon, name, and account balance -->
<GridPane AnchorPane.leftAnchor="0.0" <GridPane AnchorPane.leftAnchor="0.0"
AnchorPane.rightAnchor="0.0" AnchorPane.rightAnchor="0.0"
AnchorPane.topAnchor="20"> AnchorPane.topAnchor="20">
@@ -114,11 +114,11 @@
</columnConstraints> </columnConstraints>
<children> <children>
<fx:include source="gameuicomponents/playerstatus.fxml" GridPane.columnIndex="1"/> <fx:include source="gameuicomponents/Playerstatus.fxml" GridPane.columnIndex="1"/>
</children> </children>
</GridPane> </GridPane>
<!-- Gegner-Status 3: Eines von drei Panels, das Icon, Name und Kontostand des Mitspielers anzeigt --> <!-- Opponent Status 3: One of three panels that displays the opponent's icon, name, and account balance -->
<GridPane AnchorPane.leftAnchor="0.0" <GridPane AnchorPane.leftAnchor="0.0"
AnchorPane.rightAnchor="0.0" AnchorPane.rightAnchor="0.0"
AnchorPane.topAnchor="50"> AnchorPane.topAnchor="50">
@@ -129,7 +129,7 @@
</columnConstraints> </columnConstraints>
<children> <children>
<fx:include source="gameuicomponents/playerstatus.fxml" GridPane.columnIndex="1"/> <fx:include source="gameuicomponents/Playerstatus.fxml" GridPane.columnIndex="1"/>
</children> </children>
</GridPane> </GridPane>
</AnchorPane> </AnchorPane>
@@ -106,7 +106,7 @@
</VBox> </VBox>
<!-- RECHTER BEREICH (Info-Box) --> <!-- RECHTER BEREICH (Info-Box) -->
<fx:include source="components/chatui/chatbox.fxml" GridPane.columnIndex="2"/> <fx:include source="components/Chatbox.fxml" GridPane.columnIndex="2"/>
</children> </children>
</GridPane> </GridPane>
</AnchorPane> </AnchorPane>
@@ -0,0 +1,57 @@
<?xml version="1.0" encoding="UTF-8"?>
<?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.
The chat internally distinguishes between:
- Player-to-Player (Private 2-person chat)
- Lobby Chat (Current room)
- Global Chat (Entire server)
Features planned for later implementation:
- Stylish chat bubbles with names and timestamps.
-->
<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"
alignment="CENTER"
stylesheets="@Chatui.css">
<!-- Inner spacing: Creates 30 pixels of space at the top, right and bottom, only 10 pixels on the left (asymmetrical) -->
<padding>
<Insets top="30" right="30" bottom="30" left="10"/>
</padding>
<children>
<VBox VBox.vgrow="ALWAYS"
styleClass="chat-box"
spacing="10"
maxWidth="Infinity">
<children>
<Label text="== CHAT ==" styleClass="chat-header" alignment="CENTER" maxWidth="Infinity"/>
<Region minHeight="4" styleClass="chat-separator"/>
<!-- Chat messages will be added here -->
<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 -->
<HBox spacing="10">
<!-- TODO: Dynamically adjust the size of the text field -->
<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"/>
</HBox>
</children>
</VBox>
</children>
</VBox>
@@ -1,31 +0,0 @@
<?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>
@@ -1,34 +0,0 @@
<?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>
@@ -1,206 +0,0 @@
.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,9 +0,0 @@
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);
}
}
@@ -1,38 +0,0 @@
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";
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();
}
}
@@ -1,39 +0,0 @@
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());
}
}