Compare commits
50 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 028c3adb0d | |||
| 19b555fb47 | |||
| 8a7fad11a9 | |||
| c0a7cadf7e | |||
| 2fd07bbf60 | |||
| 15c998d549 | |||
| 062e66cdf9 | |||
| 7674ae57ad | |||
| 5e79b45d90 | |||
| 45411c4087 | |||
| cc7f8a8ef3 | |||
| d3a00aebc0 | |||
| ab2adbf311 | |||
| 8099228d4f | |||
| ac83d63ea1 | |||
| 3443b5e8f3 | |||
| ad7086e5a5 | |||
| e39cbb4357 | |||
| 238a156d91 | |||
| 621843167e | |||
| 215e7e2cae | |||
| 03daf9c3b8 | |||
| d23a00e148 | |||
| a5f0735a08 | |||
| 4855e937e6 | |||
| 007e863878 | |||
| 56aa3c0215 | |||
| 417af2e8f7 | |||
| 893bcee9bf | |||
| 6d1b467116 | |||
| 4687566816 | |||
| d27a07f3e8 | |||
| b86f18a11c | |||
| 7ff274269e | |||
| ec916aa98c | |||
| 190b30672a | |||
| 11be6e2b9b | |||
| a8599cfb4c | |||
| 08b9f652be | |||
| 3252c7e534 | |||
| 7cba7ac1dd | |||
| c83a360fec | |||
| 2631791330 | |||
| 181f538038 | |||
| 497820db05 | |||
| 88c9431bf0 | |||
| 5a53a14933 | |||
| f7f66ec5db | |||
| d85b3d21ee | |||
| 382383d38b |
@@ -1,8 +1,9 @@
|
||||
# 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.
|
||||
|
||||
|
||||
# Table of Contents
|
||||
|
||||
- [Protocol Document](#protocol-document)
|
||||
- [Table of Contents](#table-of-contents)
|
||||
- [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)
|
||||
- [Example Request](#example-request-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)
|
||||
- [Request Parameters](#request-parameters-3)
|
||||
- [Success Response](#success-response-3)
|
||||
- [Error Response](#error-response-5)
|
||||
- [Example Request](#example-request-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)
|
||||
- [Request Parameters](#request-parameters-4)
|
||||
- [Success Response](#success-response-4)
|
||||
- [Error Response](#error-response-6)
|
||||
- [Example Request](#example-request-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)
|
||||
- [Required pre-execution checks](#required-pre-execution-checks)
|
||||
- [Request Parameters](#request-parameters)
|
||||
@@ -62,6 +70,18 @@ This document describes the protocol for client-server communication in our appl
|
||||
- [Success Response](#success-response)
|
||||
- [Example Request](#example-request)
|
||||
- [Example Response](#example-response)
|
||||
- [GET_HIGHSCORES command](#get_highscores-command)
|
||||
- [Required pre-execution checks](#required-pre-execution-checks)
|
||||
- [Request Parameters](#request-parameters)
|
||||
- [Success Response](#success-response)
|
||||
- [Example Request](#example-request)
|
||||
- [Example Response](#example-response)
|
||||
- [CLEAR_HIGHSCORES command](#clear_highscores-command)
|
||||
- [Required pre-execution checks](#required-pre-execution-checks)
|
||||
- [Request Parameters](#request-parameters)
|
||||
- [Success Response](#success-response)
|
||||
- [Example Request](#example-request)
|
||||
- [Example Response](#example-response)
|
||||
- [RAISE command](#raise-command)
|
||||
- [CALL command](#call-command)
|
||||
- [FOLD command](#fold-command)
|
||||
@@ -91,6 +111,7 @@ This document describes the protocol for client-server communication in our appl
|
||||
<!-- Please see the comments for copy ‚ n' paste ready examples -->
|
||||
|
||||
# General structure of requests
|
||||
|
||||
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.
|
||||
|
||||
@@ -99,41 +120,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.
|
||||
Each collection is ended with the `END` keyword.
|
||||
|
||||
|
||||
|
||||
# Preconditions
|
||||
|
||||
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.
|
||||
|
||||
## 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.
|
||||
|
||||
### Error Response
|
||||
|
||||
| Code | Description |
|
||||
| :-------------- | :---------------------------------------------------------------------------------------------- |
|
||||
| `PARSING_ERROR` | The body of the request contains syntax errors (see message field of response for more details) |
|
||||
|
||||
|
||||
## 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.
|
||||
|
||||
### Error Response
|
||||
|
||||
| Code | Description |
|
||||
| :---------------- | :---------------------------------------------------------------- |
|
||||
| `UNKNOWN_COMMAND` | The command is unknown to this server. No parser has been defined |
|
||||
|
||||
|
||||
## 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`.
|
||||
|
||||
### Error Response
|
||||
|
||||
| Code | Description |
|
||||
| :------------------ | :------------------------------------------------------------------------------------------------ |
|
||||
| `MISSING_PARAMETER` | A required parameter is missing from the request (see message field of response for more details) |
|
||||
|
||||
|
||||
|
||||
# Pre-execution checks
|
||||
|
||||
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.
|
||||
|
||||
@@ -148,16 +171,17 @@ Description of the check, what it does and when it should be used.
|
||||
-->
|
||||
|
||||
## 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).
|
||||
|
||||
### Error Response
|
||||
|
||||
| Code | Description |
|
||||
| :------------------- | :------------------------ |
|
||||
| `USER_NOT_LOGGED_IN` | The user is not logged in |
|
||||
|
||||
|
||||
|
||||
# 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.
|
||||
The server processes these commands and responds accordingly.
|
||||
|
||||
@@ -212,41 +236,50 @@ END
|
||||
-->
|
||||
|
||||
## PING command
|
||||
|
||||
The `PING` command is a simple command that can be used to check if the server is responsive.
|
||||
|
||||
### Required pre-execution checks
|
||||
|
||||
None.
|
||||
|
||||
### Request Parameters
|
||||
|
||||
No parameters.
|
||||
|
||||
### Success Response
|
||||
|
||||
No response fields.
|
||||
|
||||
### Example Request
|
||||
|
||||
```
|
||||
PING
|
||||
```
|
||||
|
||||
### Example Response
|
||||
|
||||
```
|
||||
+OK
|
||||
END
|
||||
```
|
||||
|
||||
|
||||
## 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.
|
||||
|
||||
### Required pre-execution checks
|
||||
|
||||
None.
|
||||
|
||||
### Request Parameters
|
||||
|
||||
| Parameter Name | Type | Optional | Description |
|
||||
| :------------- | :------- | :------- | :------------------------------------- |
|
||||
| `USERNAME` | `String` | no | The username to check for availability |
|
||||
|
||||
### Success Response
|
||||
|
||||
| Field | Type | Description |
|
||||
| :------- | :--------------------------- | :---------------------------------------------------------------------- |
|
||||
| `STATUS` | `Enum<UsernameAvailability>` | Member of enum indicating if the username is available or already taken |
|
||||
@@ -257,11 +290,13 @@ None.
|
||||
| `TAKEN` | Username is already in use |
|
||||
|
||||
### Example Request
|
||||
|
||||
```
|
||||
CHECK_USERNAME USERNAME='Lars'
|
||||
```
|
||||
|
||||
### Example Response
|
||||
|
||||
```
|
||||
+OK
|
||||
STATUS=FREE
|
||||
@@ -269,33 +304,40 @@ END
|
||||
```
|
||||
|
||||
## 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.
|
||||
|
||||
### Required pre-execution checks
|
||||
|
||||
None.
|
||||
|
||||
### Request Parameters
|
||||
|
||||
| Parameter Name | Type | Optional | Description |
|
||||
| :------------- | :------- | :------- | :----------------------------------- |
|
||||
| `USERNAME` | `String` | no | The username to create the user with |
|
||||
|
||||
### Success Response
|
||||
|
||||
| Field | Type | Description |
|
||||
| :--------- | :---------------------------------------------------------------------- | :-------------------------------------------------------------------- |
|
||||
| `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 |
|
||||
|
||||
### Error Response
|
||||
|
||||
| Code | Description |
|
||||
| :------------------ | :----------------------------------------------------------------------------- |
|
||||
| `ALREADY_LOGGED_IN` | The session is already associated with a user, logging in again is prohibited. |
|
||||
|
||||
### Example Request
|
||||
|
||||
```
|
||||
LOGIN USERNAME='Lars'
|
||||
```
|
||||
|
||||
### Example Response
|
||||
|
||||
```
|
||||
+OK
|
||||
USERNAME='Lars_1234'
|
||||
@@ -303,45 +345,100 @@ LOGIN USERNAME='Lars'
|
||||
END
|
||||
```
|
||||
|
||||
## CHANGE_USERNAME command
|
||||
|
||||
## LOGOUT command
|
||||
Description of the command, what it does, and when it should be used.
|
||||
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.
|
||||
|
||||
### Required pre-execution checks
|
||||
|
||||
None.
|
||||
|
||||
### 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.
|
||||
|
||||
### Success Response
|
||||
|
||||
No response fields.
|
||||
|
||||
### Error Response
|
||||
|
||||
| Code | Description |
|
||||
| :------------------- | :--------------------------------------------------------------- |
|
||||
| `NO_USER_ASSOCIATED` | The session has no user associated, logging out is not possible. |
|
||||
|
||||
### Example Request
|
||||
|
||||
```
|
||||
LOGOUT
|
||||
```
|
||||
|
||||
### Example Response
|
||||
|
||||
```
|
||||
+OK
|
||||
END
|
||||
```
|
||||
|
||||
## LIST_USERS command
|
||||
|
||||
The `LIST_USERS` command is used to retrieve a list of all currently logged-in users.
|
||||
|
||||
### Required pre-execution checks
|
||||
|
||||
None.
|
||||
|
||||
### Request Parameters
|
||||
|
||||
No parameters.
|
||||
|
||||
### Success Response
|
||||
|
||||
| Field | Type | Description |
|
||||
| :------ | :----------------- | :--------------------------------------- |
|
||||
| `USERS` | `Collection<User>` | Collection of all users currently online |
|
||||
@@ -351,16 +448,18 @@ No parameters.
|
||||
| `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 |
|
||||
|
||||
|
||||
### Error Response
|
||||
|
||||
None.
|
||||
|
||||
### Example Request
|
||||
|
||||
```
|
||||
LIST_USERS
|
||||
```
|
||||
|
||||
### Example Response
|
||||
|
||||
```
|
||||
+OK
|
||||
USERS
|
||||
@@ -381,8 +480,11 @@ 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
|
||||
@@ -403,28 +505,36 @@ None.
|
||||
| `WHISPER` | Message is for the whisper chat |
|
||||
|
||||
### Success Response
|
||||
|
||||
No response fields.
|
||||
|
||||
### Error Response
|
||||
|
||||
None.
|
||||
|
||||
### Example Request
|
||||
|
||||
```
|
||||
SEND_MESSAGE TYPE=GLOBAL GAME=1 USER=player1 TARGET=null TIME='10:30' TEXT='Hello World'
|
||||
```
|
||||
|
||||
### Example Response
|
||||
|
||||
```
|
||||
+OK
|
||||
END
|
||||
```
|
||||
|
||||
## GET_MESSAGE_COUNT command
|
||||
|
||||
The `GET_MESSAGE_COUNT` is used to get the current number of messages that are stored in the queue for a client.
|
||||
|
||||
### Required pre-execution checks
|
||||
|
||||
None.
|
||||
|
||||
### Request Parameters
|
||||
|
||||
No parameters.
|
||||
|
||||
### Success Response
|
||||
@@ -434,17 +544,19 @@ No parameters.
|
||||
| `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
|
||||
@@ -452,14 +564,19 @@ END
|
||||
```
|
||||
|
||||
## GET_NEXT_MESSAGE command
|
||||
|
||||
The `GET_NEXT_MESSAGE` command is used to get the next message stored in a queue for the client.
|
||||
|
||||
### Required pre-execution checks
|
||||
|
||||
None.
|
||||
|
||||
### Request Parameters
|
||||
|
||||
No parameters.
|
||||
|
||||
### Success Response
|
||||
|
||||
| Field | Type | Description |
|
||||
|:---------|:----------------|:-------------------------------------------------------------------|
|
||||
| `TYPE` | `Enum<ChatType` | Member of Enum, indicating with chat type is used |
|
||||
@@ -476,16 +593,19 @@ No parameters.
|
||||
| `WHISPER` | Message is for the whisper chat |
|
||||
|
||||
### Error Response
|
||||
|
||||
| Code | Description |
|
||||
| :------------------- |:---------------------------------------------------------------------------------|
|
||||
| `NO_USER_ASSOCIATED` | The session has no user associated, there is no queue of messages for the client |
|
||||
|
||||
### Example Request
|
||||
|
||||
```
|
||||
GET_NEXT_MESSAGE
|
||||
```
|
||||
|
||||
### Example Response
|
||||
|
||||
```
|
||||
+OK
|
||||
TYPE=GLOBAL GAME=-1 USER=player1 TARGET=null TIME=9:30 TEXT="Guten Tag"
|
||||
@@ -613,9 +733,11 @@ END
|
||||
The `GET_LOBBY_LIST` command requests the server to return a list of currently available lobbies with basic metadata.
|
||||
|
||||
### Required pre-execution checks
|
||||
|
||||
None.
|
||||
|
||||
### Request Parameters
|
||||
|
||||
No parameters.
|
||||
|
||||
### Implementation notes
|
||||
@@ -667,9 +789,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.
|
||||
|
||||
### Required pre-execution checks
|
||||
|
||||
- [`UserLoggedInCheck`](#userloggedincheck)
|
||||
|
||||
### Request Parameters
|
||||
|
||||
| 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.
|
||||
@@ -1114,9 +1238,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.
|
||||
|
||||
### Required pre-execution checks
|
||||
|
||||
None.
|
||||
|
||||
### Request Parameters
|
||||
|
||||
| Parameter Name | Type | Optional | Description |
|
||||
| :------------- | :--- | :------: | :---------- |
|
||||
| `ID` | `int` | no | Numeric id of the target lobby |
|
||||
@@ -1151,6 +1277,7 @@ END
|
||||
```
|
||||
|
||||
### Error Response
|
||||
|
||||
| Code | Description |
|
||||
| :--- | :---------- |
|
||||
| `LOBBY_NOT_FOUND` | The specified lobby id does not exist |
|
||||
@@ -1223,3 +1350,71 @@ END
|
||||
MESSAGE=Maximum number of 8 lobbies reached
|
||||
END
|
||||
```
|
||||
|
||||
## GET_HIGHSCORES command
|
||||
|
||||
The `GET_HIGHSCORES` command returns the stored highscore list. The server sends a `HIGHSCORES` block containing repeated `HIGHSCORE` fields. Each entry is already formatted as `timestamp | winner name`.
|
||||
|
||||
### Required pre-execution checks
|
||||
|
||||
None.
|
||||
|
||||
### Request Parameters
|
||||
|
||||
No parameters.
|
||||
|
||||
### Success Response
|
||||
|
||||
| Field | Type | Description |
|
||||
| :----------- | :---------------------- | :--------------------------------------- |
|
||||
| `HIGHSCORES` | `Collection<Highscore>` | Collection of formatted highscore lines. |
|
||||
|
||||
| Fields of `Highscore` | Type | Description |
|
||||
| :-------------------- | :------- | :-------------------------------------------------- |
|
||||
| `HIGHSCORE` | `String` | One formatted entry in `yyyy-MM-dd HH:mm:ss | name` |
|
||||
|
||||
### Example Request
|
||||
|
||||
```
|
||||
GET_HIGHSCORES
|
||||
```
|
||||
|
||||
### Example Response
|
||||
|
||||
```
|
||||
+OK
|
||||
HIGHSCORES
|
||||
HIGHSCORE='2026-04-21 15:19:23 | Jona'
|
||||
HIGHSCORE='2026-04-21 16:02:11 | Lars'
|
||||
END
|
||||
END
|
||||
```
|
||||
|
||||
## CLEAR_HIGHSCORES command
|
||||
|
||||
The `CLEAR_HIGHSCORES` command deletes all stored highscore entries on the server.
|
||||
|
||||
### Required pre-execution checks
|
||||
|
||||
None.
|
||||
|
||||
### Request Parameters
|
||||
|
||||
No parameters.
|
||||
|
||||
### Success Response
|
||||
|
||||
No response fields.
|
||||
|
||||
### Example Request
|
||||
|
||||
```
|
||||
CLEAR_HIGHSCORES
|
||||
```
|
||||
|
||||
### Example Response
|
||||
|
||||
```
|
||||
+OK
|
||||
END
|
||||
```
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -39,6 +39,10 @@ public class ClientApp {
|
||||
return sharedUsername;
|
||||
}
|
||||
|
||||
public static void updateSharedUsername(String username) {
|
||||
setSharedUsername(username != null && !username.isBlank() ? username.trim() : null);
|
||||
}
|
||||
|
||||
private static void setSharedUsername(String username) {
|
||||
sharedUsername = username;
|
||||
LOGGER.info("sharedUsername set to '{}'", getSharedUsername());
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.client.chat;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.ClientApp;
|
||||
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 ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.RequestParameter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
import java.util.WeakHashMap;
|
||||
import java.util.function.Consumer;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
@@ -24,7 +29,7 @@ public class ChatController {
|
||||
private static final Map<ClientService, ChatController> ACTIVE_CONTROLLERS =
|
||||
new WeakHashMap<>();
|
||||
|
||||
private final String username;
|
||||
private volatile String username;
|
||||
|
||||
private final ClientService clientService;
|
||||
|
||||
@@ -37,6 +42,7 @@ public class ChatController {
|
||||
private final ChatBoxController chatBoxController;
|
||||
private int lobbyId = -1;
|
||||
private final Timer timer;
|
||||
private final Consumer<List<String>> serverEventListener;
|
||||
|
||||
public record ChatKey(ChatType type, @Nullable String targetUser) {
|
||||
public ChatKey(ChatType type) {
|
||||
@@ -71,8 +77,10 @@ public class ChatController {
|
||||
localUserList = new ArrayList<>();
|
||||
this.chatBoxController = new ChatBoxController(username, this);
|
||||
this.logger = LogManager.getLogger(ChatController.class);
|
||||
this.serverEventListener = this::handleServerEvent;
|
||||
|
||||
registerAsActiveController(clientService);
|
||||
clientService.addEventListener(serverEventListener);
|
||||
|
||||
this.timer = new Timer(true);
|
||||
timer.schedule(
|
||||
@@ -131,6 +139,7 @@ public class ChatController {
|
||||
* <p>All Messages get added to a particular {@link ChatModel}, if the checks passed.
|
||||
*/
|
||||
public void receiveMessage() {
|
||||
String currentUsername = getCurrentUsername();
|
||||
List<Message> newMessages = chatClient.getMessages();
|
||||
if (!newMessages.isEmpty()) {
|
||||
for (Message msg : newMessages) {
|
||||
@@ -146,16 +155,17 @@ public class ChatController {
|
||||
(_key) ->
|
||||
new ChatModel(
|
||||
ChatType.LOBBY,
|
||||
username,
|
||||
currentUsername,
|
||||
msg.lobbyId,
|
||||
null))
|
||||
.addMessage(msg);
|
||||
}
|
||||
break;
|
||||
case ChatType.WHISPER:
|
||||
if (msg.target.equals(username) || msg.sender.equals(username)) {
|
||||
if (msg.target.equals(currentUsername)
|
||||
|| msg.sender.equals(currentUsername)) {
|
||||
ChatKey key;
|
||||
if (msg.target.equals(username)) {
|
||||
if (msg.target.equals(currentUsername)) {
|
||||
key = new ChatKey(ChatType.WHISPER, msg.sender);
|
||||
} else {
|
||||
key = new ChatKey(ChatType.WHISPER, msg.target);
|
||||
@@ -166,7 +176,7 @@ public class ChatController {
|
||||
ChatModel chatModel =
|
||||
new ChatModel(
|
||||
ChatType.WHISPER,
|
||||
username,
|
||||
currentUsername,
|
||||
lobbyId,
|
||||
key.targetUser());
|
||||
chatBoxController.addWhisperChat(key.targetUser(), chatModel);
|
||||
@@ -203,13 +213,29 @@ public class ChatController {
|
||||
*/
|
||||
public synchronized void checkWhisperUsers() {
|
||||
List<String> users = chatClient.getUsers();
|
||||
logger.info(users);
|
||||
if (!users.isEmpty()) {
|
||||
for (String user : users) {
|
||||
String value = user.split("\\=")[1];
|
||||
logger.info(value);
|
||||
addWhisperUser(value);
|
||||
Set<String> remoteUsers = new HashSet<>();
|
||||
String currentUsername = getCurrentUsername();
|
||||
|
||||
for (String user : users) {
|
||||
if (user == null || !user.contains("=")) {
|
||||
continue;
|
||||
}
|
||||
String value = user.split("\\=", 2)[1].trim();
|
||||
if (value.isBlank() || value.equals(currentUsername)) {
|
||||
continue;
|
||||
}
|
||||
remoteUsers.add(value);
|
||||
}
|
||||
|
||||
for (String known : new ArrayList<>(localUserList)) {
|
||||
if (!remoteUsers.contains(known)) {
|
||||
localUserList.remove(known);
|
||||
chatBoxController.removeWhisperUser(known);
|
||||
}
|
||||
}
|
||||
|
||||
for (String remote : remoteUsers) {
|
||||
addWhisperUser(remote);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,16 +248,110 @@ public class ChatController {
|
||||
if (user == null || user.isBlank()) {
|
||||
return;
|
||||
}
|
||||
if (!(localUserList.contains(user) || user.equals(username))) {
|
||||
if (!(localUserList.contains(user) || user.equals(getCurrentUsername()))) {
|
||||
localUserList.add(user);
|
||||
logger.info("adding new whisper user");
|
||||
chatBoxController.addWhisperUser(user);
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void updateUsername(String newUsername) {
|
||||
if (newUsername == null || newUsername.isBlank()) {
|
||||
return;
|
||||
}
|
||||
String oldUsername = this.username;
|
||||
this.username = newUsername.trim();
|
||||
chatBoxController.setUsername(this.username);
|
||||
|
||||
if (oldUsername != null && !oldUsername.equals(this.username)) {
|
||||
localUserList.remove(oldUsername);
|
||||
chatBoxController.removeWhisperUser(oldUsername);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleServerEvent(List<String> lines) {
|
||||
if (lines == null || lines.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<RequestParameter> params;
|
||||
try {
|
||||
params = ClientService.convertToRequestParameters(lines);
|
||||
} catch (RuntimeException e) {
|
||||
return;
|
||||
}
|
||||
String event = null;
|
||||
String oldUsername = null;
|
||||
String newUsername = null;
|
||||
|
||||
for (RequestParameter p : params) {
|
||||
if ("EVENT".equalsIgnoreCase(p.key())) {
|
||||
event = p.value();
|
||||
} else if ("OLD_USERNAME".equalsIgnoreCase(p.key())) {
|
||||
oldUsername = p.value();
|
||||
} else if ("NEW_USERNAME".equalsIgnoreCase(p.key())) {
|
||||
newUsername = p.value();
|
||||
}
|
||||
}
|
||||
|
||||
if (!"USERNAME_CHANGED".equalsIgnoreCase(event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
applyUsernameMigration(oldUsername, newUsername);
|
||||
}
|
||||
|
||||
private synchronized void applyUsernameMigration(String oldUsername, String newUsername) {
|
||||
if (oldUsername == null
|
||||
|| newUsername == null
|
||||
|| oldUsername.isBlank()
|
||||
|| newUsername.isBlank()
|
||||
|| oldUsername.equals(newUsername)) {
|
||||
return;
|
||||
}
|
||||
|
||||
String currentUsername = getCurrentUsername();
|
||||
if (oldUsername.equals(currentUsername)) {
|
||||
updateUsername(newUsername);
|
||||
}
|
||||
|
||||
ChatKey oldKey = new ChatKey(ChatType.WHISPER, oldUsername);
|
||||
ChatKey newKey = new ChatKey(ChatType.WHISPER, newUsername);
|
||||
|
||||
ChatModel oldModel = chatModelMap.remove(oldKey);
|
||||
ChatModel existingNewModel = chatModelMap.get(newKey);
|
||||
if (oldModel != null) {
|
||||
oldModel.setTarget(newUsername);
|
||||
if (existingNewModel == null) {
|
||||
chatModelMap.put(newKey, oldModel);
|
||||
} else {
|
||||
// Merge possible parallel history into the already existing new-key model.
|
||||
for (Message msg : oldModel.messages) {
|
||||
existingNewModel.addMessage(msg);
|
||||
}
|
||||
}
|
||||
chatBoxController.renameWhisperUser(oldUsername, newUsername);
|
||||
}
|
||||
|
||||
localUserList.remove(oldUsername);
|
||||
chatBoxController.removeWhisperUser(oldUsername);
|
||||
if (!newUsername.equals(getCurrentUsername())) {
|
||||
addWhisperUser(newUsername);
|
||||
}
|
||||
}
|
||||
|
||||
public String getCurrentUsername() {
|
||||
String shared = ClientApp.getSharedUsername();
|
||||
if (shared != null && !shared.isBlank()) {
|
||||
return shared.trim();
|
||||
}
|
||||
return username;
|
||||
}
|
||||
|
||||
/** Stops polling background tasks for this chat controller instance. */
|
||||
public void shutdown() {
|
||||
timer.cancel();
|
||||
clientService.removeEventListener(serverEventListener);
|
||||
synchronized (ACTIVE_CONTROLLERS) {
|
||||
if (ACTIVE_CONTROLLERS.get(clientService) == this) {
|
||||
ACTIVE_CONTROLLERS.remove(clientService);
|
||||
|
||||
@@ -22,7 +22,7 @@ public class ChatModel {
|
||||
public final String username;
|
||||
|
||||
/** The person to send the message to If the chat is a whisper chat */
|
||||
private final String target;
|
||||
private String target;
|
||||
|
||||
private final IntegerProperty count;
|
||||
|
||||
@@ -82,4 +82,8 @@ public class ChatModel {
|
||||
public String getTarget() {
|
||||
return target;
|
||||
}
|
||||
|
||||
public void setTarget(String target) {
|
||||
this.target = target;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import java.util.List;
|
||||
public class GameState {
|
||||
public List<Player> players = new ArrayList<>();
|
||||
public List<Card> communityCards = new ArrayList<>();
|
||||
public List<String> highscoreEntries = new ArrayList<>();
|
||||
|
||||
public String phase;
|
||||
public int pot;
|
||||
|
||||
@@ -155,6 +155,10 @@ public class GameClient {
|
||||
s.winnerIndex = intVal(l);
|
||||
yield true;
|
||||
}
|
||||
case String l when l.startsWith("HIGHSCORE=") -> {
|
||||
s.highscoreEntries.add(value(l));
|
||||
yield true;
|
||||
}
|
||||
default -> false;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -81,6 +81,30 @@ public class LobbyClient {
|
||||
throw new RuntimeException("No LOBBY_ID in response: " + lines);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the global highscores from the server.
|
||||
*
|
||||
* @return list of formatted highscore entries, newest entries at the end
|
||||
*/
|
||||
public List<String> getHighscores() {
|
||||
List<String> lines = client.processCommand("GET_HIGHSCORES");
|
||||
List<RequestParameter> params = ClientService.convertToRequestParameters(lines);
|
||||
|
||||
List<String> entries = new ArrayList<>();
|
||||
for (RequestParameter p : params) {
|
||||
if ("HIGHSCORE".equalsIgnoreCase(p.key())) {
|
||||
entries.add(p.value());
|
||||
}
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
/** Clears all global highscores on the server. */
|
||||
public void clearHighscores() {
|
||||
client.processCommand("CLEAR_HIGHSCORES");
|
||||
}
|
||||
|
||||
/**
|
||||
* Request the server to return the id of the lobby that the client is currently in.
|
||||
*
|
||||
@@ -131,6 +155,29 @@ public class LobbyClient {
|
||||
return new LoginResult(assigned, id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the username for the currently logged-in session.
|
||||
*
|
||||
* @param newUsername desired new username
|
||||
* @return a {@link LoginResult} containing assigned username and id as returned by the server
|
||||
*/
|
||||
public LoginResult changeUsername(String newUsername) {
|
||||
List<String> lines = client.processCommand("CHANGE_USERNAME USERNAME=" + newUsername);
|
||||
|
||||
List<RequestParameter> params = ClientService.convertToRequestParameters(lines);
|
||||
|
||||
String assigned = newUsername;
|
||||
String id = null;
|
||||
for (RequestParameter p : params) {
|
||||
if ("USERNAME".equalsIgnoreCase(p.key())) {
|
||||
assigned = p.value();
|
||||
} else if ("ID".equalsIgnoreCase(p.key())) {
|
||||
id = p.value();
|
||||
}
|
||||
}
|
||||
return new LoginResult(assigned, id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Request the server for the list of available lobbies.
|
||||
*
|
||||
|
||||
@@ -60,6 +60,12 @@ public class ChatBoxController {
|
||||
usernameTabMap = new HashMap<>();
|
||||
}
|
||||
|
||||
public void setUsername(String username) {
|
||||
if (username != null && !username.isBlank()) {
|
||||
this.username = username.trim();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
@@ -84,6 +90,11 @@ public class ChatBoxController {
|
||||
public void addWhisperUser(String targetUserName) {
|
||||
Platform.runLater(
|
||||
() -> {
|
||||
for (MenuItem existing : addWhisperChatButton.getItems()) {
|
||||
if (targetUserName.equals(existing.getText())) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
MenuItem menuItem = new MenuItem(targetUserName);
|
||||
addWhisperChatButton.getItems().add(menuItem);
|
||||
menuItem.setOnAction(
|
||||
@@ -98,6 +109,54 @@ public class ChatBoxController {
|
||||
});
|
||||
}
|
||||
|
||||
public void removeWhisperUser(String targetUserName) {
|
||||
Platform.runLater(
|
||||
() ->
|
||||
addWhisperChatButton
|
||||
.getItems()
|
||||
.removeIf(item -> targetUserName.equals(item.getText())));
|
||||
}
|
||||
|
||||
public void renameWhisperUser(String oldUsername, String newUsername) {
|
||||
if (oldUsername == null
|
||||
|| newUsername == null
|
||||
|| oldUsername.isBlank()
|
||||
|| newUsername.isBlank()
|
||||
|| oldUsername.equals(newUsername)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Platform.runLater(
|
||||
() -> {
|
||||
for (MenuItem item : addWhisperChatButton.getItems()) {
|
||||
if (oldUsername.equals(item.getText())) {
|
||||
item.setText(newUsername);
|
||||
item.setOnAction(
|
||||
event ->
|
||||
addWhisperChat(
|
||||
newUsername,
|
||||
new ChatModel(
|
||||
ChatType.WHISPER,
|
||||
username,
|
||||
-1,
|
||||
newUsername)));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Tab tab = usernameTabMap.remove(oldUsername);
|
||||
if (tab != null) {
|
||||
tab.setText(newUsername);
|
||||
usernameTabMap.put(newUsername, tab);
|
||||
}
|
||||
|
||||
int idx = activeWhisperChats.indexOf(oldUsername);
|
||||
if (idx >= 0) {
|
||||
activeWhisperChats.set(idx, newUsername);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new private (whisper) chat model for a specific target user, registers it within
|
||||
* the chat system, and opens a new chat tab.
|
||||
|
||||
+2
-1
@@ -68,11 +68,12 @@ public class ChatViewController implements Initializable {
|
||||
String message = inputField.getText().trim();
|
||||
if (!message.isEmpty()) {
|
||||
inputField.clear();
|
||||
String currentUsername = controller.getCurrentUsername();
|
||||
Message msg =
|
||||
new Message(
|
||||
chatModel.getChattype(),
|
||||
chatModel.lobbyId,
|
||||
username,
|
||||
currentUsername,
|
||||
chatModel.getTarget(),
|
||||
message);
|
||||
controller.onSendToNetwork(msg);
|
||||
|
||||
+6
-5
@@ -633,12 +633,13 @@ public class CasinoGameController {
|
||||
*/
|
||||
private void updateTaskbar(GameState s) {
|
||||
TaskbarController controller = resolveTaskbarController();
|
||||
if (controller != null) {
|
||||
if (gameService != null && myPlayerId != null) {
|
||||
controller.setGameService(gameService, myPlayerId);
|
||||
}
|
||||
controller.update(s, myPlayerId);
|
||||
if (controller == null || myPlayerId == null) {
|
||||
return;
|
||||
}
|
||||
if (gameService != null) {
|
||||
controller.setGameService(gameService, myPlayerId);
|
||||
}
|
||||
controller.update(s, myPlayerId);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -114,8 +114,11 @@ public class CasinoGameUI extends Application {
|
||||
Parent root = fxmlLoader.load();
|
||||
CasinoGameController controller = fxmlLoader.getController();
|
||||
|
||||
int gameId = 1; // TODO echte gameId einsetzen
|
||||
GameClient gameClient = new GameClient(clientService, gameId);
|
||||
if (lobbyId <= 0) {
|
||||
throw new IllegalStateException("CasinoGameUI: lobbyId must be set before start()");
|
||||
}
|
||||
|
||||
GameClient gameClient = new GameClient(clientService, lobbyId);
|
||||
GameService gameService = new GameService(gameClient);
|
||||
controller.setGameService(gameService);
|
||||
|
||||
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.gameuicomponents;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.network.LobbyClient;
|
||||
import java.util.List;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ListView;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.stage.Stage;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
/** Controller for the highscore popup window. */
|
||||
public class HighscoreViewController {
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(HighscoreViewController.class);
|
||||
|
||||
@FXML private ListView<String> highscoreList;
|
||||
@FXML private Label statusLabel;
|
||||
@FXML private Node highscoreRoot;
|
||||
|
||||
private LobbyClient lobbyClient;
|
||||
private double dragOffsetX;
|
||||
private double dragOffsetY;
|
||||
|
||||
public void setLobbyClient(LobbyClient lobbyClient) {
|
||||
this.lobbyClient = lobbyClient;
|
||||
}
|
||||
|
||||
/** Starts dragging the window from anywhere in the popup. */
|
||||
@FXML
|
||||
public void onPopupPressed(MouseEvent event) {
|
||||
Stage stage = getStage();
|
||||
if (stage == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
dragOffsetX = stage.getX() - event.getScreenX();
|
||||
dragOffsetY = stage.getY() - event.getScreenY();
|
||||
}
|
||||
|
||||
/** Moves the popup window while the mouse is dragged anywhere on the popup. */
|
||||
@FXML
|
||||
public void onPopupDragged(MouseEvent event) {
|
||||
Stage stage = getStage();
|
||||
if (stage == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
stage.setX(event.getScreenX() + dragOffsetX);
|
||||
stage.setY(event.getScreenY() + dragOffsetY);
|
||||
}
|
||||
|
||||
/** Returns the popup stage if the root is already attached to a scene. */
|
||||
private Stage getStage() {
|
||||
if (highscoreRoot == null || highscoreRoot.getScene() == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (Stage) highscoreRoot.getScene().getWindow();
|
||||
}
|
||||
|
||||
/** Loads current highscores from the server and refreshes the list. */
|
||||
@FXML
|
||||
public void refreshHighscores() {
|
||||
if (highscoreList == null || statusLabel == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (lobbyClient == null) {
|
||||
statusLabel.setText("Lobby client not initialized.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
List<String> entries = lobbyClient.getHighscores();
|
||||
highscoreList.getItems().setAll(entries);
|
||||
statusLabel.setText(
|
||||
entries.isEmpty() ? "No highscores yet." : entries.size() + " entries loaded.");
|
||||
} catch (RuntimeException e) {
|
||||
LOGGER.error("Failed to load highscores: {}", e.getMessage());
|
||||
statusLabel.setText("Failed to load highscores.");
|
||||
}
|
||||
}
|
||||
|
||||
/** Clears all highscores globally on the server and reloads the list. */
|
||||
@FXML
|
||||
public void clearHighscores() {
|
||||
if (lobbyClient == null || statusLabel == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
lobbyClient.clearHighscores();
|
||||
refreshHighscores();
|
||||
statusLabel.setText("Highscores cleared.");
|
||||
} catch (RuntimeException e) {
|
||||
LOGGER.error("Failed to clear highscores: {}", e.getMessage());
|
||||
statusLabel.setText("Failed to clear highscores.");
|
||||
}
|
||||
}
|
||||
|
||||
/** Closes the highscore popup window. */
|
||||
@FXML
|
||||
public void closeWindow() {
|
||||
if (highscoreList == null || highscoreList.getScene() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Stage stage = (Stage) highscoreList.getScene().getWindow();
|
||||
stage.close();
|
||||
}
|
||||
}
|
||||
+731
-81
@@ -1,12 +1,15 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.gameuicomponents;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.ClientApp;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.game.GameService;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.game.GameState;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.game.Player;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.game.PlayerId;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.game.PlayerState;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.network.LobbyClient;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.ui.lobbyui.Casinomainui;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.Alert;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.TextField;
|
||||
@@ -25,7 +28,7 @@ import org.apache.logging.log4j.Logger;
|
||||
*/
|
||||
public class TaskbarController {
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(CasinoBrowserController.class);
|
||||
private static final Logger LOGGER = LogManager.getLogger(TaskbarController.class);
|
||||
|
||||
@FXML private HBox taskbar;
|
||||
@FXML private TextField taskbarInput;
|
||||
@@ -41,9 +44,13 @@ public class TaskbarController {
|
||||
private double xOffset = 0;
|
||||
private double yOffset = 0;
|
||||
private static final double TASKBAR_SCALE = 0.95;
|
||||
private static final int MIN_CREDITS = 5;
|
||||
private static final int MAX_CREDITS = 100000;
|
||||
private static final int CREDIT_STEP = 5;
|
||||
private static final double PREFLOP_BLOCK_RATIO = 0.50;
|
||||
private static final double FLOP_WARN_RATIO = 0.50;
|
||||
private static final double FLOP_BLOCK_RATIO = 0.80;
|
||||
private static final double TURN_RIVER_WARN_RATIO = 0.80;
|
||||
private static final double TURN_RIVER_BLOCK_RATIO = 1.00;
|
||||
private static final int SMALL_BLIND = 100;
|
||||
private static final int BIG_BLIND = 200;
|
||||
private static final String STYLE_YELLOW_BUTTON = "yellow-button";
|
||||
private static final String STYLE_GRAY_BUTTON = "gray-button";
|
||||
private static final String STYLE_RED_BUTTON = "red-button";
|
||||
@@ -51,6 +58,9 @@ public class TaskbarController {
|
||||
private static final String STYLE_GRAY_INPUT = "gray-input-field";
|
||||
private static final String STYLE_RED_INPUT = "red-input-field";
|
||||
private static final double SCALE_NORMAL = 1.0;
|
||||
private static final int DEALER_OFFSET = 3;
|
||||
private boolean inputActionAllowed;
|
||||
private int lastReferenceBet = BIG_BLIND;
|
||||
|
||||
/** Standard constructor. Used by FXML. */
|
||||
public TaskbarController() {
|
||||
@@ -58,7 +68,7 @@ public class TaskbarController {
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the state of the basic action buttons (Bet, Call, Fold, Raise) and the input field
|
||||
* Updates the state of the basic action buttons (Bet, Call, Fold, Raise) and the input field.
|
||||
*
|
||||
* @param isMyTurn Indicates whether it is currently the player's turn.
|
||||
* @param isOut Indicates whether the player is currently out of the game (folded or all-in).
|
||||
@@ -90,9 +100,10 @@ public class TaskbarController {
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the given button to a disabled state with red styling, indicating that the player is out
|
||||
* Sets the given button to a disabled state with red styling, indicating that the player is
|
||||
* out.
|
||||
*
|
||||
* @param b The button to be styled as red and disabled
|
||||
* @param b The button to be styled as red and disabled.
|
||||
*/
|
||||
private void setRedButton(Button b) {
|
||||
b.setDisable(true);
|
||||
@@ -107,9 +118,9 @@ public class TaskbarController {
|
||||
|
||||
/**
|
||||
* Sets the given input field to a disabled state with red styling, indicating that the player
|
||||
* is out
|
||||
* is out.
|
||||
*
|
||||
* @param t The text field to be styled as red and disabled
|
||||
* @param t The text field to be styled as red and disabled.
|
||||
*/
|
||||
private void setRedInputField(TextField t) {
|
||||
t.setDisable(true);
|
||||
@@ -124,9 +135,9 @@ public class TaskbarController {
|
||||
|
||||
/**
|
||||
* Activates the given button by enabling it and applying yellow styling, indicating that it is
|
||||
* the player's turn
|
||||
* the player's turn.
|
||||
*
|
||||
* @param b The button to be activated and styled for the player's turn
|
||||
* @param b The button to be activated and styled for the player's turn.
|
||||
*/
|
||||
private void activateButton(Button b) {
|
||||
b.setDisable(false);
|
||||
@@ -140,9 +151,9 @@ public class TaskbarController {
|
||||
|
||||
/**
|
||||
* Activates the given input field by enabling it and applying yellow styling, indicating that
|
||||
* it is the player's turn
|
||||
* it is the player's turn.
|
||||
*
|
||||
* @param t The text field to be activated and styled for the player's turn
|
||||
* @param t The text field to be activated and styled for the player's turn.
|
||||
*/
|
||||
private void activateInputField(TextField t) {
|
||||
t.setDisable(false);
|
||||
@@ -156,9 +167,9 @@ public class TaskbarController {
|
||||
|
||||
/**
|
||||
* Deactivates the given button by disabling it and applying gray styling, indicating that it is
|
||||
* not the player's turn
|
||||
* not the player's turn.
|
||||
*
|
||||
* @param b The button to be deactivated and styled for non-active state
|
||||
* @param b The button to be deactivated and styled for non-active state.
|
||||
*/
|
||||
private void deactivateButton(Button b) {
|
||||
b.setDisable(true);
|
||||
@@ -173,9 +184,9 @@ public class TaskbarController {
|
||||
|
||||
/**
|
||||
* Deactivates the given input field by disabling it and applying gray styling, indicating that
|
||||
* it is not the player's turn
|
||||
* it is not the player's turn.
|
||||
*
|
||||
* @param t The text field to be deactivated and styled for non-active state
|
||||
* @param t The text field to be deactivated and styled for non-active state.
|
||||
*/
|
||||
private void deactivateInputField(TextField t) {
|
||||
t.setDisable(true);
|
||||
@@ -209,15 +220,25 @@ public class TaskbarController {
|
||||
@FXML
|
||||
public void initialize() {
|
||||
updateBasicButtons(false, false);
|
||||
if (taskbarInput != null) {
|
||||
taskbarInput
|
||||
.textProperty()
|
||||
.addListener((obs, oldValue, newValue) -> refreshBetInputUi());
|
||||
}
|
||||
|
||||
setBetButtonVisible(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the taskbar based on the current game state and the player's status. It checks if
|
||||
* it's the player's turn and whether they are out of the game (folded or all-in) to adjust the
|
||||
* button states and displayed money accordingly.
|
||||
* Updates the taskbar based on the current game state and the player's status.
|
||||
*
|
||||
* @param state
|
||||
* @param myPlayerId
|
||||
* @param state The current GameState object representing the state of the game, which includes
|
||||
* information about the players, their bets, the current phase and other relevant details
|
||||
* needed to update the taskbar UI accurately based on the player's status and the game
|
||||
* context.
|
||||
* @param myPlayerId The PlayerId object representing the current player, used to identify the
|
||||
* player's status and update the taskbar UI accordingly based on whether it is their turn
|
||||
* and whether they are out of the game.
|
||||
*/
|
||||
public void update(GameState state, PlayerId myPlayerId) {
|
||||
|
||||
@@ -227,6 +248,9 @@ public class TaskbarController {
|
||||
}
|
||||
|
||||
this.lastState = state;
|
||||
if (state.currentBet > 0) {
|
||||
lastReferenceBet = state.currentBet;
|
||||
}
|
||||
|
||||
Player me =
|
||||
state.players.stream()
|
||||
@@ -243,9 +267,594 @@ public class TaskbarController {
|
||||
|
||||
boolean isMyTurn = state.activePlayer == myIndex;
|
||||
boolean isOut = me.getState() == PlayerState.FOLDED;
|
||||
boolean isGameFinished = isHandFinished(state);
|
||||
|
||||
updateBasicButtons(isMyTurn, isOut);
|
||||
updateBasicButtons(isMyTurn, isOut || isGameFinished);
|
||||
applyActionAvailability(state, me, isMyTurn, isOut || isGameFinished);
|
||||
setMoney(me.getChips());
|
||||
refreshBetInputUi();
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the availability of actions (Bet, Call, Raise, Fold) based on the current game state.
|
||||
*
|
||||
* @param state The current game state to evaluate action availability.
|
||||
* @param me The player object representing the current player, used to determine their chips
|
||||
* and bet status.
|
||||
* @param isMyTurn Indicates whether it is currently the player's turn, which affects whether
|
||||
* actions can be taken.
|
||||
* @param isOutOrFinished Indicates whether the player is out of the game (folded or all-in) or
|
||||
* if the game is finished, which disables actions.
|
||||
*/
|
||||
private void applyActionAvailability(
|
||||
GameState state, Player me, boolean isMyTurn, boolean isOutOrFinished) {
|
||||
inputActionAllowed = false;
|
||||
if (!isMyTurn || isOutOrFinished || state == null || me == null) {
|
||||
setBetButtonVisible(false);
|
||||
return;
|
||||
}
|
||||
|
||||
int chips = Math.max(0, me.getChips());
|
||||
int toCall = getToCall(state, me);
|
||||
|
||||
boolean forcedFoldOnly = chips <= 0 || toCall > chips;
|
||||
if (forcedFoldOnly) {
|
||||
setActionEnabled(betButton, false);
|
||||
setActionEnabled(callButton, false);
|
||||
setActionEnabled(raiseButton, false);
|
||||
setActionEnabled(foldButton, true);
|
||||
deactivateInputField(taskbarInput);
|
||||
setBetButtonVisible(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isFirstPreflopPlayerInputOnly(state, me)) {
|
||||
setActionEnabled(betButton, true);
|
||||
setActionEnabled(callButton, false);
|
||||
setActionEnabled(foldButton, false);
|
||||
setActionEnabled(raiseButton, false);
|
||||
activateInputField(taskbarInput);
|
||||
inputActionAllowed = true;
|
||||
return;
|
||||
}
|
||||
|
||||
int callTarget = effectiveCallTarget(state);
|
||||
int raiseTarget = safeDouble(callTarget);
|
||||
ValidationResult callValidation = validateTarget(state, me, callTarget);
|
||||
ValidationResult raiseValidation = validateTarget(state, me, raiseTarget);
|
||||
boolean canCall = callValidation.valid;
|
||||
boolean canRaise = raiseValidation.valid;
|
||||
boolean canInput = canCall || canRaise;
|
||||
|
||||
setActionEnabled(betButton, canInput);
|
||||
setActionEnabled(callButton, canCall);
|
||||
setActionEnabled(raiseButton, canRaise);
|
||||
setActionEnabled(foldButton, true);
|
||||
|
||||
if (canInput) {
|
||||
activateInputField(taskbarInput);
|
||||
inputActionAllowed = true;
|
||||
} else {
|
||||
deactivateInputField(taskbarInput);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely doubles the given value while preventing integer overflow.
|
||||
*
|
||||
* @param value The integer value to be safely doubled.
|
||||
* @return The safely doubled value, or 0 if the input is non-positive, or Integer.MAX_VALUE if
|
||||
* doubling would overflow.
|
||||
*/
|
||||
private int safeDouble(int value) {
|
||||
if (value <= 0) {
|
||||
return 0;
|
||||
}
|
||||
if (value > Integer.MAX_VALUE / 2) {
|
||||
return Integer.MAX_VALUE;
|
||||
}
|
||||
return value * 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the visibility of the Bet button in the taskbar.
|
||||
*
|
||||
* @param visible A boolean indicating whether the Bet button should be visible (true) or hidden
|
||||
* (false).
|
||||
*/
|
||||
private void setBetButtonVisible(boolean visible) {
|
||||
if (betButton == null) {
|
||||
return;
|
||||
}
|
||||
betButton.setVisible(visible);
|
||||
betButton.setManaged(visible);
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes the user interface of the bet input field and the Bet button based on the current
|
||||
* game state and the validity of the input.
|
||||
*/
|
||||
private void refreshBetInputUi() {
|
||||
if (!inputActionAllowed || taskbarInput == null || taskbarInput.isDisabled()) {
|
||||
setBetButtonVisible(false);
|
||||
return;
|
||||
}
|
||||
|
||||
ValidationResult validation = validateTypedAmount(lastState, taskbarInput.getText());
|
||||
boolean valid = validation.valid;
|
||||
setBetButtonVisible(valid);
|
||||
|
||||
if (valid) {
|
||||
activateButton(betButton);
|
||||
} else {
|
||||
deactivateButton(betButton);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables or disables the given button based on the provided boolean value.
|
||||
*
|
||||
* @param button The Button object to be enabled or disabled based on the provided boolean
|
||||
* value.
|
||||
* @param enabled A boolean value indicating whether the button should be enabled (true) or
|
||||
* disabled (false).
|
||||
*/
|
||||
private void setActionEnabled(Button button, boolean enabled) {
|
||||
if (enabled) {
|
||||
activateButton(button);
|
||||
} else {
|
||||
deactivateButton(button);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the amount needed to call the current bet in the game.
|
||||
*
|
||||
* @param state The current GameState object representing the state of the game, which includes
|
||||
* information about the current bet and the player's bet status.
|
||||
* @param me The Player object representing the current player, used to determine how much they
|
||||
* have already invested in the current bet.
|
||||
* @return An integer representing the amount needed for the player to call the current bet.
|
||||
*/
|
||||
private int getToCall(GameState state, Player me) {
|
||||
int current = effectiveCallTarget(state);
|
||||
int alreadyInvested = Math.max(0, me.getBet());
|
||||
return Math.max(0, current - alreadyInvested);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the effective call target for the current game state. If there is an active
|
||||
* current bet, it returns that as the call target.
|
||||
*
|
||||
* @param state The current GameState object representing the state of the game, which includes
|
||||
* information about the current bet and the last reference bet.
|
||||
* @return An integer representing the effective call target for the current game state.
|
||||
*/
|
||||
private int effectiveCallTarget(GameState state) {
|
||||
if (state != null && state.currentBet > 0) {
|
||||
return state.currentBet;
|
||||
}
|
||||
return Math.max(0, lastReferenceBet);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the current player is the first to act in the pre-flop phase and if the initial
|
||||
* blind layout is still in place.
|
||||
*
|
||||
* @param state The current GameState object representing the state of the game, which includes
|
||||
* information about the phase of the game, the players, the dealer position, and the active
|
||||
* player.
|
||||
* @param me The Player object representing the current player, used to determine their position
|
||||
* in the player list and whether they are the active player.
|
||||
* @return A boolean value indicating whether the current player is the first to act in the
|
||||
* pre-flop phase with only the initial blind layout in place.
|
||||
*/
|
||||
private boolean isFirstPreflopPlayerInputOnly(GameState state, Player me) {
|
||||
if (state == null || me == null || !isPreflop(state.phase) || state.players == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int size = state.players.size();
|
||||
if (size < 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int myIndex = state.players.indexOf(me);
|
||||
if (myIndex < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int dealer = state.dealer;
|
||||
int firstIndex = (size == 2) ? dealer : (dealer + DEALER_OFFSET) % size;
|
||||
if (state.activePlayer != firstIndex || myIndex != firstIndex) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return isInitialPreflopBlindLayout(state);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the initial blind layout is still in place during the pre-flop phase.
|
||||
*
|
||||
* @param state The current GameState object representing the state of the game, which includes
|
||||
* information about the players and their bets.
|
||||
* @return A boolean value indicating whether the initial blind layout is still in place during
|
||||
* the pre-flop phase.
|
||||
*/
|
||||
private boolean isInitialPreflopBlindLayout(GameState state) {
|
||||
int sbCount = 0;
|
||||
int bbCount = 0;
|
||||
int zeroCount = 0;
|
||||
|
||||
for (Player player : state.players) {
|
||||
if (player == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int bet = Math.max(0, player.getBet());
|
||||
if (bet == SMALL_BLIND) {
|
||||
sbCount++;
|
||||
} else if (bet == BIG_BLIND) {
|
||||
bbCount++;
|
||||
} else if (bet == 0) {
|
||||
zeroCount++;
|
||||
}
|
||||
}
|
||||
|
||||
int playerCount = state.players.size();
|
||||
return playerCount >= 2 && sbCount == 1 && bbCount == 1 && zeroCount == playerCount - 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given phase string corresponds to the pre-flop phase of the game.
|
||||
*
|
||||
* @param phase The string representing the current phase of the game, which is expected to be
|
||||
* compared against "PREFLOP" to determine if it is the pre-flop phase.
|
||||
* @return A boolean value indicating whether the given phase string corresponds to the pre-flop
|
||||
* phase.
|
||||
*/
|
||||
private boolean isPreflop(String phase) {
|
||||
return "PREFLOP".equalsIgnoreCase(phase);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given phase string corresponds to the flop phase of the game.
|
||||
*
|
||||
* @param phase The string representing the current phase of the game, which is expected to be
|
||||
* compared against "FLOP" to determine if it is the flop phase.
|
||||
* @return A boolean value indicating whether the given phase string corresponds to the flop
|
||||
* phase.
|
||||
*/
|
||||
private boolean isFlop(String phase) {
|
||||
return "FLOP".equalsIgnoreCase(phase);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given phase string corresponds to either the turn or river phase of the game.
|
||||
*
|
||||
* @param phase The string representing the current phase of the game, which is expected to be
|
||||
* compared against "TURN" and "RIVER" to determine if it is either the turn or river phase.
|
||||
* @return A boolean value indicating whether the given phase string corresponds to either the
|
||||
* turn or river phase.
|
||||
*/
|
||||
private boolean isTurnOrRiver(String phase) {
|
||||
return "TURN".equalsIgnoreCase(phase) || "RIVER".equalsIgnoreCase(phase);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the hand is finished based on the current game state.
|
||||
*
|
||||
* @param state The current GameState object representing the state of the game, which includes
|
||||
* information
|
||||
* @return A boolean value indicating whether the hand is finished, which can be determined by
|
||||
* checking if the phase is "FINISHED" or "SHOWDOWN",
|
||||
*/
|
||||
private boolean isHandFinished(GameState state) {
|
||||
if (state == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ("FINISHED".equalsIgnoreCase(state.phase) || "SHOWDOWN".equalsIgnoreCase(state.phase)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return state.players != null
|
||||
&& state.winnerIndex >= 0
|
||||
&& state.winnerIndex < state.players.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates the risk level of a proposed bet based on the current game state, the amount of the
|
||||
* bet and the player's available chips.
|
||||
*/
|
||||
private enum BetRisk {
|
||||
ALLOWED,
|
||||
WARNING,
|
||||
BLOCKED
|
||||
}
|
||||
|
||||
/** Enum representing the types of actions that can be submitted from the taskbar. */
|
||||
private enum ActionType {
|
||||
INPUT,
|
||||
CALL_BUTTON,
|
||||
RAISE_BUTTON
|
||||
}
|
||||
|
||||
/** Class representing the result of validating a proposed bet or action. */
|
||||
private static final class ValidationResult {
|
||||
private final boolean valid;
|
||||
private final boolean warning;
|
||||
private final String message;
|
||||
|
||||
private ValidationResult(boolean valid, boolean warning, String message) {
|
||||
this.valid = valid;
|
||||
this.warning = warning;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
private static ValidationResult ok() {
|
||||
return new ValidationResult(true, false, null);
|
||||
}
|
||||
|
||||
private static ValidationResult warning(String message) {
|
||||
return new ValidationResult(true, true, message);
|
||||
}
|
||||
|
||||
private static ValidationResult blocked(String message) {
|
||||
return new ValidationResult(false, false, message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Submits a player action based on the specified ActionType.
|
||||
*
|
||||
* @param actionType The type of action being submitted, which can be an input-based action
|
||||
* (where the player types an amount) or a specific button action for calling or raising.
|
||||
*/
|
||||
private void submitAction(ActionType actionType) {
|
||||
if (gameService == null) {
|
||||
LOGGER.error("GameService not initialized");
|
||||
return;
|
||||
}
|
||||
|
||||
GameState state = ensureLatestStateForAction(actionType.name().toLowerCase());
|
||||
if (state == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isHandFinished(state)) {
|
||||
LOGGER.info("Action {} ignored: hand is already finished", actionType);
|
||||
update(state, myPlayerId);
|
||||
return;
|
||||
}
|
||||
|
||||
Player me = findCurrentPlayer(state);
|
||||
if (me == null) {
|
||||
LOGGER.error("Action {} blocked: player missing in state", actionType);
|
||||
return;
|
||||
}
|
||||
|
||||
int callTarget = effectiveCallTarget(state);
|
||||
int targetBet;
|
||||
if (actionType == ActionType.CALL_BUTTON) {
|
||||
targetBet = callTarget;
|
||||
} else if (actionType == ActionType.RAISE_BUTTON) {
|
||||
targetBet = safeDouble(callTarget);
|
||||
} else {
|
||||
Integer fromInput = parseInputTarget(taskbarInput.getText());
|
||||
if (fromInput == null) {
|
||||
return;
|
||||
}
|
||||
targetBet = fromInput;
|
||||
}
|
||||
|
||||
ValidationResult validation = validateTarget(state, me, targetBet);
|
||||
if (!validation.valid) {
|
||||
refreshBetInputUi();
|
||||
return;
|
||||
}
|
||||
|
||||
if (validation.warning) {
|
||||
showWarningDialog("WARNING", validation.message);
|
||||
}
|
||||
|
||||
if (state.currentBet <= 0) {
|
||||
gameService.bet(targetBet);
|
||||
LOGGER.info("Player BET (target={})", targetBet);
|
||||
} else if (targetBet == callTarget) {
|
||||
gameService.call();
|
||||
LOGGER.info("Player CALL (target={})", targetBet);
|
||||
} else {
|
||||
int raiseBy = Math.max(0, targetBet - state.currentBet);
|
||||
gameService.raise(raiseBy);
|
||||
LOGGER.info("Player RAISE by {} (target={})", raiseBy, targetBet);
|
||||
}
|
||||
|
||||
if (targetBet > 0) {
|
||||
lastReferenceBet = targetBet;
|
||||
}
|
||||
|
||||
taskbarInput.clear();
|
||||
refreshGame();
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that the latest game state is available for processing a player action. If the last
|
||||
* known state is null, it attempts to fetch the current state from the GameService.
|
||||
*
|
||||
* @param text The text associated with the action being processed, used for logging purposes to
|
||||
* indicate which action is being attempted when the state is not available.
|
||||
* @return The latest GameState object if available, or null if the state cannot be retrieved,
|
||||
* indicating that the action cannot be processed.
|
||||
*/
|
||||
private Integer parseInputTarget(String text) {
|
||||
if (text == null || text.trim().isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return Integer.parseInt(text.trim());
|
||||
} catch (NumberFormatException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the current player in the given game state based on the player's ID.
|
||||
*
|
||||
* @param state The current GameState object representing the state of the game, which includes
|
||||
* a list of players.
|
||||
* @param text The text associated with the action being processed, used for logging purposes to
|
||||
* indicate which action is being attempted when the player cannot be found in the state.
|
||||
* @return The Player object representing the current player if found in the game state, or null
|
||||
* if no matching player is found, indicating that the player cannot be identified in the
|
||||
* current game state.
|
||||
*/
|
||||
private ValidationResult validateTypedAmount(GameState state, String text) {
|
||||
Integer target = parseInputTarget(text);
|
||||
if (target == null || state == null) {
|
||||
return ValidationResult.blocked("Invalid input");
|
||||
}
|
||||
Player me = findCurrentPlayer(state);
|
||||
if (me == null) {
|
||||
return ValidationResult.blocked("Player not found");
|
||||
}
|
||||
return validateTarget(state, me, target);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a proposed target bet against the current game state and the player's status. It
|
||||
* checks if the target bet is either a valid call or a valid raise (specifically, exactly
|
||||
* double the call target).
|
||||
*
|
||||
* @param state The current GameState object representing the state of the game, which includes
|
||||
* information about the current bet, the phase of the game, and the players.
|
||||
* @param me The Player object representing the current player, used to determine their current
|
||||
* bet, available chips, and to evaluate whether the proposed target bet is valid for this
|
||||
* player based on their status in the game.
|
||||
* @param targetBet The integer value representing the proposed target bet that the player
|
||||
* intends to make.
|
||||
* @return A ValidationResult object indicating whether the proposed target bet is valid, if it
|
||||
* triggers a warning, or if it is blocked due to being invalid or too risky.
|
||||
*/
|
||||
private ValidationResult validateTarget(GameState state, Player me, int targetBet) {
|
||||
if (state == null || me == null) {
|
||||
return ValidationResult.blocked("Invalid game state");
|
||||
}
|
||||
|
||||
int callTarget = effectiveCallTarget(state);
|
||||
int raiseTarget = safeDouble(callTarget);
|
||||
if (targetBet != callTarget && targetBet != raiseTarget) {
|
||||
return ValidationResult.blocked("Only calls or exactly two raises allowed");
|
||||
}
|
||||
|
||||
int alreadyInvested = Math.max(0, me.getBet());
|
||||
int required = Math.max(0, targetBet - alreadyInvested);
|
||||
int chips = Math.max(0, me.getChips());
|
||||
|
||||
if (targetBet == callTarget && required == 0) {
|
||||
return ValidationResult.ok();
|
||||
}
|
||||
|
||||
if (required <= 0) {
|
||||
return ValidationResult.blocked("Invalid bet");
|
||||
}
|
||||
|
||||
if (required > chips) {
|
||||
return ValidationResult.blocked("Not enough chips for the next bet");
|
||||
}
|
||||
|
||||
BetRisk risk = evaluateBetRisk(state, required, chips);
|
||||
if (risk == BetRisk.BLOCKED) {
|
||||
return ValidationResult.blocked("Assignment for this phase is blocked");
|
||||
}
|
||||
if (risk == BetRisk.WARNING) {
|
||||
return ValidationResult.warning(warningTextForPhase(state));
|
||||
}
|
||||
return ValidationResult.ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a warning message based on the current phase of the game.
|
||||
*
|
||||
* @param state The current GameState object representing the state of the game, which includes
|
||||
* information about the phase of the game.
|
||||
* @return A string containing the warning message appropriate for the current phase of the game
|
||||
* when a player's bet triggers a warning due to being at least 50% of their stack.
|
||||
*/
|
||||
private String warningTextForPhase(GameState state) {
|
||||
String phase = state != null ? state.phase : null;
|
||||
if (isFlop(phase)) {
|
||||
return "WARNING: On the flop, you're betting at least 50% of your stack.";
|
||||
}
|
||||
if (isTurnOrRiver(phase)) {
|
||||
return "WARNING: On the flop, you're betting at least 50% of your stack.";
|
||||
}
|
||||
return "WARNING: High stakes.";
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates the risk level of a proposed bet based on the current game state, the amount of the
|
||||
* bet, and the player's available chips.
|
||||
*
|
||||
* @param state The current GameState object representing the state of the game, which includes
|
||||
* information about the phase of the game.
|
||||
* @param amount The integer value representing the amount of the proposed bet that the player
|
||||
* intends to make.
|
||||
* @param chips The integer value representing the player's available chips.
|
||||
* @return A BetRisk enum value indicating the risk level of the proposed bet.
|
||||
*/
|
||||
private BetRisk evaluateBetRisk(GameState state, int amount, int chips) {
|
||||
if (chips <= 0 || amount <= 0) {
|
||||
return BetRisk.BLOCKED;
|
||||
}
|
||||
|
||||
double ratio = (double) amount / chips;
|
||||
String phase = state != null ? state.phase : null;
|
||||
|
||||
if (isPreflop(phase)) {
|
||||
if (ratio > PREFLOP_BLOCK_RATIO) {
|
||||
return BetRisk.BLOCKED;
|
||||
}
|
||||
return BetRisk.ALLOWED;
|
||||
}
|
||||
|
||||
if (isFlop(phase)) {
|
||||
if (ratio > FLOP_BLOCK_RATIO) {
|
||||
return BetRisk.BLOCKED;
|
||||
}
|
||||
if (ratio >= FLOP_WARN_RATIO) {
|
||||
return BetRisk.WARNING;
|
||||
}
|
||||
return BetRisk.ALLOWED;
|
||||
}
|
||||
|
||||
if (isTurnOrRiver(phase)) {
|
||||
if (ratio >= TURN_RIVER_BLOCK_RATIO) {
|
||||
return BetRisk.BLOCKED;
|
||||
}
|
||||
if (ratio > TURN_RIVER_WARN_RATIO) {
|
||||
return BetRisk.WARNING;
|
||||
}
|
||||
return BetRisk.ALLOWED;
|
||||
}
|
||||
|
||||
return BetRisk.ALLOWED;
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a warning dialog with the specified title and content.
|
||||
*
|
||||
* @param title The string representing the title of the warning dialog, which is displayed in
|
||||
* the title bar of the dialog window.
|
||||
* @param content The string representing the content of the warning message, which is displayed
|
||||
* in the body of the dialog.
|
||||
*/
|
||||
private void showWarningDialog(String title, String content) {
|
||||
Alert alert = new Alert(Alert.AlertType.WARNING);
|
||||
alert.setTitle(title);
|
||||
alert.setHeaderText(null);
|
||||
alert.setContentText(content);
|
||||
alert.showAndWait();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -297,6 +906,12 @@ public class TaskbarController {
|
||||
@FXML
|
||||
private void onInputSubmitted(KeyEvent event) {
|
||||
if (event.getCode() == KeyCode.ENTER) {
|
||||
ValidationResult validation = validateTypedAmount(lastState, taskbarInput.getText());
|
||||
if (!validation.valid) {
|
||||
event.consume();
|
||||
setBetButtonVisible(false);
|
||||
return;
|
||||
}
|
||||
processBet();
|
||||
}
|
||||
}
|
||||
@@ -313,16 +928,7 @@ public class TaskbarController {
|
||||
/** Called when the Call button is clicked. */
|
||||
@FXML
|
||||
private void onInputPlayerCall() {
|
||||
|
||||
if (gameService == null) {
|
||||
LOGGER.error("GameService not initialized");
|
||||
return;
|
||||
}
|
||||
|
||||
gameService.call();
|
||||
LOGGER.info("Player CALL");
|
||||
|
||||
refreshGame();
|
||||
submitPresetInputAndProcess("call");
|
||||
}
|
||||
|
||||
/** Called when the Fold button is clicked. */
|
||||
@@ -334,6 +940,13 @@ public class TaskbarController {
|
||||
return;
|
||||
}
|
||||
|
||||
GameState state = ensureLatestStateForAction("fold");
|
||||
if (isHandFinished(state)) {
|
||||
LOGGER.info("Fold ignored: hand is already finished");
|
||||
update(state, myPlayerId);
|
||||
return;
|
||||
}
|
||||
|
||||
gameService.fold();
|
||||
LOGGER.info("Player FOLD");
|
||||
|
||||
@@ -343,36 +956,25 @@ public class TaskbarController {
|
||||
/** Called when the Raise button is clicked. */
|
||||
@FXML
|
||||
private void onInputPlayerRaise() {
|
||||
submitPresetInputAndProcess("raise");
|
||||
}
|
||||
|
||||
if (gameService == null) {
|
||||
LOGGER.error("GameService not initialized");
|
||||
/**
|
||||
* Submits a preset input for either calling or raising based on the specified mode.
|
||||
*
|
||||
* @param mode A string indicating the mode of the action, which can be "call" for calling the
|
||||
* current bet or "raise" for raising to double the current bet.
|
||||
*/
|
||||
private void submitPresetInputAndProcess(String mode) {
|
||||
GameState state = ensureLatestStateForAction(mode);
|
||||
if (state == null || taskbarInput == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
GameState state = lastState;
|
||||
if (state == null) {
|
||||
try {
|
||||
state = gameService.refresh();
|
||||
lastState = state;
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("Failed to refresh game state for raise: {}", e.getMessage());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (state == null || state.currentBet <= 0) {
|
||||
LOGGER.warn(
|
||||
"Raise not possible: current bet is {}",
|
||||
state != null ? state.currentBet : null);
|
||||
return;
|
||||
}
|
||||
|
||||
int raiseAmount = state.currentBet;
|
||||
gameService.raise(raiseAmount);
|
||||
LOGGER.info("Player RAISE by {} (target: double table bet)", raiseAmount);
|
||||
|
||||
taskbarInput.clear();
|
||||
refreshGame();
|
||||
int callTarget = effectiveCallTarget(state);
|
||||
int target = "raise".equals(mode) ? safeDouble(callTarget) : callTarget;
|
||||
taskbarInput.setText(String.valueOf(target));
|
||||
processBet();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -433,35 +1035,40 @@ public class TaskbarController {
|
||||
* displayed on the console.
|
||||
*/
|
||||
private void processBet() {
|
||||
submitAction(ActionType.INPUT);
|
||||
}
|
||||
|
||||
if (gameService == null) {
|
||||
LOGGER.error("Error: GameService not initialized");
|
||||
return;
|
||||
private Player findCurrentPlayer(GameState state) {
|
||||
if (state == null || state.players == null || myPlayerId == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String input = taskbarInput.getText();
|
||||
return state.players.stream()
|
||||
.filter(player -> player != null && myPlayerId.equals(player.getId()))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that the latest game state is available for processing a player action.
|
||||
*
|
||||
* @param actionName The name of the action being processed, used for logging purposes to
|
||||
* indicate which action is being attempted when the state is not available.
|
||||
* @return The latest GameState object if available, or null if the state cannot be retrieved,
|
||||
* indicating that the action cannot be processed due to the lack of a valid game state.
|
||||
*/
|
||||
private GameState ensureLatestStateForAction(String actionName) {
|
||||
try {
|
||||
|
||||
int credits = Integer.parseInt(input.trim());
|
||||
|
||||
if (credits >= MIN_CREDITS && credits <= MAX_CREDITS && credits % CREDIT_STEP == 0) {
|
||||
|
||||
gameService.bet(credits);
|
||||
|
||||
LOGGER.info("Bet placed: {}", credits);
|
||||
|
||||
taskbarInput.clear();
|
||||
|
||||
refreshGame();
|
||||
|
||||
} else {
|
||||
LOGGER.error("Error: Bet must be between 5 and 100000 and multiple of 5");
|
||||
GameState refreshed = gameService.refresh();
|
||||
if (refreshed != null) {
|
||||
lastState = refreshed;
|
||||
return refreshed;
|
||||
}
|
||||
|
||||
} catch (NumberFormatException e) {
|
||||
LOGGER.error("Error: Invalid number entered");
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("Failed to refresh game state for {}: {}", actionName, e.getMessage());
|
||||
}
|
||||
|
||||
return lastState;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -474,4 +1081,47 @@ public class TaskbarController {
|
||||
private void onBrowserButtonClick() {
|
||||
CasinoBrowserController.open("wikipedia.org");
|
||||
}
|
||||
|
||||
/** Opens the highscore popup window from the taskbar. */
|
||||
@FXML
|
||||
private void onHighscoreButtonClick() {
|
||||
try {
|
||||
var shared = ClientApp.getSharedClientService();
|
||||
if (shared == null || shared.isOffline()) {
|
||||
Alert alert = new Alert(Alert.AlertType.INFORMATION);
|
||||
alert.setTitle("Info");
|
||||
alert.setHeaderText(null);
|
||||
alert.setContentText("No active server connection for highscores.");
|
||||
alert.showAndWait();
|
||||
return;
|
||||
}
|
||||
|
||||
javafx.fxml.FXMLLoader loader =
|
||||
new javafx.fxml.FXMLLoader(
|
||||
getClass()
|
||||
.getResource(
|
||||
"/ui-structure/gameuicomponents/HighscoreView.fxml"));
|
||||
javafx.scene.Parent root = loader.load();
|
||||
|
||||
HighscoreViewController controller = loader.getController();
|
||||
controller.setLobbyClient(new LobbyClient(shared));
|
||||
controller.refreshHighscores();
|
||||
|
||||
javafx.stage.Stage stage = new javafx.stage.Stage();
|
||||
stage.initStyle(javafx.stage.StageStyle.TRANSPARENT);
|
||||
stage.setTitle("Casono Highscores");
|
||||
javafx.scene.image.Image icon =
|
||||
new javafx.scene.image.Image(
|
||||
getClass().getResource("/images/logoinverted.png").toExternalForm());
|
||||
stage.getIcons().add(icon);
|
||||
javafx.scene.Scene scene = new javafx.scene.Scene(root);
|
||||
scene.setFill(javafx.scene.paint.Color.TRANSPARENT);
|
||||
stage.setScene(scene);
|
||||
stage.show();
|
||||
stage.setAlwaysOnTop(true);
|
||||
stage.toFront();
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("Could not open highscore window from taskbar: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+100
-5
@@ -4,12 +4,15 @@ import ch.unibas.dmi.dbis.cs108.casono.client.ClientApp;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.chat.ChatController;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.network.ClientService;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.network.LobbyClient;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.gameuicomponents.HighscoreViewController;
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import javafx.application.Platform;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.fxml.FXMLLoader;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.Parent;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.control.Alert;
|
||||
import javafx.scene.control.Alert.AlertType;
|
||||
import javafx.scene.control.Button;
|
||||
@@ -20,6 +23,8 @@ import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.AnchorPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.scene.shape.Rectangle;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.stage.StageStyle;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
@@ -104,6 +109,13 @@ public class CasinomainuiController {
|
||||
casinoTable.getChildren().add(gridManager.getGridPane());
|
||||
gridManager.renderLobbyButtons();
|
||||
|
||||
String sharedUsername = ClientApp.getSharedUsername();
|
||||
updateUsernameFieldPresentation(sharedUsername);
|
||||
if (loginButton != null) {
|
||||
loginButton.setText(
|
||||
sharedUsername != null && !sharedUsername.isBlank() ? "Change Name" : "Login");
|
||||
}
|
||||
|
||||
initializeChat(clientService);
|
||||
}
|
||||
|
||||
@@ -156,7 +168,7 @@ public class CasinomainuiController {
|
||||
/** Handles the login button action. Validates input and calls LobbyClient.login(). */
|
||||
@FXML
|
||||
public void handleLoginButton() {
|
||||
String username = usernameField.getText();
|
||||
String username = usernameField == null ? null : usernameField.getText();
|
||||
if (username == null || username.isBlank()) {
|
||||
showAlert("Please enter a username.");
|
||||
return;
|
||||
@@ -170,12 +182,63 @@ public class CasinomainuiController {
|
||||
showAlert("Offline mode: cannot send login to server.");
|
||||
return;
|
||||
}
|
||||
|
||||
String trimmed = username.trim();
|
||||
try {
|
||||
lobbyClient.login(username);
|
||||
showAlert("Login sent: " + username);
|
||||
var result = lobbyClient.login(trimmed);
|
||||
String assigned = result != null ? result.getUsername() : trimmed;
|
||||
ClientApp.updateSharedUsername(assigned);
|
||||
if (chatController != null) {
|
||||
chatController.updateUsername(assigned);
|
||||
}
|
||||
updateUsernameFieldPresentation(assigned);
|
||||
if (loginButton != null) {
|
||||
loginButton.setText("Change Name");
|
||||
}
|
||||
return;
|
||||
} catch (RuntimeException loginError) {
|
||||
if (!containsProtocolError(loginError, "ALREADY_LOGGED_IN")) {
|
||||
LOGGER.error("Login failed: {}", loginError.getMessage());
|
||||
showAlert("Login failed: " + loginError.getMessage());
|
||||
return;
|
||||
}
|
||||
LOGGER.info("Session already logged in, trying username change");
|
||||
}
|
||||
|
||||
try {
|
||||
var result = lobbyClient.changeUsername(trimmed);
|
||||
String assigned = result != null ? result.getUsername() : trimmed;
|
||||
ClientApp.updateSharedUsername(assigned);
|
||||
if (chatController != null) {
|
||||
chatController.updateUsername(assigned);
|
||||
}
|
||||
updateUsernameFieldPresentation(assigned);
|
||||
if (loginButton != null) {
|
||||
loginButton.setText("Change Name");
|
||||
}
|
||||
} catch (RuntimeException e) {
|
||||
LOGGER.error("Login failed: {}", e.getMessage());
|
||||
showAlert("Login failed: " + e.getMessage());
|
||||
LOGGER.error("Change username failed: {}", e.getMessage());
|
||||
showAlert("Change username failed: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private boolean containsProtocolError(RuntimeException error, String code) {
|
||||
String msg = error.getMessage();
|
||||
return msg != null && msg.contains(code);
|
||||
}
|
||||
|
||||
private void updateUsernameFieldPresentation(String username) {
|
||||
if (usernameField == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (username != null && !username.isBlank()) {
|
||||
usernameField.setText(username.trim());
|
||||
usernameField.positionCaret(usernameField.getText().length());
|
||||
usernameField.setPromptText("Username");
|
||||
} else {
|
||||
usernameField.clear();
|
||||
usernameField.setPromptText("Username");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -229,4 +292,36 @@ public class CasinomainuiController {
|
||||
LOGGER.error("Failed to create or add lobby: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/** Opens the highscore popup from the lobby UI. */
|
||||
@FXML
|
||||
public void handleOpenHighscores() {
|
||||
try {
|
||||
FXMLLoader loader =
|
||||
new FXMLLoader(
|
||||
getClass()
|
||||
.getResource(
|
||||
"/ui-structure/gameuicomponents/HighscoreView.fxml"));
|
||||
Parent root = loader.load();
|
||||
|
||||
HighscoreViewController controller = loader.getController();
|
||||
controller.setLobbyClient(lobbyClient);
|
||||
controller.refreshHighscores();
|
||||
|
||||
Stage stage = new Stage();
|
||||
stage.initStyle(StageStyle.TRANSPARENT);
|
||||
stage.setTitle("Casono Highscores");
|
||||
String iconPath = getClass().getResource("/images/logoinverted.png").toExternalForm();
|
||||
stage.getIcons().add(new Image(iconPath));
|
||||
Scene scene = new Scene(root);
|
||||
scene.setFill(javafx.scene.paint.Color.TRANSPARENT);
|
||||
stage.setScene(scene);
|
||||
stage.show();
|
||||
stage.setAlwaysOnTop(true);
|
||||
stage.toFront();
|
||||
} catch (IOException e) {
|
||||
LOGGER.warn("Could not open highscore window: {}", e.getMessage());
|
||||
showAlert("Could not open highscore window.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,56 @@
|
||||
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.CheckUsernameParser;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.check_nick.CheckUsernameRequest;
|
||||
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.game.fold.PlayerFoldHandler;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.fold.PlayerFoldParser;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.fold.PlayerFoldRequest;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.get_game_state.GetGameStateHandler;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.get_game_state.GetGameStateParser;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.get_game_state.GetGameStateRequest;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.raise.PlayerRaiseHandler;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.raise.PlayerRaiseParser;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.raise.PlayerRaiseRequest;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.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.highscore.clear_highscores.ClearHighscoresHandler;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.highscore.clear_highscores.ClearHighscoresParser;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.highscore.clear_highscores.ClearHighscoresRequest;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.highscore.get_highscores.GetHighscoresHandler;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.highscore.get_highscores.GetHighscoresParser;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.highscore.get_highscores.GetHighscoresRequest;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.list_users.ListUsersHandler;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.list_users.ListUsersParser;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.list_users.ListUsersRequest;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.create_lobby.CreateLobbyHandler;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.create_lobby.CreateLobbyParser;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.create_lobby.CreateLobbyRequest;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.get_lobby_list.GetLobbyListHandler;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.get_lobby_list.GetLobbyListParser;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.get_lobby_list.GetLobbyListRequest;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.get_lobby_status.GetLobbyStatusHandler;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.get_lobby_status.GetLobbyStatusParser;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.get_lobby_status.GetLobbyStatusRequest;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.join_lobby.JoinLobbyHandler;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.join_lobby.JoinLobbyParser;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.join_lobby.JoinLobbyRequest;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.start_game.StartGameHandler;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.start_game.StartGameParser;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.start_game.StartGameRequest;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.login.LoginHandler;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.login.LoginParser;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.login.LoginRequest;
|
||||
@@ -28,6 +67,7 @@ import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.LobbyManager;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.UserCleanupJob;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.UserRegistry;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.NetworkManager;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandlerExecutor;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandRouter;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.CommandParserDispatcher;
|
||||
@@ -168,6 +208,15 @@ public class ServerApp {
|
||||
commandRouter.register(
|
||||
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());
|
||||
commandRouter.register(
|
||||
LogoutRequest.class, new LogoutHandler(responseDispatcher, userRegistry));
|
||||
@@ -192,152 +241,96 @@ public class ServerApp {
|
||||
ListUsersRequest.class, new ListUsersHandler(responseDispatcher, userRegistry));
|
||||
|
||||
// GET_LOBBY_LIST registration
|
||||
parserDispatcher.register(
|
||||
"GET_LOBBY_LIST",
|
||||
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.get_lobby_list
|
||||
.GetLobbyListParser());
|
||||
parserDispatcher.register("GET_LOBBY_LIST", new GetLobbyListParser());
|
||||
commandRouter.register(
|
||||
ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.get_lobby_list
|
||||
.GetLobbyListRequest.class,
|
||||
(ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler<
|
||||
ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby
|
||||
.get_lobby_list.GetLobbyListRequest>)
|
||||
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.get_lobby_list
|
||||
.GetLobbyListHandler(responseDispatcher, context.lobbyManager()));
|
||||
GetLobbyListRequest.class,
|
||||
(CommandHandler<GetLobbyListRequest>)
|
||||
new GetLobbyListHandler(responseDispatcher, context.lobbyManager()));
|
||||
|
||||
// GET_GAME_STATE registration
|
||||
parserDispatcher.register(
|
||||
"GET_GAME_STATE",
|
||||
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.get_game_state
|
||||
.GetGameStateParser());
|
||||
parserDispatcher.register("GET_GAME_STATE", new GetGameStateParser());
|
||||
commandRouter.register(
|
||||
ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.get_game_state
|
||||
.GetGameStateRequest.class,
|
||||
(ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler<
|
||||
ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game
|
||||
.get_game_state.GetGameStateRequest>)
|
||||
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.get_game_state
|
||||
.GetGameStateHandler(
|
||||
GetGameStateRequest.class,
|
||||
(CommandHandler<GetGameStateRequest>)
|
||||
new GetGameStateHandler(
|
||||
responseDispatcher, context.lobbyManager(), userRegistry));
|
||||
|
||||
// BET registration
|
||||
parserDispatcher.register(
|
||||
"BET",
|
||||
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.bet.PlayerBetParser());
|
||||
parserDispatcher.register("GET_HIGHSCORES", new GetHighscoresParser());
|
||||
commandRouter.register(
|
||||
ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.bet.PlayerBetRequest.class,
|
||||
(ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler<
|
||||
ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.bet
|
||||
.PlayerBetRequest>)
|
||||
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.bet
|
||||
.PlayerBetHandler(
|
||||
GetHighscoresRequest.class,
|
||||
(CommandHandler<GetHighscoresRequest>)
|
||||
new GetHighscoresHandler(responseDispatcher));
|
||||
|
||||
parserDispatcher.register("CLEAR_HIGHSCORES", new ClearHighscoresParser());
|
||||
commandRouter.register(
|
||||
ClearHighscoresRequest.class,
|
||||
(CommandHandler<ClearHighscoresRequest>)
|
||||
new ClearHighscoresHandler(responseDispatcher));
|
||||
|
||||
// BET registration
|
||||
parserDispatcher.register("BET", new PlayerBetParser());
|
||||
commandRouter.register(
|
||||
PlayerBetRequest.class,
|
||||
(CommandHandler<PlayerBetRequest>)
|
||||
new PlayerBetHandler(
|
||||
responseDispatcher, userRegistry, context.lobbyManager()));
|
||||
|
||||
// RAISE registration
|
||||
parserDispatcher.register(
|
||||
"RAISE",
|
||||
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.raise
|
||||
.PlayerRaiseParser());
|
||||
parserDispatcher.register("RAISE", new PlayerRaiseParser());
|
||||
commandRouter.register(
|
||||
ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.raise.PlayerRaiseRequest
|
||||
.class,
|
||||
(ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler<
|
||||
ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.raise
|
||||
.PlayerRaiseRequest>)
|
||||
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.raise
|
||||
.PlayerRaiseHandler(
|
||||
PlayerRaiseRequest.class,
|
||||
(CommandHandler<PlayerRaiseRequest>)
|
||||
new PlayerRaiseHandler(
|
||||
responseDispatcher, userRegistry, context.lobbyManager()));
|
||||
|
||||
// CALL registration
|
||||
parserDispatcher.register(
|
||||
"CALL",
|
||||
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.call
|
||||
.PlayerCallParser());
|
||||
parserDispatcher.register("CALL", new PlayerCallParser());
|
||||
commandRouter.register(
|
||||
ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.call.PlayerCallRequest
|
||||
.class,
|
||||
(ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler<
|
||||
ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.call
|
||||
.PlayerCallRequest>)
|
||||
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.call
|
||||
.PlayerCallHandler(
|
||||
PlayerCallRequest.class,
|
||||
(CommandHandler<PlayerCallRequest>)
|
||||
new PlayerCallHandler(
|
||||
responseDispatcher, userRegistry, context.lobbyManager()));
|
||||
|
||||
// FOLD registration
|
||||
parserDispatcher.register(
|
||||
"FOLD",
|
||||
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.fold
|
||||
.PlayerFoldParser());
|
||||
parserDispatcher.register("FOLD", new PlayerFoldParser());
|
||||
commandRouter.register(
|
||||
ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.fold.PlayerFoldRequest
|
||||
.class,
|
||||
(ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler<
|
||||
ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.fold
|
||||
.PlayerFoldRequest>)
|
||||
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.fold
|
||||
.PlayerFoldHandler(
|
||||
PlayerFoldRequest.class,
|
||||
(CommandHandler<PlayerFoldRequest>)
|
||||
new PlayerFoldHandler(
|
||||
responseDispatcher, userRegistry, context.lobbyManager()));
|
||||
|
||||
// GET_LOBBY_STATUS registration
|
||||
parserDispatcher.register(
|
||||
"GET_LOBBY_STATUS",
|
||||
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.get_lobby_status
|
||||
.GetLobbyStatusParser());
|
||||
parserDispatcher.register("GET_LOBBY_STATUS", new GetLobbyStatusParser());
|
||||
commandRouter.register(
|
||||
ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.get_lobby_status
|
||||
.GetLobbyStatusRequest.class,
|
||||
(ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler<
|
||||
ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby
|
||||
.get_lobby_status.GetLobbyStatusRequest>)
|
||||
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby
|
||||
.get_lobby_status.GetLobbyStatusHandler(
|
||||
GetLobbyStatusRequest.class,
|
||||
(CommandHandler<GetLobbyStatusRequest>)
|
||||
new GetLobbyStatusHandler(
|
||||
responseDispatcher, context.lobbyManager(), userRegistry));
|
||||
|
||||
// CREATE_LOBBY registration
|
||||
parserDispatcher.register(
|
||||
"CREATE_LOBBY",
|
||||
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.create_lobby
|
||||
.CreateLobbyParser());
|
||||
parserDispatcher.register("CREATE_LOBBY", new CreateLobbyParser());
|
||||
commandRouter.register(
|
||||
ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.create_lobby
|
||||
.CreateLobbyRequest.class,
|
||||
(ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler<
|
||||
ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby
|
||||
.create_lobby.CreateLobbyRequest>)
|
||||
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.create_lobby
|
||||
.CreateLobbyHandler(
|
||||
CreateLobbyRequest.class,
|
||||
(CommandHandler<CreateLobbyRequest>)
|
||||
new CreateLobbyHandler(
|
||||
responseDispatcher,
|
||||
context.lobbyManager(),
|
||||
context.sessionManager()));
|
||||
|
||||
// JOIN_LOBBY registration
|
||||
parserDispatcher.register(
|
||||
"JOIN_LOBBY",
|
||||
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.join_lobby
|
||||
.JoinLobbyParser());
|
||||
parserDispatcher.register("JOIN_LOBBY", new JoinLobbyParser());
|
||||
commandRouter.register(
|
||||
ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.join_lobby
|
||||
.JoinLobbyRequest.class,
|
||||
(ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler<
|
||||
ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.join_lobby
|
||||
.JoinLobbyRequest>)
|
||||
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.join_lobby
|
||||
.JoinLobbyHandler(
|
||||
JoinLobbyRequest.class,
|
||||
(CommandHandler<JoinLobbyRequest>)
|
||||
new JoinLobbyHandler(
|
||||
responseDispatcher, context.lobbyManager(), userRegistry));
|
||||
|
||||
// START_GAME registration
|
||||
parserDispatcher.register(
|
||||
"START_GAME",
|
||||
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.start_game
|
||||
.StartGameParser());
|
||||
parserDispatcher.register("START_GAME", new StartGameParser());
|
||||
commandRouter.register(
|
||||
ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.start_game
|
||||
.StartGameRequest.class,
|
||||
(ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler<
|
||||
ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.start_game
|
||||
.StartGameRequest>)
|
||||
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.start_game
|
||||
.StartGameHandler(
|
||||
StartGameRequest.class,
|
||||
(CommandHandler<StartGameRequest>)
|
||||
new StartGameHandler(
|
||||
responseDispatcher, context.lobbyManager(), userRegistry));
|
||||
}
|
||||
}
|
||||
|
||||
+112
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+15
@@ -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"));
|
||||
}
|
||||
}
|
||||
+25
@@ -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;
|
||||
}
|
||||
}
|
||||
+23
@@ -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());
|
||||
}
|
||||
}
|
||||
+26
-1
@@ -1,6 +1,7 @@
|
||||
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.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.LobbyId;
|
||||
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);
|
||||
|
||||
Lobby lobby;
|
||||
LobbyId lobbyId;
|
||||
if (gameId != null) {
|
||||
lobby = lobbyManager.getLobby(LobbyId.of(gameId));
|
||||
lobbyId = LobbyId.of(gameId);
|
||||
lobby = lobbyManager.getLobby(lobbyId);
|
||||
if (lobby == null) {
|
||||
responseDispatcher.dispatch(
|
||||
new ErrorResponse(
|
||||
@@ -59,6 +62,7 @@ public class GetGameStateHandler extends CommandHandler<GetGameStateRequest> {
|
||||
request.getContext(), "NOT_IN_LOBBY", "User not in a lobby"));
|
||||
return;
|
||||
}
|
||||
lobbyId = lobby.getId();
|
||||
}
|
||||
|
||||
var game = lobby.getGameController();
|
||||
@@ -69,9 +73,30 @@ public class GetGameStateHandler extends CommandHandler<GetGameStateRequest> {
|
||||
return;
|
||||
}
|
||||
|
||||
if (game.getState().getPhase() == GamePhase.FINISHED) {
|
||||
cleanupLobby(lobby, lobbyId);
|
||||
}
|
||||
|
||||
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) {
|
||||
String username = request.getUsername();
|
||||
if (username != null && !username.isBlank()) {
|
||||
|
||||
+62
-1
@@ -6,7 +6,9 @@ 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.player.Player;
|
||||
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.highscore.HighscoreService;
|
||||
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;
|
||||
@@ -33,11 +35,13 @@ public class GetGameStateResponse extends SuccessResponse {
|
||||
super(
|
||||
context,
|
||||
buildBody(
|
||||
game,
|
||||
Objects.requireNonNull(game, "game must not be null").getState(),
|
||||
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");
|
||||
|
||||
ResponseBodyBuilder builder = ResponseBody.builder();
|
||||
@@ -47,6 +51,11 @@ public class GetGameStateResponse extends SuccessResponse {
|
||||
builder.param("CURRENT_BET", computeGlobalCurrentBet(state));
|
||||
builder.param("DEALER", state.getDealerIndex());
|
||||
builder.param("ACTIVE_PLAYER", state.getCurrentPlayerIndex());
|
||||
int winnerIndex = computeWinnerIndex(state, game);
|
||||
builder.param("WINNER", winnerIndex);
|
||||
|
||||
appendWinnerToHighscoresIfFinished(state, winnerIndex);
|
||||
appendHighscoreEntries(builder);
|
||||
|
||||
appendCommunityCards(builder, state);
|
||||
|
||||
@@ -55,6 +64,58 @@ public class GetGameStateResponse extends SuccessResponse {
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private static void appendWinnerToHighscoresIfFinished(GameState state, int winnerIndex) {
|
||||
if (winnerIndex < 0 || state.getPhase() != GamePhase.FINISHED) {
|
||||
return;
|
||||
}
|
||||
|
||||
Player winner = findPlayerByIndex(state, winnerIndex);
|
||||
if (winner == null || winner.getId() == null || winner.getId().value() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
HighscoreService.getInstance().appendWinner(winner.getId().value());
|
||||
}
|
||||
|
||||
private static Player findPlayerByIndex(GameState state, int winnerIndex) {
|
||||
int currentIndex = 0;
|
||||
for (Player player : state.getPlayers()) {
|
||||
if (currentIndex == winnerIndex) {
|
||||
return player;
|
||||
}
|
||||
currentIndex++;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void appendHighscoreEntries(ResponseBodyBuilder builder) {
|
||||
for (String entry : HighscoreService.getInstance().readFormattedEntries()) {
|
||||
builder.param("HIGHSCORE", entry);
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
int globalCurrentBet = 0;
|
||||
for (Player p : state.getPlayers()) {
|
||||
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.highscore.clear_highscores;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.highscore.HighscoreService;
|
||||
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;
|
||||
|
||||
/** Handler for CLEAR_HIGHSCORES. */
|
||||
public class ClearHighscoresHandler extends CommandHandler<ClearHighscoresRequest> {
|
||||
private final HighscoreService highscoreService;
|
||||
|
||||
public ClearHighscoresHandler(ResponseDispatcher responseDispatcher) {
|
||||
super(responseDispatcher);
|
||||
this.highscoreService = HighscoreService.getInstance();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(ClearHighscoresRequest request) {
|
||||
highscoreService.clearAll();
|
||||
responseDispatcher.dispatch(new OkResponse(request.getContext()));
|
||||
}
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.highscore.clear_highscores;
|
||||
|
||||
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 CLEAR_HIGHSCORES. */
|
||||
public class ClearHighscoresParser implements CommandParser<ClearHighscoresRequest> {
|
||||
@Override
|
||||
public ClearHighscoresRequest parse(PrimitiveRequest primitiveRequest) {
|
||||
return new ClearHighscoresRequest(primitiveRequest.context());
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.highscore.clear_highscores;
|
||||
|
||||
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 object for CLEAR_HIGHSCORES. */
|
||||
public class ClearHighscoresRequest extends Request {
|
||||
public ClearHighscoresRequest(RequestContext context) {
|
||||
super(context);
|
||||
}
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.highscore.get_highscores;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.highscore.HighscoreService;
|
||||
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;
|
||||
|
||||
/** Handler for GET_HIGHSCORES. */
|
||||
public class GetHighscoresHandler extends CommandHandler<GetHighscoresRequest> {
|
||||
private final HighscoreService highscoreService;
|
||||
|
||||
public GetHighscoresHandler(ResponseDispatcher responseDispatcher) {
|
||||
super(responseDispatcher);
|
||||
this.highscoreService = HighscoreService.getInstance();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(GetHighscoresRequest request) {
|
||||
responseDispatcher.dispatch(
|
||||
new GetHighscoresResponse(
|
||||
request.getContext(), highscoreService.readFormattedEntries()));
|
||||
}
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.highscore.get_highscores;
|
||||
|
||||
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 GET_HIGHSCORES. */
|
||||
public class GetHighscoresParser implements CommandParser<GetHighscoresRequest> {
|
||||
@Override
|
||||
public GetHighscoresRequest parse(PrimitiveRequest primitiveRequest) {
|
||||
return new GetHighscoresRequest(primitiveRequest.context());
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.highscore.get_highscores;
|
||||
|
||||
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 object for GET_HIGHSCORES. */
|
||||
public class GetHighscoresRequest extends Request {
|
||||
public GetHighscoresRequest(RequestContext context) {
|
||||
super(context);
|
||||
}
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.highscore.get_highscores;
|
||||
|
||||
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;
|
||||
import java.util.List;
|
||||
|
||||
/** Success response containing repeated HIGHSCORE parameters. */
|
||||
public class GetHighscoresResponse extends SuccessResponse {
|
||||
public GetHighscoresResponse(RequestContext context, List<String> entries) {
|
||||
super(
|
||||
context,
|
||||
ResponseBody.builder()
|
||||
.block(
|
||||
"HIGHSCORES",
|
||||
block -> {
|
||||
for (String entry : entries) {
|
||||
block.param("HIGHSCORE", entry);
|
||||
}
|
||||
})
|
||||
.build());
|
||||
}
|
||||
}
|
||||
+6
@@ -9,6 +9,7 @@ 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;
|
||||
import java.util.Optional;
|
||||
|
||||
public class SendMessageHandler extends CommandHandler<SendMessageRequest> {
|
||||
private final UserRegistry userRegistry;
|
||||
@@ -39,6 +40,11 @@ public class SendMessageHandler extends CommandHandler<SendMessageRequest> {
|
||||
@Override
|
||||
public void execute(SendMessageRequest request) {
|
||||
Message message = request.getMessage();
|
||||
Optional<ch.unibas.dmi.dbis.cs108.casono.server.domain.user.User> senderUser =
|
||||
userRegistry.getBySessionId(request.getSessionId());
|
||||
if (senderUser.isPresent()) {
|
||||
message.sender = senderUser.get().getName();
|
||||
}
|
||||
broadcast(request, message);
|
||||
OkResponse response = new OkResponse(request.getContext());
|
||||
responseDispatcher.dispatch(response);
|
||||
|
||||
+53
-2
@@ -10,6 +10,7 @@ import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.deck.Deck;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.engine.GameEngine;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.evaluator.HandEvaluator;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.evaluator.HandRank;
|
||||
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.state.GamePhase;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.state.GameState;
|
||||
@@ -61,6 +62,35 @@ public class GameController {
|
||||
engine.getState().addPlayer(name, chips);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renames a player id in the controller list and underlying game state.
|
||||
*
|
||||
* @param oldId old player id
|
||||
* @param newId new player id
|
||||
* @return true if rename succeeded
|
||||
*/
|
||||
public boolean renamePlayer(PlayerId oldId, PlayerId newId) {
|
||||
if (oldId == null || newId == null) {
|
||||
return false;
|
||||
}
|
||||
if (oldId.equals(newId)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
int idx = players.indexOf(oldId);
|
||||
if (idx < 0 || players.contains(newId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
boolean stateRenamed = engine.getState().renamePlayerId(oldId, newId);
|
||||
if (!stateRenamed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
players.set(idx, newId);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes a new hand by preparing the deck, setting the phase to PREFLOP, rotating the
|
||||
* dealer, dealing hole cards, posting blinds, and setting the first active player.
|
||||
@@ -154,9 +184,22 @@ public class GameController {
|
||||
*
|
||||
* @param playerId The ID of the player who is folding.
|
||||
*/
|
||||
public void playerFold(PlayerId playerId) {
|
||||
// engine.processAction(new FoldAction(PlayerId.of(playerId)));
|
||||
public PlayerId playerFold(PlayerId playerId) {
|
||||
|
||||
engine.processAction(new FoldAction(playerId));
|
||||
|
||||
GameState state = engine.getState();
|
||||
|
||||
if (state.countNonFoldedPlayers() == 1) {
|
||||
|
||||
for (Player p : state.getPlayers()) {
|
||||
if (!p.isFolded()) {
|
||||
return p.getId();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -254,4 +297,12 @@ public class GameController {
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
+29
@@ -1,6 +1,9 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server.domain.game.engine;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.action.Action;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.action.ActionType;
|
||||
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.rules.RuleEngine;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.state.GameState;
|
||||
|
||||
@@ -80,12 +83,38 @@ public class GameEngine {
|
||||
*/
|
||||
public void handleAction(GameState state, Action action) {
|
||||
|
||||
if (action.getType() != ActionType.BLIND) {
|
||||
|
||||
Player currentPlayer = state.getCurrentPlayer();
|
||||
PlayerId actingPlayerId = action.getPlayerId();
|
||||
|
||||
if (currentPlayer == null || actingPlayerId == null) {
|
||||
throw new RuntimeException("Invalid turn state");
|
||||
}
|
||||
|
||||
if (!state.isAllowOutOfTurn() && !currentPlayer.getId().equals(actingPlayerId)) {
|
||||
throw new RuntimeException("Not your turn");
|
||||
}
|
||||
|
||||
Player actingPlayer = state.getPlayer(actingPlayerId);
|
||||
if (actingPlayer.isFolded()) {
|
||||
throw new RuntimeException("Player already folded");
|
||||
}
|
||||
if (actingPlayer.isAllIn()) {
|
||||
throw new RuntimeException("Player is all-in");
|
||||
}
|
||||
}
|
||||
|
||||
// 1.Check the rules
|
||||
ruleEngine.validate(state, action);
|
||||
|
||||
// 2. Perform action
|
||||
action.execute(state);
|
||||
|
||||
if (action.getType() != ActionType.BLIND) {
|
||||
state.markActedThisRound(action.getPlayerId());
|
||||
}
|
||||
|
||||
// 3. Next turn
|
||||
turnManager.nextPlayer(state);
|
||||
|
||||
|
||||
+98
-12
@@ -4,7 +4,6 @@ import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.deck.Card;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.deck.Deck;
|
||||
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.PlayerStatus;
|
||||
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 java.util.ArrayList;
|
||||
@@ -27,6 +26,13 @@ public class RoundManager {
|
||||
public static final int SMALL_BLIND = 100;
|
||||
public static final int BIG_BLIND = 200;
|
||||
|
||||
/**
|
||||
* Starts a new hand by resetting the game state, ensuring a deck is available, dealing hole
|
||||
* cards to players, posting blinds, and setting the first player to act for the preflop phase.
|
||||
*
|
||||
* @param state The GameState object representing the current state of the game, which will be
|
||||
* modified to
|
||||
*/
|
||||
public void startNewHand(GameState state) {
|
||||
// Use GameState's canonical reset
|
||||
state.startNewHand();
|
||||
@@ -44,49 +50,92 @@ public class RoundManager {
|
||||
state.setCurrentPlayerToPreflopFirstToAct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the current betting round is finished and advances the game phase if necessary.
|
||||
*
|
||||
* @param state The GameState object representing the current state of the game, which may be
|
||||
* modified to advance the phase or end the hand.
|
||||
*/
|
||||
public void progressIfNeeded(GameState state) {
|
||||
if (state.getPhase() == null) {
|
||||
state.setPhase(GamePhase.PREFLOP);
|
||||
}
|
||||
|
||||
if (state.countNonFoldedPlayers() == 1) {
|
||||
state.setHandActive(false);
|
||||
state.setPhase(GamePhase.FINISHED);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isBettingRoundFinished(state)) {
|
||||
advancePhase(state);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the current betting round is finished by checking if all active players have
|
||||
* met the current bet or are all-in and by handling special cases for preflop betting rounds.
|
||||
*
|
||||
* @param state The GameState object representing the current state of the game, which is used
|
||||
* to evaluate the betting round status.
|
||||
* @return true if the betting round is finished and the game can progress to the next phase,
|
||||
* false otherwise.
|
||||
*/
|
||||
private boolean isBettingRoundFinished(GameState state) {
|
||||
int target = state.getTableState().getCurrentBet();
|
||||
// A betting round only ends after every active player had at least one chance to act.
|
||||
return allActivePlayersActedThisRound(state);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to check if all active players have acted in the current betting round. This is
|
||||
* used to handle the special case of preflop rounds where no bets have been made yet, but
|
||||
* players still need to have the opportunity to act.
|
||||
*
|
||||
* @param state The GameState object representing the current state of the game, which is used
|
||||
* to check if all active players have acted in the current round.
|
||||
* @return true if all active players have acted in the current round, false if there are still
|
||||
* active players who have not acted yet.
|
||||
*/
|
||||
private boolean allActivePlayersActedThisRound(GameState state) {
|
||||
for (Player p : state.getPlayers()) {
|
||||
if (p == null) {
|
||||
if (p == null || p.isFolded() || p.isAllIn()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// be tolerant if codebase mixes status + boolean flags
|
||||
if (p.getStatus() == PlayerStatus.FOLDED || p.isFolded()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int bet = state.getCurrentBet(p.getId());
|
||||
if (bet < target && !p.isAllIn()) {
|
||||
if (!state.hasActedThisRound(p.getId())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Advances the game phase to the next stage (FLOP, TURN, RIVER, SHOWDOWN) based on the current
|
||||
* phase.
|
||||
*
|
||||
* @param state The GameState object representing the current state of the game, which will be
|
||||
* modified to advance the phase and deal community cards as needed when progressing to the
|
||||
* next stage of the hand.
|
||||
*/
|
||||
private void advancePhase(GameState state) {
|
||||
switch (state.getPhase()) {
|
||||
case PREFLOP -> dealFlop(state);
|
||||
case FLOP -> dealTurn(state);
|
||||
case TURN -> dealRiver(state);
|
||||
case RIVER -> showdown(state);
|
||||
case SHOWDOWN -> {
|
||||
/* nothing */
|
||||
case SHOWDOWN -> state.setPhase(GamePhase.FINISHED);
|
||||
case FINISHED -> {
|
||||
/* game is over */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that a deck of cards is available in the game state.
|
||||
*
|
||||
* @param state The GameState object representing the current state of the game, which will be
|
||||
* modified to include a new shuffled deck if one does not already exist.
|
||||
*/
|
||||
private void ensureDeck(GameState state) {
|
||||
Deck deck = state.getDeck();
|
||||
if (deck == null) {
|
||||
@@ -96,6 +145,13 @@ public class RoundManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deals hole cards to each player in the game state by drawing two cards from the deck for each
|
||||
* player and assigning them as their hole cards.
|
||||
*
|
||||
* @param state The GameState object representing the current state of the game, which will be
|
||||
* modified to assign hole cards to each active player.
|
||||
*/
|
||||
private void dealHoleCards(GameState state) {
|
||||
Deck deck = state.getDeck();
|
||||
|
||||
@@ -159,6 +215,14 @@ public class RoundManager {
|
||||
state.getTableState().setCurrentBet(BIG_BLIND);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deals the flop by drawing three community cards from the deck and adding them to the game
|
||||
* state, then setting the game phase to FLOP and resetting bets for the new betting round.
|
||||
*
|
||||
* @param state The GameState object representing the current state of the game, which will be
|
||||
* modified to add three community cards for the flop, set the phase to FLOP, and reset bets
|
||||
* for the new betting round.
|
||||
*/
|
||||
private void dealFlop(GameState state) {
|
||||
ensureDeck(state);
|
||||
Deck deck = state.getDeck();
|
||||
@@ -174,6 +238,14 @@ public class RoundManager {
|
||||
state.setCurrentPlayerToPostflopFirstToAct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Deals the turn by drawing one community card from the deck and adding it to the game state,
|
||||
* then setting the game phase to TURN and resetting bets for the new betting round.
|
||||
*
|
||||
* @param state The GameState object representing the current state of the game, which will be
|
||||
* modified to add one community card for the turn, set the phase to TURN, and reset bets
|
||||
* for the new betting round.
|
||||
*/
|
||||
private void dealTurn(GameState state) {
|
||||
ensureDeck(state);
|
||||
Deck deck = state.getDeck();
|
||||
@@ -187,6 +259,14 @@ public class RoundManager {
|
||||
state.setCurrentPlayerToPostflopFirstToAct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Deals the river by drawing one community card from the deck and adding it to the game state,
|
||||
* then setting the game phase to RIVER and resetting bets for the new betting round.
|
||||
*
|
||||
* @param state The GameState object representing the current state of the game, which will be
|
||||
* modified to add one community card for the river, set the phase to RIVER, and reset bets
|
||||
* for the new betting round.
|
||||
*/
|
||||
private void dealRiver(GameState state) {
|
||||
ensureDeck(state);
|
||||
Deck deck = state.getDeck();
|
||||
@@ -200,6 +280,12 @@ public class RoundManager {
|
||||
state.setCurrentPlayerToPostflopFirstToAct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the showdown phase by setting the game phase to SHOWDOWN.
|
||||
*
|
||||
* @param state The GameState object representing the current state of the game, which will be
|
||||
* modified to set the phase to SHOWDOWN.
|
||||
*/
|
||||
private void showdown(GameState state) {
|
||||
state.setPhase(GamePhase.SHOWDOWN);
|
||||
|
||||
|
||||
@@ -49,6 +49,16 @@ public class Player {
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the player's id. This is used when a username change is propagated into an already
|
||||
* running game.
|
||||
*
|
||||
* @param id new player id
|
||||
*/
|
||||
public void setId(PlayerId id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the display name of the player. Currently identical to the player ID.
|
||||
*
|
||||
|
||||
+299
-31
@@ -7,23 +7,21 @@ import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.player.PlayerId;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* The GameState class encapsulates the entire state of a poker game at any
|
||||
* given moment.
|
||||
* The GameState class encapsulates the entire state of a poker game at any given moment.
|
||||
*
|
||||
* <p>
|
||||
* Important invariants this implementation maintains:
|
||||
* <p>Important invariants this implementation maintains:
|
||||
*
|
||||
* <ul>
|
||||
* <li>{@code playerOrder} defines the stable seating/turn order.
|
||||
* <li>{@code currentBets} always contains an entry for every player in
|
||||
* {@code playerOrder}.
|
||||
* <li>{@code holeCards} maps every player to a mutable list; if not present, it
|
||||
* is created on
|
||||
* demand.
|
||||
* <li>{@code playerOrder} defines the stable seating/turn order.
|
||||
* <li>{@code currentBets} always contains an entry for every player in {@code playerOrder}.
|
||||
* <li>{@code holeCards} maps every player to a mutable list; if not present, it is created on
|
||||
* demand.
|
||||
* </ul>
|
||||
*/
|
||||
public class GameState {
|
||||
@@ -42,6 +40,7 @@ public class GameState {
|
||||
|
||||
private final Map<PlayerId, Integer> currentBets = new HashMap<>();
|
||||
private final Map<PlayerId, Integer> playerBetCommitments = new HashMap<>();
|
||||
private final Set<PlayerId> actedThisRound = new HashSet<>();
|
||||
|
||||
private final Map<PlayerId, List<Card>> holeCards = new HashMap<>();
|
||||
private final List<Card> communityCards = new ArrayList<>();
|
||||
@@ -50,7 +49,11 @@ public class GameState {
|
||||
|
||||
private static final int DEALER_OFFSET = 3;
|
||||
|
||||
// Getters
|
||||
/**
|
||||
* Returns the players in seating/turn order.
|
||||
*
|
||||
* @return a collection of players in the order they are seated/act.
|
||||
*/
|
||||
public Collection<Player> getPlayers() {
|
||||
List<Player> ordered = new ArrayList<>(playerOrder.size());
|
||||
for (PlayerId id : playerOrder) {
|
||||
@@ -62,57 +65,123 @@ public class GameState {
|
||||
return ordered;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current player whose turn it is to act.
|
||||
*
|
||||
* @return the current player, or null if there are no players or the index is out of bounds.
|
||||
*/
|
||||
public Player getCurrentPlayer() {
|
||||
PlayerId id = playerOrder.get(currentPlayerIndex);
|
||||
return players.get(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the index of the current player in the player order list.
|
||||
*
|
||||
* @return the index of the current player, or 0 if there are no players.
|
||||
*/
|
||||
public int getCurrentPlayerIndex() {
|
||||
return currentPlayerIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether a hand is currently active (i.e., in progress).
|
||||
*
|
||||
* @return true if a hand is active, false otherwise.
|
||||
*/
|
||||
public boolean isHandActive() {
|
||||
return handActive;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current phase of the game (e.g., PREFLOP, FLOP, TURN, RIVER).
|
||||
*
|
||||
* @return the current game phase.
|
||||
*/
|
||||
public GamePhase getPhase() {
|
||||
return phase;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current state of the table, including information such as the current bet and pot
|
||||
* size.
|
||||
*
|
||||
* @return the current table state.
|
||||
*/
|
||||
public TableState getTableState() {
|
||||
return tableState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current pot, which includes the total amount of chips in the pot and the
|
||||
* contributions from each player.
|
||||
*
|
||||
* @return the current pot.
|
||||
*/
|
||||
public Pot getPot() {
|
||||
return pot;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current deck of cards being used in the game.
|
||||
*
|
||||
* @return the current deck of cards.
|
||||
*/
|
||||
public Deck getDeck() {
|
||||
return deck;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of community cards currently on the table.
|
||||
*
|
||||
* @return a list of community cards, which may be empty if no cards have been dealt yet.
|
||||
*/
|
||||
public List<Card> getCommunityCards() {
|
||||
return communityCards;
|
||||
}
|
||||
|
||||
/** Always returns a mutable list (never null). */
|
||||
/**
|
||||
* Returns the hole cards for the specified player. If the player does not have hole cards yet,
|
||||
* an empty list is returned and stored in the map for future reference.
|
||||
*
|
||||
* @param playerId the ID of the player whose hole cards are being requested.
|
||||
* @return a list of hole cards for the specified player, which may be empty if the player has
|
||||
* not been dealt cards yet.
|
||||
*/
|
||||
public List<Card> getHoleCards(PlayerId playerId) {
|
||||
return holeCards.computeIfAbsent(playerId, k -> new ArrayList<>());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the index of the dealer in the player order list.
|
||||
*
|
||||
* @return the index of the dealer, or 0 if there are no players.
|
||||
*/
|
||||
public int getDealerIndex() {
|
||||
return dealerIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a map of player IDs to their respective hole cards. Each player's hole cards are
|
||||
* represented as a list of Card objects. If a player does not have hole cards yet, they will be
|
||||
* associated with an empty list.
|
||||
*
|
||||
* @return a map where the key is the player's ID and the value is a list of their hole cards.
|
||||
*/
|
||||
public Map<PlayerId, List<Card>> getPlayerCards() {
|
||||
return holeCards;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of players currently in the game, based on the size of the player order
|
||||
* list.
|
||||
*
|
||||
* @return the number of players in the game.
|
||||
*/
|
||||
public int getPlayerCount() {
|
||||
return players.size();
|
||||
}
|
||||
|
||||
// Setters / Mutators
|
||||
public void setCurrentPlayerIndex(int index) {
|
||||
if (playerOrder.isEmpty()) {
|
||||
this.currentPlayerIndex = 0;
|
||||
@@ -121,6 +190,10 @@ public class GameState {
|
||||
this.currentPlayerIndex = index;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the current player index to the first player who should act preflop, which is determined
|
||||
* based on the dealer index and the number of players.
|
||||
*/
|
||||
public void setCurrentPlayerToPreflopFirstToAct() {
|
||||
int size = playerOrder.size();
|
||||
if (size == 0) {
|
||||
@@ -131,11 +204,16 @@ public class GameState {
|
||||
// Heads-up: dealer (small blind) acts first preflop.
|
||||
int first = (size == 2) ? dealerIndex : (dealerIndex + DEALER_OFFSET) % size;
|
||||
currentPlayerIndex = first;
|
||||
resetRoundActions();
|
||||
if (!canPlayerAct(currentPlayerIndex)) {
|
||||
nextPlayer();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the current player index to the first player who should act postflop, which is the
|
||||
* player immediately to the left of the dealer (i.e., dealerIndex + 1).
|
||||
*/
|
||||
public void setCurrentPlayerToPostflopFirstToAct() {
|
||||
int size = playerOrder.size();
|
||||
if (size == 0) {
|
||||
@@ -145,27 +223,86 @@ public class GameState {
|
||||
|
||||
int first = (dealerIndex + 1) % size;
|
||||
currentPlayerIndex = first;
|
||||
resetRoundActions();
|
||||
if (!canPlayerAct(currentPlayerIndex)) {
|
||||
nextPlayer();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks the specified player as having acted in the current round.
|
||||
*
|
||||
* @param playerId the ID of the player to mark as having acted this round.
|
||||
*/
|
||||
public void markActedThisRound(PlayerId playerId) {
|
||||
if (playerId != null) {
|
||||
actedThisRound.add(playerId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the specified player has already acted in the current round by checking if their ID
|
||||
* is present in the set of players who have acted this round.
|
||||
*
|
||||
* @param playerId the ID of the player to check for having acted this round.
|
||||
* @return true if the player has acted this round, false otherwise.
|
||||
*/
|
||||
public boolean hasActedThisRound(PlayerId playerId) {
|
||||
return playerId != null && actedThisRound.contains(playerId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the tracking of which players have acted this round by clearing the set of player IDs.
|
||||
*/
|
||||
public void resetRoundActions() {
|
||||
actedThisRound.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether a hand is currently active (i.e., in progress).
|
||||
*
|
||||
* @param handActive a boolean value indicating whether a hand is active (true) or not (false).
|
||||
*/
|
||||
public void setHandActive(boolean handActive) {
|
||||
this.handActive = handActive;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the current phase of the game (e.g., PREFLOP, FLOP, TURN, RIVER).
|
||||
*
|
||||
* @param phase the GamePhase to set as the current phase of the game.
|
||||
*/
|
||||
public void setPhase(GamePhase phase) {
|
||||
this.phase = phase;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the index of the dealer in the player order list.
|
||||
*
|
||||
* @param dealerIndex the index to set as the dealer index, which should be within the bounds of
|
||||
* the player order list if it is not empty.
|
||||
*/
|
||||
public void setDealerIndex(int dealerIndex) {
|
||||
this.dealerIndex = dealerIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the current deck of cards being used in the game.
|
||||
*
|
||||
* @param deck the Deck object to set as the current deck of cards for the game. This should be
|
||||
* a valid Deck instance that can be used for dealing cards during the game.
|
||||
*/
|
||||
public void setDeck(Deck deck) {
|
||||
this.deck = deck;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new player to the game with the specified ID and initial chip count.
|
||||
*
|
||||
* @param id the PlayerId of the new player to add to the game.
|
||||
* @param chips the initial number of chips the player has, which can be used for betting during
|
||||
* the game.
|
||||
*/
|
||||
public void addPlayer(PlayerId id, int chips) {
|
||||
Player player = new Player(id, chips);
|
||||
|
||||
@@ -178,19 +315,87 @@ public class GameState {
|
||||
holeCards.computeIfAbsent(id, k -> new ArrayList<>());
|
||||
}
|
||||
|
||||
// Betting
|
||||
/**
|
||||
* Renames a player id across all game-state structures.
|
||||
*
|
||||
* @param oldId existing player id
|
||||
* @param newId new player id
|
||||
* @return true if the rename was applied, false otherwise
|
||||
*/
|
||||
public synchronized boolean renamePlayerId(PlayerId oldId, PlayerId newId) {
|
||||
if (oldId == null || newId == null) {
|
||||
return false;
|
||||
}
|
||||
if (!players.containsKey(oldId)) {
|
||||
return false;
|
||||
}
|
||||
if (oldId.equals(newId)) {
|
||||
return true;
|
||||
}
|
||||
if (players.containsKey(newId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Player player = players.remove(oldId);
|
||||
if (player == null) {
|
||||
return false;
|
||||
}
|
||||
player.setId(newId);
|
||||
players.put(newId, player);
|
||||
|
||||
int idx = playerOrder.indexOf(oldId);
|
||||
if (idx >= 0) {
|
||||
playerOrder.set(idx, newId);
|
||||
}
|
||||
|
||||
moveMapEntry(currentBets, oldId, newId, 0);
|
||||
moveMapEntry(playerBetCommitments, oldId, newId, 0);
|
||||
moveMapEntry(holeCards, oldId, newId, new ArrayList<>());
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to move an entry in a map from an old key to a new key, with a fallback value
|
||||
* if the old key is not present.
|
||||
*
|
||||
* @param map the map in which to move the entry, where the key is a PlayerId and the value is
|
||||
* of type T.
|
||||
* @param oldId the existing PlayerId key that is being renamed.
|
||||
* @param newId the new PlayerId key to which the entry should be moved.
|
||||
* @param fallback the value to use if the oldId is not present in the map.
|
||||
* @param <T> the type of the values in the map, which can be any type that is used for
|
||||
* player-related data in the game state.
|
||||
*/
|
||||
private <T> void moveMapEntry(
|
||||
Map<PlayerId, T> map, PlayerId oldId, PlayerId newId, T fallback) {
|
||||
T value = map.remove(oldId);
|
||||
map.put(newId, value != null ? value : fallback);
|
||||
}
|
||||
|
||||
// BETTING
|
||||
|
||||
/**
|
||||
* Returns the current bet amount for the specified player.
|
||||
*
|
||||
* @param playerId the ID of the player whose current bet is being requested.
|
||||
* @return the current bet amount for the specified player, or 0 if the player does not have a
|
||||
* current bet.
|
||||
*/
|
||||
public int getCurrentBet(PlayerId playerId) {
|
||||
return currentBets.getOrDefault(playerId, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the current bet amount for the specified player.
|
||||
*
|
||||
* @param playerId the ID of the player whose current bet is being set.
|
||||
* @param amount the new bet amount to set for the specified player.
|
||||
*/
|
||||
public void setCurrentBet(PlayerId playerId, int amount) {
|
||||
currentBets.put(playerId, amount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset bets to 0 for ALL players (do not clear the map). Also resets table
|
||||
* current bet to 0.
|
||||
*/
|
||||
/** Reset bets to 0 for ALL players (do not clear the map). */
|
||||
public void resetBets() {
|
||||
for (PlayerId id : playerOrder) {
|
||||
currentBets.put(id, 0);
|
||||
@@ -198,10 +403,20 @@ public class GameState {
|
||||
tableState.setCurrentBet(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the specified amount to the pot.
|
||||
*
|
||||
* @param amount the amount to add to the pot.
|
||||
*/
|
||||
public void addToPot(int amount) {
|
||||
pot.add(amount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether out-of-turn actions are allowed in the current game state.
|
||||
*
|
||||
* @return true if out-of-turn actions are allowed, false otherwise.
|
||||
*/
|
||||
public boolean isAllowOutOfTurn() {
|
||||
return allowOutOfTurn;
|
||||
}
|
||||
@@ -210,7 +425,13 @@ public class GameState {
|
||||
this.allowOutOfTurn = allowOutOfTurn;
|
||||
}
|
||||
|
||||
// Player helpers
|
||||
/**
|
||||
* Retrieves the Player object associated with the given PlayerId.
|
||||
*
|
||||
* @param id the PlayerId of the player to retrieve.
|
||||
* @return the Player object associated with the specified PlayerId, or a RuntimeException if
|
||||
* the player is not found.
|
||||
*/
|
||||
public Player getPlayer(PlayerId id) {
|
||||
Player player = players.get(id);
|
||||
if (player == null) {
|
||||
@@ -220,7 +441,13 @@ public class GameState {
|
||||
return player;
|
||||
}
|
||||
|
||||
// Bet commitments
|
||||
/**
|
||||
* Retrieves the current bet commitment for the specified player.
|
||||
*
|
||||
* @param playerId the ID of the player whose current bet commitment is being requested.
|
||||
* @return the current bet commitment for the specified player, or 0 if the player does not have
|
||||
* a bet commitment.
|
||||
*/
|
||||
public int getCurrentBetCommitment(PlayerId playerId) {
|
||||
return playerBetCommitments.getOrDefault(playerId, 0);
|
||||
}
|
||||
@@ -229,11 +456,10 @@ public class GameState {
|
||||
playerBetCommitments.put(playerId, amount);
|
||||
}
|
||||
|
||||
// Cards
|
||||
// CARDS
|
||||
|
||||
/**
|
||||
* Gives (overwrites) the two hole cards for the given player. Ensures stable
|
||||
* list identity
|
||||
* Gives (overwrites) the two hole cards for the given player. Ensures stable list identity
|
||||
* (important if other code holds references).
|
||||
*/
|
||||
public void giveHoleCards(PlayerId playerId, Card c1, Card c2) {
|
||||
@@ -243,15 +469,21 @@ public class GameState {
|
||||
cards.add(c2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a community card to the list of community cards on the table.
|
||||
*
|
||||
* @param card the Card object representing the community card to be added to the table.
|
||||
*/
|
||||
public void addCommunityCard(Card card) {
|
||||
communityCards.add(card);
|
||||
}
|
||||
|
||||
/** Resets the community cards by clearing the list of community cards on the table. */
|
||||
public void resetCommunityCards() {
|
||||
communityCards.clear();
|
||||
}
|
||||
|
||||
// Turn / dealer management
|
||||
/** Advances the current player index to the next player who can act. */
|
||||
public void nextPlayer() {
|
||||
if (playerOrder.isEmpty()) {
|
||||
currentPlayerIndex = 0;
|
||||
@@ -269,6 +501,14 @@ public class GameState {
|
||||
} while (currentPlayerIndex != start);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the player at the specified index in the player order list is able to take an
|
||||
* action.
|
||||
*
|
||||
* @param index the index of the player in the player order list to check for actionability.
|
||||
* @return true if the player at the specified index can act (i.e., is not folded and not
|
||||
* all-in), false otherwise.
|
||||
*/
|
||||
private boolean canPlayerAct(int index) {
|
||||
if (index < 0 || index >= playerOrder.size()) {
|
||||
return false;
|
||||
@@ -283,24 +523,30 @@ public class GameState {
|
||||
dealerIndex = (dealerIndex + 1) % playerOrder.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Player object representing the current dealer based on the dealer index in the
|
||||
* player order list.
|
||||
*
|
||||
* @return the Player object representing the current dealer, or null if there are no players or
|
||||
* the dealer index is out of bounds.
|
||||
*/
|
||||
public Player getDealer() {
|
||||
PlayerId id = playerOrder.get(dealerIndex);
|
||||
return players.get(id);
|
||||
}
|
||||
|
||||
// Hand reset / lifecycle
|
||||
// HAND RESET / LIFECYCLE
|
||||
|
||||
/**
|
||||
* Starts a new hand by resetting the game state for the next round of poker.
|
||||
*
|
||||
* <p>
|
||||
* This:
|
||||
* <p>This:
|
||||
*
|
||||
* <ul>
|
||||
* <li>sets {@code phase=PREFLOP}
|
||||
* <li>resets pot/bets/commitments
|
||||
* <li>clears community + hole cards
|
||||
* <li>creates & shuffles a new deck
|
||||
* <li>sets {@code phase=PREFLOP}
|
||||
* <li>resets pot/bets/commitments
|
||||
* <li>clears community + hole cards
|
||||
* <li>creates & shuffles a new deck
|
||||
* </ul>
|
||||
*/
|
||||
public void startNewHand() {
|
||||
@@ -333,7 +579,29 @@ public class GameState {
|
||||
getPlayer(playerId).setFolded(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the specified player has folded by retrieving their Player object and checking
|
||||
* their folded status.
|
||||
*
|
||||
* @param playerId the ID of the player to check for folded status.
|
||||
* @return true if the player has folded, false otherwise.
|
||||
*/
|
||||
public boolean isFolded(PlayerId playerId) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server.domain.highscore;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/** Stores and reads persistent highscore entries in timestamp + winner format. */
|
||||
public final class HighscoreService {
|
||||
|
||||
private static final HighscoreService INSTANCE = new HighscoreService();
|
||||
private static final DateTimeFormatter DISPLAY_FORMATTER =
|
||||
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").withZone(ZoneId.systemDefault());
|
||||
private static final int MAX_RETURNED_ENTRIES = 100;
|
||||
private static final String LINE_SEPARATOR = "\t";
|
||||
|
||||
private final Path storagePath;
|
||||
|
||||
private HighscoreService() {
|
||||
this.storagePath =
|
||||
Path.of(System.getProperty("user.home"), ".casono", "highscores", "winners.log");
|
||||
}
|
||||
|
||||
public static HighscoreService getInstance() {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
public synchronized void appendWinner(String winnerName) {
|
||||
if (winnerName == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
String sanitized = sanitizeName(winnerName);
|
||||
if (sanitized.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
String line = Instant.now() + LINE_SEPARATOR + sanitized + System.lineSeparator();
|
||||
|
||||
try {
|
||||
Files.createDirectories(storagePath.getParent());
|
||||
Files.writeString(
|
||||
storagePath,
|
||||
line,
|
||||
StandardCharsets.UTF_8,
|
||||
StandardOpenOption.CREATE,
|
||||
StandardOpenOption.APPEND);
|
||||
} catch (IOException ignored) {
|
||||
// Highscore persistence must never break the game state response path.
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized List<String> readFormattedEntries() {
|
||||
if (!Files.exists(storagePath)) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
List<String> lines;
|
||||
try {
|
||||
lines = Files.readAllLines(storagePath, StandardCharsets.UTF_8);
|
||||
} catch (IOException ignored) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
List<String> formatted = new ArrayList<>();
|
||||
|
||||
for (String raw : lines) {
|
||||
if (raw == null || raw.isBlank()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String[] parts = raw.split(LINE_SEPARATOR, 2);
|
||||
if (parts.length < 2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
Instant ts = Instant.parse(parts[0].trim());
|
||||
String display = DISPLAY_FORMATTER.format(ts) + " | " + parts[1].trim();
|
||||
formatted.add(display);
|
||||
} catch (Exception ignored) {
|
||||
// Skip malformed lines and keep all valid entries.
|
||||
}
|
||||
}
|
||||
|
||||
int from = Math.max(0, formatted.size() - MAX_RETURNED_ENTRIES);
|
||||
return new ArrayList<>(formatted.subList(from, formatted.size()));
|
||||
}
|
||||
|
||||
public synchronized void clearAll() {
|
||||
try {
|
||||
Files.deleteIfExists(storagePath);
|
||||
} catch (IOException ignored) {
|
||||
// Clearing highscores must not break request handling.
|
||||
}
|
||||
}
|
||||
|
||||
private static String sanitizeName(String winnerName) {
|
||||
return winnerName.trim().replace("\n", " ").replace("\r", " ").replace("\t", " ");
|
||||
}
|
||||
}
|
||||
@@ -70,6 +70,30 @@ public class Lobby {
|
||||
return playerNames.remove(playerName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renames a player in this lobby's player list.
|
||||
*
|
||||
* @param oldName old username
|
||||
* @param newName new username
|
||||
* @return true if renamed successfully
|
||||
*/
|
||||
public boolean renamePlayer(String oldName, String newName) {
|
||||
if (oldName == null || newName == null) {
|
||||
return false;
|
||||
}
|
||||
synchronized (playerNames) {
|
||||
if (oldName.equals(newName)) {
|
||||
return playerNames.contains(oldName);
|
||||
}
|
||||
int idx = playerNames.indexOf(oldName);
|
||||
if (idx < 0 || playerNames.contains(newName)) {
|
||||
return false;
|
||||
}
|
||||
playerNames.set(idx, newName);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public void initGame(GameController controller) {
|
||||
this.gameController = controller;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.player.PlayerId;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.Lobby.AddResult;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
@@ -170,6 +171,56 @@ public class LobbyManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Renames a player across lobby mapping, lobby player list and running game ids.
|
||||
*
|
||||
* @param oldUsername old username
|
||||
* @param newUsername new username
|
||||
* @return true if rename was applied
|
||||
*/
|
||||
public synchronized boolean renamePlayer(String oldUsername, String newUsername) {
|
||||
if (oldUsername == null || newUsername == null) {
|
||||
return false;
|
||||
}
|
||||
if (oldUsername.equals(newUsername)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
LobbyId lobbyId = playerToLobby.get(oldUsername);
|
||||
if (lobbyId == null) {
|
||||
return true;
|
||||
}
|
||||
if (playerToLobby.containsKey(newUsername)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Lobby lobby = activeLobbies.get(lobbyId);
|
||||
if (lobby == null) {
|
||||
playerToLobby.remove(oldUsername);
|
||||
return true;
|
||||
}
|
||||
|
||||
boolean lobbyRenamed = lobby.renamePlayer(oldUsername, newUsername);
|
||||
if (!lobbyRenamed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (lobby.getGameController() != null) {
|
||||
boolean gameRenamed =
|
||||
lobby.getGameController()
|
||||
.renamePlayer(PlayerId.of(oldUsername), PlayerId.of(newUsername));
|
||||
if (!gameRenamed) {
|
||||
// Best-effort rollback to keep structures consistent.
|
||||
lobby.renamePlayer(newUsername, oldUsername);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
playerToLobby.remove(oldUsername);
|
||||
playerToLobby.put(newUsername, lobbyId);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the given action to every player username in the lobby identified by {@code lobbyId}.
|
||||
* This is a small helper that keeps iteration logic centralized and avoids leaking internal
|
||||
|
||||
@@ -200,6 +200,10 @@ public class UserRegistry {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (user.getName().equals(newName)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (byName.containsKey(newName)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -81,25 +81,32 @@
|
||||
</GridPane.margin>
|
||||
</Button>
|
||||
|
||||
<!-- Username input and login button -->
|
||||
<!-- <HBox alignment="CENTER_LEFT" spacing="10"-->
|
||||
<!-- GridPane.rowIndex="0"-->
|
||||
<!-- GridPane.columnIndex="0"-->
|
||||
<!-- GridPane.halignment="LEFT"-->
|
||||
<!-- GridPane.valignment="TOP">-->
|
||||
<!-- <GridPane.margin>-->
|
||||
<!-- <!– place below the 'CREATE A LOBBY' button –>-->
|
||||
<!-- <Insets top="100" left="20" />-->
|
||||
<!-- </GridPane.margin>-->
|
||||
<!-- <TextField fx:id="usernameField"-->
|
||||
<!-- promptText="Username"-->
|
||||
<!-- styleClass="gray-input-field"-->
|
||||
<!-- maxWidth="180" />-->
|
||||
<!-- <Button text="Login"-->
|
||||
<!-- fx:id="loginButton"-->
|
||||
<!-- onAction="#handleLoginButton"-->
|
||||
<!-- styleClass="button-create-lobby" />-->
|
||||
<!-- </HBox>-->
|
||||
<!-- Username input + change name, with highscores directly below -->
|
||||
<VBox alignment="TOP_LEFT" spacing="10"
|
||||
GridPane.rowIndex="0"
|
||||
GridPane.columnIndex="0"
|
||||
GridPane.halignment="LEFT"
|
||||
GridPane.valignment="TOP">
|
||||
<GridPane.margin>
|
||||
<Insets top="140" left="20" />
|
||||
</GridPane.margin>
|
||||
|
||||
<HBox alignment="CENTER_LEFT" spacing="10">
|
||||
<TextField fx:id="usernameField"
|
||||
promptText="Username"
|
||||
styleClass="gray-input-field"
|
||||
maxWidth="180" />
|
||||
<Button text="Apply Name"
|
||||
fx:id="loginButton"
|
||||
onAction="#handleLoginButton"
|
||||
styleClass="button-create-lobby" />
|
||||
</HBox>
|
||||
|
||||
<Button text="HIGHSCORES"
|
||||
styleClass="button-create-lobby"
|
||||
fx:id="highscoreButton"
|
||||
onAction="#handleOpenHighscores" />
|
||||
</VBox>
|
||||
|
||||
<VBox fx:id="casinoTable"
|
||||
alignment="CENTER"
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
.highscore-root {
|
||||
-fx-background-color: rgba(13, 158, 59, 0.96);
|
||||
-fx-background-radius: 16;
|
||||
-fx-border-radius: 16;
|
||||
-fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10;
|
||||
-fx-border-width: 3;
|
||||
}
|
||||
|
||||
.highscore-title {
|
||||
-fx-text-fill: #ffffff;
|
||||
-fx-font-size: 24px;
|
||||
-fx-font-weight: bold;
|
||||
-fx-effect: dropshadow(one-pass-box, #000000, 0, 0, 2, 2);
|
||||
}
|
||||
|
||||
.highscore-status {
|
||||
-fx-text-fill: #ffffff;
|
||||
-fx-font-size: 13px;
|
||||
}
|
||||
|
||||
.list-view {
|
||||
-fx-background-color: #114d26;
|
||||
-fx-control-inner-background: #114d26;
|
||||
-fx-background-insets: 0;
|
||||
-fx-background-radius: 12;
|
||||
-fx-border-radius: 12;
|
||||
-fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10;
|
||||
-fx-border-width: 2;
|
||||
}
|
||||
|
||||
.list-view .list-cell {
|
||||
-fx-background-color: transparent;
|
||||
-fx-text-fill: #ffe89a;
|
||||
-fx-font-family: "Courier New";
|
||||
-fx-font-size: 13px;
|
||||
}
|
||||
|
||||
.list-view .list-cell:filled {
|
||||
-fx-background-color: rgba(0, 0, 0, 0.14);
|
||||
}
|
||||
|
||||
.list-view .list-cell:filled:hover {
|
||||
-fx-background-color: rgba(0, 0, 0, 0.24);
|
||||
}
|
||||
|
||||
.list-view .list-cell:filled:selected,
|
||||
.list-view .list-cell:filled:selected:focused {
|
||||
-fx-background-color: rgba(0, 0, 0, 0.35);
|
||||
-fx-text-fill: #ffffff;
|
||||
}
|
||||
|
||||
.gray-button,
|
||||
.yellow-button,
|
||||
.red-button {
|
||||
-fx-background-radius: 10;
|
||||
-fx-border-radius: 10;
|
||||
-fx-font-family: "Courier New";
|
||||
-fx-font-weight: bold;
|
||||
-fx-border-width: 2;
|
||||
-fx-padding: 6 12;
|
||||
-fx-cursor: hand;
|
||||
}
|
||||
|
||||
.gray-button {
|
||||
-fx-background-color: #333333;
|
||||
-fx-border-color: #666666;
|
||||
-fx-text-fill: #cccccc;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.highscore-root {
|
||||
-fx-background-color: #0d9e3b;
|
||||
-fx-background-radius: 16;
|
||||
-fx-border-radius: 16;
|
||||
-fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10;
|
||||
-fx-border-width: 3;
|
||||
}
|
||||
|
||||
.highscore-title {
|
||||
-fx-text-fill: #ffffff;
|
||||
-fx-font-size: 22px;
|
||||
-fx-font-weight: bold;
|
||||
}
|
||||
|
||||
.highscore-subtitle {
|
||||
-fx-text-fill: #f2f2f2;
|
||||
-fx-font-size: 13px;
|
||||
}
|
||||
|
||||
.highscore-text-area {
|
||||
-fx-control-inner-background: #1b1b1b;
|
||||
-fx-text-fill: #ffffff;
|
||||
-fx-font-family: "Courier New";
|
||||
-fx-font-size: 14px;
|
||||
-fx-border-color: #666666;
|
||||
-fx-border-width: 2;
|
||||
-fx-border-radius: 10;
|
||||
-fx-background-radius: 10;
|
||||
}
|
||||
|
||||
.gray-button,
|
||||
.green-button,
|
||||
.red-button {
|
||||
-fx-background-radius: 10;
|
||||
-fx-border-radius: 10;
|
||||
-fx-font-family: "Courier New";
|
||||
-fx-font-weight: bold;
|
||||
-fx-border-width: 2;
|
||||
-fx-padding: 6 14;
|
||||
}
|
||||
|
||||
.green-button {
|
||||
-fx-background-color: #1f7d33;
|
||||
-fx-border-color: #35c759;
|
||||
-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: #cccccc;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import javafx.geometry.Insets?>
|
||||
<?import javafx.scene.control.Button?>
|
||||
<?import javafx.scene.control.Label?>
|
||||
<?import javafx.scene.control.ListView?>
|
||||
<?import javafx.scene.layout.HBox?>
|
||||
<?import javafx.scene.layout.VBox?>
|
||||
|
||||
<VBox fx:id="highscoreRoot"
|
||||
xmlns="http://javafx.com/javafx/21"
|
||||
xmlns:fx="http://javafx.com/fxml/1"
|
||||
fx:controller="ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.gameuicomponents.HighscoreViewController"
|
||||
spacing="12"
|
||||
styleClass="highscore-root"
|
||||
onMousePressed="#onPopupPressed"
|
||||
onMouseDragged="#onPopupDragged"
|
||||
stylesheets="@HighscoreView.css">
|
||||
|
||||
<padding>
|
||||
<Insets top="14" right="14" bottom="14" left="14"/>
|
||||
</padding>
|
||||
|
||||
<Label text="Highscores" styleClass="highscore-title"/>
|
||||
|
||||
<ListView fx:id="highscoreList" prefHeight="320" VBox.vgrow="ALWAYS"/>
|
||||
|
||||
<Label fx:id="statusLabel" text="Ready." styleClass="highscore-status"/>
|
||||
|
||||
<HBox spacing="10">
|
||||
<Button text="Refresh" onAction="#refreshHighscores" styleClass="gray-button" HBox.hgrow="ALWAYS" maxWidth="Infinity"/>
|
||||
<Button text="Clear" onAction="#clearHighscores" styleClass="yellow-button" HBox.hgrow="ALWAYS" maxWidth="Infinity"/>
|
||||
<Button text="Close" onAction="#closeWindow" styleClass="red-button" HBox.hgrow="ALWAYS" maxWidth="Infinity"/>
|
||||
</HBox>
|
||||
|
||||
</VBox>
|
||||
@@ -19,7 +19,7 @@
|
||||
|
||||
<!-- Placeholder buttons -->
|
||||
<Button text="SETTINGS" styleClass="gray-button" />
|
||||
<Button text="INFO" styleClass="gray-button" />
|
||||
<Button text="HIGHSCORE" onAction="#onHighscoreButtonClick" styleClass="gray-button" />
|
||||
<!-- Buten for the Casono Browser -->
|
||||
<Button text="HELP" onAction="#onBrowserButtonClick" styleClass="gray-button" />
|
||||
|
||||
|
||||
-2
@@ -1,7 +1,5 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.client.ui.lobbyui;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.network.ClientService;
|
||||
import javafx.scene.layout.GridPane;
|
||||
import org.junit.jupiter.api.*;
|
||||
|
||||
+152
-21
@@ -37,6 +37,127 @@ public class GameControllerTest {
|
||||
|
||||
private static final Logger LOGGER = Logger.getLogger(GameControllerTest.class.getName());
|
||||
|
||||
/**
|
||||
* Helper method to retrieve the current player's ID from the game state.
|
||||
*
|
||||
* @param game The GameController instance from which to retrieve the current player's ID.
|
||||
* @return The PlayerId of the current player in the game.
|
||||
*/
|
||||
private static PlayerId currentPlayerId(GameController game) {
|
||||
return game.getState().getCurrentPlayer().getId();
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to simulate a call action for the current player in the game.
|
||||
*
|
||||
* @param game The GameController instance on which to perform the call action for the current
|
||||
* player.
|
||||
*/
|
||||
private static void callCurrent(GameController game) {
|
||||
game.playerCall(currentPlayerId(game));
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to simulate a fold action for the current player in the game.
|
||||
*
|
||||
* @param game The GameController instance on which to perform the fold action for the current
|
||||
* player.
|
||||
*/
|
||||
private static void foldCurrent(GameController game) {
|
||||
game.playerFold(currentPlayerId(game));
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that the preflop phase ends once all active (non-folded) players have completed
|
||||
* their required actions for the current betting round.
|
||||
*/
|
||||
@Test
|
||||
public void testPreflopEndsAfterOneActionPerActivePlayer() {
|
||||
|
||||
GameState state = new GameState();
|
||||
GameEngine engine =
|
||||
new GameEngine(
|
||||
state,
|
||||
new RuleEngine(new ArrayList<>()),
|
||||
new RoundManager(),
|
||||
new TurnManager());
|
||||
|
||||
GameController game = new GameController(engine);
|
||||
game.addPlayer(PlayerId.of("P1"), 10000);
|
||||
game.addPlayer(PlayerId.of("P2"), 10000);
|
||||
game.addPlayer(PlayerId.of("P3"), 10000);
|
||||
game.addPlayer(PlayerId.of("P4"), 10000);
|
||||
|
||||
game.startGame();
|
||||
|
||||
assertEquals(GamePhase.PREFLOP, game.getState().getPhase());
|
||||
assertEquals(0, game.getCommunityCards().size());
|
||||
|
||||
callCurrent(game);
|
||||
callCurrent(game);
|
||||
callCurrent(game);
|
||||
|
||||
assertEquals(GamePhase.PREFLOP, game.getState().getPhase());
|
||||
assertEquals(0, game.getCommunityCards().size());
|
||||
|
||||
callCurrent(game);
|
||||
|
||||
assertEquals(GamePhase.FLOP, game.getState().getPhase());
|
||||
assertEquals(3, game.getCommunityCards().size());
|
||||
|
||||
callCurrent(game);
|
||||
callCurrent(game);
|
||||
callCurrent(game);
|
||||
callCurrent(game);
|
||||
|
||||
assertEquals(GamePhase.TURN, game.getState().getPhase());
|
||||
assertEquals(4, game.getCommunityCards().size());
|
||||
|
||||
callCurrent(game);
|
||||
callCurrent(game);
|
||||
callCurrent(game);
|
||||
callCurrent(game);
|
||||
|
||||
assertEquals(GamePhase.RIVER, game.getState().getPhase());
|
||||
assertEquals(5, game.getCommunityCards().size());
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that during the preflop phase, folded players are excluded from progression checks
|
||||
* and do not block phase advancement.
|
||||
*/
|
||||
@Test
|
||||
public void testPreflopCountsOnlyNonFoldedPlayersForProgress() {
|
||||
|
||||
GameState state = new GameState();
|
||||
GameEngine engine =
|
||||
new GameEngine(
|
||||
state,
|
||||
new RuleEngine(new ArrayList<>()),
|
||||
new RoundManager(),
|
||||
new TurnManager());
|
||||
|
||||
GameController game = new GameController(engine);
|
||||
game.addPlayer(PlayerId.of("A"), 10000);
|
||||
game.addPlayer(PlayerId.of("B"), 10000);
|
||||
game.addPlayer(PlayerId.of("C"), 10000);
|
||||
game.addPlayer(PlayerId.of("D"), 10000);
|
||||
|
||||
game.startGame();
|
||||
assertEquals(GamePhase.PREFLOP, game.getState().getPhase());
|
||||
|
||||
callCurrent(game);
|
||||
foldCurrent(game);
|
||||
callCurrent(game);
|
||||
|
||||
assertEquals(GamePhase.PREFLOP, game.getState().getPhase());
|
||||
|
||||
callCurrent(game);
|
||||
|
||||
assertEquals(GamePhase.FLOP, game.getState().getPhase());
|
||||
assertEquals(3, game.getCommunityCards().size());
|
||||
}
|
||||
|
||||
/**
|
||||
* This test simulates a specific game scenario where Julian is expected to win with a Two Pair
|
||||
* hand. It sets up a fixed deck to ensure that the desired cards are dealt to the players and
|
||||
@@ -83,11 +204,14 @@ public class GameControllerTest {
|
||||
}
|
||||
}
|
||||
|
||||
game.playerCall(PlayerId.of("Julian"));
|
||||
callCurrent(game);
|
||||
game.playerFold(PlayerId.of("Mathis"));
|
||||
game.playerCall(PlayerId.of("Jona"));
|
||||
callCurrent(game);
|
||||
game.playerRaise(PlayerId.of("Lars"), 1200);
|
||||
|
||||
// New street model: once all non-folded players acted, preflop is complete.
|
||||
assertEquals(GamePhase.FLOP, game.getState().getPhase());
|
||||
|
||||
List<Card> board = game.getCommunityCards();
|
||||
assertTrue(board.size() >= 3 && board.size() <= 5);
|
||||
|
||||
@@ -170,10 +294,10 @@ public class GameControllerTest {
|
||||
|
||||
int potBefore = game.getState().getPot().getAmount();
|
||||
|
||||
game.playerRaise(PlayerId.of("Lars"), 1200);
|
||||
game.playerCall(PlayerId.of("Julian"));
|
||||
game.playerFold(PlayerId.of("Mathis"));
|
||||
game.playerCall(PlayerId.of("Jona"));
|
||||
game.playerRaise(currentPlayerId(game), 1200);
|
||||
callCurrent(game);
|
||||
foldCurrent(game);
|
||||
callCurrent(game);
|
||||
|
||||
System.out.println("Pot after actions: " + game.getState().getPot().getAmount());
|
||||
|
||||
@@ -183,11 +307,15 @@ public class GameControllerTest {
|
||||
|
||||
assertEquals(3, game.getCommunityCards().size(), "The flop must have 3 cards");
|
||||
|
||||
game.playerCall(PlayerId.of("Julian"));
|
||||
for (int i = 0; i < 10 && game.getCommunityCards().size() < 4; i++) {
|
||||
callCurrent(game);
|
||||
}
|
||||
|
||||
assertEquals(4, game.getCommunityCards().size(), "A turn must result in 4 cards");
|
||||
|
||||
game.playerCall(PlayerId.of("Jona"));
|
||||
for (int i = 0; i < 10 && game.getCommunityCards().size() < 5; i++) {
|
||||
callCurrent(game);
|
||||
}
|
||||
|
||||
assertEquals(5, game.getCommunityCards().size(), "The river must consist of 5 cards");
|
||||
|
||||
@@ -278,13 +406,12 @@ public class GameControllerTest {
|
||||
game.playerCall(PlayerId.of("Jona"));
|
||||
game.playerRaise(PlayerId.of("Lars"), 1200);
|
||||
|
||||
game.playerCall(PlayerId.of("Julian"));
|
||||
game.playerCall(PlayerId.of("Jona"));
|
||||
assertEquals(GamePhase.FLOP, game.getState().getPhase());
|
||||
|
||||
assertTrue(game.getCommunityCards().size() >= 3);
|
||||
|
||||
for (int i = 0; i < 10 && game.getCommunityCards().size() < 5; i++) {
|
||||
game.playerCall(PlayerId.of("Julian"));
|
||||
callCurrent(game);
|
||||
}
|
||||
|
||||
assertEquals(5, game.getCommunityCards().size());
|
||||
@@ -367,10 +494,14 @@ public class GameControllerTest {
|
||||
|
||||
assertEquals(3, game.getCommunityCards().size());
|
||||
|
||||
game.playerCall(PlayerId.of("Julian"));
|
||||
for (int i = 0; i < 10 && game.getCommunityCards().size() < 4; i++) {
|
||||
callCurrent(game);
|
||||
}
|
||||
assertEquals(4, game.getCommunityCards().size());
|
||||
|
||||
game.playerCall(PlayerId.of("Mathis"));
|
||||
for (int i = 0; i < 10 && game.getCommunityCards().size() < 5; i++) {
|
||||
callCurrent(game);
|
||||
}
|
||||
assertEquals(5, game.getCommunityCards().size());
|
||||
|
||||
Set<String> boardSeen = new HashSet<>();
|
||||
@@ -424,10 +555,10 @@ public class GameControllerTest {
|
||||
|
||||
game.startGame();
|
||||
|
||||
game.playerRaise(PlayerId.of("BigStack"), 1000);
|
||||
game.playerCall(PlayerId.of("Caller"));
|
||||
game.playerCall(PlayerId.of("MidStack"));
|
||||
game.playerCall(PlayerId.of("ShortStack"));
|
||||
game.playerRaise(currentPlayerId(game), 1000);
|
||||
callCurrent(game);
|
||||
callCurrent(game);
|
||||
callCurrent(game);
|
||||
|
||||
for (Player p : game.getState().getPlayers()) {
|
||||
assertTrue(p.getChips() >= 0, "Negative chips detected for " + p.getId());
|
||||
@@ -435,13 +566,13 @@ public class GameControllerTest {
|
||||
|
||||
assertTrue(game.getState().getPot().getAmount() > 0);
|
||||
|
||||
game.playerRaise(PlayerId.of("BigStack"), 100);
|
||||
game.playerRaise(currentPlayerId(game), 100);
|
||||
|
||||
int pot = game.getState().getPot().getAmount();
|
||||
assertTrue(pot > 0, "Pot must remain valid after re-raises");
|
||||
|
||||
game.playerFold(PlayerId.of("Caller"));
|
||||
game.playerFold(PlayerId.of("MidStack"));
|
||||
foldCurrent(game);
|
||||
foldCurrent(game);
|
||||
|
||||
long activePlayers =
|
||||
game.getState().getPlayers().stream().filter(p -> p.getChips() > 0).count();
|
||||
@@ -451,7 +582,7 @@ public class GameControllerTest {
|
||||
assertTrue(game.getCommunityCards().size() >= 3);
|
||||
|
||||
for (int i = 0; i < 10 && game.getCommunityCards().size() < 5; i++) {
|
||||
game.playerCall(PlayerId.of("BigStack"));
|
||||
callCurrent(game);
|
||||
}
|
||||
|
||||
assertEquals(5, game.getCommunityCards().size());
|
||||
|
||||
Reference in New Issue
Block a user