Merge branch 'main' into feat/100-add-start-game-command-on-server-side

This commit is contained in:
Jona Walpert
2026-04-12 12:45:24 +02:00
34 changed files with 1846 additions and 120 deletions
-1
View File
@@ -36,7 +36,6 @@ This protocol handles game state synchronization, player actions, lobby and glob
├── documents/ # Project resources and documentation
│ ├── images/ # Images for README and documentation
│ ├── diary # Diary files for each organizational meetup
│ ├── blog # Blog files covering project-related topics
│ ├── docs # Documentation
│ ├── milestones/ # Milestone deliverables (6 milestones)
│ └── (Other resources) # Additional project materials
-28
View File
@@ -1,28 +0,0 @@
# Blog
## Purpose
This folder contains a collection of blog posts documenting the development process of our application. These blogs serve two key purposes:
1. **For External Audiences**
The blogs provide insights into the internal program flow and the interaction between components without requiring readers to dive into the source code. They offer a mixed-level overview of how different parts of the system work together.
2. **For Our Team**
The blogs serve as a record of our decision-making process. By documenting what decisions were made and why, we can track the evolution of our design choices and understand the reasoning behind them. This is invaluable for maintaining consistency across the team.
But keep in mind that these blogs **do not replace** the detailed source code documentation.
## Format
All blog posts follow the naming convention: `<YYYY-MM-DD>_<Title>.md`
## Blog Posts
### General
*(No posts yet)*
### Client
*(No posts yet)*
### Server
*(No posts yet)*
@@ -62,6 +62,10 @@ This document describes the protocol for client-server communication in our appl
- [Success Response](#success-response)
- [Example Request](#example-request)
- [Example Response](#example-response)
- [RAISE command](#raise-command)
- [CALL command](#call-command)
- [FOLD command](#fold-command)
- [BET command](#bet-command)
- [SEND_MESSAGE command](#send_message-command)
- [Required pre-execution checks](#required-pre-execution-checks)
- [Request Parameters](#request-parameters)
@@ -720,6 +724,391 @@ GET_GAME_STATE GAME_ID=1
END
```
## BET command
The `BET` command lets the currently logged-in player place a bet in the ongoing game for their lobby.
### Required pre-execution checks
- [`UserLoggedInCheck`](#userloggedincheck)
### Request Parameters
| Parameter Name | Type | Optional | Description |
| :------------- | :--- | :------: | :---------- |
| `GAME_ID` | `int` | yes | Numeric id of the lobby/game to target. If omitted the server resolves the lobby by the requesting session's user. |
| `AMOUNT` | `int` | no | Amount the player wants to bet |
### Implementation notes
- Parser: `PlayerBetParser` — reads `AMOUNT` and optionally `GAME_ID`.
- Handler: `PlayerBetHandler` — validates the session, lobby (by id when provided or else by session), player's turn and balance and forwards to `GameController`.
### Success Response
No additional response fields. Server replies with `+OK` on success.
### Error Response
| Code | Description |
| :--- | :---------- |
| `NOT_YOUR_TURN` | The player attempted to bet when not their turn |
| `INSUFFICIENT_FUNDS` | Player does not have enough chips |
| `INVALID_AMOUNT` | Amount parameter is invalid |
| `GAME_NOT_STARTED` | No game is running in the lobby |
| `NOT_IN_LOBBY` | Requesting user is not a member of the lobby |
| `LOBBY_NOT_FOUND` | The specified `GAME_ID` does not exist |
### Example Request
```
BET GAME_ID=1 AMOUNT=50
```
### Example Response (success)
```
+OK
END
```
### Example Response (error)
```
-ERR
CODE=INSUFFICIENT_FUNDS
MSG=Not enough chips
END
```
## RAISE command
The `RAISE` command lets the currently logged-in player increase the current bet in the ongoing game for their lobby.
### Required pre-execution checks
- [`UserLoggedInCheck`](#userloggedincheck)
### Request Parameters
| Parameter Name | Type | Optional | Description |
| :------------- | :--- | :------: | :---------- |
| `GAME_ID` | `int` | yes | Numeric id of the lobby/game to target. If omitted the server resolves the lobby by the requesting session's user. |
| `AMOUNT` | `int` | no | Amount the player wants to raise |
### Implementation notes
- Parser: `PlayerRaiseParser` — reads `AMOUNT` and optionally `GAME_ID`.
- Handler: `PlayerRaiseHandler` — validates the session, lobby (by id when provided or else by session), player's turn and balance and forwards to `GameController`.
### Success Response
No additional response fields. Server replies with `+OK` on success.
### Error Response
| Code | Description |
| :--- | :---------- |
| `NOT_YOUR_TURN` | The player attempted to raise when not their turn |
| `INSUFFICIENT_FUNDS` | Player does not have enough chips |
| `INVALID_AMOUNT` | Amount parameter is invalid |
| `GAME_NOT_STARTED` | No game is running in the lobby |
| `NOT_IN_LOBBY` | Requesting user is not a member of the lobby |
| `LOBBY_NOT_FOUND` | The specified `GAME_ID` does not exist |
### Example Request
```
RAISE GAME_ID=1 AMOUNT=100
```
### Example Response (success)
```
+OK
END
```
### Example Response (error)
```
-ERR
CODE=INSUFFICIENT_FUNDS
MSG=Not enough chips
END
```
## CALL command
The `CALL` command lets the currently logged-in player match the current bet (call) in the ongoing game for their lobby.
### Required pre-execution checks
- [`UserLoggedInCheck`](#userloggedincheck)
### Request Parameters
| Parameter Name | Type | Optional | Description |
| :------------- | :--- | :------: | :---------- |
| `GAME_ID` | `int` | yes | Numeric id of the lobby/game to target. If omitted the server resolves the lobby by the requesting session's user. |
### Implementation notes
- Parser: `PlayerCallParser` — accepts optional `GAME_ID`.
- Handler: `PlayerCallHandler` — validates the session, resolves the lobby by id when provided or by session otherwise and forwards to `GameController`.
### Success Response
No additional response fields. Server replies with `+OK` on success.
### Error Response
| Code | Description |
| :--- | :---------- |
| `NOT_YOUR_TURN` | The player attempted to call when not their turn |
| `INSUFFICIENT_FUNDS` | Player does not have enough chips |
| `GAME_NOT_STARTED` | No game is running in the lobby |
| `NOT_IN_LOBBY` | Requesting user is not a member of the lobby |
| `LOBBY_NOT_FOUND` | The specified `GAME_ID` does not exist |
### Example Request
```
CALL GAME_ID=1
```
### Example Response (success)
```
+OK
END
```
### Example Response (error)
```
-ERR
CODE=NOT_YOUR_TURN
MSG=It is not your turn
END
```
## FOLD command
The `FOLD` command lets the currently logged-in player leave the current hand (fold) in the ongoing game for their lobby.
### Required pre-execution checks
- [`UserLoggedInCheck`](#userloggedincheck)
### Request Parameters
| Parameter Name | Type | Optional | Description |
| :------------- | :--- | :------: | :---------- |
| `GAME_ID` | `int` | yes | Numeric id of the lobby/game to target. If omitted the server resolves the lobby by the requesting session's user. |
### Implementation notes
- Parser: `PlayerFoldParser` — accepts optional `GAME_ID`.
- Handler: `PlayerFoldHandler` — validates the session, resolves the lobby by id when provided or by session otherwise and forwards to `GameController`.
### Success Response
No additional response fields. Server replies with `+OK` on success.
### Error Response
| Code | Description |
| :--- | :---------- |
| `NOT_YOUR_TURN` | The player attempted to fold when not their turn |
| `GAME_NOT_STARTED` | No game is running in the lobby |
| `NOT_IN_LOBBY` | Requesting user is not a member of the lobby |
| `LOBBY_NOT_FOUND` | The specified `GAME_ID` does not exist |
### Example Request
```
FOLD GAME_ID=1
```
### Example Response (success)
```
+OK
END
```
### Example Response (error)
```
-ERR
CODE=NOT_YOUR_TURN
MSG=It is not your turn
END
```
## FOLD command
The `FOLD` command lets the currently logged-in player leave the current hand (fold) in the ongoing game for their lobby.
### Required pre-execution checks
- [`UserLoggedInCheck`](#userloggedincheck)
### Request Parameters
| Parameter Name | Type | Optional | Description |
| :------------- | :--- | :------: | :---------- |
| `GAME_ID` | `int` | yes | Numeric id of the lobby/game to target. If omitted the server resolves the lobby by the requesting session's user. |
### Implementation notes
- Parser: `PlayerFoldParser` — accepts optional `GAME_ID`.
- Handler: `PlayerFoldHandler` — validates the session, resolves the lobby by id when provided or by session otherwise and forwards to `GameController`.
### Success Response
No additional response fields. Server replies with `+OK` on success.
### Error Response
| Code | Description |
| :--- | :---------- |
| `NOT_YOUR_TURN` | The player attempted to fold when not their turn |
| `GAME_NOT_STARTED` | No game is running in the lobby |
| `NOT_IN_LOBBY` | Requesting user is not a member of the lobby |
| `LOBBY_NOT_FOUND` | The specified `GAME_ID` does not exist |
### Example Request
```
FOLD GAME_ID=1
```
### Example Response (success)
```
+OK
END
```
### Example Response (error)
```
-ERR
CODE=NOT_YOUR_TURN
MSG=It is not your turn
END
```
## FOLD command
The `FOLD` command lets the currently logged-in player leave the current hand (fold) in the ongoing game for their lobby.
### Required pre-execution checks
- [`UserLoggedInCheck`](#userloggedincheck)
### Request Parameters
| Parameter Name | Type | Optional | Description |
| :------------- | :--- | :------: | :---------- |
| `GAME_ID` | `int` | yes | Numeric id of the lobby/game to target. If omitted the server resolves the lobby by the requesting session's user. |
### Implementation notes
- Parser: `PlayerFoldParser` — accepts optional `GAME_ID`.
- Handler: `PlayerFoldHandler` — validates the session, resolves the lobby by id when provided or by session otherwise and forwards to `GameController`.
### Success Response
No additional response fields. Server replies with `+OK` on success.
### Error Response
| Code | Description |
| :--- | :---------- |
| `NOT_YOUR_TURN` | The player attempted to fold when not their turn |
| `GAME_NOT_STARTED` | No game is running in the lobby |
| `NOT_IN_LOBBY` | Requesting user is not a member of the lobby |
| `LOBBY_NOT_FOUND` | The specified `GAME_ID` does not exist |
### Example Request
```
FOLD GAME_ID=1
```
### Example Response (success)
```
+OK
END
```
### Example Response (error)
```
-ERR
CODE=NOT_YOUR_TURN
MSG=It is not your turn
END
```
## FOLD command
The `FOLD` command lets the currently logged-in player leave the current hand (fold) in the ongoing game for their lobby.
### Required pre-execution checks
- [`UserLoggedInCheck`](#userloggedincheck)
### Request Parameters
| Parameter Name | Type | Optional | Description |
| :------------- | :--- | :------: | :---------- |
| `GAME_ID` | `int` | yes | Numeric id of the lobby/game to target. If omitted the server resolves the lobby by the requesting session's user. |
### Implementation notes
- Parser: `PlayerFoldParser` — accepts optional `GAME_ID`.
- Handler: `PlayerFoldHandler` — validates the session, resolves the lobby by id when provided or by session otherwise and forwards to `GameController`.
### Success Response
No additional response fields. Server replies with `+OK` on success.
### Error Response
| Code | Description |
| :--- | :---------- |
| `NOT_YOUR_TURN` | The player attempted to fold when not their turn |
| `GAME_NOT_STARTED` | No game is running in the lobby |
| `NOT_IN_LOBBY` | Requesting user is not a member of the lobby |
| `LOBBY_NOT_FOUND` | The specified `GAME_ID` does not exist |
### Example Request
```
FOLD GAME_ID=1
```
### Example Response (success)
```
+OK
END
```
### Example Response (error)
```
-ERR
CODE=NOT_YOUR_TURN
MSG=It is not your turn
END
```
## GET_LOBBY_STATUS command
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.
@@ -779,4 +1168,58 @@ GET_LOBBY_STATUS ID=1
CODE=LOBBY_NOT_FOUND
MESSAGE=Lobby not found
END
```
## CREATE_LOBBY command
The `CREATE_LOBBY` command requests the server to create a new lobby and return its identifier.
### Required pre-execution checks
None.
### Request Parameters
No parameters.
### Implementation notes
- Parser: CreateLobbyParser — constructs a CreateLobbyRequest from the incoming PrimitiveRequest context (no parameters are read).
- Handler: CreateLobbyHandler — calls LobbyManager.createNewLobby(null).
- If the returned LobbyId is null, the handler dispatches an ErrorResponse with code `LOBBIES_FULL` and message "Maximum number of 8 lobbies reached".
- On success, the handler dispatches a CreateLobbyResponse containing the new lobby id.
### Success Response
| Field | Type | Description |
| :-------- | :--- | :---------- |
| `LOBBY_ID`| `int`| Numeric id of the newly created lobby |
### Error Response
| Code | Description |
| :---------- | :--------------------------------------------------------------------- |
| `LOBBIES_FULL` | The server cannot create a new lobby because the maximum number of lobbies has been reached |
### Example Request
```
CREATE_LOBBY
```
### Example Success Response
```
+OK
LOBBY_ID=1
END
```
### Example Error Response
```
-ERR
CODE=LOBBIES_FULL
MESSAGE=Maximum number of 8 lobbies reached
END
```
@@ -7,6 +7,12 @@ import org.apache.logging.log4j.Logger;
/** Main entry point for Casono application. Handles client and server startup. */
public final class Main {
private static final int MIN_ARGS_FOR_USERNAME = 3;
private static final int ARGS_COUNT_SERVER = 2;
private static final int ARGS_COUNT_CLIENT_MIN = 2;
private static final int ARGS_COUNT_CLIENT_WITH_USER = 3;
/**
* Main entry point for Casono.
*
@@ -17,10 +23,13 @@ public final class Main {
printUsage();
System.exit(1);
}
switch (args[0]) {
case "server" -> ServerApp.start(args[1]);
case "client" -> ClientApp.start(args[1]);
case "client" -> {
String address = args[1];
String username = args.length >= MIN_ARGS_FOR_USERNAME ? args[2] : null;
ClientApp.start(address, username);
}
default -> {
printUsage();
System.exit(1);
@@ -29,12 +38,15 @@ public final class Main {
}
private static boolean isValid(String[] args) {
if (args.length != 2) {
if (args.length < ARGS_COUNT_SERVER) {
return false;
}
return switch (args[0]) {
case "server", "client" -> true;
case "server" -> args.length == ARGS_COUNT_SERVER;
case "client" ->
args.length == ARGS_COUNT_CLIENT_MIN
|| args.length == ARGS_COUNT_CLIENT_WITH_USER;
default -> false;
};
}
@@ -45,7 +57,7 @@ public final class Main {
"""
Usage:
java -jar xyz.jar server <listenPort>
java -jar xyz.jar client <serverIp>:<serverPort>
java -jar xyz.jar client <serverIp>:<serverPort> [<username>]
""");
}
}
@@ -1,33 +1,51 @@
package ch.unibas.dmi.dbis.cs108.casono.client;
/**
* Entry point for the Casono client application. Handles client startup and connection parameters.
*/
import ch.unibas.dmi.dbis.cs108.casono.client.network.ClientService;
import ch.unibas.dmi.dbis.cs108.casono.client.network.LobbyClient;
import ch.unibas.dmi.dbis.cs108.casono.client.network.LoginResult;
import ch.unibas.dmi.dbis.cs108.casono.client.ui.Launcher;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/**
* Entry point for the Casono client application. Handles client startup and connection parameters.
* Entry point and bootstrap helper for the Casono client application.
*
* <p>Default constructor for the application.
* <p>This class is responsible for two tasks: - performing an optional startup LOGIN when a
* username is supplied on the command line, and - providing a shared {@link ClientService} instance
* that the UI can reuse so the initial connection remains active.
*/
public class ClientApp {
private static final Logger LOGGER = LogManager.getLogger(ClientApp.class);
/** Shared client connection used when a username is provided at startup. */
private static volatile ClientService sharedClientService;
/** Default constructor. */
public ClientApp() {
// Default constructor
}
/**
* Returns the shared {@link ClientService} instance created during startup, or {@code null} if
* none exists. The UI may call this to reuse the connection that already performed the initial
* LOGIN.
*/
public static ClientService getSharedClientService() {
return sharedClientService;
}
private static void setSharedClientService(ClientService cs) {
sharedClientService = cs;
}
/**
* Starts the client application with the given address.
*
* @param arg Address in the format "ip:port".
* @throws IllegalArgumentException if the address format is invalid.
*/
public static void start(String arg) {
public static void start(String arg, String username) {
String[] parts = arg.split(":", 2);
if (parts.length != 2) {
throw new IllegalArgumentException("Address must be in format <ip>:<port>.");
@@ -36,16 +54,69 @@ public class ClientApp {
int port = Integer.parseInt(parts[1]);
LOGGER.info("You've selected the client. It will connect port {} at host {}", port, host);
// Expose the chosen host/port to the UI via system properties so controllers
// (which read System.getProperty("casono.server.host"/"casono.server.port"))
// can obtain the correct connection information.
System.setProperty("casono.server.host", host);
System.setProperty("casono.server.port", Integer.toString(port));
// Forward the original address argument to the launcher as well.
Launcher.main(new String[] {arg});
System.setProperty("casono.server.port", String.valueOf(port));
// If a username was provided, create a shared connection and perform
// the initial LOGIN asynchronously on that connection. The connection
// is intentionally NOT closed so the UI can reuse it.
if (username != null && !username.isBlank()) {
try {
ClientService clientService = new ClientService(host, port);
setSharedClientService(clientService);
java.util.concurrent.ExecutorService bg =
java.util.concurrent.Executors.newSingleThreadExecutor(
r -> {
Thread t = new Thread(r);
t.setDaemon(true);
t.setName("casono-startup-login");
return t;
});
bg.submit(
() -> {
try {
LobbyClient lobbyClient = new LobbyClient(clientService);
LoginResult res = lobbyClient.login(username);
LOGGER.info(
"Assigned username='{}' id={}",
res.getUsername(),
res.getId());
} catch (RuntimeException e) {
LOGGER.warn("Startup login failed: {}", e.getMessage());
} catch (Exception e) {
LOGGER.warn("Unexpected error during startup login", e);
} finally {
bg.shutdown();
}
});
} catch (RuntimeException e) {
LOGGER.warn(
"Could not establish initial connection for startup login: {}",
e.getMessage());
// UI will create its own connection when it initializes
}
}
if (username != null && !username.isBlank()) {
Launcher.main(new String[] {arg, username});
} else {
Launcher.main(new String[] {arg});
}
}
/**
* Main entry point. {@code <host:port>} (e.g. {@code 127.0.0.1:1234})
*
* <p>If a username is provided the application attempts a startup LOGIN on the shared
* connection; the UI will still start and will reuse the connection when possible. The username
* is not propagated via a system property for UI display.
*/
public static void main(String[] args) {
start(args[0]);
if (args == null || args.length == 0) {
throw new IllegalArgumentException("Address argument required: <host:port>");
}
// Optional username argument
String username = args.length > 1 && args[1] != null && !args[1].isBlank() ? args[1] : null;
start(args[0], username);
}
}
@@ -138,33 +138,64 @@ public class ClientService {
() -> {
try {
writeToTransport(message);
String responseText = null;
String responseText = clienttcptransport.read().payload();
logger.info("Raw message '{}'", responseText);
responseText = clienttcptransport.read().payload();
logger.info("Raw message '" + responseText + "'");
Boolean success = null;
int count = 0;
for (String line : responseText.split("\n")) {
if (success == null) {
boolean hasStatus = false;
boolean success = false;
for (String rawLine : responseText.split("\n")) {
String line = rawLine;
if (!hasStatus) {
if ("+OK".equals(line)) {
success = true;
hasStatus = true;
continue;
} else if (("-ERROR").equals(responseText)) {
success = false;
}
if (line.startsWith("-ERR") || line.startsWith("-ERROR")) {
success = false;
hasStatus = true;
continue;
}
// ignore any lines before the status indicator
continue;
} else if ("END".equals(line)) {
}
if ("END".equals(line)) {
break;
}
line = line.replaceFirst("^\t", "");
// strip a single leading tab if present (protocol formatting)
if (line.startsWith("\t")) {
line = line.substring(1);
}
response.add(line);
}
if (success != null && success) {
return;
} else {
throw new RuntimeException("Error in " + message + ": " + response);
if (!hasStatus) {
// Fallback for servers that do not place the status on a
// dedicated line: try to detect status markers anywhere
// in the payload to remain compatible with older servers.
if (responseText.contains("+OK")) {
success = true;
hasStatus = true;
} else if (responseText.contains("-ERR")
|| responseText.contains("-ERROR")) {
success = false;
hasStatus = true;
} else {
throw new RuntimeException(
"No status line in response for '"
+ message
+ "': "
+ responseText);
}
}
if (success) {
return;
}
throw new RuntimeException("Error in " + message + ": " + response);
} catch (Exception e) {
throw getRuntimeException(e);
}
@@ -200,15 +231,14 @@ public class ClientService {
* @return A RuntimeException representing the cause of the original exception.
*/
private static RuntimeException getRuntimeException(Exception e) {
Throwable reason = e.getCause();
RuntimeException re;
if (reason == null) {
reason = e;
} else if (reason instanceof RuntimeException rte) {
re = rte;
Throwable cause = e.getCause();
if (cause == null) {
return e instanceof RuntimeException ? (RuntimeException) e : new RuntimeException(e);
}
re = new RuntimeException(reason);
return re;
if (cause instanceof RuntimeException) {
return (RuntimeException) cause;
}
return new RuntimeException(cause);
}
/**
@@ -1,5 +1,8 @@
package ch.unibas.dmi.dbis.cs108.casono.client.network;
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.RequestParameter;
import java.util.List;
/**
* The LobbyClient class is responsible for communicating with the server to manage game lobbies. It
* provides methods to create a lobby, join a lobby, and fetch the current status of a lobby by
@@ -38,8 +41,25 @@ public class LobbyClient {
* @return The id of the newly created lobby, as returned by the server.
*/
public int createLobby() {
String response = client.processCommand("CREATE_LOBBY").getFirst();
return Integer.parseInt(response);
List<String> lines = client.processCommand("CREATE_LOBBY");
List<RequestParameter> params = ClientService.convertToRequestParameters(lines);
for (RequestParameter p : params) {
if ("LOBBY_ID".equalsIgnoreCase(p.key())) {
return Integer.parseInt(p.value());
}
}
// Fallback for simple legacy/test servers that return the id as a single plain
// line
if (!lines.isEmpty()) {
try {
return Integer.parseInt(lines.get(0));
} catch (NumberFormatException ignored) {
}
}
throw new RuntimeException("No LOBBY_ID in response: " + lines);
}
/**
@@ -65,8 +85,23 @@ public class LobbyClient {
* Logs in to the server with the given username by sending a "LOGIN" command.
*
* @param user The username to log in with.
* @return a {@link LoginResult} containing the assigned username and id as returned by the
* server
*/
public void login(String user) {
client.processCommand("LOGIN USERNAME=" + user);
public LoginResult login(String user) {
List<String> lines = client.processCommand("LOGIN USERNAME=" + user);
List<RequestParameter> params = ClientService.convertToRequestParameters(lines);
String assigned = user;
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);
}
}
@@ -0,0 +1,38 @@
package ch.unibas.dmi.dbis.cs108.casono.client.network;
/**
* Result of a successful login request.
*
* <p>Holds the username assigned by the server and the server-side id as a string (UUID). The
* client previously attempted to parse the id as an integer which failed when the server returned a
* UUID; this class therefore stores the id as a {@link String}.
*/
public class LoginResult {
private final String username;
private final String id;
/**
* Create a login result.
*
* @param username the assigned username
* @param id the assigned id (UUID string) returned by the server
*/
public LoginResult(String username, String id) {
this.username = username;
this.id = id;
}
/**
* @return the assigned username
*/
public String getUsername() {
return username;
}
/**
* @return the assigned id as returned by the server (UUID string)
*/
public String getId() {
return id;
}
}
@@ -1,5 +1,6 @@
package ch.unibas.dmi.dbis.cs108.casono.client.ui.lobbyui;
import ch.unibas.dmi.dbis.cs108.casono.client.ClientApp;
import ch.unibas.dmi.dbis.cs108.casono.client.network.ClientService;
import ch.unibas.dmi.dbis.cs108.casono.client.network.LobbyClient;
import javafx.application.Platform;
@@ -36,12 +37,16 @@ public class CasinomainuiController {
private int nextButtonId = 1;
private LobbyClient lobbyClient;
/** Default constructor for dependency injection by FXMLLoader. */
/** Default constructor used by FXMLLoader. */
public CasinomainuiController() {
// Default constructor
}
/** Initializes the UI components and sets default values. */
/**
* Initializes the UI components and sets default values. If a shared {@link
* ch.unibas.dmi.dbis.cs108.casono.client.network.ClientService} exists (created at application
* start), the controller reuses it so the connection remains open and already-logged-in.
*/
@FXML
public void initialize() {
titleLabel.setText("Casono");
@@ -51,16 +56,18 @@ public class CasinomainuiController {
translationManager = LobbyButtonTranslationManager.getInstance();
String host = System.getProperty("casono.server.host");
int port = Integer.parseInt(System.getProperty("casono.server.port"));
ClientService clientService;
try {
clientService = new ClientService(host, port);
} catch (RuntimeException e) {
LOGGER.warn(
"Could not connect to server {}:{} — starting in offline mode: {}",
host,
port,
e.getMessage());
clientService = new ClientService(true); // offline mode
ClientService clientService = ClientApp.getSharedClientService();
if (clientService == null) {
try {
clientService = new ClientService(host, port);
} catch (RuntimeException e) {
LOGGER.warn(
"Could not connect to server {}:{} — starting in offline mode: {}",
host,
port,
e.getMessage());
clientService = new ClientService(true); // offline mode
}
}
gridManager =
new LobbyButtonGridManager(
@@ -117,7 +124,11 @@ public class CasinomainuiController {
Platform.exit();
}
/** Handles creation of a new lobby button. */
/**
* Handles creation of a new lobby button. Attempts to create a lobby on the server via the
* {@link LobbyButtonGridManager} and registers the new button in the local translation manager.
* Errors are logged and displayed as an informational alert.
*/
@FXML
public void handleCreateLobbyButton() {
if (translationManager.isFull()) {
@@ -169,6 +169,61 @@ public class ServerApp {
.GetGameStateHandler(
responseDispatcher, lobbyManager, userRegistry));
// BET registration
parserDispatcher.register(
"BET",
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.bet.PlayerBetParser());
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(responseDispatcher, userRegistry, lobbyManager));
// RAISE registration
parserDispatcher.register(
"RAISE",
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.raise
.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(
responseDispatcher, userRegistry, lobbyManager));
// CALL registration
parserDispatcher.register(
"CALL",
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.call
.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(responseDispatcher, userRegistry, lobbyManager));
// FOLD registration
parserDispatcher.register(
"FOLD",
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.fold
.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(responseDispatcher, userRegistry, lobbyManager));
// GET_LOBBY_STATUS registration
parserDispatcher.register(
"GET_LOBBY_STATUS",
@@ -184,6 +239,20 @@ public class ServerApp {
.get_lobby_status.GetLobbyStatusHandler(
responseDispatcher, lobbyManager, userRegistry));
// CREATE_LOBBY registration
parserDispatcher.register(
"CREATE_LOBBY",
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.create_lobby
.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(responseDispatcher, lobbyManager));
// JOIN_LOBBY registration
parserDispatcher.register(
"JOIN_LOBBY",
@@ -0,0 +1,108 @@
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.bet;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.GameController;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.player.PlayerId;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.LobbyId;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.LobbyManager;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.UserRegistry;
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.ErrorResponse;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.OkResponse;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.dispatcher.ResponseDispatcher;
import java.util.Optional;
/**
* Handler for the `BET` command.
*
* <p>This handler validates the request (amount >= 0), resolves the user and target lobby (by
* `GAME_ID` when provided or by session username otherwise), checks that a game is running and then
* forwards the action to the {@code GameController}.
*
* <p>On failure the handler dispatches an {@link ErrorResponse} with an appropriate error code
* (e.g. {@code INVALID_AMOUNT}, {@code NOT_IN_LOBBY}, {@code LOBBY_NOT_FOUND}, {@code
* GAME_NOT_STARTED}, {@code GAME_ACTION_FAILED}). On success it dispatches an {@link OkResponse}.
*/
public class PlayerBetHandler extends CommandHandler<PlayerBetRequest> {
private final UserRegistry userRegistry;
private final LobbyManager lobbyManager;
/**
* Creates a new {@code PlayerBetHandler}.
*
* @param responseDispatcher dispatcher used to send responses back to the client
* @param userRegistry registry to resolve users from session ids
* @param lobbyManager manager used to lookup lobbies and their game controllers
*/
public PlayerBetHandler(
ResponseDispatcher responseDispatcher,
UserRegistry userRegistry,
LobbyManager lobbyManager) {
super(responseDispatcher);
this.userRegistry = userRegistry;
this.lobbyManager = lobbyManager;
}
/**
* Execute the bet request: validate parameters, resolve lobby and game, and forward the bet
* action to the game controller. Sends an {@link ErrorResponse} on failure or an {@link
* OkResponse} on success.
*
* @param request the parsed {@link PlayerBetRequest}
*/
@Override
public void execute(PlayerBetRequest request) {
if (request.getAmount() < 0) {
responseDispatcher.dispatch(
new ErrorResponse(
request.getContext(), "INVALID_AMOUNT", "Amount must be non-negative"));
return;
}
Optional<ch.unibas.dmi.dbis.cs108.casono.server.domain.user.User> opt =
userRegistry.getBySessionId(request.getSessionId());
if (opt.isEmpty()) {
responseDispatcher.dispatch(
new ErrorResponse(request.getContext(), "NOT_LOGGED_IN", "User not logged in"));
return;
}
String username = opt.get().getName();
Integer gameId = request.getGameId();
var lobby =
(gameId != null)
? lobbyManager.getLobby(LobbyId.of(gameId))
: lobbyManager.getLobbyByUsername(username);
if (lobby == null) {
if (gameId != null) {
responseDispatcher.dispatch(
new ErrorResponse(
request.getContext(), "LOBBY_NOT_FOUND", "Lobby not found"));
} else {
responseDispatcher.dispatch(
new ErrorResponse(
request.getContext(), "NOT_IN_LOBBY", "User not in a lobby"));
}
return;
}
GameController game = lobby.getGameController();
if (game == null) {
responseDispatcher.dispatch(
new ErrorResponse(
request.getContext(), "GAME_NOT_STARTED", "Game not started"));
return;
}
try {
game.playerBet(PlayerId.of(username), request.getAmount());
} catch (Exception e) {
responseDispatcher.dispatch(
new ErrorResponse(request.getContext(), "GAME_ACTION_FAILED", e.getMessage()));
return;
}
responseDispatcher.dispatch(new OkResponse(request.getContext()));
}
}
@@ -0,0 +1,40 @@
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.bet;
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.CommandParser;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.PrimitiveRequest;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.accessor.RequestParameterAccessor;
/**
* Parser for the `BET` command.
*
* <p>Parses the request parameters and builds a {@link PlayerBetRequest}. Expected parameters:
*
* <ul>
* <li>`GAME_ID` (optional) - numeric id of the lobby/game to target
* <li>`AMOUNT` (required) - amount to bet
* </ul>
*/
public class PlayerBetParser implements CommandParser<PlayerBetRequest> {
/**
* Parse a primitive request into a {@link PlayerBetRequest}.
*
* @param primitiveRequest the raw parsed request containing parameters and context
* @return a {@link PlayerBetRequest} with the parsed values (gameId may be null)
*/
@Override
public PlayerBetRequest parse(PrimitiveRequest primitiveRequest) {
RequestParameterAccessor accessor =
new RequestParameterAccessor(primitiveRequest.parameters());
Integer gameId = null;
try {
gameId = accessor.optional("GAME_ID", null, Integer::parseInt);
} catch (Exception e) {
// parse handled by framework
}
int amount = accessor.require("AMOUNT", Integer::parseInt);
return new PlayerBetRequest(primitiveRequest.context(), gameId, amount);
}
}
@@ -0,0 +1,45 @@
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.bet;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.Request;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext;
/**
* Request for the `BET` command.
*
* <p>Contains the optional target `gameId` and the `amount` the player wants to bet.
*/
public class PlayerBetRequest extends Request {
private final Integer gameId;
private final int amount;
/**
* Creates a new {@code PlayerBetRequest}.
*
* @param context the request context
* @param gameId optional game id of the targeted lobby (may be {@code null})
* @param amount the bet amount (non-negative)
*/
public PlayerBetRequest(RequestContext context, Integer gameId, int amount) {
super(context);
this.gameId = gameId;
this.amount = amount;
}
/**
* Returns the optional target game id.
*
* @return the game id or {@code null} if not provided
*/
public Integer getGameId() {
return gameId;
}
/**
* Returns the requested bet amount.
*
* @return the bet amount
*/
public int getAmount() {
return amount;
}
}
@@ -0,0 +1,96 @@
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.call;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.GameController;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.player.PlayerId;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.LobbyId;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.LobbyManager;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.UserRegistry;
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.ErrorResponse;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.OkResponse;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.dispatcher.ResponseDispatcher;
import java.util.Optional;
/**
* Handler for the `CALL` command.
*
* <p>This handler validates the session, resolves the lobby (by `GAME_ID` when provided or by
* session username otherwise), checks for a running game and forwards the call action to the {@code
* GameController}.
*/
public class PlayerCallHandler extends CommandHandler<PlayerCallRequest> {
private final UserRegistry userRegistry;
private final LobbyManager lobbyManager;
/**
* Creates a new {@code PlayerCallHandler}.
*
* @param responseDispatcher dispatcher used to send responses back to the client
* @param userRegistry registry to resolve users from session ids
* @param lobbyManager manager used to lookup lobbies and their game controllers
*/
public PlayerCallHandler(
ResponseDispatcher responseDispatcher,
UserRegistry userRegistry,
LobbyManager lobbyManager) {
super(responseDispatcher);
this.userRegistry = userRegistry;
this.lobbyManager = lobbyManager;
}
/**
* Execute the call request: resolve lobby by `GAME_ID` if present, otherwise by session user,
* verify game is running, and forward the call to the game controller.
*
* @param request the parsed {@link PlayerCallRequest}
*/
@Override
public void execute(PlayerCallRequest request) {
Optional<ch.unibas.dmi.dbis.cs108.casono.server.domain.user.User> opt =
userRegistry.getBySessionId(request.getSessionId());
if (opt.isEmpty()) {
responseDispatcher.dispatch(
new ErrorResponse(request.getContext(), "NOT_LOGGED_IN", "User not logged in"));
return;
}
String username = opt.get().getName();
Integer gameId = request.getGameId();
var lobby =
(gameId != null)
? lobbyManager.getLobby(LobbyId.of(gameId))
: lobbyManager.getLobbyByUsername(username);
if (lobby == null) {
if (gameId != null) {
responseDispatcher.dispatch(
new ErrorResponse(
request.getContext(), "LOBBY_NOT_FOUND", "Lobby not found"));
} else {
responseDispatcher.dispatch(
new ErrorResponse(
request.getContext(), "NOT_IN_LOBBY", "User not in a lobby"));
}
return;
}
GameController game = lobby.getGameController();
if (game == null) {
responseDispatcher.dispatch(
new ErrorResponse(
request.getContext(), "GAME_NOT_STARTED", "Game not started"));
return;
}
try {
game.playerCall(PlayerId.of(username));
} catch (RuntimeException e) {
responseDispatcher.dispatch(
new ErrorResponse(request.getContext(), "GAME_ACTION_FAILED", e.getMessage()));
return;
}
responseDispatcher.dispatch(new OkResponse(request.getContext()));
}
}
@@ -0,0 +1,28 @@
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.call;
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.CommandParser;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.PrimitiveRequest;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.accessor.RequestParameterAccessor;
/**
* Parser for the `CALL` command.
*
* <p>Accepts an optional `GAME_ID` parameter. If provided, the handler will target the specified
* lobby; otherwise the server resolves the lobby by the requesting session's user.
*/
public class PlayerCallParser implements CommandParser<PlayerCallRequest> {
@Override
public PlayerCallRequest parse(PrimitiveRequest primitiveRequest) {
RequestParameterAccessor accessor =
new RequestParameterAccessor(primitiveRequest.parameters());
Integer gameId = null;
try {
gameId = accessor.optional("GAME_ID", null, Integer::parseInt);
} catch (Exception e) {
// parse errors handled by framework
}
return new PlayerCallRequest(primitiveRequest.context(), gameId);
}
}
@@ -0,0 +1,34 @@
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.call;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.Request;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext;
/**
* Request for the `CALL` command.
*
* <p>Contains an optional `gameId` when the client targets a specific lobby, otherwise the server
* will resolve the lobby from the requesting session's user.
*/
public class PlayerCallRequest extends Request {
private final Integer gameId;
/**
* Creates a new {@code PlayerCallRequest}.
*
* @param context the request context
* @param gameId optional game id of the targeted lobby (may be {@code null})
*/
public PlayerCallRequest(RequestContext context, Integer gameId) {
super(context);
this.gameId = gameId;
}
/**
* Returns the optional target game id.
*
* @return the game id or {@code null} if not provided
*/
public Integer getGameId() {
return gameId;
}
}
@@ -0,0 +1,96 @@
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.fold;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.GameController;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.player.PlayerId;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.LobbyId;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.LobbyManager;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.UserRegistry;
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.ErrorResponse;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.OkResponse;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.dispatcher.ResponseDispatcher;
import java.util.Optional;
/**
* Handler for the `FOLD` command.
*
* <p>This handler validates the session, resolves the lobby (by `GAME_ID` when provided or by
* session username otherwise), checks for a running game and forwards the fold action to the {@code
* GameController}.
*/
public class PlayerFoldHandler extends CommandHandler<PlayerFoldRequest> {
private final UserRegistry userRegistry;
private final LobbyManager lobbyManager;
/**
* Creates a new {@code PlayerFoldHandler}.
*
* @param responseDispatcher dispatcher used to send responses back to the client
* @param userRegistry registry to resolve users from session ids
* @param lobbyManager manager used to lookup lobbies and their game controllers
*/
public PlayerFoldHandler(
ResponseDispatcher responseDispatcher,
UserRegistry userRegistry,
LobbyManager lobbyManager) {
super(responseDispatcher);
this.userRegistry = userRegistry;
this.lobbyManager = lobbyManager;
}
/**
* Execute the fold request: resolve lobby by `GAME_ID` if present, otherwise by session user,
* verify game is running, and forward the fold to the game controller.
*
* @param request the parsed {@link PlayerFoldRequest}
*/
@Override
public void execute(PlayerFoldRequest request) {
Optional<ch.unibas.dmi.dbis.cs108.casono.server.domain.user.User> opt =
userRegistry.getBySessionId(request.getSessionId());
if (opt.isEmpty()) {
responseDispatcher.dispatch(
new ErrorResponse(request.getContext(), "NOT_LOGGED_IN", "User not logged in"));
return;
}
String username = opt.get().getName();
Integer gameId = request.getGameId();
var lobby =
(gameId != null)
? lobbyManager.getLobby(LobbyId.of(gameId))
: lobbyManager.getLobbyByUsername(username);
if (lobby == null) {
if (gameId != null) {
responseDispatcher.dispatch(
new ErrorResponse(
request.getContext(), "LOBBY_NOT_FOUND", "Lobby not found"));
} else {
responseDispatcher.dispatch(
new ErrorResponse(
request.getContext(), "NOT_IN_LOBBY", "User not in a lobby"));
}
return;
}
GameController game = lobby.getGameController();
if (game == null) {
responseDispatcher.dispatch(
new ErrorResponse(
request.getContext(), "GAME_NOT_STARTED", "Game not started"));
return;
}
try {
game.playerFold(PlayerId.of(username));
} catch (RuntimeException e) {
responseDispatcher.dispatch(
new ErrorResponse(request.getContext(), "GAME_ACTION_FAILED", e.getMessage()));
return;
}
responseDispatcher.dispatch(new OkResponse(request.getContext()));
}
}
@@ -0,0 +1,28 @@
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.fold;
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.CommandParser;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.PrimitiveRequest;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.accessor.RequestParameterAccessor;
/**
* Parser for the `FOLD` command.
*
* <p>Accepts an optional `GAME_ID` parameter. If provided, the handler will target the specified
* lobby; otherwise the server resolves the lobby by the requesting session's user.
*/
public class PlayerFoldParser implements CommandParser<PlayerFoldRequest> {
@Override
public PlayerFoldRequest parse(PrimitiveRequest primitiveRequest) {
RequestParameterAccessor accessor =
new RequestParameterAccessor(primitiveRequest.parameters());
Integer gameId = null;
try {
gameId = accessor.optional("GAME_ID", null, Integer::parseInt);
} catch (Exception e) {
// parse handled by framework
}
return new PlayerFoldRequest(primitiveRequest.context(), gameId);
}
}
@@ -0,0 +1,34 @@
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.fold;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.Request;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext;
/**
* Request for the `FOLD` command.
*
* <p>Contains an optional `gameId` when the client targets a specific lobby, otherwise the server
* will resolve the lobby from the requesting session's user.
*/
public class PlayerFoldRequest extends Request {
private final Integer gameId;
/**
* Creates a new {@code PlayerFoldRequest}.
*
* @param context the request context
* @param gameId optional game id of the targeted lobby (may be {@code null})
*/
public PlayerFoldRequest(RequestContext context, Integer gameId) {
super(context);
this.gameId = gameId;
}
/**
* Returns the optional target game id.
*
* @return the game id or {@code null} if not provided
*/
public Integer getGameId() {
return gameId;
}
}
@@ -0,0 +1,110 @@
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.raise;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.GameController;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.player.PlayerId;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.LobbyId;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.LobbyManager;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.UserRegistry;
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.ErrorResponse;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.OkResponse;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.dispatcher.ResponseDispatcher;
import java.util.Optional;
/**
* Handler for the `RAISE` command.
*
* <p>This handler validates the request (amount >= 0), resolves the user and target lobby (by
* `GAME_ID` when provided or by session username otherwise), checks that a game is running and then
* forwards the action to the {@code GameController}.
*
* <p>On failure the handler dispatches an {@link ErrorResponse} with an appropriate error code
* (e.g. {@code INVALID_AMOUNT}, {@code NOT_IN_LOBBY}, {@code LOBBY_NOT_FOUND}, {@code
* GAME_NOT_STARTED}, {@code GAME_ACTION_FAILED}). On success it dispatches an {@link OkResponse}.
*/
public class PlayerRaiseHandler extends CommandHandler<PlayerRaiseRequest> {
private final UserRegistry userRegistry;
private final LobbyManager lobbyManager;
/**
* Creates a new {@code PlayerRaiseHandler}.
*
* @param responseDispatcher dispatcher used to send responses back to the client
* @param userRegistry registry to resolve users from session ids
* @param lobbyManager manager used to lookup lobbies and their game controllers
*/
public PlayerRaiseHandler(
ResponseDispatcher responseDispatcher,
UserRegistry userRegistry,
LobbyManager lobbyManager) {
super(responseDispatcher);
this.userRegistry = userRegistry;
this.lobbyManager = lobbyManager;
}
/**
* Execute the raise request: validate parameters, resolve lobby and game, and forward the raise
* action to the game controller. Sends an {@link ErrorResponse} on failure or an {@link
* OkResponse} on success.
*
* @param request the parsed {@link PlayerRaiseRequest}
*/
@Override
public void execute(PlayerRaiseRequest request) {
int amount = request.getAmount();
if (amount < 0) {
responseDispatcher.dispatch(
new ErrorResponse(
request.getContext(), "INVALID_AMOUNT", "Amount must be non-negative"));
return;
}
Optional<ch.unibas.dmi.dbis.cs108.casono.server.domain.user.User> opt =
userRegistry.getBySessionId(request.getSessionId());
if (opt.isEmpty()) {
responseDispatcher.dispatch(
new ErrorResponse(request.getContext(), "NOT_LOGGED_IN", "User not logged in"));
return;
}
String username = opt.get().getName();
Integer gameId = request.getGameId();
var lobby =
(gameId != null)
? lobbyManager.getLobby(LobbyId.of(gameId))
: lobbyManager.getLobbyByUsername(username);
if (lobby == null) {
if (gameId != null) {
responseDispatcher.dispatch(
new ErrorResponse(
request.getContext(), "LOBBY_NOT_FOUND", "Lobby not found"));
} else {
responseDispatcher.dispatch(
new ErrorResponse(
request.getContext(), "NOT_IN_LOBBY", "User not in a lobby"));
}
return;
}
GameController game = lobby.getGameController();
if (game == null) {
responseDispatcher.dispatch(
new ErrorResponse(
request.getContext(), "GAME_NOT_STARTED", "Game not started"));
return;
}
try {
game.playerRaise(PlayerId.of(username), amount);
} catch (RuntimeException e) {
responseDispatcher.dispatch(
new ErrorResponse(request.getContext(), "GAME_ACTION_FAILED", e.getMessage()));
return;
}
responseDispatcher.dispatch(new OkResponse(request.getContext()));
}
}
@@ -0,0 +1,34 @@
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.raise;
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.CommandParser;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.PrimitiveRequest;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.accessor.RequestParameterAccessor;
/**
* Parser for the `RAISE` command.
*
* <p>Parses the request parameters and builds a {@link PlayerRaiseRequest}. Expected parameters:
*
* <ul>
* <li>`GAME_ID` (optional) - numeric id of the lobby/game to target
* <li>`AMOUNT` (required) - amount to raise
* </ul>
*/
public class PlayerRaiseParser implements CommandParser<PlayerRaiseRequest> {
@Override
public PlayerRaiseRequest parse(PrimitiveRequest primitiveRequest) {
RequestParameterAccessor accessor =
new RequestParameterAccessor(primitiveRequest.parameters());
Integer gameId = null;
try {
gameId = accessor.optional("GAME_ID", null, Integer::parseInt);
} catch (Exception e) {
// parse handled by framework
}
int amount = accessor.require("AMOUNT", Integer::parseInt);
return new PlayerRaiseRequest(primitiveRequest.context(), gameId, amount);
}
}
@@ -0,0 +1,45 @@
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.raise;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.Request;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext;
/**
* Request for the `RAISE` command.
*
* <p>Contains the optional target `gameId` and the `amount` the player wants to raise.
*/
public class PlayerRaiseRequest extends Request {
private final Integer gameId;
private final int amount;
/**
* Creates a new {@code PlayerRaiseRequest}.
*
* @param context the request context
* @param gameId optional game id of the targeted lobby (may be {@code null})
* @param amount the raise amount (non-negative)
*/
public PlayerRaiseRequest(RequestContext context, Integer gameId, int amount) {
super(context);
this.gameId = gameId;
this.amount = amount;
}
/**
* Returns the optional target game id.
*
* @return the game id or {@code null} if not provided
*/
public Integer getGameId() {
return gameId;
}
/**
* Returns the requested raise amount.
*
* @return the raise amount
*/
public int getAmount() {
return amount;
}
}
@@ -0,0 +1,32 @@
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.create_lobby;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.LobbyId;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.LobbyManager;
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.ErrorResponse;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.dispatcher.ResponseDispatcher;
public class CreateLobbyHandler extends CommandHandler<CreateLobbyRequest> {
private final LobbyManager lobbyManager;
public CreateLobbyHandler(ResponseDispatcher responseDispatcher, LobbyManager lobbyManager) {
super(responseDispatcher);
this.lobbyManager = lobbyManager;
}
@Override
public void execute(CreateLobbyRequest request) {
LobbyId id = lobbyManager.createNewLobby(null);
if (id == null) {
responseDispatcher.dispatch(
new ErrorResponse(
request.getContext(),
"LOBBIES_FULL",
"Maximum number of 8 lobbies reached"));
return;
}
responseDispatcher.dispatch(new CreateLobbyResponse(request.getContext(), id.value()));
}
}
@@ -0,0 +1,11 @@
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.create_lobby;
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.CommandParser;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.PrimitiveRequest;
public class CreateLobbyParser implements CommandParser<CreateLobbyRequest> {
@Override
public CreateLobbyRequest parse(PrimitiveRequest primitiveRequest) {
return new CreateLobbyRequest(primitiveRequest.context());
}
}
@@ -0,0 +1,10 @@
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.create_lobby;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.Request;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext;
public class CreateLobbyRequest extends Request {
public CreateLobbyRequest(RequestContext context) {
super(context);
}
}
@@ -0,0 +1,14 @@
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.create_lobby;
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;
/** Sends ID of newly created lobby back to client. */
public class CreateLobbyResponse extends SuccessResponse {
public CreateLobbyResponse(RequestContext context, int lobbyId) {
super(
context,
new ResponseBodyBuilder().param("LOBBY_ID", String.valueOf(lobbyId)).build());
}
}
@@ -1,5 +1,6 @@
package ch.unibas.dmi.dbis.cs108.casono.server.domain.game;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.action.BetAction;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.action.BlindAction;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.action.CallAction;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.action.FoldAction;
@@ -180,6 +181,16 @@ public class GameController {
engine.processAction(new RaiseAction(playerId, amount));
}
/**
* Processes a player's bet action by sending a BetAction to the GameEngine.
*
* @param playerId The ID of the player who is betting.
* @param amount The amount the player is betting.
*/
public void playerBet(PlayerId playerId, int amount) {
engine.processAction(new BetAction(playerId, amount));
}
/**
* Retrieves the current game state from the GameEngine.
*
@@ -13,7 +13,7 @@ import java.util.concurrent.ConcurrentLinkedQueue;
/** Represents an authenticated user on the server. */
public class User {
private final UserId id;
private final String name;
private String name;
private SessionId sessionId;
private Instant disconnectedAt;
private final Queue<Message> messages;
@@ -47,10 +47,20 @@ public class User {
*
* @return the user name
*/
public String getName() {
public synchronized String getName() {
return name;
}
/**
* Sets a new display name for this user. Thread-safe; callers must ensure the registry is
* updated to maintain uniqueness when needed.
*
* @param newName the new display name
*/
public synchronized void setName(String newName) {
this.name = newName;
}
/**
* Returns the session currently associated with this user, if any.
*
@@ -1,10 +1,15 @@
package ch.unibas.dmi.dbis.cs108.casono.server.domain.user;
import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionId;
import java.util.concurrent.atomic.AtomicInteger;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/** Creates new users, resolving name conflicts automatically. */
public class UserFactory {
private final UserRegistry registry;
private final AtomicInteger anonymousCounter = new AtomicInteger(1);
private static final Logger LOGGER = LogManager.getLogger(UserFactory.class);
/**
* Creates a new UserFactory backed by the given registry.
@@ -18,16 +23,40 @@ public class UserFactory {
/**
* Creates and registers a new user with the given name and session. If the name is already
* taken, a numeric suffix is appended and incremented until a free name is found (e.g.
* Lars_001, Lars_002, ...).
* Lars_001, Lars_002, ...). If the desiredName is null or empty, an automatic name of the form
* "playerN" is assigned, incrementing N until a free name is found.
*
* @param desiredName the preferred display name
* @param sessionId the session to associate with the new user
* @param sessionId the session to associate with the new userwas
* @return the newly created and registered user
*/
public User create(String desiredName, SessionId sessionId) {
if (desiredName == null || desiredName.isBlank()) {
// assign playerN names for anonymous logins
while (true) {
String candidate = "player" + anonymousCounter.getAndIncrement();
var result = registry.registerIfAvailable(candidate, sessionId);
if (result.isPresent()) {
var user = result.get();
LOGGER.info(
"Registered user '{}' with id {} for session {}",
user.getName(),
user.getId().value(),
sessionId == null ? "null" : sessionId.value());
return user;
}
}
}
var result = registry.registerIfAvailable(desiredName, sessionId);
if (result.isPresent()) {
return result.get();
var user = result.get();
LOGGER.info(
"Registered user '{}' with id {} for session {}",
user.getName(),
user.getId().value(),
sessionId == null ? "null" : sessionId.value());
return user;
}
int suffix = 1;
@@ -35,7 +64,13 @@ public class UserFactory {
String candidate = desiredName + "_" + String.format("%03d", suffix);
result = registry.registerIfAvailable(candidate, sessionId);
if (result.isPresent()) {
return result.get();
var user = result.get();
LOGGER.info(
"Registered user '{}' with id {} for session {}",
user.getName(),
user.getId().value(),
sessionId == null ? "null" : sessionId.value());
return user;
}
suffix++;
}
@@ -185,4 +185,29 @@ public class UserRegistry {
public Collection<User> getAllUsers() {
return byId.values();
}
/**
* Attempts to change the username of an existing user. Ensures the new name is not already
* taken and updates internal indices atomically.
*
* @param userId the id of the user to rename
* @param newName the desired new name
* @return true if the rename succeeded, false if the name was already taken or user not found
*/
public synchronized boolean changeUsername(UserId userId, String newName) {
User user = byId.get(userId);
if (user == null) {
return false;
}
if (byName.containsKey(newName)) {
return false;
}
// remove old mapping and put new mapping
byName.remove(user.getName());
user.setName(newName);
byName.put(newName, user);
return true;
}
}
@@ -0,0 +1,47 @@
package ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.dispatcher;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.Lobby;
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.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 ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionId;
import java.util.Optional;
/** Helper to send responses to sessions identified by usernames. */
public class SessionDispatcher {
private final ResponseDispatcher responseDispatcher;
private final UserRegistry userRegistry;
public SessionDispatcher(ResponseDispatcher responseDispatcher, UserRegistry userRegistry) {
this.responseDispatcher = responseDispatcher;
this.userRegistry = userRegistry;
}
/**
* Dispatch a success response built from {@code body} to the user with {@code username}. If the
* user is disconnected or unknown, the call is a no-op.
*/
public void dispatchToUser(String username, ResponseBody body) {
Optional<User> opt = userRegistry.getByUsername(username);
if (opt.isEmpty()) {
return;
}
User user = opt.get();
Optional<SessionId> sid = user.getSessionId();
if (sid.isEmpty()) {
return;
}
RequestContext ctx = new RequestContext(sid.get(), 0);
SuccessResponse resp = new SuccessResponse(ctx, body) {};
responseDispatcher.dispatch(resp);
}
/** Broadcast the provided {@code body} to every player in the given lobby. */
public void broadcastToLobby(Lobby lobby, ResponseBody body) {
for (String username : lobby.getPlayerNames()) {
dispatchToUser(username, body);
}
}
}
+104 -18
View File
@@ -1,4 +1,3 @@
/* Pixel-Art Font (WICHTIG: Nutze eine Monospace-Schrift für Pixel-Look) */
.root {
-fx-background-color: #000000;
-fx-font-family: "Monospaced", "Courier New";
@@ -6,23 +5,23 @@
.anchor-pane-bg {
-fx-background-color: #ffffff;
/* Optional: Bild entfernen oder als Overlay nutzen */
/* Optional: Remove the image or use it as an overlay */
-fx-background-image: url("/images/background.png");
-fx-background-size: cover;
-fx-background-position: center center;
-fx-background-repeat: no-repeat;
}
/* DIE SCHWEBENDE INFO-BOX (PIXEL-STYLE) */
/* THE FLOATING INFO BOX */
.floating-chat-box {
-fx-background-color: #0d9e3b;
/* Pixel-Ränder: Keine Rundung (0px) oder nur sehr kleine Stufen */
/* Pixel edges: No rounding (0px) or only very slight steps */
-fx-background-radius: 58;
-fx-border-radius: 50;
-fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10;
-fx-border-width: 12;
-fx-padding: 20;
/* Harter Schatten statt weicher Glow */
/* Hard shadows instead of a soft glow */
-fx-effect: dropshadow(one-pass-box, rgba(0,0,0,1), 0, 0, 15, 15);
}
@@ -38,20 +37,19 @@
-fx-font-size: 16px;
}
/* DER OVALE PIXEL-TISCH */
/* THE OVAL PIXEL TABLE */
.casino-table {
-fx-background-color: #0d9e3b; /* Feste Farbe statt Gradient für Pixel-Look */
/* Oval durch festen Radius - Pixel-Art-Ovale sind stufig */
-fx-background-color: #0d9e3b; /* Solid color instead of a gradient for a pixelated look */
/* Oval with a fixed radius - Pixel art ovals are jagged */
-fx-background-radius: 2000;
-fx-border-radius: 2000;
/* Dicker, eckiger Holzrand */
/* Thick, square wooden edge */
-fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10;
-fx-border-width: 12;
}
.table-title {
-fx-text-fill: #ffffff;
-fx-font-size: 28px;
@@ -59,10 +57,98 @@
-fx-effect: dropshadow(one-pass-box, #000, 0, 0, 3, 3);
}
.black-input-field {
-fx-background-color: #1a1a1a;
-fx-text-fill: #ffffff;
-fx-font-family: "Courier New";
-fx-font-weight: bold;
-fx-background-radius: 8;
-fx-border-radius: 8;
-fx-border-color: #444444;
-fx-border-width: 2;
-fx-padding: 5 12;
}
.black-input-field:hover {
-fx-translate-y: -3;
-fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0, 0.8), 0, 0, 3, 3);
}
.black-input-field:focused {
-fx-border-color: #ffffff;
-fx-background-color: #000000;
}
/* Hauptcontainer der Statusbox */
.gray-input-field {
-fx-background-color: #333333;
-fx-text-fill: #ffffff;
-fx-font-family: "Courier New";
-fx-font-weight: bold;
-fx-background-radius: 8;
-fx-border-radius: 8;
-fx-border-color: #666666;
-fx-color-color: #cccccc;
-fx-border-width: 2;
-fx-padding: 5 12;
}
.gray-input-field:hover {
-fx-translate-y: -3;
-fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0, 0.8), 0, 0, 3, 3);
}
.gray-input-field:focused {
-fx-border-color: #ffffff;
-fx-background-color: #333333;
}
.yellow-input-field {
-fx-background-color: #b8860b;
-fx-text-fill: #ffffff;
-fx-prompt-text-fill: #ffffff;
-fx-font-family: "Courier New";
-fx-font-weight: bold;
-fx-background-radius: 8;
-fx-border-radius: 8;
-fx-border-color: #ffd700;
-fx-border-width: 2;
-fx-padding: 5 12;
}
.yellow-input-field:hover {
-fx-translate-y: -3;
-fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0, 0.8), 0, 0, 3, 3);
}
.yellow-input-field:focused {
-fx-border-color: #ffffff;
-fx-background-color: #b8860b;
}
.red-input-field {
-fx-background-color: #8b0000;
-fx-text-fill: #ffffff;
-fx-prompt-text-fill: #ffffff;
-fx-font-family: "Courier New";
-fx-font-weight: bold;
-fx-background-radius: 8;
-fx-border-radius: 8;
-fx-border-color: #ff4444;
-fx-border-width: 2;
-fx-padding: 5 12;
}
.red-input-field:hover {
-fx-translate-y: -3;
-fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0, 0.8), 0, 0, 3, 3);
}
.red-input-field:focused {
-fx-border-color: #ffffff;
-fx-background-color: #8b0000;
}
/* MAIN CONTAINER OF THE STATUS BOX */
.player-status-pane {
-fx-background-color: rgb(129, 13, 158);
-fx-background-radius: 15;
@@ -77,7 +163,7 @@
-fx-translate-y: -3;
}
/* Die zwei inneren Boxen */
/* THE TWO INNER BOXES */
.status-inner-box-top {
-fx-background-color: rgb(13, 93, 158);
-fx-background-radius: 19;
@@ -103,16 +189,16 @@
}
.table-title {
/* Titel-Text anpassen */
/* Customize the title text */
-fx-text-fill: #ffffff;
-fx-font-size: 32px;
-fx-font-weight: bold;
-fx-effect: dropshadow(one-pass-box, #000, 0, 0, 3, 3);
-fx-padding: 10 0 0 0; /* Abstand nach oben */
-fx-padding: 10 0 0 0; /* Space above */
}
.green-box {
/* Grüne Box anpassen */
/* Customize the green box */
-fx-fill: #4CAF50;
-fx-stroke: #FFFFFF;
-fx-stroke-width: 3;
@@ -133,12 +219,13 @@
-fx-border-color: #666666;
-fx-text-fill: #CCCCCC;
}
.button-exit:hover {
-fx-translate-y: -3;
-fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0, 0.8), 0, 0, 3, 3);
}
/* LOBBY ERSTELLEN BUTTON */
/* CREATE LOBBY BUTTON */
.button-create-lobby {
-fx-background-radius: 12;
-fx-border-radius: 12;
@@ -151,10 +238,9 @@
-fx-border-color: #2ecc71;
-fx-text-fill: #ffffff;
}
.button-create-lobby:hover {
-fx-translate-y: -3;
-fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0, 0.8), 0, 0, 3, 3);
-fx-background-color: #27ae60;
}
@@ -12,9 +12,9 @@
<GridPane prefWidth="1200" prefHeight="800" AnchorPane.topAnchor="0" AnchorPane.bottomAnchor="0" AnchorPane.leftAnchor="0" AnchorPane.rightAnchor="0">
<columnConstraints>
<!-- Tisch bekommt 75% vom Fenster -->
<!-- The table takes up 70% of the window -->
<ColumnConstraints percentWidth="70.0" hgrow="ALWAYS" />
<!-- Info-Box bekommt 25% vom Fenster inkl. 5 prozent buffer -->
<!-- The info box takes up 25% of the window, including a 5% buffer -->
<ColumnConstraints percentWidth="5.0" hgrow="ALWAYS" />
<ColumnConstraints percentWidth="25.0" hgrow="ALWAYS" />
</columnConstraints>
@@ -24,7 +24,7 @@
</rowConstraints>
<children>
<!-- LINKER BEREICH (Tisch) -->
<!-- LEFT SIDE (Table) -->
<GridPane GridPane.columnIndex="0" style="-fx-background-color: transparent;" alignment="CENTER">
<rowConstraints>
<RowConstraints percentHeight="25.0" vgrow="ALWAYS" />
@@ -68,7 +68,7 @@
<Insets top="20" left="20" />
</GridPane.margin>
</Button>
<Button text="Lobby erstellen"
<Button text="CREATE A LOBBY"
styleClass="button-create-lobby"
fx:id="createLobbyButton"
onAction="#handleCreateLobbyButton"
@@ -88,10 +88,13 @@
GridPane.halignment="LEFT"
GridPane.valignment="TOP">
<GridPane.margin>
<!-- place below the 'Lobby erstellen' button -->
<!-- place below the 'CREATE A LOBBY' button -->
<Insets top="100" left="20" />
</GridPane.margin>
<TextField fx:id="usernameField" promptText="Benutzername" maxWidth="180" />
<TextField fx:id="usernameField"
promptText="Username"
styleClass="gray-input-field"
maxWidth="180" />
<Button text="Login"
fx:id="loginButton"
onAction="#handleLoginButton"
@@ -119,10 +122,10 @@
alignment="CENTER"
minWidth="100"
minHeight="100">
<!-- Bleibt leer -->
<!-- Remains empty -->
</VBox>
<!-- RECHTER BEREICH (Info-Box) -->
<!-- RIGHT SIDE (Chat Box) -->
<fx:include source="components/chatui/chatbox.fxml" GridPane.columnIndex="2"/>
</children>
</GridPane>
@@ -0,0 +1,64 @@
package ch.unibas.dmi.dbis.cs108.casono.server.domain.user;
import static org.junit.jupiter.api.Assertions.*;
import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionId;
import org.junit.jupiter.api.Test;
/** Tests for {@link UserFactory} and {@link UserRegistry} name assignment behavior. */
public class UserFactoryTest {
// Logger removed as per patch requirement
@Test
public void createUsersWithDuplicateDesiredNamesAssignsUniqueNames() {
UserRegistry registry = new UserRegistry();
UserFactory factory = new UserFactory(registry);
SessionId s1 = new SessionId();
SessionId s2 = new SessionId();
SessionId s3 = new SessionId();
// Create first user with desired name "Alice"
User u1 = factory.create("Alice", s1);
// Create second user requesting the same name
User u2 = factory.create("Alice", s2);
// Create third user requesting the same name again
User u3 = factory.create("Alice", s3);
assertNotNull(u1);
assertNotNull(u2);
assertNotNull(u3);
// All assigned names must be distinct
String n1 = u1.getName();
String n2 = u2.getName();
String n3 = u3.getName();
// assigned names: n1, n2, n3
assertNotEquals(n1, n2);
assertNotEquals(n1, n3);
assertNotEquals(n2, n3);
// Registry should contain all assigned names
assertTrue(registry.getByUsername(n1).isPresent());
assertTrue(registry.getByUsername(n2).isPresent());
assertTrue(registry.getByUsername(n3).isPresent());
}
@Test
public void createUserWithNullDesiredAssignsPlayerN() {
UserRegistry registry = new UserRegistry();
UserFactory factory = new UserFactory(registry);
SessionId s1 = new SessionId();
User u = factory.create(null, s1);
assertNotNull(u);
String name = u.getName();
// assigned anonymous name: name
assertTrue(name.startsWith("player"), "expected automatic name starting with 'player'");
assertTrue(registry.getByUsername(name).isPresent());
}
}