Compare commits

...

7 Commits

Author SHA1 Message Date
Jona Walpert 190b30672a Feat: Added CHANGE_USERNAME server command, registration and documentation 2026-04-15 20:49:29 +02:00
Jona Walpert 497820db05 Merge branch 'fix/lobby-auto-cleanup' into 'main'
Feat: Auto-cleanup lobbies when game finishes

See merge request cs108-fs26/Gruppe-13!129
2026-04-14 21:35:43 +00:00
Jona Walpert 88c9431bf0 Feat: Auto-cleanup lobbies when game finishes
- Add countNonFoldedPlayers() to GameState for fold-only victory detection
- Auto-transition to FINISHED phase when betting ends
- Clean up lobby (remove players, reset gameController) when game finishes
- Lobby now switches from RUNNING to CREATED and disappears immediately

Fixes: lobby not disappearing after game ends
2026-04-14 23:33:01 +02:00
Jona Walpert 5a53a14933 Merge branch 'feat/use-real-lobby-id' into 'main'
Feat: uses real lobby id and not hardcoded id

See merge request cs108-fs26/Gruppe-13!128
2026-04-14 21:31:12 +00:00
Jona Walpert f7f66ec5db Feat: uses real lobby id and not hardcoded id 2026-04-14 23:15:39 +02:00
Jona Walpert d85b3d21ee Merge branch 'feat/117-send-winner-with-get-gamestate-command' into 'main'
Feat: Winner is now sent to the frontend with get_gamestate

Closes #117

See merge request cs108-fs26/Gruppe-13!127
2026-04-14 21:13:57 +00:00
Jona Walpert 382383d38b Feat: Winenr is now sent to the frontend with get_gamestate 2026-04-14 23:09:48 +02:00
12 changed files with 425 additions and 47 deletions
@@ -1,8 +1,9 @@
# Protocol Document # Protocol Document
This document describes the protocol for client-server communication in our application. It defines the structure of requests and responses, the supported commands along with their request parameters, response formats, and possible errors. This document describes the protocol for client-server communication in our application. It defines the structure of requests and responses, the supported commands along with their request parameters, response formats, and possible errors.
# Table of Contents # Table of Contents
- [Protocol Document](#protocol-document) - [Protocol Document](#protocol-document)
- [Table of Contents](#table-of-contents) - [Table of Contents](#table-of-contents)
- [General structure of requests](#general-structure-of-requests) - [General structure of requests](#general-structure-of-requests)
@@ -36,20 +37,27 @@ This document describes the protocol for client-server communication in our appl
- [Error Response](#error-response-4) - [Error Response](#error-response-4)
- [Example Request](#example-request-2) - [Example Request](#example-request-2)
- [Example Response](#example-response-2) - [Example Response](#example-response-2)
- [LOGOUT command](#logout-command) - [CHANGE\_USERNAME command](#change_username-command)
- [Required pre-execution checks](#required-pre-execution-checks-3) - [Required pre-execution checks](#required-pre-execution-checks-3)
- [Request Parameters](#request-parameters-3) - [Request Parameters](#request-parameters-3)
- [Success Response](#success-response-3) - [Success Response](#success-response-3)
- [Error Response](#error-response-5) - [Error Response](#error-response-5)
- [Example Request](#example-request-3) - [Example Request](#example-request-3)
- [Example Response](#example-response-3) - [Example Response](#example-response-3)
- [LIST\_USERS command](#list_users-command) - [LOGOUT command](#logout-command)
- [Required pre-execution checks](#required-pre-execution-checks-4) - [Required pre-execution checks](#required-pre-execution-checks-4)
- [Request Parameters](#request-parameters-4) - [Request Parameters](#request-parameters-4)
- [Success Response](#success-response-4) - [Success Response](#success-response-4)
- [Error Response](#error-response-6) - [Error Response](#error-response-6)
- [Example Request](#example-request-4) - [Example Request](#example-request-4)
- [Example Response](#example-response-4) - [Example Response](#example-response-4)
- [LIST\_USERS command](#list_users-command)
- [Required pre-execution checks](#required-pre-execution-checks-5)
- [Request Parameters](#request-parameters-5)
- [Success Response](#success-response-5)
- [Error Response](#error-response-7)
- [Example Request](#example-request-5)
- [Example Response](#example-response-5)
- [GET_LOBBY_LIST command](#get_lobby_list-command) - [GET_LOBBY_LIST command](#get_lobby_list-command)
- [Required pre-execution checks](#required-pre-execution-checks) - [Required pre-execution checks](#required-pre-execution-checks)
- [Request Parameters](#request-parameters) - [Request Parameters](#request-parameters)
@@ -91,6 +99,7 @@ This document describes the protocol for client-server communication in our appl
<!-- Please see the comments for copy n' paste ready examples --> <!-- Please see the comments for copy n' paste ready examples -->
# General structure of requests # General structure of requests
As mentioned before, our protocol is based on POP3. As mentioned before, our protocol is based on POP3.
Each command is represented as a single line of text, starting with the command name followed by parameters. The server responds with a status line indicating success or failure, followed by the body. Each command is represented as a single line of text, starting with the command name followed by parameters. The server responds with a status line indicating success or failure, followed by the body.
@@ -99,41 +108,43 @@ Requests can have parameters that provide additional information for the command
Responses are collections of key-value pairs, containing either a value or another collection, allowing for nested structures. Responses are collections of key-value pairs, containing either a value or another collection, allowing for nested structures.
Each collection is ended with the `END` keyword. Each collection is ended with the `END` keyword.
# Preconditions # Preconditions
The serverside pipeline to process incoming requests consists of multiple stages. The serverside pipeline to process incoming requests consists of multiple stages.
Each of these stages can yield an error response if the request does not meet the requirements of that stage. Each of these stages can yield an error response if the request does not meet the requirements of that stage.
## Parsing ## Parsing
One of these stages is the parsing. It is responsible for parsing the raw request into a structured format that can be easily processed by the command handlers. It validates the syntax of the request as well. One of these stages is the parsing. It is responsible for parsing the raw request into a structured format that can be easily processed by the command handlers. It validates the syntax of the request as well.
### Error Response ### Error Response
| Code | Description | | Code | Description |
| :-------------- | :---------------------------------------------------------------------------------------------- | | :-------------- | :---------------------------------------------------------------------------------------------- |
| `PARSING_ERROR` | The body of the request contains syntax errors (see message field of response for more details) | | `PARSING_ERROR` | The body of the request contains syntax errors (see message field of response for more details) |
## Command dispatching ## Command dispatching
After the request has been successfully parsed, the next stage is to dispatch the `PrimitiveRequest` to the appropriate `CommandParser`. This is done by the `CommandDispatcherDispatcher`, which uses the command name to determine which parser to use. After the request has been successfully parsed, the next stage is to dispatch the `PrimitiveRequest` to the appropriate `CommandParser`. This is done by the `CommandDispatcherDispatcher`, which uses the command name to determine which parser to use.
### Error Response ### Error Response
| Code | Description | | Code | Description |
| :---------------- | :---------------------------------------------------------------- | | :---------------- | :---------------------------------------------------------------- |
| `UNKNOWN_COMMAND` | The command is unknown to this server. No parser has been defined | | `UNKNOWN_COMMAND` | The command is unknown to this server. No parser has been defined |
## Command parsing ## Command parsing
Once the `PrimitiveRequest` has been dispatched to the appropriate `CommandParser`, the parser is responsible for parsing the parameters of the request and creating a `Request` that can be executed by the responsible `CommandHandler`. Once the `PrimitiveRequest` has been dispatched to the appropriate `CommandParser`, the parser is responsible for parsing the parameters of the request and creating a `Request` that can be executed by the responsible `CommandHandler`.
### Error Response ### Error Response
| Code | Description | | Code | Description |
| :------------------ | :------------------------------------------------------------------------------------------------ | | :------------------ | :------------------------------------------------------------------------------------------------ |
| `MISSING_PARAMETER` | A required parameter is missing from the request (see message field of response for more details) | | `MISSING_PARAMETER` | A required parameter is missing from the request (see message field of response for more details) |
# Pre-execution checks # Pre-execution checks
Pre-execution checks are reusable validation steps that can be registered on command handlers. Pre-execution checks are reusable validation steps that can be registered on command handlers.
They are implemented as `HandlerCheck` instances and are executed by the `CommandHandlerExecutor` before the handler's main logic is invoked. They are implemented as `HandlerCheck` instances and are executed by the `CommandHandlerExecutor` before the handler's main logic is invoked.
@@ -148,16 +159,17 @@ Description of the check, what it does and when it should be used.
--> -->
## UserLoggedInCheck ## UserLoggedInCheck
The `UserLoggedInCheck` is a common pre-execution check that verifies whether the user is logged in (i.e. has a user associated with his session). The `UserLoggedInCheck` is a common pre-execution check that verifies whether the user is logged in (i.e. has a user associated with his session).
### Error Response ### Error Response
| Code | Description | | Code | Description |
| :------------------- | :------------------------ | | :------------------- | :------------------------ |
| `USER_NOT_LOGGED_IN` | The user is not logged in | | `USER_NOT_LOGGED_IN` | The user is not logged in |
# Commands # Commands
Commands are the core of our protocol, representing the various actions that clients can request from the server. Each command has a unique name and may require specific parameters in addition to pre-execution checks. Commands are the core of our protocol, representing the various actions that clients can request from the server. Each command has a unique name and may require specific parameters in addition to pre-execution checks.
The server processes these commands and responds accordingly. The server processes these commands and responds accordingly.
@@ -212,41 +224,50 @@ END
--> -->
## PING command ## PING command
The `PING` command is a simple command that can be used to check if the server is responsive. The `PING` command is a simple command that can be used to check if the server is responsive.
### Required pre-execution checks ### Required pre-execution checks
None. None.
### Request Parameters ### Request Parameters
No parameters. No parameters.
### Success Response ### Success Response
No response fields. No response fields.
### Example Request ### Example Request
``` ```
PING PING
``` ```
### Example Response ### Example Response
``` ```
+OK +OK
END END
``` ```
## CHECK_USERNAME command ## CHECK_USERNAME command
The `CHECK_USERNAME` command is used to check if a username is already taken by another user. Additional users can still log in with the same username, but their name will be substituted with a suffix. The `CHECK_USERNAME` command is used to check if a username is already taken by another user. Additional users can still log in with the same username, but their name will be substituted with a suffix.
### Required pre-execution checks ### Required pre-execution checks
None. None.
### Request Parameters ### Request Parameters
| Parameter Name | Type | Optional | Description | | Parameter Name | Type | Optional | Description |
| :------------- | :------- | :------- | :------------------------------------- | | :------------- | :------- | :------- | :------------------------------------- |
| `USERNAME` | `String` | no | The username to check for availability | | `USERNAME` | `String` | no | The username to check for availability |
### Success Response ### Success Response
| Field | Type | Description | | Field | Type | Description |
| :------- | :--------------------------- | :---------------------------------------------------------------------- | | :------- | :--------------------------- | :---------------------------------------------------------------------- |
| `STATUS` | `Enum<UsernameAvailability>` | Member of enum indicating if the username is available or already taken | | `STATUS` | `Enum<UsernameAvailability>` | Member of enum indicating if the username is available or already taken |
@@ -257,11 +278,13 @@ None.
| `TAKEN` | Username is already in use | | `TAKEN` | Username is already in use |
### Example Request ### Example Request
``` ```
CHECK_USERNAME USERNAME='Lars' CHECK_USERNAME USERNAME='Lars'
``` ```
### Example Response ### Example Response
``` ```
+OK +OK
STATUS=FREE STATUS=FREE
@@ -269,33 +292,40 @@ END
``` ```
## LOGIN command ## LOGIN command
The `LOGIN` command is used to log in a user with a specified username. If the username is already taken by another user, the server will append a suffix to the username to make it unique. The `LOGIN` command is used to log in a user with a specified username. If the username is already taken by another user, the server will append a suffix to the username to make it unique.
### Required pre-execution checks ### Required pre-execution checks
None. None.
### Request Parameters ### Request Parameters
| Parameter Name | Type | Optional | Description | | Parameter Name | Type | Optional | Description |
| :------------- | :------- | :------- | :----------------------------------- | | :------------- | :------- | :------- | :----------------------------------- |
| `USERNAME` | `String` | no | The username to create the user with | | `USERNAME` | `String` | no | The username to create the user with |
### Success Response ### Success Response
| Field | Type | Description | | Field | Type | Description |
| :--------- | :---------------------------------------------------------------------- | :-------------------------------------------------------------------- | | :--------- | :---------------------------------------------------------------------- | :-------------------------------------------------------------------- |
| `USERNAME` | `String` | Username of the newly created user, can differ from the requested one | | `USERNAME` | `String` | Username of the newly created user, can differ from the requested one |
| `ID` | [`UUID`](https://docs.oracle.com/javase/8/docs/api/java/util/UUID.html) | The ID of the created user | | `ID` | [`UUID`](https://docs.oracle.com/javase/8/docs/api/java/util/UUID.html) | The ID of the created user |
### Error Response ### Error Response
| Code | Description | | Code | Description |
| :------------------ | :----------------------------------------------------------------------------- | | :------------------ | :----------------------------------------------------------------------------- |
| `ALREADY_LOGGED_IN` | The session is already associated with a user, logging in again is prohibited. | | `ALREADY_LOGGED_IN` | The session is already associated with a user, logging in again is prohibited. |
### Example Request ### Example Request
``` ```
LOGIN USERNAME='Lars' LOGIN USERNAME='Lars'
``` ```
### Example Response ### Example Response
``` ```
+OK +OK
USERNAME='Lars_1234' USERNAME='Lars_1234'
@@ -303,45 +333,100 @@ LOGIN USERNAME='Lars'
END END
``` ```
## CHANGE_USERNAME command
## LOGOUT command The `CHANGE_USERNAME` command is used to change the username of an already logged-in user. The request is tied to the current session and updates all affected server-side mappings.
Description of the command, what it does, and when it should be used.
### Required pre-execution checks ### Required pre-execution checks
None. None.
### Request Parameters ### Request Parameters
| Parameter Name | Type | Optional | Description |
| :------------- | :------- | :------- | :------------------------------------ |
| `USERNAME` | `String` | no | The new username for the active user |
### Success Response
| Field | Type | Description |
| :--------- | :---------------------------------------------------------------------- | :-------------------------------------- |
| `USERNAME` | `String` | Effective username after rename |
| `ID` | [`UUID`](https://docs.oracle.com/javase/8/docs/api/java/util/UUID.html) | The ID of the renamed user |
### Error Response
| Code | Description |
| :------------------- | :-------------------------------------------------------------------- |
| `USER_NOT_LOGGED_IN` | No active user is associated with this session. |
| `INVALID_USERNAME` | Username contains disallowed characters or is empty. |
| `USERNAME_TAKEN` | Requested username is already used by another user. |
| `RENAME_CONFLICT` | Rename could not be propagated to all current lobby/game structures. |
### Example Request
```
CHANGE_USERNAME USERNAME='Lars_New'
```
### Example Response
```
+OK
USERNAME='Lars_New'
ID=e47a671e-2b2a-42df-bb82-953fe2ebd307
END
```
## LOGOUT command
Description of the command, what it does, and when it should be used.
### Required pre-execution checks
None.
### Request Parameters
No parameters. No parameters.
### Success Response ### Success Response
No response fields. No response fields.
### Error Response ### Error Response
| Code | Description | | Code | Description |
| :------------------- | :--------------------------------------------------------------- | | :------------------- | :--------------------------------------------------------------- |
| `NO_USER_ASSOCIATED` | The session has no user associated, logging out is not possible. | | `NO_USER_ASSOCIATED` | The session has no user associated, logging out is not possible. |
### Example Request ### Example Request
``` ```
LOGOUT LOGOUT
``` ```
### Example Response ### Example Response
``` ```
+OK +OK
END END
``` ```
## LIST_USERS command ## LIST_USERS command
The `LIST_USERS` command is used to retrieve a list of all currently logged-in users. The `LIST_USERS` command is used to retrieve a list of all currently logged-in users.
### Required pre-execution checks ### Required pre-execution checks
None. None.
### Request Parameters ### Request Parameters
No parameters. No parameters.
### Success Response ### Success Response
| Field | Type | Description | | Field | Type | Description |
| :------ | :----------------- | :--------------------------------------- | | :------ | :----------------- | :--------------------------------------- |
| `USERS` | `Collection<User>` | Collection of all users currently online | | `USERS` | `Collection<User>` | Collection of all users currently online |
@@ -351,16 +436,18 @@ No parameters.
| `USERNAME` | `String` | Username of the newly created user, can differ from the requested one | | `USERNAME` | `String` | Username of the newly created user, can differ from the requested one |
| `ID` | [`UUID`](https://docs.oracle.com/javase/8/docs/api/java/util/UUID.html) | The ID of the created user | | `ID` | [`UUID`](https://docs.oracle.com/javase/8/docs/api/java/util/UUID.html) | The ID of the created user |
### Error Response ### Error Response
None. None.
### Example Request ### Example Request
``` ```
LIST_USERS LIST_USERS
``` ```
### Example Response ### Example Response
``` ```
+OK +OK
USERS USERS
@@ -381,8 +468,11 @@ END
``` ```
## SEND_MESSAGE command ## SEND_MESSAGE command
The `SEND_MESSAGE` command is used to transfer the chat message sent by a user to the server. The `SEND_MESSAGE` command is used to transfer the chat message sent by a user to the server.
### Required pre-execution checks ### Required pre-execution checks
None. None.
### Request Parameters ### Request Parameters
@@ -403,28 +493,36 @@ None.
| `WHISPER` | Message is for the whisper chat | | `WHISPER` | Message is for the whisper chat |
### Success Response ### Success Response
No response fields. No response fields.
### Error Response ### Error Response
None. None.
### Example Request ### Example Request
``` ```
SEND_MESSAGE TYPE=GLOBAL GAME=1 USER=player1 TARGET=null TIME='10:30' TEXT='Hello World' SEND_MESSAGE TYPE=GLOBAL GAME=1 USER=player1 TARGET=null TIME='10:30' TEXT='Hello World'
``` ```
### Example Response ### Example Response
``` ```
+OK +OK
END END
``` ```
## GET_MESSAGE_COUNT command ## 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. 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 ### Required pre-execution checks
None. None.
### Request Parameters ### Request Parameters
No parameters. No parameters.
### Success Response ### Success Response
@@ -434,17 +532,19 @@ No parameters.
| `COUNT` | `int` | The current number of messages | | `COUNT` | `int` | The current number of messages |
### Error Response ### Error Response
| Code | Description | | Code | Description |
| :------------------- |:---------------------------------------------------------------------------------| | :------------------- |:---------------------------------------------------------------------------------|
| `NO_USER_ASSOCIATED` | The session has no user associated, there is no queue of messages for the client | | `NO_USER_ASSOCIATED` | The session has no user associated, there is no queue of messages for the client |
### Example Request ### Example Request
``` ```
GET_MESSAGE_COUNT GET_MESSAGE_COUNT
``` ```
### Example Response ### Example Response
``` ```
+OK +OK
COUNT=10 COUNT=10
@@ -452,14 +552,19 @@ END
``` ```
## GET_NEXT_MESSAGE command ## GET_NEXT_MESSAGE command
The `GET_NEXT_MESSAGE` command is used to get the next message stored in a queue for the client. The `GET_NEXT_MESSAGE` command is used to get the next message stored in a queue for the client.
### Required pre-execution checks ### Required pre-execution checks
None. None.
### Request Parameters ### Request Parameters
No parameters. No parameters.
### Success Response ### Success Response
| Field | Type | Description | | Field | Type | Description |
|:---------|:----------------|:-------------------------------------------------------------------| |:---------|:----------------|:-------------------------------------------------------------------|
| `TYPE` | `Enum<ChatType` | Member of Enum, indicating with chat type is used | | `TYPE` | `Enum<ChatType` | Member of Enum, indicating with chat type is used |
@@ -476,16 +581,19 @@ No parameters.
| `WHISPER` | Message is for the whisper chat | | `WHISPER` | Message is for the whisper chat |
### Error Response ### Error Response
| Code | Description | | Code | Description |
| :------------------- |:---------------------------------------------------------------------------------| | :------------------- |:---------------------------------------------------------------------------------|
| `NO_USER_ASSOCIATED` | The session has no user associated, there is no queue of messages for the client | | `NO_USER_ASSOCIATED` | The session has no user associated, there is no queue of messages for the client |
### Example Request ### Example Request
``` ```
GET_NEXT_MESSAGE GET_NEXT_MESSAGE
``` ```
### Example Response ### Example Response
``` ```
+OK +OK
TYPE=GLOBAL GAME=-1 USER=player1 TARGET=null TIME=9:30 TEXT="Guten Tag" TYPE=GLOBAL GAME=-1 USER=player1 TARGET=null TIME=9:30 TEXT="Guten Tag"
@@ -613,9 +721,11 @@ END
The `GET_LOBBY_LIST` command requests the server to return a list of currently available lobbies with basic metadata. The `GET_LOBBY_LIST` command requests the server to return a list of currently available lobbies with basic metadata.
### Required pre-execution checks ### Required pre-execution checks
None. None.
### Request Parameters ### Request Parameters
No parameters. No parameters.
### Implementation notes ### Implementation notes
@@ -667,9 +777,11 @@ END
The `GET_GAME_STATE` command returns a snapshot of the current game for a lobby. It includes global fields (`PHASE`, `POT`, `CURRENT_BET`, `DEALER`, `ACTIVE_PLAYER`), repeated `CARD` blocks for community cards and repeated `PLAYER` blocks for per-player information. If the requester is a player in the game, their hole cards are included inside their `PLAYER` block as nested `CARD` blocks. The `GET_GAME_STATE` command returns a snapshot of the current game for a lobby. It includes global fields (`PHASE`, `POT`, `CURRENT_BET`, `DEALER`, `ACTIVE_PLAYER`), repeated `CARD` blocks for community cards and repeated `PLAYER` blocks for per-player information. If the requester is a player in the game, their hole cards are included inside their `PLAYER` block as nested `CARD` blocks.
### Required pre-execution checks ### Required pre-execution checks
- [`UserLoggedInCheck`](#userloggedincheck) - [`UserLoggedInCheck`](#userloggedincheck)
### Request Parameters ### Request Parameters
| Parameter Name | Type | Optional | Description | | Parameter Name | Type | Optional | Description |
| :------------- | :----- | :------: | :---------- | | :------------- | :----- | :------: | :---------- |
| `GAME_ID` | `int` | yes | Numeric id of the lobby/game to query. If omitted the server will resolve the lobby by the requesting session's associated user. | `GAME_ID` | `int` | yes | Numeric id of the lobby/game to query. If omitted the server will resolve the lobby by the requesting session's associated user.
@@ -1114,9 +1226,11 @@ END
The `GET_LOBBY_STATUS` command requests the server to return the current state of a lobby, including the list of players and their ready state. The `GET_LOBBY_STATUS` command requests the server to return the current state of a lobby, including the list of players and their ready state.
### Required pre-execution checks ### Required pre-execution checks
None. None.
### Request Parameters ### Request Parameters
| Parameter Name | Type | Optional | Description | | Parameter Name | Type | Optional | Description |
| :------------- | :--- | :------: | :---------- | | :------------- | :--- | :------: | :---------- |
| `ID` | `int` | no | Numeric id of the target lobby | | `ID` | `int` | no | Numeric id of the target lobby |
@@ -1151,6 +1265,7 @@ END
``` ```
### Error Response ### Error Response
| Code | Description | | Code | Description |
| :--- | :---------- | | :--- | :---------- |
| `LOBBY_NOT_FOUND` | The specified lobby id does not exist | | `LOBBY_NOT_FOUND` | The specified lobby id does not exist |
@@ -114,8 +114,11 @@ public class CasinoGameUI extends Application {
Parent root = fxmlLoader.load(); Parent root = fxmlLoader.load();
CasinoGameController controller = fxmlLoader.getController(); CasinoGameController controller = fxmlLoader.getController();
int gameId = 1; // TODO echte gameId einsetzen if (lobbyId <= 0) {
GameClient gameClient = new GameClient(clientService, gameId); throw new IllegalStateException("CasinoGameUI: lobbyId must be set before start()");
}
GameClient gameClient = new GameClient(clientService, lobbyId);
GameService gameService = new GameService(gameClient); GameService gameService = new GameService(gameClient);
controller.setGameService(gameService); controller.setGameService(gameService);
@@ -1,5 +1,8 @@
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.change_username.ChangeUsernameHandler;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.change_username.ChangeUsernameParser;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.change_username.ChangeUsernameRequest;
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;
@@ -168,6 +171,15 @@ public class ServerApp {
commandRouter.register( commandRouter.register(
LoginRequest.class, new LoginHandler(responseDispatcher, userRegistry)); LoginRequest.class, new LoginHandler(responseDispatcher, userRegistry));
parserDispatcher.register("CHANGE_USERNAME", new ChangeUsernameParser());
commandRouter.register(
ChangeUsernameRequest.class,
new ChangeUsernameHandler(
responseDispatcher,
userRegistry,
context.lobbyManager(),
context.sessionManager()));
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));
@@ -0,0 +1,112 @@
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.change_username;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.LobbyManager;
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.request.RequestContext;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.ErrorResponse;
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.dispatcher.ResponseDispatcher;
import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.Session;
import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionManager;
import java.util.Optional;
import java.util.regex.Pattern;
/** Handles CHANGE_USERNAME requests for already logged-in users. */
public class ChangeUsernameHandler extends CommandHandler<ChangeUsernameRequest> {
private static final Pattern VALID_USERNAME = Pattern.compile("[a-zA-Z0-9_-]+");
private final UserRegistry userRegistry;
private final LobbyManager lobbyManager;
private final SessionManager sessionManager;
/**
* @param responseDispatcher dispatcher used for responses
* @param userRegistry registry containing all users
* @param lobbyManager lobby manager used to keep lobby/game mappings in sync
* @param sessionManager session manager used to broadcast rename events
*/
public ChangeUsernameHandler(
ResponseDispatcher responseDispatcher,
UserRegistry userRegistry,
LobbyManager lobbyManager,
SessionManager sessionManager) {
super(responseDispatcher);
this.userRegistry = userRegistry;
this.lobbyManager = lobbyManager;
this.sessionManager = sessionManager;
}
@Override
public void execute(ChangeUsernameRequest request) {
Optional<User> user = userRegistry.getBySessionId(request.getSessionId());
if (user.isEmpty()) {
responseDispatcher.dispatch(
new ErrorResponse(
request.getContext(),
"USER_NOT_LOGGED_IN",
"This session is not associated with an active user."));
return;
}
String newUsername = request.getUsername() == null ? "" : request.getUsername().trim();
if (newUsername.isEmpty() || !VALID_USERNAME.matcher(newUsername).matches()) {
responseDispatcher.dispatch(
new ErrorResponse(
request.getContext(),
"INVALID_USERNAME",
"Only letters, numbers, '_' and '-' are allowed."));
return;
}
User currentUser = user.get();
String oldUsername = currentUser.getName();
boolean changed = userRegistry.changeUsername(currentUser.getId(), newUsername);
if (!changed) {
responseDispatcher.dispatch(
new ErrorResponse(
request.getContext(),
"USERNAME_TAKEN",
"The requested username is already taken."));
return;
}
boolean lobbySynced =
lobbyManager == null || lobbyManager.renamePlayer(oldUsername, newUsername);
if (!lobbySynced) {
userRegistry.changeUsername(currentUser.getId(), oldUsername);
responseDispatcher.dispatch(
new ErrorResponse(
request.getContext(),
"RENAME_CONFLICT",
"Could not update username in current lobby/game state."));
return;
}
responseDispatcher.dispatch(
new ChangeUsernameResponse(
request.getContext(), currentUser.getName(), currentUser.getId()));
broadcastUsernameChanged(oldUsername, currentUser.getName());
}
private void broadcastUsernameChanged(String oldUsername, String newUsername) {
if (sessionManager == null) {
return;
}
for (Session session : sessionManager.getAllSessions()) {
RequestContext ctx = new RequestContext(session.getId(), 0);
SuccessResponse ev =
new SuccessResponse(
ctx,
new ResponseBodyBuilder()
.param("EVENT", "USERNAME_CHANGED")
.param("OLD_USERNAME", oldUsername)
.param("NEW_USERNAME", newUsername)
.build()) {};
responseDispatcher.dispatch(ev);
}
}
}
@@ -0,0 +1,15 @@
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.change_username;
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;
/** Parses CHANGE_USERNAME requests. */
public class ChangeUsernameParser implements CommandParser<ChangeUsernameRequest> {
@Override
public ChangeUsernameRequest parse(PrimitiveRequest primitiveRequest) {
RequestParameterAccessor accessor =
new RequestParameterAccessor(primitiveRequest.parameters());
return new ChangeUsernameRequest(primitiveRequest.context(), accessor.require("USERNAME"));
}
}
@@ -0,0 +1,25 @@
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.change_username;
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 used to change the username of the current session user. */
public class ChangeUsernameRequest extends Request {
private final String username;
/**
* @param context request context for responses
* @param username desired new username
*/
public ChangeUsernameRequest(RequestContext context, String username) {
super(context);
this.username = username;
}
/**
* @return desired new username
*/
public String getUsername() {
return username;
}
}
@@ -0,0 +1,23 @@
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.change_username;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.UserId;
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;
/** Response for successful username changes. */
public class ChangeUsernameResponse extends SuccessResponse {
/**
* @param context request context
* @param username current username after the rename operation
* @param id user id of renamed user
*/
public ChangeUsernameResponse(RequestContext context, String username, UserId id) {
super(
context,
new ResponseBodyBuilder()
.param("USERNAME", username)
.param("ID", id.value())
.build());
}
}
@@ -1,6 +1,7 @@
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.get_game_state; package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.get_game_state;
import ch.unibas.dmi.dbis.cs108.casono.server.app.checks.UserLoggedInCheck; import ch.unibas.dmi.dbis.cs108.casono.server.app.checks.UserLoggedInCheck;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.state.GamePhase;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.Lobby; import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.Lobby;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.LobbyId; import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.LobbyId;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.LobbyManager; import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.LobbyManager;
@@ -31,8 +32,10 @@ public class GetGameStateHandler extends CommandHandler<GetGameStateRequest> {
String username = resolveUsername(request); String username = resolveUsername(request);
Lobby lobby; Lobby lobby;
LobbyId lobbyId;
if (gameId != null) { if (gameId != null) {
lobby = lobbyManager.getLobby(LobbyId.of(gameId)); lobbyId = LobbyId.of(gameId);
lobby = lobbyManager.getLobby(lobbyId);
if (lobby == null) { if (lobby == null) {
responseDispatcher.dispatch( responseDispatcher.dispatch(
new ErrorResponse( new ErrorResponse(
@@ -59,6 +62,7 @@ public class GetGameStateHandler extends CommandHandler<GetGameStateRequest> {
request.getContext(), "NOT_IN_LOBBY", "User not in a lobby")); request.getContext(), "NOT_IN_LOBBY", "User not in a lobby"));
return; return;
} }
lobbyId = lobby.getId();
} }
var game = lobby.getGameController(); var game = lobby.getGameController();
@@ -69,9 +73,30 @@ public class GetGameStateHandler extends CommandHandler<GetGameStateRequest> {
return; return;
} }
if (game.getState().getPhase() == GamePhase.FINISHED) {
cleanupLobby(lobby, lobbyId);
}
responseDispatcher.dispatch(new GetGameStateResponse(request.getContext(), game, username)); responseDispatcher.dispatch(new GetGameStateResponse(request.getContext(), game, username));
} }
/**
* Cleans up a lobby by removing all players and resetting the game controller. This is called
* when the game reaches FINISHED phase.
*/
private void cleanupLobby(Lobby lobby, LobbyId lobbyId) {
try {
// Remove all players from the lobby
for (String playerName : lobby.getPlayerNames()) {
lobbyManager.removePlayer(playerName);
}
// Reset the game controller so the lobby returns to CREATED state
lobby.initGame(null);
} catch (RuntimeException e) {
// Log silently to avoid disrupting game state response
}
}
private String resolveUsername(GetGameStateRequest request) { private String resolveUsername(GetGameStateRequest request) {
String username = request.getUsername(); String username = request.getUsername();
if (username != null && !username.isBlank()) { if (username != null && !username.isBlank()) {
@@ -6,6 +6,7 @@ import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.deck.Rank;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.deck.Suit; import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.deck.Suit;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.player.Player; import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.player.Player;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.player.PlayerId; import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.player.PlayerId;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.state.GamePhase;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.state.GameState; import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.state.GameState;
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;
@@ -33,11 +34,13 @@ public class GetGameStateResponse extends SuccessResponse {
super( super(
context, context,
buildBody( buildBody(
game,
Objects.requireNonNull(game, "game must not be null").getState(), Objects.requireNonNull(game, "game must not be null").getState(),
requestingUsername)); requestingUsername));
} }
private static ResponseBody buildBody(GameState state, String requestingUsername) { private static ResponseBody buildBody(
GameController game, GameState state, String requestingUsername) {
Objects.requireNonNull(state, "state must not be null"); Objects.requireNonNull(state, "state must not be null");
ResponseBodyBuilder builder = ResponseBody.builder(); ResponseBodyBuilder builder = ResponseBody.builder();
@@ -47,6 +50,7 @@ public class GetGameStateResponse extends SuccessResponse {
builder.param("CURRENT_BET", computeGlobalCurrentBet(state)); builder.param("CURRENT_BET", computeGlobalCurrentBet(state));
builder.param("DEALER", state.getDealerIndex()); builder.param("DEALER", state.getDealerIndex());
builder.param("ACTIVE_PLAYER", state.getCurrentPlayerIndex()); builder.param("ACTIVE_PLAYER", state.getCurrentPlayerIndex());
builder.param("WINNER", computeWinnerIndex(state, game));
appendCommunityCards(builder, state); appendCommunityCards(builder, state);
@@ -55,6 +59,28 @@ public class GetGameStateResponse extends SuccessResponse {
return builder.build(); return builder.build();
} }
private static int computeWinnerIndex(GameState state, GameController game) {
GamePhase phase = state.getPhase();
if (phase != GamePhase.SHOWDOWN && phase != GamePhase.FINISHED) {
return -1;
}
PlayerId winnerId = game.determineWinner();
if (winnerId == null) {
return -1;
}
int index = 0;
for (Player p : state.getPlayers()) {
if (p != null && winnerId.equals(p.getId())) {
return index;
}
index++;
}
return -1;
}
private static int computeGlobalCurrentBet(GameState state) { private static int computeGlobalCurrentBet(GameState state) {
int globalCurrentBet = 0; int globalCurrentBet = 0;
for (Player p : state.getPlayers()) { for (Player p : state.getPlayers()) {
@@ -254,4 +254,12 @@ public class GameController {
return bestPlayer; return bestPlayer;
} }
/**
* Ends the current game by setting the phase to FINISHED. This should be called when the
* showdown is complete and a winner has been determined.
*/
public void endGame() {
engine.getState().setPhase(GamePhase.FINISHED);
}
} }
@@ -49,6 +49,11 @@ public class RoundManager {
state.setPhase(GamePhase.PREFLOP); state.setPhase(GamePhase.PREFLOP);
} }
if (state.countNonFoldedPlayers() == 1) {
state.setPhase(GamePhase.FINISHED);
return;
}
if (isBettingRoundFinished(state)) { if (isBettingRoundFinished(state)) {
advancePhase(state); advancePhase(state);
} }
@@ -81,8 +86,9 @@ public class RoundManager {
case FLOP -> dealTurn(state); case FLOP -> dealTurn(state);
case TURN -> dealRiver(state); case TURN -> dealRiver(state);
case RIVER -> showdown(state); case RIVER -> showdown(state);
case SHOWDOWN -> { case SHOWDOWN -> state.setPhase(GamePhase.FINISHED);
/* nothing */ case FINISHED -> {
/* game is over */
} }
} }
} }
@@ -11,19 +11,15 @@ import java.util.List;
import java.util.Map; import java.util.Map;
/** /**
* The GameState class encapsulates the entire state of a poker game at any * The GameState class encapsulates the entire state of a poker game at any given moment.
* given moment.
* *
* <p> * <p>Important invariants this implementation maintains:
* Important invariants this implementation maintains:
* *
* <ul> * <ul>
* <li>{@code playerOrder} defines the stable seating/turn order. * <li>{@code playerOrder} defines the stable seating/turn order.
* <li>{@code currentBets} always contains an entry for every player in * <li>{@code currentBets} always contains an entry for every player in {@code playerOrder}.
* {@code playerOrder}. * <li>{@code holeCards} maps every player to a mutable list; if not present, it is created on
* <li>{@code holeCards} maps every player to a mutable list; if not present, it * demand.
* is created on
* demand.
* </ul> * </ul>
*/ */
public class GameState { public class GameState {
@@ -188,8 +184,7 @@ public class GameState {
} }
/** /**
* Reset bets to 0 for ALL players (do not clear the map). Also resets table * Reset bets to 0 for ALL players (do not clear the map). Also resets table current bet to 0.
* current bet to 0.
*/ */
public void resetBets() { public void resetBets() {
for (PlayerId id : playerOrder) { for (PlayerId id : playerOrder) {
@@ -232,8 +227,7 @@ public class GameState {
// Cards // Cards
/** /**
* Gives (overwrites) the two hole cards for the given player. Ensures stable * Gives (overwrites) the two hole cards for the given player. Ensures stable list identity
* list identity
* (important if other code holds references). * (important if other code holds references).
*/ */
public void giveHoleCards(PlayerId playerId, Card c1, Card c2) { public void giveHoleCards(PlayerId playerId, Card c1, Card c2) {
@@ -293,14 +287,13 @@ public class GameState {
/** /**
* Starts a new hand by resetting the game state for the next round of poker. * Starts a new hand by resetting the game state for the next round of poker.
* *
* <p> * <p>This:
* This:
* *
* <ul> * <ul>
* <li>sets {@code phase=PREFLOP} * <li>sets {@code phase=PREFLOP}
* <li>resets pot/bets/commitments * <li>resets pot/bets/commitments
* <li>clears community + hole cards * <li>clears community + hole cards
* <li>creates &amp; shuffles a new deck * <li>creates &amp; shuffles a new deck
* </ul> * </ul>
*/ */
public void startNewHand() { public void startNewHand() {
@@ -336,4 +329,19 @@ public class GameState {
public boolean isFolded(PlayerId playerId) { public boolean isFolded(PlayerId playerId) {
return getPlayer(playerId).isFolded(); return getPlayer(playerId).isFolded();
} }
/**
* Counts the number of players who have not folded.
*
* @return The number of non-folded players
*/
public int countNonFoldedPlayers() {
int count = 0;
for (Player p : players.values()) {
if (p != null && !p.isFolded()) {
count++;
}
}
return count;
}
} }