Merge branch 'main' into feat/100-add-start-game-command-on-server-side
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
+24
-13
@@ -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",
|
||||
|
||||
+108
@@ -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()));
|
||||
}
|
||||
}
|
||||
+40
@@ -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);
|
||||
}
|
||||
}
|
||||
+45
@@ -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;
|
||||
}
|
||||
}
|
||||
+96
@@ -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()));
|
||||
}
|
||||
}
|
||||
+28
@@ -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);
|
||||
}
|
||||
}
|
||||
+34
@@ -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;
|
||||
}
|
||||
}
|
||||
+96
@@ -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()));
|
||||
}
|
||||
}
|
||||
+28
@@ -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);
|
||||
}
|
||||
}
|
||||
+34
@@ -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;
|
||||
}
|
||||
}
|
||||
+110
@@ -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()));
|
||||
}
|
||||
}
|
||||
+34
@@ -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);
|
||||
}
|
||||
}
|
||||
+45
@@ -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;
|
||||
}
|
||||
}
|
||||
+32
@@ -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()));
|
||||
}
|
||||
}
|
||||
+11
@@ -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());
|
||||
}
|
||||
}
|
||||
+10
@@ -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);
|
||||
}
|
||||
}
|
||||
+14
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+47
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user