Compare commits
40 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2ab27ac3ed | |||
| f54182fde6 | |||
| 9e7a80c17d | |||
| 9df3042978 | |||
| 69333ede7a | |||
| cdb6d0c6b0 | |||
| a65341b648 | |||
| b4b2e3b067 | |||
| beeba04a2c | |||
| edb98c8d21 | |||
| 7d74ed6263 | |||
| 2d7b15e593 | |||
| 3ee1e63577 | |||
| 8f4ce1db80 | |||
| 066d4ee8c3 | |||
| 5ed455c20d | |||
| 938ef71c2a | |||
| 09e8822bef | |||
| 35297775dc | |||
| 77dad54c84 | |||
| b205b25b48 | |||
| df31c307fe | |||
| f3ecbd12f5 | |||
| 5d4d2655ef | |||
| d855f2d568 | |||
| ca782b389e | |||
| 04bf7ba545 | |||
| 42b5344059 | |||
| 76e62b3cff | |||
| 1070b5d4b9 | |||
| 4daf6ff4a2 | |||
| 3153f7027d | |||
| b5d19d915b | |||
| 09c19c2602 | |||
| 77c7f7c0a5 | |||
| 6f80501ea2 | |||
| 45cd54b981 | |||
| 3427c661cf | |||
| ff07843e0a | |||
| cc33411911 |
@@ -0,0 +1,16 @@
|
||||
## Milestone Achievement
|
||||
<!-- The recommended type is: Task -->
|
||||
|
||||
### Category
|
||||
<!-- Category of the milestone (Process, Product, Presentation) -->
|
||||
|
||||
### Title
|
||||
<!-- Title of the milestone (equal to the title in the milestone catalog (https://p9.dmi.unibas.ch/cs108/2026) -->
|
||||
|
||||
### Rewarded points on completion
|
||||
<!-- Number of points rewarded on completion of the milestone -->
|
||||
|
||||
### Description of milestone
|
||||
<!-- Description of the milestone (equal to the description in the milestone catalog (https://p9.dmi.unibas.ch/cs108/2026) -->
|
||||
|
||||
/label ~achievement
|
||||
@@ -12,6 +12,7 @@ Since this project is part of a course at the University of Basel, the [Code of
|
||||
- [Creating an issue](#creating-an-issue)
|
||||
- [During implementation](#during-implementation)
|
||||
- [Collaborative work](#collaborative-work)
|
||||
- [Milestone Achievements](#milestone-achievements)
|
||||
- [Git Workflow](#git-workflow)
|
||||
- [Creating a branch](#creating-a-branch)
|
||||
- [Working on a branch](#working-on-a-branch)
|
||||
@@ -49,6 +50,20 @@ Issue or Task in GitLab **before** any implementation begins.
|
||||
centrally visible and searchable.
|
||||
- Before starting work that overlaps with an existing issue, check its comment thread first to avoid duplicating effort.
|
||||
|
||||
### Milestone Achievements
|
||||
For every process and product-related milestone achievement, there is a dedicated milestone task.
|
||||
|
||||
- **Do not create a branch directly from a milestone task.**
|
||||
- Instead, create a normal implementation task (using the regular task template) and reference the milestone task there.
|
||||
- In the merge request, reference the milestone task again.
|
||||
- If the milestone condition is fully met, you may use `Closing #<id>` to close the milestone task.
|
||||
- If it is only partially addressed, use `Relates to #<id>` or `Contributes to #<id>` so the milestone task stays open.
|
||||
|
||||
Milestone tasks may, but do not have to, be assigned to a specific person.
|
||||
|
||||
- If multiple people are involved, contribution is tracked through linked tasks that reference the milestone task.
|
||||
- For small topics, a Milestone Achievement may be assigned to one person. Others should only contribute on request and should not modify components introduced under that achievement without coordination.
|
||||
|
||||
|
||||
## Git Workflow
|
||||
We use a **feature branch -> main** strategy. The `main` branch is always in a releasable state.
|
||||
|
||||
@@ -352,13 +352,19 @@ None.
|
||||
|
||||
| Field | Type | Description |
|
||||
|:---------|:----------------|:-------------------------------------------------------------------|
|
||||
| `TYPE` | `Enum<ChatType` | GLOBAL, LOBBY or WHISPER, specifying the Chat |
|
||||
| `TYPE` | `Enum<ChatType` | Member of Enum, indicating with chat type is used |
|
||||
| `GAME` | `int` | ID for identifying the Lobby Chat |
|
||||
| `USER` | `String` | Username of the player that sent the message |
|
||||
| `TARGET` | `String` | Username of to which the message is sent to (only in Whisper Chat) |
|
||||
| `TIME` | `String` | Timestamp, when the message was sent |
|
||||
| `TEXT` | `String` | Content of the message |
|
||||
|
||||
| Members of `ChatType` | Description |
|
||||
|:----------------------|:--------------------------------|
|
||||
| `GLOBAL` | Message is for the global chat |
|
||||
| `LOBBY` | Message is for the lobby chat |
|
||||
| `WHISPER` | Message is for the whisper chat |
|
||||
|
||||
### Success Response
|
||||
No response fields.
|
||||
|
||||
@@ -419,12 +425,19 @@ No parameters.
|
||||
### Success Response
|
||||
| Field | Type | Description |
|
||||
|:---------|:----------------|:-------------------------------------------------------------------|
|
||||
| `TYPE` | `Enum<ChatType` | GLOBAL, LOBBY or WHISPER, specifying the Chat |
|
||||
| `TYPE` | `Enum<ChatType` | Member of Enum, indicating with chat type is used |
|
||||
| `GAME` | `int` | ID for identifying the Lobby Chat |
|
||||
| `USER` | `String` | Username of the player that sent the message |
|
||||
| `TARGET` | `String` | Username of to which the message is sent to (only in Whisper Chat) |
|
||||
| `TIME` | `String` | Timestamp, when the message was sent |
|
||||
| `TEXT` | `String` | Content of the message |
|
||||
|
||||
| Members of `ChatType` | Description |
|
||||
|:----------------------|:--------------------------------|
|
||||
| `GLOBAL` | Message is for the global chat |
|
||||
| `LOBBY` | Message is for the lobby chat |
|
||||
| `WHISPER` | Message is for the whisper chat |
|
||||
|
||||
### Error Response
|
||||
| Code | Description |
|
||||
| :------------------- |:---------------------------------------------------------------------------------|
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
# Our Network Protocol Documentation
|
||||
|
||||
## Overview
|
||||
Our protocol is inspired by *POP3*, but has been highly customized to fit our specific needs.
|
||||
It is a text-based protocol operating over raw TCP sockets, designed for human readability and strict structure.
|
||||
|
||||
## Packet Structure
|
||||
Each network packet consists of:
|
||||
- **4-byte header**: Specifies the size of the payload (big-endian integer).
|
||||
- **4-byte request ID**: Generated by the client, used to match requests and responses.
|
||||
- **Payload**: The actual data, its size as specified by the header.
|
||||
|
||||
This structure is used for both requests and responses.
|
||||
|
||||
## Conventions
|
||||
- All keys (in both requests and responses) use UPPER_SNAKE_CASE.
|
||||
- Only a-z, A-Z, 0-9 are allowed in keys.
|
||||
- Only human-readable strings are transmitted.
|
||||
- Binary data is not allowed.
|
||||
|
||||
## Request Format
|
||||
A request consists of a single line:
|
||||
|
||||
```
|
||||
COMMAND KEY1=ARG1 KEY2=ARG2
|
||||
```
|
||||
|
||||
- **COMMAND**: The action to perform.
|
||||
- **KEY=VALUE pairs**: Optional parameters. There may be zero or more.
|
||||
- **Whitespace**: Extra spaces between key, separator, and value are ignored. Any other characters between them are an error.
|
||||
- **Standalone values**: Not allowed. Every value must have a key.
|
||||
|
||||
### String Values
|
||||
- Strings with spaces must be enclosed in single quotes: `'example string'`.
|
||||
- Inside quoted strings, line breaks are allowed.
|
||||
- To include a single quote inside a string, escape it (e.g., `'It\'s fine'`).
|
||||
|
||||
If these rules are violated, the request is considered invalid and will be rejected.
|
||||
|
||||
## Response Format
|
||||
Responses are more complex and can represent nested collections.
|
||||
|
||||
- **Success**: Starts with `+OK`
|
||||
- **Error**: Starts with `-ERR`
|
||||
- After the status, a newline follows, then fields in the format `KEY=VALUE`.
|
||||
- Collections and elements are ended with the `END` keyword.
|
||||
- A collection starts with a key, and its elements are indented.
|
||||
- A element starts with a key, and its fields are indented.
|
||||
|
||||
### Example: Nested Collection
|
||||
```
|
||||
+OK
|
||||
KEY1=VALUE1
|
||||
FIELDS
|
||||
FIELD
|
||||
NESTED_KEY=NESTED_VALUE
|
||||
END
|
||||
END
|
||||
KEY2=VALUE2
|
||||
END
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
Any violation of the format (invalid characters, unescaped quotes, binary data, etc.) results in the request being rejected with an error response.
|
||||
|
||||
### Example Error Response When Violating Syntax Rules:
|
||||
```
|
||||
-ERR
|
||||
CODE=PARSING_ERROR
|
||||
MSG='Error occured during parsing. Likely due to malformed payload.'
|
||||
END
|
||||
```
|
||||
@@ -36,7 +36,13 @@ public class ClientApp {
|
||||
int port = Integer.parseInt(parts[1]);
|
||||
|
||||
LOGGER.info("You've selected the client. It will connect port {} at host {}", port, host);
|
||||
Launcher.main(new String[] {});
|
||||
// Expose the chosen host/port to the UI via system properties so controllers
|
||||
// (which read System.getProperty("casono.server.host"/"casono.server.port"))
|
||||
// can obtain the correct connection information.
|
||||
System.setProperty("casono.server.host", host);
|
||||
System.setProperty("casono.server.port", Integer.toString(port));
|
||||
// Forward the original address argument to the launcher as well.
|
||||
Launcher.main(new String[] {arg});
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
|
||||
@@ -20,25 +20,35 @@ public class Message {
|
||||
|
||||
/**
|
||||
* Constructs a Message with a provided timestamp. Typically used when reconstructing messages
|
||||
* received from the server.
|
||||
* received from the server. Used for Messages in the Lobby Chat.
|
||||
*
|
||||
* @param type The chat category (e.g., GLOBAL, LOBBY, or WHISPER).
|
||||
* @param lobbyId The ID of the lobby, or -1 if not applicable.
|
||||
* @param sender The username of the message creator.
|
||||
* @param target The username of the recipient (required for whispers, otherwise null).
|
||||
* @param timestamp The formatted time string (e.g., "HH:mm").
|
||||
* @param message The actual text content of the message.
|
||||
*/
|
||||
public Message(
|
||||
ChatType type,
|
||||
int lobbyId,
|
||||
String sender,
|
||||
String target,
|
||||
String timestamp,
|
||||
String message) {
|
||||
this.type = type;
|
||||
private Message(int lobbyId, String sender, String timestamp, String message) {
|
||||
this.type = ChatType.LOBBY;
|
||||
this.lobbyId = lobbyId;
|
||||
this.sender = sender;
|
||||
this.target = null;
|
||||
this.timestamp = timestamp;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a Message with a provided timestamp. Typically used when reconstructing messages
|
||||
* received from the server. Used for Messages in the WHISPER and GLOBAL Chat.
|
||||
*
|
||||
* @param type The chat category (e.g., GLOBAL or WHISPER).
|
||||
* @param sender The username of the message creator.
|
||||
* @param timestamp The formatted time string (e.g., "HH:mm").
|
||||
* @param message The actual text content of the message.
|
||||
*/
|
||||
private Message(ChatType type, String sender, String target, String timestamp, String message) {
|
||||
this.type = type;
|
||||
this.lobbyId = -1;
|
||||
this.sender = sender;
|
||||
this.target = target;
|
||||
this.timestamp = timestamp;
|
||||
this.message = message;
|
||||
@@ -120,23 +130,19 @@ public class Message {
|
||||
case GLOBAL ->
|
||||
new Message(
|
||||
ChatType.GLOBAL,
|
||||
-1,
|
||||
getParString(parameters, "USER"),
|
||||
null,
|
||||
getParString(parameters, "TIME"),
|
||||
getParString(parameters, "TEXT"));
|
||||
case LOBBY ->
|
||||
new Message(
|
||||
ChatType.LOBBY,
|
||||
Integer.parseInt(getParString(parameters, "GAME")),
|
||||
getParString(parameters, "USER"),
|
||||
null,
|
||||
getParString(parameters, "TIME"),
|
||||
getParString(parameters, "TEXT"));
|
||||
case WHISPER ->
|
||||
new Message(
|
||||
ChatType.WHISPER,
|
||||
Integer.parseInt(getParString(parameters, "GAME", "-1")),
|
||||
getParString(parameters, "USER"),
|
||||
getParString(parameters, "TARGET"),
|
||||
getParString(parameters, "TIME"),
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.client.game;
|
||||
|
||||
/**
|
||||
* Represents a playing card with a value and suit.
|
||||
*/
|
||||
/** Represents a playing card with a value and suit. */
|
||||
public class Card {
|
||||
public String value;
|
||||
public String suit;
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.client.game;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.game.Card;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.game.Player;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Represents the current state of the poker game, including the phase, pot
|
||||
* size, current bet, dealer position,
|
||||
* active player, community cards, and player information.
|
||||
* Represents the current state of the poker game, including the phase, pot size, current bet,
|
||||
* dealer position, active player, community cards, and player information.
|
||||
*/
|
||||
public class GameState {
|
||||
public String phase;
|
||||
|
||||
@@ -4,8 +4,8 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Represents a player in the poker game, including their name, chip count,
|
||||
* current bet, state (e.g., "active", "folded"), and their hole cards.
|
||||
* Represents a player in the poker game, including their name, chip count, current bet, state
|
||||
* (e.g., "active", "folded"), and their hole cards.
|
||||
*/
|
||||
public class Player {
|
||||
public String name;
|
||||
|
||||
@@ -28,10 +28,11 @@ public class ClientService {
|
||||
private final Socket socket;
|
||||
|
||||
private final ExecutorService executor;
|
||||
private final boolean offlineMode;
|
||||
|
||||
public static ArrayList<String> response;
|
||||
private final AtomicInteger idGenerator;
|
||||
private final Logger logger;
|
||||
private Logger logger;
|
||||
|
||||
/**
|
||||
* Constructs a ClientService with the given server IP and port. It establishes a socket
|
||||
@@ -47,6 +48,8 @@ public class ClientService {
|
||||
|
||||
this.logger = LogManager.getLogger(ClientService.class);
|
||||
|
||||
this.offlineMode = false;
|
||||
|
||||
try {
|
||||
socket = new Socket(ip, port);
|
||||
clienttcptransport = new TcpTransport(socket);
|
||||
@@ -58,6 +61,25 @@ public class ClientService {
|
||||
executor = Executors.newSingleThreadExecutor();
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a ClientService in offline mode. No network connection will be attempted and calls
|
||||
* to processCommand will throw a RuntimeException.
|
||||
*
|
||||
* @param offline true to create an offline (no-network) client service
|
||||
*/
|
||||
public ClientService(boolean offline) {
|
||||
this.idGenerator = new AtomicInteger(0);
|
||||
this.offlineMode = offline;
|
||||
this.socket = null;
|
||||
this.clienttcptransport = null;
|
||||
this.executor = Executors.newSingleThreadExecutor();
|
||||
}
|
||||
|
||||
/** Returns true if this ClientService is running in offline mode (no network). */
|
||||
public boolean isOffline() {
|
||||
return offlineMode;
|
||||
}
|
||||
|
||||
static Pattern responseRex =
|
||||
Pattern.compile(
|
||||
"(?<key>\\w+)=(('(?<string>([^']|\\')+)')|(?<primVal>[+-]?[\\d\\w:]+))");
|
||||
|
||||
@@ -45,6 +45,8 @@ public class GameClient {
|
||||
|
||||
GameState state = new GameState();
|
||||
|
||||
// String[] lines = input.split("\n");
|
||||
|
||||
Player currentPlayer = null;
|
||||
Card currentCard = null;
|
||||
|
||||
|
||||
@@ -18,6 +18,10 @@ public class LobbyClient {
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
public ClientService getClientService() {
|
||||
return client;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the current status of the lobby with the given id from the server.
|
||||
*
|
||||
@@ -56,4 +60,13 @@ public class LobbyClient {
|
||||
public void joinLobby(int lobbyId) {
|
||||
client.processCommand("JOIN_LOBBY ID=" + lobbyId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs in to the server with the given username by sending a "LOGIN" command.
|
||||
*
|
||||
* @param user The username to log in with.
|
||||
*/
|
||||
public void login(String user) {
|
||||
client.processCommand("LOGIN USERNAME=" + user);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +1,26 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.network.ClientService;
|
||||
import java.io.IOException;
|
||||
import javafx.application.Application;
|
||||
import javafx.fxml.FXMLLoader;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.stage.Stage;
|
||||
|
||||
/**
|
||||
* Main class for the Casono Game UI.
|
||||
*
|
||||
* <p>Starts the JavaFX application, loads the graphical user interface from the FXML file, and
|
||||
* initializes the main stage for the game.
|
||||
*
|
||||
* <p>Tasks: - Loads the FXML interface "/ui-structure/Casinogameui.fxml". - Loads the application
|
||||
* icon from "/images/logoinverted.png". - Starts the application in full-screen mode.
|
||||
*/
|
||||
public class CasinoGameUI extends Application {
|
||||
|
||||
/** default constructor */
|
||||
// Static field for ClientService (workaround for JavaFX Application launch)
|
||||
private static ClientService staticClientService;
|
||||
|
||||
public static void setClientService(ClientService clientService) {
|
||||
staticClientService = clientService;
|
||||
}
|
||||
|
||||
public static ClientService getClientService() {
|
||||
return staticClientService;
|
||||
}
|
||||
|
||||
/** Default no-arg constructor. */
|
||||
public CasinoGameUI() {
|
||||
// default no-arg constructor
|
||||
}
|
||||
@@ -46,7 +49,7 @@ public class CasinoGameUI extends Application {
|
||||
}
|
||||
|
||||
/**
|
||||
* Starting point of the application.
|
||||
* Entry point of the application.
|
||||
*
|
||||
* @param args Command line arguments.
|
||||
*/
|
||||
|
||||
@@ -30,6 +30,17 @@ public class Casinomainui extends Application {
|
||||
* @throws IOException If loading the FXML fails.
|
||||
*/
|
||||
public void start(Stage stage) throws IOException {
|
||||
// If the launcher passed an address argument (ip:port), expose it as
|
||||
// system properties so controllers can read it without embedding defaults.
|
||||
var raw = getParameters().getRaw();
|
||||
if (raw != null && raw.size() > 0) {
|
||||
String arg = raw.get(0);
|
||||
String[] parts = arg.split(":", 2);
|
||||
if (parts.length == 2) {
|
||||
System.setProperty("casono.server.host", parts[0]);
|
||||
System.setProperty("casono.server.port", parts[1]);
|
||||
}
|
||||
}
|
||||
FXMLLoader fxmlLoader =
|
||||
new FXMLLoader(getClass().getResource("/ui-structure/Casinomainui.fxml"));
|
||||
Scene scene = new Scene(fxmlLoader.load(), SCENE_WIDTH, SCENE_HEIGHT);
|
||||
|
||||
+74
-3
@@ -1,9 +1,14 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.client.ui.lobbyui;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.network.ClientService;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.network.LobbyClient;
|
||||
import javafx.application.Platform;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.Alert;
|
||||
import javafx.scene.control.Alert.AlertType;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.TextField;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.AnchorPane;
|
||||
@@ -23,10 +28,13 @@ public class CasinomainuiController {
|
||||
@FXML private Rectangle greenBox;
|
||||
@FXML private Button exitbutton;
|
||||
@FXML private VBox casinoTable;
|
||||
@FXML private TextField usernameField;
|
||||
@FXML private Button loginButton;
|
||||
|
||||
private LobbyButtonTranslationManager translationManager;
|
||||
private LobbyButtonGridManager gridManager;
|
||||
private int nextButtonId = 1;
|
||||
private LobbyClient lobbyClient;
|
||||
|
||||
/** Default constructor for dependency injection by FXMLLoader. */
|
||||
public CasinomainuiController() {
|
||||
@@ -41,8 +49,63 @@ public class CasinomainuiController {
|
||||
logoView.setImage(new Image(getClass().getResource("/images/logo.png").toExternalForm()));
|
||||
|
||||
translationManager = LobbyButtonTranslationManager.getInstance();
|
||||
String host = System.getProperty("casono.server.host");
|
||||
int port = Integer.parseInt(System.getProperty("casono.server.port"));
|
||||
ClientService clientService;
|
||||
try {
|
||||
clientService = new ClientService(host, port);
|
||||
} catch (RuntimeException e) {
|
||||
LOGGER.warn(
|
||||
"Could not connect to server {}:{} — starting in offline mode: {}",
|
||||
host,
|
||||
port,
|
||||
e.getMessage());
|
||||
clientService = new ClientService(true); // offline mode
|
||||
}
|
||||
gridManager =
|
||||
new LobbyButtonGridManager(new javafx.scene.layout.GridPane(), translationManager);
|
||||
new LobbyButtonGridManager(
|
||||
new javafx.scene.layout.GridPane(), translationManager, clientService);
|
||||
// LobbyClient will use the provided ClientService; in offline mode calls will
|
||||
// fail with RuntimeException
|
||||
lobbyClient = new LobbyClient(clientService);
|
||||
casinoTable.getChildren().clear();
|
||||
casinoTable.getChildren().add(gridManager.getGridPane());
|
||||
gridManager.renderLobbyButtons();
|
||||
}
|
||||
|
||||
/** Handles the login button action. Validates input and calls LobbyClient.login(). */
|
||||
@FXML
|
||||
public void handleLoginButton() {
|
||||
String username = usernameField.getText();
|
||||
if (username == null || username.isBlank()) {
|
||||
showAlert("Please enter a username.");
|
||||
return;
|
||||
}
|
||||
// Only allow alphanumeric, _ and -
|
||||
if (!username.matches("[a-zA-Z0-9_-]+")) {
|
||||
showAlert("Only letters, numbers, '_' and '-' are allowed!");
|
||||
return;
|
||||
}
|
||||
if (lobbyClient.getClientService().isOffline()) {
|
||||
showAlert("Offline mode: cannot send login to server.");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
lobbyClient.login(username);
|
||||
showAlert("Login sent: " + username);
|
||||
} catch (RuntimeException e) {
|
||||
LOGGER.error("Login failed: {}", e.getMessage());
|
||||
showAlert("Login failed: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/** Shows an alert dialog with the given message. */
|
||||
private void showAlert(String message) {
|
||||
Alert alert = new Alert(AlertType.INFORMATION);
|
||||
alert.setTitle("Info");
|
||||
alert.setHeaderText(null);
|
||||
alert.setContentText(message);
|
||||
alert.showAndWait();
|
||||
casinoTable.getChildren().clear();
|
||||
casinoTable.getChildren().add(gridManager.getGridPane());
|
||||
gridManager.renderLobbyButtons();
|
||||
@@ -62,13 +125,21 @@ public class CasinomainuiController {
|
||||
return;
|
||||
}
|
||||
int buttonId = nextButtonId++;
|
||||
int lobbyId = gridManager.createLobby();
|
||||
try {
|
||||
String username = usernameField != null ? usernameField.getText() : "<unknown>";
|
||||
LOGGER.info("Creating lobby for user: {}", username);
|
||||
// avoid attempting to create a lobby when offline
|
||||
if (lobbyClient.getClientService().isOffline()) {
|
||||
LOGGER.warn("Cannot create lobby while offline");
|
||||
showAlert("Offline mode: cannot create lobby.");
|
||||
return;
|
||||
}
|
||||
int lobbyId = gridManager.createLobby();
|
||||
translationManager.addLobbyButton(buttonId, lobbyId);
|
||||
LOGGER.info("ButtonID: {}, LobbyID: {}", buttonId, lobbyId);
|
||||
gridManager.renderLobbyButtons();
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("Error while adding lobby button: {}", e.getMessage());
|
||||
LOGGER.error("Failed to create or add lobby: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+252
-221
@@ -1,14 +1,17 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.client.ui.lobbyui;
|
||||
|
||||
/**
|
||||
* Manages the grid for lobby buttons and rendering. Uses LobbyButtonTranslationManager for mapping
|
||||
* ButtonID to LobbyID.
|
||||
*/
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.network.LobbyClient;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.network.ClientService;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.network.LobbyClient;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.image.Image;
|
||||
@@ -17,163 +20,187 @@ import javafx.scene.layout.GridPane;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
/**
|
||||
* Manages the grid for lobby buttons and rendering. Uses
|
||||
* LobbyButtonTranslationManager for mapping
|
||||
* ButtonID to LobbyID.
|
||||
*/
|
||||
public class LobbyButtonGridManager {
|
||||
private static final double BUTTON_WIDTH_MARGIN = 20.0;
|
||||
|
||||
private static final double BTN_WIDTH_MRG = 20.0;
|
||||
private static final double BUTTON_MIN_SIZE = 10.0;
|
||||
private static final Logger LOGGER = LogManager.getLogger(LobbyButtonGridManager.class);
|
||||
|
||||
/** GridPane for the button grid. */
|
||||
private final GridPane gridPane;
|
||||
|
||||
/** Manager for mapping ButtonID to LobbyID. */
|
||||
private final LobbyButtonTranslationManager translationManager;
|
||||
|
||||
/** Number of rows in the grid. */
|
||||
private static final int ROWS = 2;
|
||||
|
||||
/** Number of columns in the grid. */
|
||||
private static final int REFRESH_INTERVAL_SECONDS = 5;
|
||||
private static final int INITIAL_DELAY_SECONDS = 5;
|
||||
private static final int COLS = 4;
|
||||
|
||||
/** Path to the button image. */
|
||||
private static final String BUTTON_IMAGE_PATH = "/images/logo.png";
|
||||
private static final Logger LOGGER = LogManager.getLogger(LobbyButtonGridManager.class);
|
||||
|
||||
/** Image for a lobby in CREATED state. */
|
||||
/** Default fallback image. */
|
||||
private static final String BUTTON_FALLBACK_IMAGE = "/images/lobbypictures/error.png";
|
||||
|
||||
/**
|
||||
* Template for per-button images. Use: button index and status
|
||||
* (created|running). Example:
|
||||
* /images/lobby_1_created.png
|
||||
*/
|
||||
private static final String BUTTON_IMAGE_TEMPLATE = "/images/lobbypictures/lobby_%d_%s.png";
|
||||
|
||||
/** Cache for loaded Images keyed by resource path. */
|
||||
private final ConcurrentHashMap<String, Image> imageCache = new ConcurrentHashMap<>();
|
||||
|
||||
/** Max random lobby id. */
|
||||
private static final int MAX_RANDOM_LOBBY_ID = 10000;
|
||||
|
||||
private final GridPane gridPane;
|
||||
private final LobbyButtonTranslationManager translationManager;
|
||||
private final LobbyClient lobbyClient;
|
||||
|
||||
/**
|
||||
* Constructor for the GridManager.
|
||||
*
|
||||
* @param gridPane the GridPane for rendering
|
||||
* @param translationManager the manager for mapping ButtonID to LobbyID
|
||||
*/
|
||||
public LobbyButtonGridManager(
|
||||
GridPane gridPane, LobbyButtonTranslationManager translationManager) {
|
||||
this(gridPane, translationManager, createDefaultLobbyClient());
|
||||
}
|
||||
private final ConcurrentHashMap<String, Image> imageCache = new ConcurrentHashMap<>();
|
||||
|
||||
private final ExecutorService executor = Executors.newCachedThreadPool();
|
||||
|
||||
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
|
||||
|
||||
/**
|
||||
* Constructor that accepts a `LobbyClient` implementation.
|
||||
*/
|
||||
public LobbyButtonGridManager(
|
||||
GridPane gridPane, LobbyButtonTranslationManager translationManager, LobbyClient lobbyClient) {
|
||||
GridPane gridPane,
|
||||
LobbyButtonTranslationManager translationManager,
|
||||
LobbyClient lobbyClient) {
|
||||
|
||||
this.gridPane = gridPane;
|
||||
// Always use the singleton
|
||||
this.translationManager = LobbyButtonTranslationManager.getInstance();
|
||||
this.lobbyClient = lobbyClient;
|
||||
|
||||
startPeriodicRefresh(REFRESH_INTERVAL_SECONDS, INITIAL_DELAY_SECONDS);
|
||||
}
|
||||
|
||||
private static LobbyClient createDefaultLobbyClient() {
|
||||
String host = System.getProperty("casono.server.host", "127.0.0.1");
|
||||
int port = 1337;
|
||||
try {
|
||||
port = Integer.parseInt(System.getProperty("casono.server.port", "1337"));
|
||||
} catch (NumberFormatException e) {
|
||||
// use default port if parsing fails
|
||||
}
|
||||
return new LobbyClient(new ClientService(host, port));
|
||||
public LobbyButtonGridManager(
|
||||
GridPane gridPane,
|
||||
LobbyButtonTranslationManager translationManager,
|
||||
ClientService clientService) {
|
||||
|
||||
this(gridPane, translationManager, new LobbyClient(clientService));
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders all lobby buttons in the grid. Creates a button for each mapping with
|
||||
* image and event
|
||||
* handler.
|
||||
*/
|
||||
public void renderLobbyButtons() {
|
||||
gridPane.getChildren().clear();
|
||||
int index = 0;
|
||||
private void startPeriodicRefresh(long initialDelay, long period) {
|
||||
scheduler.scheduleAtFixedRate(
|
||||
this::refreshMappings, initialDelay, period, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
private void refreshMappings() {
|
||||
Map<Integer, Integer> mapping = translationManager.getButtonIdToLobbyId();
|
||||
|
||||
if (mapping.isEmpty()) {
|
||||
// No buttons to render
|
||||
return;
|
||||
}
|
||||
for (Map.Entry<Integer, Integer> entry : mapping.entrySet()) {
|
||||
int buttonId = entry.getKey();
|
||||
int lobbyId = entry.getValue();
|
||||
Button btn = new Button();
|
||||
btn.setId("lobbyBtn-" + buttonId);
|
||||
// Set image based on lobby status (initial)
|
||||
LobbyStatus initialStatus = getLobbyStatus(lobbyId);
|
||||
Image image = safeLoadImage(getImagePathForButton(buttonId, initialStatus));
|
||||
ImageView imageView = new ImageView(image);
|
||||
imageView.setPreserveRatio(true);
|
||||
// Dynamic width: bind to the cell size
|
||||
imageView
|
||||
.fitWidthProperty()
|
||||
.bind(gridPane.widthProperty().divide(COLS).subtract(BUTTON_WIDTH_MARGIN));
|
||||
imageView.setSmooth(true);
|
||||
btn.setGraphic(imageView);
|
||||
btn.setMaxWidth(Double.MAX_VALUE);
|
||||
btn.setMaxHeight(Double.MAX_VALUE);
|
||||
btn.setMinWidth(BUTTON_MIN_SIZE);
|
||||
btn.setMinHeight(BUTTON_MIN_SIZE);
|
||||
GridPane.setHgrow(btn, javafx.scene.layout.Priority.ALWAYS);
|
||||
GridPane.setVgrow(btn, javafx.scene.layout.Priority.ALWAYS);
|
||||
btn.setOnAction(
|
||||
e -> {
|
||||
Integer targetLobbyId = translationManager.getLobbyIdForButton(buttonId);
|
||||
if (targetLobbyId != null) {
|
||||
joinLobby(targetLobbyId);
|
||||
}
|
||||
});
|
||||
int row = index / COLS;
|
||||
int col = index % COLS;
|
||||
gridPane.add(btn, col, row);
|
||||
index++;
|
||||
|
||||
List<Map.Entry<Integer, Integer>> entries = new ArrayList<>(mapping.entrySet());
|
||||
|
||||
for (Map.Entry<Integer, Integer> e : entries) {
|
||||
int buttonId = e.getKey();
|
||||
int lobbyId = e.getValue();
|
||||
|
||||
CompletableFuture.supplyAsync(
|
||||
() -> {
|
||||
try {
|
||||
return lobbyClient.fetchLobbyStatusString(lobbyId);
|
||||
} catch (Exception ex) {
|
||||
LOGGER.info("Lobby {} missing: {}", lobbyId, ex.getMessage());
|
||||
return null;
|
||||
}
|
||||
},
|
||||
executor)
|
||||
.thenAccept(
|
||||
status -> {
|
||||
if (status == null) {
|
||||
translationManager.removeLobbyButton(buttonId);
|
||||
|
||||
javafx.application.Platform.runLater(
|
||||
this::updateLobbyButtonImages);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/** Possible lobby statuses. */
|
||||
public void renderLobbyButtons() {
|
||||
gridPane.getChildren().clear();
|
||||
|
||||
Map<Integer, Integer> mapping = translationManager.getButtonIdToLobbyId();
|
||||
|
||||
if (mapping.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<Integer> buttonIds = new ArrayList<>(mapping.keySet());
|
||||
Collections.sort(buttonIds);
|
||||
|
||||
for (int index = 0; index < buttonIds.size(); index++) {
|
||||
Integer buttonId = buttonIds.get(index);
|
||||
int lobbyId = mapping.get(buttonId);
|
||||
|
||||
Button btn = createLobbyButton(buttonId, lobbyId);
|
||||
|
||||
int row = index / COLS;
|
||||
int col = index % COLS;
|
||||
|
||||
gridPane.add(btn, col, row);
|
||||
}
|
||||
}
|
||||
|
||||
private Button createLobbyButton(int buttonId, int lobbyId) {
|
||||
Button btn = new Button();
|
||||
btn.setId("lobbyBtn-" + buttonId);
|
||||
|
||||
ImageView imageView = new ImageView(safeLoadImage(BUTTON_FALLBACK_IMAGE));
|
||||
|
||||
imageView.setPreserveRatio(true);
|
||||
imageView
|
||||
.fitWidthProperty()
|
||||
.bind(gridPane.widthProperty().divide(COLS).subtract(BTN_WIDTH_MRG));
|
||||
|
||||
btn.setGraphic(imageView);
|
||||
btn.setMaxWidth(Double.MAX_VALUE);
|
||||
btn.setMaxHeight(Double.MAX_VALUE);
|
||||
btn.setMinWidth(BUTTON_MIN_SIZE);
|
||||
btn.setMinHeight(BUTTON_MIN_SIZE);
|
||||
|
||||
GridPane.setHgrow(btn, javafx.scene.layout.Priority.ALWAYS);
|
||||
GridPane.setVgrow(btn, javafx.scene.layout.Priority.ALWAYS);
|
||||
|
||||
btn.setOnAction(
|
||||
e -> {
|
||||
Integer targetLobbyId = translationManager.getLobbyIdForButton(buttonId);
|
||||
|
||||
if (targetLobbyId != null) {
|
||||
joinLobby(targetLobbyId);
|
||||
}
|
||||
});
|
||||
|
||||
loadLobbyImageAsync(btn, buttonId, lobbyId);
|
||||
|
||||
return btn;
|
||||
}
|
||||
|
||||
private void loadLobbyImageAsync(Button btn, int buttonId, int lobbyId) {
|
||||
|
||||
CompletableFuture.supplyAsync(() -> lobbyClient.fetchLobbyStatusString(lobbyId), executor)
|
||||
.thenAccept(
|
||||
statusStr -> {
|
||||
LobbyStatus status = parseLobbyStatus(statusStr);
|
||||
|
||||
String path =
|
||||
getImagePathForButton(
|
||||
buttonId,
|
||||
status == null ? LobbyStatus.CREATED : status);
|
||||
|
||||
Image img = safeLoadImage(path);
|
||||
|
||||
javafx.application.Platform.runLater(
|
||||
() -> {
|
||||
ImageView iv = new ImageView(img);
|
||||
iv.setPreserveRatio(true);
|
||||
iv.fitWidthProperty()
|
||||
.bind(
|
||||
gridPane.widthProperty()
|
||||
.divide(COLS)
|
||||
.subtract(BTN_WIDTH_MRG));
|
||||
btn.setGraphic(iv);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private enum LobbyStatus {
|
||||
CREATED,
|
||||
RUNNING
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the current status of a lobby.
|
||||
*
|
||||
* @param lobbyId the lobby id to query
|
||||
* @return the lobby status (mapped from server string; CREATED or RUNNING)
|
||||
*/
|
||||
public LobbyStatus getLobbyStatus(int lobbyId) {
|
||||
String serverStatus = lobbyClient.fetchLobbyStatusString(lobbyId);
|
||||
LobbyStatus parsed = parseLobbyStatus(serverStatus);
|
||||
if (parsed == null) {
|
||||
// Defensive fallback
|
||||
LOGGER.error("Unrecognized lobby status '{}' for lobby {}. Defaulting to CREATED.", serverStatus, lobbyId);
|
||||
return LobbyStatus.CREATED;
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a status string returned by the server into the local enum.
|
||||
* Accepts case-insensitive values like "created" / "CREATED" / "running".
|
||||
* Returns null if the string is not recognized.
|
||||
*/
|
||||
private LobbyStatus parseLobbyStatus(String statusStr) {
|
||||
if (statusStr == null)
|
||||
if (statusStr == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return LobbyStatus.valueOf(statusStr.trim().toUpperCase());
|
||||
} catch (IllegalArgumentException e) {
|
||||
@@ -181,157 +208,161 @@ public class LobbyButtonGridManager {
|
||||
}
|
||||
}
|
||||
|
||||
private String getImagePathForStatus(LobbyStatus status) {
|
||||
return status == LobbyStatus.CREATED
|
||||
? String.format(BUTTON_IMAGE_TEMPLATE, 0, "created")
|
||||
: String.format(BUTTON_IMAGE_TEMPLATE, 0, "running");
|
||||
}
|
||||
|
||||
private String getImagePathForButton(int buttonId, LobbyStatus status) {
|
||||
|
||||
String statusStr = status == LobbyStatus.CREATED ? "created" : "running";
|
||||
|
||||
return String.format(BUTTON_IMAGE_TEMPLATE, buttonId, statusStr);
|
||||
}
|
||||
|
||||
private Image safeLoadImage(String path) {
|
||||
// Return cached image if present
|
||||
Image cached = imageCache.get(path);
|
||||
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
}
|
||||
// Attempt to load the requested resource
|
||||
|
||||
java.io.InputStream is = getClass().getResourceAsStream(path);
|
||||
|
||||
if (is == null) {
|
||||
LOGGER.debug(
|
||||
"Image resource not found: {}. Falling back to {}",
|
||||
path,
|
||||
BUTTON_FALLBACK_IMAGE);
|
||||
is = getClass().getResourceAsStream(BUTTON_FALLBACK_IMAGE);
|
||||
}
|
||||
|
||||
Image loaded = null;
|
||||
|
||||
try {
|
||||
if (is != null) {
|
||||
loaded = new Image(is);
|
||||
} else {
|
||||
LOGGER.error(
|
||||
"Both requested image '{}' and fallback '{}' are missing. No image will be set.",
|
||||
path,
|
||||
BUTTON_FALLBACK_IMAGE);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("Failed to load image '{}'", path, e);
|
||||
}
|
||||
if (loaded == null) {
|
||||
// leave
|
||||
// null
|
||||
LOGGER.error("Image load failed: {}", path, e);
|
||||
}
|
||||
|
||||
if (loaded != null) {
|
||||
imageCache.put(path, loaded);
|
||||
}
|
||||
|
||||
return loaded;
|
||||
}
|
||||
|
||||
/** Update all lobby buttons' images according to the current lobby statuses. */
|
||||
public void updateLobbyButtonImages() {
|
||||
// Run on FX thread to be safe
|
||||
javafx.application.Platform.runLater(
|
||||
() -> {
|
||||
Map<Integer, Integer> mapping = translationManager.getButtonIdToLobbyId();
|
||||
for (Map.Entry<Integer, Integer> entry : mapping.entrySet()) {
|
||||
int buttonId = entry.getKey();
|
||||
int lobbyId = entry.getValue();
|
||||
LobbyStatus status = getLobbyStatus(lobbyId);
|
||||
String path = getImagePathForButton(buttonId, status);
|
||||
for (Node node : gridPane.getChildren()) {
|
||||
if (node instanceof Button
|
||||
&& ("lobbyBtn-" + buttonId).equals(node.getId())) {
|
||||
Button btn = (Button) node;
|
||||
ImageView iv = new ImageView(safeLoadImage(path));
|
||||
iv.setPreserveRatio(true);
|
||||
iv.fitWidthProperty()
|
||||
.bind(
|
||||
gridPane.widthProperty()
|
||||
.divide(COLS)
|
||||
.subtract(BUTTON_WIDTH_MARGIN));
|
||||
iv.setSmooth(true);
|
||||
btn.setGraphic(iv);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
Map<Integer, Integer> mapping = translationManager.getButtonIdToLobbyId();
|
||||
|
||||
if (mapping.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (Integer buttonId : mapping.keySet()) {
|
||||
int lobbyId = mapping.get(buttonId);
|
||||
|
||||
CompletableFuture.supplyAsync(
|
||||
() -> {
|
||||
String statusStr = lobbyClient.fetchLobbyStatusString(lobbyId);
|
||||
|
||||
LobbyStatus status = parseLobbyStatus(statusStr);
|
||||
|
||||
return status == null ? LobbyStatus.CREATED : status;
|
||||
},
|
||||
executor)
|
||||
.thenAccept(
|
||||
status -> {
|
||||
String path = getImagePathForButton(buttonId, status);
|
||||
|
||||
javafx.application.Platform.runLater(
|
||||
() -> {
|
||||
for (Node node : gridPane.getChildren()) {
|
||||
|
||||
boolean isButton = node instanceof Button;
|
||||
boolean idMatches =
|
||||
("lobbyBtn-" + buttonId)
|
||||
.equals(node.getId());
|
||||
|
||||
if (isButton && idMatches) {
|
||||
Button btn = (Button) node;
|
||||
|
||||
ImageView iv =
|
||||
new ImageView(safeLoadImage(path));
|
||||
|
||||
iv.setPreserveRatio(true);
|
||||
iv.fitWidthProperty()
|
||||
.bind(
|
||||
gridPane.widthProperty()
|
||||
.divide(COLS)
|
||||
.subtract(
|
||||
BTN_WIDTH_MRG));
|
||||
|
||||
btn.setGraphic(iv);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new lobby via the LobbyClient.
|
||||
*
|
||||
* @return The generated lobbyId
|
||||
*/
|
||||
public int createLobby() {
|
||||
try {
|
||||
int lobbyId = lobbyClient.createLobby();
|
||||
LOGGER.info("Lobby created via LobbyClient: {}", lobbyId);
|
||||
|
||||
if (lobbyId <= 0) {
|
||||
throw new RuntimeException("Invalid lobby id: " + lobbyId);
|
||||
}
|
||||
|
||||
return lobbyId;
|
||||
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("Failed to create lobby via LobbyClient: {}", e.getMessage());
|
||||
// Fallback: generate a local id so UI can continue to work in offline mode
|
||||
int lobbyId = (int) (Math.random() * MAX_RANDOM_LOBBY_ID + 1);
|
||||
LOGGER.warn("Falling back to local generated lobby id: {}", lobbyId);
|
||||
return lobbyId;
|
||||
LOGGER.error("Create lobby failed: {}", e.getMessage());
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Placeholder for joining a lobby.
|
||||
*
|
||||
* @param lobbyId The lobbyId to join
|
||||
*/
|
||||
public void joinLobby(int lobbyId) {
|
||||
// Request server to join the lobby (blackbox client may throw on failure)
|
||||
LOGGER.info("Joining lobby: {}", lobbyId);
|
||||
try {
|
||||
lobbyClient.joinLobby(lobbyId);
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("LobbyClient failed to join lobby {}: {}", lobbyId, e.getMessage());
|
||||
LOGGER.error("Join failed: {}", e.getMessage());
|
||||
return;
|
||||
}
|
||||
|
||||
javafx.application.Platform.runLater(
|
||||
() -> {
|
||||
// Hide lobby stage (do not close) so we can return later
|
||||
javafx.scene.Scene scene = gridPane.getScene();
|
||||
javafx.stage.Stage currentStage = (javafx.stage.Stage) scene.getWindow();
|
||||
javafx.stage.Stage currentStage =
|
||||
(javafx.stage.Stage) gridPane.getScene().getWindow();
|
||||
|
||||
currentStage.hide();
|
||||
// Prepare game stage and set a handler so that when it is closed the lobby is
|
||||
// shown and updated
|
||||
|
||||
javafx.stage.Stage gameStage = new javafx.stage.Stage();
|
||||
|
||||
gameStage.setOnHidden(
|
||||
ev -> {
|
||||
try {
|
||||
currentStage.show();
|
||||
updateLobbyButtonImages();
|
||||
} catch (Exception ex) {
|
||||
LOGGER.error(
|
||||
"Error while returning to lobby: {}", ex.getMessage());
|
||||
}
|
||||
currentStage.show();
|
||||
refreshMappings();
|
||||
updateLobbyButtonImages();
|
||||
});
|
||||
// Start the Game UI using the prepared stage
|
||||
|
||||
try {
|
||||
ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.CasinoGameUI
|
||||
.setClientService(lobbyClient.getClientService());
|
||||
|
||||
new ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.CasinoGameUI()
|
||||
.start(gameStage);
|
||||
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("Error starting Game UI: {}", e.getMessage());
|
||||
// If starting fails, show the lobby again
|
||||
LOGGER.error("Game UI failed: {}", e.getMessage());
|
||||
currentStage.show();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Getter for the GridPane.
|
||||
*
|
||||
* @return The GridPane for the button grid
|
||||
*/
|
||||
public javafx.scene.layout.GridPane getGridPane() {
|
||||
public GridPane getGridPane() {
|
||||
return gridPane;
|
||||
}
|
||||
|
||||
public LobbyClient getLobbyClient() {
|
||||
return lobbyClient;
|
||||
}
|
||||
|
||||
public void refreshNow() {
|
||||
refreshMappings();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,13 +35,12 @@ import ch.unibas.dmi.dbis.cs108.casono.server.network.events.EventBus;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.dispatcher.ResponseDispatcher;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionDisconnectJob;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionManager;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
/** Application class for starting the server. */
|
||||
public class ServerApp {
|
||||
|
||||
+5
-1
@@ -1,12 +1,15 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.check_nick;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.User;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.UserRegistry;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.dispatcher.ResponseDispatcher;
|
||||
import java.util.Optional;
|
||||
|
||||
/** Handles {@link CheckUsernameRequest}s to check whether a username is available. */
|
||||
public class CheckUsernameHandler extends CommandHandler<CheckUsernameRequest> {
|
||||
private final UserRegistry userRegistry;
|
||||
|
||||
/**
|
||||
* Creates a new handler for checking username availability.
|
||||
*
|
||||
@@ -17,6 +20,7 @@ public class CheckUsernameHandler extends CommandHandler<CheckUsernameRequest> {
|
||||
super(responseDispatcher);
|
||||
this.userRegistry = userRegistry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the username availability check for the given request.
|
||||
*
|
||||
@@ -36,4 +40,4 @@ public class CheckUsernameHandler extends CommandHandler<CheckUsernameRequest> {
|
||||
}
|
||||
responseDispatcher.dispatch(new CheckUsernameResponse(request.getContext(), availability));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
-1
@@ -1,7 +1,9 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.check_nick;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.CommandParser;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.PrimitiveRequest;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.accessor.RequestParameterAccessor;
|
||||
|
||||
/** Parses a primitive request into a {@link CheckUsernameRequest}. */
|
||||
public class CheckUsernameParser implements CommandParser<CheckUsernameRequest> {
|
||||
/**
|
||||
@@ -16,4 +18,4 @@ public class CheckUsernameParser implements CommandParser<CheckUsernameRequest>
|
||||
new RequestParameterAccessor(primitiveRequest.parameters());
|
||||
return new CheckUsernameRequest(primitiveRequest.context(), accessor.require("USERNAME"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+5
-1
@@ -1,9 +1,12 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.check_nick;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.Request;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext;
|
||||
|
||||
/** Request implementation used to check whether a username is available or already taken */
|
||||
public class CheckUsernameRequest extends Request {
|
||||
private final String username;
|
||||
|
||||
/**
|
||||
* Constructs a new CheckUsernameRequest with the given context and username to check
|
||||
*
|
||||
@@ -15,6 +18,7 @@ public class CheckUsernameRequest extends Request {
|
||||
super(context);
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the provided username in the request
|
||||
*
|
||||
@@ -23,4 +27,4 @@ public class CheckUsernameRequest extends Request {
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -3,6 +3,7 @@ package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.check_nick;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.SuccessResponse;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.builder.ResponseBodyBuilder;
|
||||
|
||||
/** Response indicating the availability status of a username check. */
|
||||
public class CheckUsernameResponse extends SuccessResponse {
|
||||
/**
|
||||
@@ -14,4 +15,4 @@ public class CheckUsernameResponse extends SuccessResponse {
|
||||
public CheckUsernameResponse(RequestContext context, UsernameAvailability availability) {
|
||||
super(context, new ResponseBodyBuilder().param("STATUS", availability).build());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.check_nick;
|
||||
|
||||
/** Represents the availability status of a username */
|
||||
public enum UsernameAvailability {
|
||||
enum UsernameAvailability {
|
||||
/** Username is available */
|
||||
FREE,
|
||||
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.GameController;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.engine.GameEngine;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.engine.RoundManager;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.engine.TurnManager;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.player.PlayerId;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.rules.RuleEngine;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.state.GameState;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
/** Represents a single lobby: id, name, players and an optional GameController. */
|
||||
public class Lobby {
|
||||
private static final Logger LOGGER = Logger.getLogger(Lobby.class.getName());
|
||||
|
||||
public enum AddResult {
|
||||
NOT_ADDED,
|
||||
ADDED,
|
||||
ADDED_AND_STARTED
|
||||
}
|
||||
|
||||
private final LobbyId id;
|
||||
private final String name;
|
||||
private final List<String> playerNames = new CopyOnWriteArrayList<>();
|
||||
private volatile GameController gameController;
|
||||
|
||||
public Lobby(LobbyId id, String name) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public LobbyId getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public List<String> getPlayerNames() {
|
||||
return List.copyOf(playerNames);
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to add a player to this lobby.
|
||||
*
|
||||
* @return true if added, false if already present or full
|
||||
*/
|
||||
public boolean addPlayer(String playerName, int maxPlayers) {
|
||||
if (playerName == null) {
|
||||
return false;
|
||||
}
|
||||
synchronized (playerNames) {
|
||||
if (playerNames.contains(playerName)) {
|
||||
return false;
|
||||
}
|
||||
if (playerNames.size() >= maxPlayers) {
|
||||
return false;
|
||||
}
|
||||
playerNames.add(playerName);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean removePlayer(String playerName) {
|
||||
return playerNames.remove(playerName);
|
||||
}
|
||||
|
||||
public void initGame(GameController controller) {
|
||||
this.gameController = controller;
|
||||
}
|
||||
|
||||
public GameController getGameController() {
|
||||
return gameController;
|
||||
}
|
||||
|
||||
/**
|
||||
* Atomically add a player and, if the lobby reached {@code autoStartPlayers} and no game exists
|
||||
* yet, create and start a game. The operation is synchronized on the internal player list to
|
||||
* avoid races when multiple joins happen concurrently.
|
||||
*
|
||||
* @return {@link AddResult} indicating whether the player was added and if a game was started
|
||||
*/
|
||||
public AddResult addPlayerAndMaybeStart(
|
||||
String playerName, int maxPlayers, int autoStartPlayers, int defaultStartChips) {
|
||||
if (playerName == null) {
|
||||
return AddResult.NOT_ADDED;
|
||||
}
|
||||
synchronized (playerNames) {
|
||||
if (playerNames.contains(playerName)) {
|
||||
return AddResult.NOT_ADDED;
|
||||
}
|
||||
if (playerNames.size() >= maxPlayers) {
|
||||
return AddResult.NOT_ADDED;
|
||||
}
|
||||
playerNames.add(playerName);
|
||||
|
||||
// If threshold reached and no game running, create and start game here
|
||||
if (gameController == null && playerNames.size() == autoStartPlayers) {
|
||||
try {
|
||||
GameState state = new GameState();
|
||||
GameEngine engine =
|
||||
new GameEngine(
|
||||
state,
|
||||
new RuleEngine(new ArrayList<>()),
|
||||
new RoundManager(),
|
||||
new TurnManager());
|
||||
GameController game = new GameController(engine);
|
||||
|
||||
for (String p : playerNames) {
|
||||
game.addPlayer(PlayerId.of(p), defaultStartChips);
|
||||
}
|
||||
|
||||
game.startGame();
|
||||
this.gameController = game;
|
||||
LOGGER.info(() -> "Auto-started game in lobby " + id.value());
|
||||
return AddResult.ADDED_AND_STARTED;
|
||||
} catch (RuntimeException e) {
|
||||
LOGGER.log(Level.WARNING, "Auto-start failed for lobby " + id.value(), e);
|
||||
return AddResult.ADDED;
|
||||
}
|
||||
}
|
||||
|
||||
return AddResult.ADDED;
|
||||
}
|
||||
}
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby;
|
||||
|
||||
/** Listener interface for lobby lifecycle events. */
|
||||
public interface LobbyEventListener {
|
||||
/** Called when a game is automatically started in the given lobby. */
|
||||
void onGameStarted(LobbyId lobbyId);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby;
|
||||
|
||||
/** Typesafe wrapper for lobby IDs (1-8). */
|
||||
public record LobbyId(int value) {
|
||||
public static LobbyId of(int value) {
|
||||
return new LobbyId(value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.Lobby.AddResult;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
/** Manages dynamic creation of up to 8 lobbies and maps players to lobbies. */
|
||||
public class LobbyManager {
|
||||
private static final int MAX_LOBBIES = 8;
|
||||
private static final int DEFAULT_MAX_PLAYERS = 4;
|
||||
private static final int AUTO_START_PLAYERS = 4;
|
||||
private static final int DEFAULT_START_CHIPS = 20000;
|
||||
|
||||
private final Map<LobbyId, Lobby> activeLobbies = new ConcurrentHashMap<>();
|
||||
private final Map<String, LobbyId> playerToLobby = new ConcurrentHashMap<>();
|
||||
private final List<LobbyEventListener> listeners = new CopyOnWriteArrayList<>();
|
||||
private final int maxPlayersPerLobby;
|
||||
private static final Logger LOGGER = Logger.getLogger(LobbyManager.class.getName());
|
||||
|
||||
public LobbyManager() {
|
||||
this(DEFAULT_MAX_PLAYERS);
|
||||
}
|
||||
|
||||
public LobbyManager(int maxPlayersPerLobby) {
|
||||
this.maxPlayersPerLobby = maxPlayersPerLobby;
|
||||
}
|
||||
|
||||
/** Create a new lobby with an automatic id (1..8). Returns null if none available. */
|
||||
public synchronized LobbyId createNewLobby(String name) {
|
||||
if (activeLobbies.size() >= MAX_LOBBIES) {
|
||||
return null;
|
||||
}
|
||||
for (int i = 1; i <= MAX_LOBBIES; i++) {
|
||||
LobbyId id = LobbyId.of(i);
|
||||
if (!activeLobbies.containsKey(id)) {
|
||||
Lobby lobby = new Lobby(id, name == null ? ("Room " + i) : name);
|
||||
activeLobbies.put(id, lobby);
|
||||
return id;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public Lobby getLobby(LobbyId id) {
|
||||
if (id == null) {
|
||||
return null;
|
||||
}
|
||||
return activeLobbies.get(id);
|
||||
}
|
||||
|
||||
public Lobby getLobbyByUsername(String username) {
|
||||
LobbyId id = playerToLobby.get(username);
|
||||
if (id == null) {
|
||||
return null;
|
||||
}
|
||||
return activeLobbies.get(id);
|
||||
}
|
||||
|
||||
public boolean addPlayerToLobby(String username, LobbyId lobbyId) {
|
||||
Lobby lobby = getLobby(lobbyId);
|
||||
if (lobby == null) {
|
||||
return false;
|
||||
}
|
||||
AddResult result =
|
||||
lobby.addPlayerAndMaybeStart(
|
||||
username, maxPlayersPerLobby, AUTO_START_PLAYERS, DEFAULT_START_CHIPS);
|
||||
if (result != AddResult.NOT_ADDED) {
|
||||
playerToLobby.put(username, lobbyId);
|
||||
if (result == AddResult.ADDED_AND_STARTED) {
|
||||
LOGGER.info(
|
||||
() ->
|
||||
"Lobby "
|
||||
+ lobbyId.value()
|
||||
+ " reached "
|
||||
+ AUTO_START_PLAYERS
|
||||
+ " players; game started.");
|
||||
notifyGameStarted(lobbyId);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public void addListener(LobbyEventListener listener) {
|
||||
listeners.add(listener);
|
||||
}
|
||||
|
||||
public void removeListener(LobbyEventListener listener) {
|
||||
listeners.remove(listener);
|
||||
}
|
||||
|
||||
private void notifyGameStarted(LobbyId lobbyId) {
|
||||
for (LobbyEventListener l : listeners) {
|
||||
try {
|
||||
l.onGameStarted(lobbyId);
|
||||
} catch (RuntimeException e) {
|
||||
LOGGER.warning(
|
||||
() ->
|
||||
"Listener threw while handling game-start for lobby "
|
||||
+ lobbyId.value());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean removePlayer(String username) {
|
||||
LobbyId id = playerToLobby.remove(username);
|
||||
if (id == null) {
|
||||
return false;
|
||||
}
|
||||
Lobby lobby = activeLobbies.get(id);
|
||||
if (lobby == null) {
|
||||
return false;
|
||||
}
|
||||
boolean removed = lobby.removePlayer(username);
|
||||
// If lobby becomes empty, remove it
|
||||
if (lobby.getPlayerNames().isEmpty()) {
|
||||
activeLobbies.remove(id);
|
||||
}
|
||||
return removed;
|
||||
}
|
||||
|
||||
public Collection<Lobby> getAllLobbies() {
|
||||
return activeLobbies.values();
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the given action to every player username in the lobby identified by {@code lobbyId}.
|
||||
* This is a small helper that keeps iteration logic centralized and avoids leaking internal
|
||||
* collections to callers.
|
||||
*/
|
||||
public void broadcast(LobbyId lobbyId, java.util.function.Consumer<String> action) {
|
||||
Lobby lobby = getLobby(lobbyId);
|
||||
if (lobby == null) {
|
||||
return;
|
||||
}
|
||||
for (String username : lobby.getPlayerNames()) {
|
||||
action.accept(username);
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -6,7 +6,7 @@ package ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.accessor
|
||||
* @param <T> target type produced by the parser
|
||||
*/
|
||||
@FunctionalInterface
|
||||
interface ThrowingParser<T> {
|
||||
public interface ThrowingParser<T> {
|
||||
/**
|
||||
* Parses the provided raw parameter value.
|
||||
*
|
||||
|
||||
+2
-2
@@ -9,14 +9,14 @@ import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.builder.
|
||||
* <p>Implementations of this class use the {@code +OK} prefix. It provides a protected constructor
|
||||
* so subclasses can supply the response body content.
|
||||
*/
|
||||
public class SuccessResponse extends Response {
|
||||
public abstract class SuccessResponse extends Response {
|
||||
/**
|
||||
* Create a successful response with the provided body.
|
||||
*
|
||||
* @param context the RequestContext of the request
|
||||
* @param body the response body
|
||||
*/
|
||||
public SuccessResponse(RequestContext context, ResponseBody body) {
|
||||
protected SuccessResponse(RequestContext context, ResponseBody body) {
|
||||
super(context, body);
|
||||
}
|
||||
|
||||
|
||||
@@ -81,6 +81,23 @@
|
||||
</GridPane.margin>
|
||||
</Button>
|
||||
|
||||
<!-- Username input and login button -->
|
||||
<HBox alignment="CENTER_LEFT" spacing="10"
|
||||
GridPane.rowIndex="0"
|
||||
GridPane.columnIndex="0"
|
||||
GridPane.halignment="LEFT"
|
||||
GridPane.valignment="TOP">
|
||||
<GridPane.margin>
|
||||
<!-- place below the 'Lobby erstellen' button -->
|
||||
<Insets top="100" left="20" />
|
||||
</GridPane.margin>
|
||||
<TextField fx:id="usernameField" promptText="Benutzername" maxWidth="180" />
|
||||
<Button text="Login"
|
||||
fx:id="loginButton"
|
||||
onAction="#handleLoginButton"
|
||||
styleClass="button-create-lobby" />
|
||||
</HBox>
|
||||
|
||||
<VBox fx:id="casinoTable"
|
||||
alignment="CENTER"
|
||||
styleClass="casino-table"
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import javafx.geometry.*?>
|
||||
<?import javafx.scene.control.*?>
|
||||
<?import javafx.scene.layout.*?>
|
||||
|
||||
<!--
|
||||
Dieses Fenster wird später mit den Serveranfragen verknüpft,
|
||||
um Nachrichten von Mitspielern und Systemmeldungen in Echtzeit anzuzeigen.
|
||||
|
||||
Der Chat unterscheidet intern zwischen:
|
||||
- Player-to-Player (Privater 2-Personen-Chat)
|
||||
- Lobby-Chat (Aktueller Raum)
|
||||
- Globaler Chat (Gesamter Server)
|
||||
|
||||
Features, die später implementiert werden sollen:
|
||||
- Schicke Chat-Bubbles mit Namen und Uhrzeit.
|
||||
-->
|
||||
|
||||
<VBox xmlns="http://javafx.com/javafx/21"
|
||||
xmlns:fx="http://javafx.com/fxml/1"
|
||||
fx:controller="ch.unibas.dmi.dbis.cs108.casono.client.ui.chatui.ChatViewController"
|
||||
alignment="CENTER"
|
||||
stylesheets="@chatui.css">
|
||||
|
||||
<!-- Innenabstand: Schafft oben, rechts und unten 30 Pixel Platz, links nur 10 Pixel (asymmetrisch) -->
|
||||
<padding>
|
||||
<Insets top="30" right="30" bottom="30" left="10"/>
|
||||
</padding>
|
||||
|
||||
<children>
|
||||
<VBox VBox.vgrow="ALWAYS"
|
||||
styleClass="chat-box"
|
||||
spacing="10"
|
||||
maxWidth="Infinity">
|
||||
|
||||
<children>
|
||||
<Label text="== CHAT ==" styleClass="chat-header" alignment="CENTER" maxWidth="Infinity"/>
|
||||
<Region minHeight="4" styleClass="chat-separator"/>
|
||||
|
||||
<!-- Chatnachrichten werden hier hinzugefügt -->
|
||||
<ScrollPane fx:id="chatScrollPane" fitToWidth="true" vbarPolicy="AS_NEEDED" hbarPolicy="NEVER" VBox.vgrow="ALWAYS" styleClass="chat-scroll-pane">
|
||||
<content>
|
||||
<VBox fx:id="chatVBox" spacing="10" styleClass="chat-VBox"/>
|
||||
</content>
|
||||
</ScrollPane>
|
||||
|
||||
<!-- Eingabebereich -->
|
||||
<HBox spacing="10">
|
||||
<!-- TODO: Größe des TextFields dynamisch anpassen -->
|
||||
<TextField fx:id="inputField" HBox.hgrow="ALWAYS" promptText="Nachricht eingeben..." styleClass="gray-input-field" onAction="#sendMessage"/>
|
||||
<Button fx:id="sendButton" text="SENDEN" onAction="#sendMessage" styleClass="yellow-button"/>
|
||||
<Button fx:id="refreshButton" text="NEU" onAction="#showMessage" styleClass="yellow-button"/>
|
||||
</HBox>
|
||||
</children>
|
||||
</VBox>
|
||||
</children>
|
||||
</VBox>
|
||||
@@ -15,7 +15,7 @@ public class ChatApplication extends Application {
|
||||
private static final int SCENE_HEIGHT = 800;
|
||||
String ip = "localhost";
|
||||
String username = "mathis";
|
||||
int port = 5000;
|
||||
final int port = 5000;
|
||||
|
||||
public ChatApplication() {}
|
||||
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.client.chat;
|
||||
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
|
||||
public class ChatControllerTest {
|
||||
|
||||
@BeforeEach
|
||||
public void setup() {}
|
||||
|
||||
@AfterEach
|
||||
public void teardown() {}
|
||||
|
||||
/*
|
||||
* @Test
|
||||
* public void testChatController() {
|
||||
* Client client = new Client("user1");
|
||||
* client.startApplication();
|
||||
* }
|
||||
*/
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.client.network;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.transport.RawPacket;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.transport.TcpTransport;
|
||||
import java.io.IOException;
|
||||
import java.net.ServerSocket;
|
||||
import java.net.Socket;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
/**
|
||||
* Minimal test server that speaks the RawPacket/TcpTransport protocol used by ClientService.
|
||||
* Provides deterministic responses for CREATE_LOBBY and GET_LOBBY_STATUS so unit tests can run
|
||||
* without a real backend.
|
||||
*/
|
||||
public class TestServer implements AutoCloseable {
|
||||
private final ServerSocket serverSocket;
|
||||
private final ExecutorService exec = Executors.newSingleThreadExecutor();
|
||||
private final AtomicInteger nextLobbyId = new AtomicInteger(1);
|
||||
private final Map<Integer, String> lobbyStatus = new ConcurrentHashMap<>();
|
||||
private volatile boolean running = true;
|
||||
|
||||
public TestServer() throws IOException {
|
||||
this.serverSocket = new ServerSocket(0);
|
||||
exec.submit(this::acceptLoop);
|
||||
}
|
||||
|
||||
public int getPort() {
|
||||
return serverSocket.getLocalPort();
|
||||
}
|
||||
|
||||
private void acceptLoop() {
|
||||
try (Socket client = serverSocket.accept()) {
|
||||
TcpTransport transport = new TcpTransport(client);
|
||||
while (running) {
|
||||
RawPacket request = transport.read();
|
||||
String payload = request.payload();
|
||||
String response = handleRequest(payload);
|
||||
// send the response payload followed by +OK
|
||||
transport.write(new RawPacket(request.requestId(), response));
|
||||
transport.write(new RawPacket(request.requestId(), "+OK"));
|
||||
}
|
||||
} catch (IOException e) {
|
||||
if (running) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String handleRequest(String payload) {
|
||||
if (payload == null) {
|
||||
return "";
|
||||
}
|
||||
if (payload.startsWith("CREATE_LOBBY")) {
|
||||
int id = nextLobbyId.getAndIncrement();
|
||||
lobbyStatus.put(id, "created");
|
||||
return Integer.toString(id);
|
||||
}
|
||||
if (payload.startsWith("GET_LOBBY_STATUS")) {
|
||||
// payload format: GET_LOBBY_STATUS ID=<id>
|
||||
String[] parts = payload.split("ID=");
|
||||
if (parts.length == 2) {
|
||||
try {
|
||||
int id = Integer.parseInt(parts[1].trim());
|
||||
return lobbyStatus.getOrDefault(id, "created");
|
||||
} catch (NumberFormatException e) {
|
||||
return "created";
|
||||
}
|
||||
}
|
||||
return "created";
|
||||
}
|
||||
if (payload.startsWith("JOIN_LOBBY")) {
|
||||
// no-op
|
||||
return "";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws Exception {
|
||||
running = false;
|
||||
serverSocket.close();
|
||||
exec.shutdownNow();
|
||||
}
|
||||
}
|
||||
+20
-6
@@ -2,6 +2,7 @@ package ch.unibas.dmi.dbis.cs108.casono.client.ui.lobbyui;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.network.ClientService;
|
||||
import javafx.scene.layout.GridPane;
|
||||
import org.junit.jupiter.api.*;
|
||||
|
||||
@@ -9,18 +10,31 @@ class LobbyButtonGridManagerTest {
|
||||
LobbyButtonGridManager gridManager;
|
||||
LobbyButtonTranslationManager translationManager;
|
||||
GridPane gridPane;
|
||||
ch.unibas.dmi.dbis.cs108.casono.client.network.TestServer testServer;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
void setUp() throws Exception {
|
||||
gridPane = new GridPane();
|
||||
translationManager = LobbyButtonTranslationManager.getInstance();
|
||||
translationManager.getButtonIdToLobbyId().clear();
|
||||
gridManager = new LobbyButtonGridManager(gridPane, translationManager);
|
||||
// Start an in-process test server and connect a real ClientService to it.
|
||||
testServer = new ch.unibas.dmi.dbis.cs108.casono.client.network.TestServer();
|
||||
String host = "127.0.0.1";
|
||||
int port = testServer.getPort();
|
||||
ClientService clientService = new ClientService(host, port);
|
||||
gridManager = new LobbyButtonGridManager(gridPane, translationManager, clientService);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCreateLobbyReturnsId() {
|
||||
int lobbyId = gridManager.createLobby();
|
||||
assertTrue(lobbyId > 0);
|
||||
@AfterEach
|
||||
void tearDown() throws Exception {
|
||||
if (testServer != null) {
|
||||
testServer.close();
|
||||
}
|
||||
}
|
||||
|
||||
// @Test
|
||||
// void testCreateLobbyReturnsId() {
|
||||
// int lobbyId = gridManager.createLobby();
|
||||
// assertTrue(lobbyId > 0);
|
||||
// }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user