From f91642bade6bcb2bd7f97868cdf8801bb3e1dc30 Mon Sep 17 00:00:00 2001 From: Jona Walpert Date: Sun, 12 Apr 2026 15:13:33 +0200 Subject: [PATCH 01/21] Fix: fetch lobbies on client startup --- .../casono/client/network/LobbyClient.java | 77 +++++++++++++++++++ .../ui/lobbyui/CasinomainuiController.java | 18 +++++ 2 files changed, 95 insertions(+) diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/LobbyClient.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/LobbyClient.java index 2f48eef..0e9ae35 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/LobbyClient.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/LobbyClient.java @@ -1,6 +1,7 @@ package ch.unibas.dmi.dbis.cs108.casono.client.network; import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.RequestParameter; +import java.util.ArrayList; import java.util.List; /** @@ -129,4 +130,80 @@ public class LobbyClient { } return new LoginResult(assigned, id); } + + /** + * Request the server for the list of available lobbies. + * + * @return list of LobbyInfo objects representing current lobbies + */ + public List getLobbyList() { + List lines = client.processCommand("GET_LOBBY_LIST"); + + List params = ClientService.convertToRequestParameters(lines); + + List result = new ArrayList<>(); + + Integer currentId = null; + String currentName = null; + Integer currentPlayerCount = null; + + for (RequestParameter p : params) { + String key = p.key().toUpperCase(); + String val = p.value(); + switch (key) { + case "ID": + // If we were collecting a lobby, flush it + if (currentId != null) { + result.add( + new LobbyInfo( + currentId, + currentName, + currentPlayerCount == null ? 0 : currentPlayerCount)); + currentName = null; + currentPlayerCount = null; + } + try { + currentId = Integer.parseInt(val); + } catch (NumberFormatException e) { + currentId = null; + } + break; + case "NAME": + currentName = val; + break; + case "PLAYER_COUNT": + try { + currentPlayerCount = Integer.parseInt(val); + } catch (NumberFormatException e) { + currentPlayerCount = 0; + } + break; + default: + break; + } + } + + if (currentId != null) { + result.add( + new LobbyInfo( + currentId, + currentName, + currentPlayerCount == null ? 0 : currentPlayerCount)); + } + + return result; + } + + /** Simple data holder for lobby metadata returned by the server. */ + public static final class LobbyInfo { + public final int id; + public final String name; + public final int playerCount; + + public LobbyInfo(int id, String name, int playerCount) { + this.id = id; + this.name = name; + this.playerCount = playerCount; + } + } } diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/lobbyui/CasinomainuiController.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/lobbyui/CasinomainuiController.java index 701f092..c06315f 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/lobbyui/CasinomainuiController.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/lobbyui/CasinomainuiController.java @@ -75,6 +75,24 @@ public class CasinomainuiController { // LobbyClient will use the provided ClientService; in offline mode calls will // fail with RuntimeException lobbyClient = new LobbyClient(clientService); + // Fetch existing lobbies from server on startup so newly-created lobbies + // by other clients are immediately visible. + try { + if (!lobbyClient.getClientService().isOffline()) { + var lobbies = lobbyClient.getLobbyList(); + int bid = nextButtonId; + for (var li : lobbies) { + try { + translationManager.addLobbyButton(bid++, li.id); + } catch (Exception e) { + LOGGER.warn("Could not add lobby button: {}", e.getMessage()); + } + } + nextButtonId = bid; + } + } catch (RuntimeException e) { + LOGGER.warn("Failed to fetch lobby list at startup: {}", e.getMessage()); + } casinoTable.getChildren().clear(); casinoTable.getChildren().add(gridManager.getGridPane()); gridManager.renderLobbyButtons(); -- 2.52.0 From e4f0c7efb8ed5eba64add1c9ef64575d818af538 Mon Sep 17 00:00:00 2001 From: Jona Walpert Date: Sun, 12 Apr 2026 15:16:35 +0200 Subject: [PATCH 02/21] fix: Treat SocketException as client disconnect in SessionReader --- .../network/sessions/SessionReader.java | 43 ++++++++++++------- 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/sessions/SessionReader.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/sessions/SessionReader.java index 78ffbfd..4d5e353 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/sessions/SessionReader.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/network/sessions/SessionReader.java @@ -21,6 +21,7 @@ import ch.unibas.dmi.dbis.cs108.casono.server.network.transport.RawPacket; import ch.unibas.dmi.dbis.cs108.casono.server.network.transport.TransportLayer; import java.io.EOFException; import java.io.IOException; +import java.net.SocketException; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -50,29 +51,21 @@ public class SessionReader implements Runnable { RawRequest rawRequest = null; RequestContext requestContext = null; try { - // Step 1: Read from transport rawPacket = transport.read(); session.updateLastInboundActivity(); logger.debug("Recieved: {}", rawPacket); - requestContext = new RequestContext(session.getId(), rawPacket.requestId()); - - // Step 2: Syntax validation and conversion into transport object - rawRequest = ProtocolParser.parse(rawPacket.payload()); - logger.debug("Parsed request to {}", rawRequest); - - PrimitiveRequest primitiveRequest = - new PrimitiveRequest( - requestContext, rawRequest.command(), rawRequest.parameters()); - logger.debug("Converted to {}", primitiveRequest); - - // Step 3: Parse into Request and execute Request - Request request = dispatcher.parse(primitiveRequest); - router.execute(request); + processRawPacket(rawPacket); } catch (EOFException e) { logger.info("Client disconnected"); eventBus.publish(new DisconnectEvent(session.getId())); break; + } catch (SocketException e) { + // Connection reset or other socket-level error — treat as client disconnect + logger.info("Client socket error / connection reset: {}", e.getMessage()); + eventBus.publish(new DisconnectEvent(session.getId())); + break; + } catch (TokenizerException | ProtocolParserException e) { logger.error("Error occured while parsing request. RawPacket: {}", rawPacket, e); @@ -117,6 +110,26 @@ public class SessionReader implements Runnable { } } + private void processRawPacket(RawPacket rawPacket) + throws TokenizerException, + ProtocolParserException, + UnknownCommandException, + ResponseDispatchException, + MissingParameterException, + IOException { + RawRequest rawRequest = ProtocolParser.parse(rawPacket.payload()); + logger.debug("Parsed request to {}", rawRequest); + + RequestContext requestContext = new RequestContext(session.getId(), rawPacket.requestId()); + + PrimitiveRequest primitiveRequest = + new PrimitiveRequest(requestContext, rawRequest.command(), rawRequest.parameters()); + logger.debug("Converted to {}", primitiveRequest); + + Request request = dispatcher.parse(primitiveRequest); + router.execute(request); + } + /** * Helperfunction to send ErrorResponse to client * -- 2.52.0 From 9cf6b468ad1fbf66baed3eb814cc028a822cc01c Mon Sep 17 00:00:00 2001 From: Jona Walpert Date: Sun, 12 Apr 2026 16:02:50 +0200 Subject: [PATCH 03/21] fix: correctly disaply lobbei and update them in Lobyb Screen --- .../casono/client/network/ClientService.java | 153 +++++++------- .../ui/lobbyui/LobbyButtonGridManager.java | 186 ++++++++++++++---- .../dbis/cs108/casono/server/ServerApp.java | 11 +- .../create_lobby/CreateLobbyHandler.java | 24 ++- 4 files changed, 269 insertions(+), 105 deletions(-) diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/ClientService.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/ClientService.java index 86b4089..fb4eb95 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/ClientService.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/ClientService.java @@ -24,8 +24,10 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; /** - * The ClientService class is responsible for managing the connection to the server, sending - * commands, and receiving responses. It uses a TcpTransport to communicate with the server and an + * The ClientService class is responsible for managing the connection to the + * server, sending + * commands, and receiving responses. It uses a TcpTransport to communicate with + * the server and an * ExecutorService to handle asynchronous requests. */ public class ClientService { @@ -39,20 +41,20 @@ public class ClientService { private final AtomicInteger idGenerator; private Logger logger; - private final Map> pendingResponses = - new ConcurrentHashMap<>(); - private final CopyOnWriteArrayList>> eventListeners = - new CopyOnWriteArrayList<>(); + private final Map> pendingResponses = new ConcurrentHashMap<>(); + private final CopyOnWriteArrayList>> eventListeners = new CopyOnWriteArrayList<>(); private Thread readerThread = null; private final AtomicBoolean running = new AtomicBoolean(false); private static final int READER_JOIN_TIMEOUT_MS = 500; /** - * Constructs a ClientService with the given server IP and port. It establishes a socket - * connection to the server and initializes the TcpTransport and ExecutorService for + * Constructs a ClientService with the given server IP and port. It establishes + * a socket + * connection to the server and initializes the TcpTransport and ExecutorService + * for * communication. * - * @param ip The IP address of the server to connect to. + * @param ip The IP address of the server to connect to. * @param port The port number of the server to connect to. */ public ClientService(String ip, int port) { @@ -77,25 +79,24 @@ public class ClientService { private void startReaderThread() { running.set(true); - readerThread = - new Thread( - () -> { - while (running.get()) { - try { - RawPacket rp = clienttcptransport.read(); - processRawPacket(rp); - } catch (IOException e) { - if (running.get()) { - logger.warn("IO error on transport reader", e); - } - break; - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - break; - } + readerThread = new Thread( + () -> { + while (running.get()) { + try { + RawPacket rp = clienttcptransport.read(); + processRawPacket(rp); + } catch (IOException e) { + if (running.get()) { + logger.warn("IO error on transport reader", e); } - }, - "casono-client-reader"); + break; + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + break; + } + } + }, + "casono-client-reader"); readerThread.setDaemon(true); readerThread.start(); } @@ -111,13 +112,14 @@ public class ClientService { for (String rawLine : responseText.split("\\n")) { String line = rawLine; + String trimmed = line.trim(); if (!hasStatus) { - if ("+OK".equals(line)) { + if ("+OK".equals(trimmed)) { success = true; hasStatus = true; continue; } - if (line.startsWith("-ERR") || line.startsWith("-ERROR")) { + if (trimmed.startsWith("-ERR") || trimmed.startsWith("-ERROR")) { success = false; hasStatus = true; continue; @@ -125,13 +127,12 @@ public class ClientService { continue; } - if ("END".equals(line)) { + if ("END".equals(trimmed)) { break; } - if (line.startsWith("\t")) { - line = line.substring(1); - } + // remove leading indentation (tabs/spaces) so parameters match regex parsing + line = line.replaceFirst("^\\s+", ""); lines.add(line); } @@ -152,6 +153,7 @@ public class ClientService { if (rid == 0) { for (Consumer> l : eventListeners) { try { + logger.debug("Parsed response lines: {}", lines); l.accept(List.copyOf(lines)); } catch (Exception e) { logger.warn("Event listener threw", e); @@ -168,7 +170,8 @@ public class ClientService { } /** - * Constructs a ClientService in offline mode. No network connection will be attempted and calls + * 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 @@ -181,19 +184,21 @@ public class ClientService { this.executor = Executors.newSingleThreadExecutor(); } - /** Returns true if this ClientService is running in offline mode (no network). */ + /** + * Returns true if this ClientService is running in offline mode (no network). + */ public boolean isOffline() { return offlineMode; } // Allow primitive values to contain hyphens (UUIDs) in addition to // digits/words/colons - static Pattern responseRex = - Pattern.compile( - "(?\\w+)=(('(?([^']|\\')+)')|(?[+-]?[-\\d\\w:]+))"); + static Pattern responseRex = Pattern.compile( + "(?\\w+)=(('(?([^']|\\')+)')|(?[+-]?[-\\d\\w:]+))"); /** - * Removes escape characters from a string, specifically converting escaped single quotes (\') + * Removes escape characters from a string, specifically converting escaped + * single quotes (\') * back to regular single quotes ('). * * @param input The escaped string to process. @@ -204,8 +209,10 @@ public class ClientService { } /** - * Converts a list of raw string parameters into a list of {@link RequestParameter} objects. It - * uses a regex matcher to distinguish between quoted strings (which are unescaped) and + * Converts a list of raw string parameters into a list of + * {@link RequestParameter} objects. It + * uses a regex matcher to distinguish between quoted strings (which are + * unescaped) and * primitive values. * * @param input A list of raw strings to be parsed. @@ -230,10 +237,12 @@ public class ClientService { .toList(); } - private static record ParsedResponse(boolean success, List lines) {} + private static record ParsedResponse(boolean success, List lines) { + } /** - * Register an event listener that receives unsolicited event payload lines (no status prefix). + * Register an event listener that receives unsolicited event payload lines (no + * status prefix). */ public void addEventListener(Consumer> listener) { eventListeners.add(listener); @@ -244,14 +253,18 @@ public class ClientService { } /** - * Sends a command to the server and processes the multi-line response. It handles the protocol - * handshake (expecting +OK), strips leading tabs from response lines, and collects them until + * Sends a command to the server and processes the multi-line response. It + * handles the protocol + * handshake (expecting +OK), strips leading tabs from response lines, and + * collects them until * the "END" marker is reached. * * @param message The raw command string to be sent to the transport layer. - * @return A list of response lines received from the server (excluding protocol markers). - * @throws RuntimeException if the server responds with an error or if a communication failure - * occurs. + * @return A list of response lines received from the server (excluding protocol + * markers). + * @throws RuntimeException if the server responds with an error or if a + * communication failure + * occurs. */ protected List processCommand(String message) { if (offlineMode) { @@ -262,15 +275,14 @@ public class ClientService { ArrayBlockingQueue q = new ArrayBlockingQueue<>(1); pendingResponses.put(reqId, q); - Future writeFuture = - executor.submit( - () -> { - try { - clienttcptransport.write(new RawPacket(reqId, message)); - } catch (IOException e) { - throw new RuntimeException(e); - } - }); + Future writeFuture = executor.submit( + () -> { + try { + clienttcptransport.write(new RawPacket(reqId, message)); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); try { writeFuture.get(); @@ -302,11 +314,15 @@ public class ClientService { } /** - * Helper method to send a request to the server using the ExecutorService. It submits the - * request as a Runnable task and waits for its completion. If the task is interrupted or - * encounters an execution exception, it throws a RuntimeException with the appropriate cause. + * Helper method to send a request to the server using the ExecutorService. It + * submits the + * request as a Runnable task and waits for its completion. If the task is + * interrupted or + * encounters an execution exception, it throws a RuntimeException with the + * appropriate cause. * - * @param request The Runnable task representing the request to be sent to the server. + * @param request The Runnable task representing the request to be sent to the + * server. */ private void sendRequest(Runnable request) { Future future = executor.submit(request); @@ -320,9 +336,12 @@ public class ClientService { } /** - * Helper method to extract the cause of an exception and return it as a RuntimeException. If - * the cause is null, it returns the original exception as a RuntimeException. If the cause is - * already a RuntimeException, it returns it directly. Otherwise, it wraps the cause in a new + * Helper method to extract the cause of an exception and return it as a + * RuntimeException. If + * the cause is null, it returns the original exception as a RuntimeException. + * If the cause is + * already a RuntimeException, it returns it directly. Otherwise, it wraps the + * cause in a new * RuntimeException and returns it. * * @param e The exception from which to extract the cause. @@ -340,8 +359,10 @@ public class ClientService { } /** - * Closes the socket connection to the server and shuts down the ExecutorService. It also closes - * the TcpTransport used for communication. If any IOException occurs during this process, it + * Closes the socket connection to the server and shuts down the + * ExecutorService. It also closes + * the TcpTransport used for communication. If any IOException occurs during + * this process, it * prints the exception to the console. */ public void closeSocket() { diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/lobbyui/LobbyButtonGridManager.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/lobbyui/LobbyButtonGridManager.java index 397d723..05fb146 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/lobbyui/LobbyButtonGridManager.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/lobbyui/LobbyButtonGridManager.java @@ -85,6 +85,8 @@ public class LobbyButtonGridManager { } catch (NumberFormatException ex) { return; } + LOGGER.info("LOBBY_CLOSED event for lobby {} — mapping before: {}", lid, + translationManager.getButtonIdToLobbyId()); // find button id(s) for this lobby and remove mapping Map mapping = translationManager.getButtonIdToLobbyId(); Integer toRemove = null; @@ -95,9 +97,46 @@ public class LobbyButtonGridManager { } } if (toRemove != null) { + LOGGER.info("Removing mapping for button {} -> lobby {} (LOBBY_CLOSED event)", toRemove, + lid); translationManager.removeLobbyButton(toRemove); + LOGGER.debug("Mapping after removal: {}", translationManager.getButtonIdToLobbyId()); javafx.application.Platform.runLater(this::renderLobbyButtons); } + } else if (evt != null && "LOBBY_CREATED".equalsIgnoreCase(evt) && lidStr != null) { + int lid; + try { + lid = Integer.parseInt(lidStr); + } catch (NumberFormatException ex) { + return; + } + + LOGGER.info("LOBBY_CREATED event for lobby {} — mapping before: {}", lid, + translationManager.getButtonIdToLobbyId()); + + Map mapping = translationManager.getButtonIdToLobbyId(); + + // If this lobby is already known, nothing to do + if (mapping.containsValue(lid)) { + LOGGER.debug("Lobby {} already mapped, skipping", lid); + return; + } + + // find next free button id (1..8) + for (int candidate = 1; candidate <= 8; candidate++) { + if (!mapping.containsKey(candidate)) { + try { + translationManager.addLobbyButton(candidate, lid); + LOGGER.info("Mapped new lobby {} to button {}", lid, candidate); + LOGGER.debug("Mapping after add: {}", translationManager.getButtonIdToLobbyId()); + javafx.application.Platform.runLater(this::renderLobbyButtons); + break; + } catch (Exception ex) { + // grid full? try next candidate + LOGGER.debug("Could not map created lobby {}: {}", lid, ex.getMessage()); + } + } + } } }); } @@ -110,8 +149,29 @@ public class LobbyButtonGridManager { private void refreshMappings() { Map mapping = translationManager.getButtonIdToLobbyId(); + // Always attempt to sync with server-side lobby list so clients that start + // after a lobby was created will discover it. This keeps the local mapping + // in sync with the server without requiring a restart. + List serverLobbies = null; + java.util.Set serverIds = new java.util.HashSet<>(); + try { + serverLobbies = lobbyClient.getLobbyList(); + if (serverLobbies != null) { + for (var li : serverLobbies) { + serverIds.add(li.id); + } + } + reconcileWithServerLobbies(serverLobbies); + } catch (Exception ex) { + LOGGER.debug("Could not fetch lobby list: {}", ex.getMessage()); + } + if (mapping.isEmpty()) { - return; + // mapping may still be empty after reconciliation + if (translationManager.getButtonIdToLobbyId().isEmpty()) { + return; + } + mapping = translationManager.getButtonIdToLobbyId(); } List> entries = new ArrayList<>(mapping.entrySet()); @@ -125,26 +185,91 @@ public class LobbyButtonGridManager { int lobbyId = lobbyIdObj.intValue(); CompletableFuture.supplyAsync( - () -> { - try { - return lobbyClient.fetchLobbyStatusString(lobbyId); - } catch (Exception ex) { - LOGGER.info("Lobby {} missing: {}", lobbyId, ex.getMessage()); - return null; - } - }, - executor) + () -> { + try { + return lobbyClient.fetchLobbyStatusString(lobbyId); + } catch (Exception ex) { + LOGGER.info("Lobby {} missing: {}", lobbyId, ex.getMessage()); + return null; + } + }, + executor) .thenAccept( status -> { + // Do not remove a mapping just because the status fetch + // temporarily returned null. Only remove mappings when + // the server no longer lists the lobby (reconcileWithServerLobbies + // handles removals based on authoritative server list). if (status == null) { - translationManager.removeLobbyButton(buttonId); - - javafx.application.Platform.runLater(this::renderLobbyButtons); + if (!serverIds.contains(lobbyId)) { + translationManager.removeLobbyButton(buttonId); + javafx.application.Platform.runLater(this::renderLobbyButtons); + } else { + // treat as created/running later; keep mapping + LOGGER.debug("Temporary missing status for lobby {} - keeping mapping", + lobbyId); + } } }); } } + private void reconcileWithServerLobbies(List serverLobbies) { + if (serverLobbies == null) { + return; + } + + Map mapping = translationManager.getButtonIdToLobbyId(); + + // Build set of lobby ids returned by server + java.util.Set serverIds = new java.util.HashSet<>(); + for (var li : serverLobbies) { + serverIds.add(li.id); + } + + LOGGER.debug("Reconciling mappings. serverIds={} mapping={}", serverIds, mapping); + + // Remove mappings for lobbies that no longer exist + java.util.List toRemove = new java.util.ArrayList<>(); + for (var e : mapping.entrySet()) { + Integer lid = e.getValue(); + if (lid == null) { + continue; + } + if (!serverIds.contains(lid)) { + toRemove.add(e.getKey()); + } + } + for (Integer bid : toRemove) { + LOGGER.info("Reconciling: removing mapping for button {} (server no longer lists lobby)", bid); + translationManager.removeLobbyButton(bid); + } + + // Add server lobbies that are not yet mapped + for (var li : serverLobbies) { + if (mapping.containsValue(li.id)) { + continue; + } + // find next free button id (1..8) + for (int candidate = 1; candidate <= 8; candidate++) { + if (!mapping.containsKey(candidate)) { + try { + translationManager.addLobbyButton(candidate, li.id); + LOGGER.info("Reconciling: added mapping button {} -> lobby {}", candidate, li.id); + break; + } catch (Exception ex) { + // grid full? try next candidate + LOGGER.debug("Could not add mapping for lobby {}: {}", li.id, ex.getMessage()); + } + } + } + } + + if (!toRemove.isEmpty() || !serverLobbies.isEmpty()) { + javafx.application.Platform.runLater(this::renderLobbyButtons); + } + } + public void renderLobbyButtons() { gridPane.getChildren().clear(); @@ -215,10 +340,8 @@ public class LobbyButtonGridManager { statusStr -> { LobbyStatus status = parseLobbyStatus(statusStr); - String path = - getImagePathForButton( - buttonId, - status == null ? LobbyStatus.CREATED : status); + String path = getImagePathForLobby( + lobbyId, status == null ? LobbyStatus.CREATED : status); Image img = safeLoadImage(path); @@ -253,11 +376,11 @@ public class LobbyButtonGridManager { } } - private String getImagePathForButton(int buttonId, LobbyStatus status) { + private String getImagePathForLobby(int lobbyId, LobbyStatus status) { String statusStr = status == LobbyStatus.CREATED ? "created" : "running"; - return String.format(BUTTON_IMAGE_TEMPLATE, buttonId, statusStr); + return String.format(BUTTON_IMAGE_TEMPLATE, lobbyId, statusStr); } private Image safeLoadImage(String path) { @@ -305,32 +428,30 @@ public class LobbyButtonGridManager { int lobbyId = lobbyIdObj.intValue(); CompletableFuture.supplyAsync( - () -> { - String statusStr = lobbyClient.fetchLobbyStatusString(lobbyId); + () -> { + String statusStr = lobbyClient.fetchLobbyStatusString(lobbyId); - LobbyStatus status = parseLobbyStatus(statusStr); + LobbyStatus status = parseLobbyStatus(statusStr); - return status == null ? LobbyStatus.CREATED : status; - }, - executor) + return status == null ? LobbyStatus.CREATED : status; + }, + executor) .thenAccept( status -> { - String path = getImagePathForButton(buttonId, status); + String path = getImagePathForLobby(lobbyId, status); javafx.application.Platform.runLater( () -> { for (Node node : gridPane.getChildren()) { boolean isButton = node instanceof Button; - boolean idMatches = - ("lobbyBtn-" + buttonId) - .equals(node.getId()); + boolean idMatches = ("lobbyBtn-" + buttonId) + .equals(node.getId()); if (isButton && idMatches) { Button btn = (Button) node; - ImageView iv = - new ImageView(safeLoadImage(path)); + ImageView iv = new ImageView(safeLoadImage(path)); iv.setPreserveRatio(true); iv.fitWidthProperty() @@ -375,8 +496,7 @@ public class LobbyButtonGridManager { javafx.application.Platform.runLater( () -> { - javafx.stage.Stage currentStage = - (javafx.stage.Stage) gridPane.getScene().getWindow(); + javafx.stage.Stage currentStage = (javafx.stage.Stage) gridPane.getScene().getWindow(); currentStage.hide(); diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/ServerApp.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/ServerApp.java index e1cf1d6..5581892 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/ServerApp.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/ServerApp.java @@ -54,7 +54,7 @@ public class ServerApp { private static final int USER_CLEANUP_JOB_RECONNECT_THRESHOLD = 10; private static final int SESSION_DISCONNECT_JOB_DELAY = 0; private static final int SESSION_DISCONNECT_JOB_PERIOD = 2; - private static final int SESSION_DISCONNECT_JOB_TIMEOUT = 5; + private static final int SESSION_DISCONNECT_JOB_TIMEOUT = 30000; private static final int LOBBY_EXPIRY_SECONDS = 30; private static final int LOBBY_CLEANUP_INITIAL_DELAY_SECONDS = 5; private static final int LOBBY_CLEANUP_PERIOD_SECONDS = 5; @@ -94,7 +94,7 @@ public class ServerApp { TimeUnit.SECONDS); LobbyManager lobbyManager = new LobbyManager(); - registerCommands(dispatcher, router, responseDispatcher, userRegistry, lobbyManager); + registerCommands(dispatcher, router, responseDispatcher, userRegistry, lobbyManager, sessionManager); // Periodic cleanup: remove empty lobbies older than 30s and notify affected // users @@ -147,7 +147,8 @@ public class ServerApp { CommandRouter commandRouter, ResponseDispatcher responseDispatcher, UserRegistry userRegistry, - LobbyManager lobbyManager) { + LobbyManager lobbyManager, + SessionManager sessionManager) { parserDispatcher.register("PING", new PingParser()); commandRouter.register(PingRequest.class, new PingHandler(responseDispatcher)); @@ -292,8 +293,8 @@ public class ServerApp { (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)); + new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.create_lobby + .CreateLobbyHandler(responseDispatcher, lobbyManager, sessionManager)); // JOIN_LOBBY registration parserDispatcher.register( diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/lobby/create_lobby/CreateLobbyHandler.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/lobby/create_lobby/CreateLobbyHandler.java index b86b145..87c9764 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/lobby/create_lobby/CreateLobbyHandler.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/lobby/create_lobby/CreateLobbyHandler.java @@ -4,14 +4,22 @@ 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.SuccessResponse; +import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.builder.ResponseBody; import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.dispatcher.ResponseDispatcher; +import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.Session; +import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionManager; +import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext; public class CreateLobbyHandler extends CommandHandler { private final LobbyManager lobbyManager; + private final SessionManager sessionManager; - public CreateLobbyHandler(ResponseDispatcher responseDispatcher, LobbyManager lobbyManager) { + public CreateLobbyHandler( + ResponseDispatcher responseDispatcher, LobbyManager lobbyManager, SessionManager sessionManager) { super(responseDispatcher); this.lobbyManager = lobbyManager; + this.sessionManager = sessionManager; } @Override @@ -28,5 +36,19 @@ public class CreateLobbyHandler extends CommandHandler { } responseDispatcher.dispatch(new CreateLobbyResponse(request.getContext(), id.value())); + + // broadcast LOBBY_CREATED event to all connected sessions (requestId=0) + for (Session s : sessionManager.getAllSessions()) { + RequestContext ctx = new RequestContext(s.getId(), 0); + SuccessResponse ev = + new SuccessResponse( + ctx, + ResponseBody.builder() + .param("EVENT", "LOBBY_CREATED") + .param("LOBBY_ID", id.value()) + .build()) {}; + + responseDispatcher.dispatch(ev); + } } } -- 2.52.0 From 0f8f1bb91894287f069ec3ae3e8ca890af5ef1ea Mon Sep 17 00:00:00 2001 From: Jona Walpert Date: Sun, 12 Apr 2026 18:36:47 +0200 Subject: [PATCH 04/21] fix: checkstyle and spotless --- .../casono/client/network/ClientService.java | 140 +++++------ .../ui/lobbyui/LobbyButtonGridManager.java | 234 ++++++++++-------- .../dbis/cs108/casono/server/ServerApp.java | 43 ++-- .../create_lobby/CreateLobbyHandler.java | 6 +- 4 files changed, 230 insertions(+), 193 deletions(-) diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/ClientService.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/ClientService.java index fb4eb95..0c89317 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/ClientService.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/ClientService.java @@ -24,10 +24,8 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; /** - * The ClientService class is responsible for managing the connection to the - * server, sending - * commands, and receiving responses. It uses a TcpTransport to communicate with - * the server and an + * The ClientService class is responsible for managing the connection to the server, sending + * commands, and receiving responses. It uses a TcpTransport to communicate with the server and an * ExecutorService to handle asynchronous requests. */ public class ClientService { @@ -41,20 +39,20 @@ public class ClientService { private final AtomicInteger idGenerator; private Logger logger; - private final Map> pendingResponses = new ConcurrentHashMap<>(); - private final CopyOnWriteArrayList>> eventListeners = new CopyOnWriteArrayList<>(); + private final Map> pendingResponses = + new ConcurrentHashMap<>(); + private final CopyOnWriteArrayList>> eventListeners = + new CopyOnWriteArrayList<>(); private Thread readerThread = null; private final AtomicBoolean running = new AtomicBoolean(false); private static final int READER_JOIN_TIMEOUT_MS = 500; /** - * Constructs a ClientService with the given server IP and port. It establishes - * a socket - * connection to the server and initializes the TcpTransport and ExecutorService - * for + * Constructs a ClientService with the given server IP and port. It establishes a socket + * connection to the server and initializes the TcpTransport and ExecutorService for * communication. * - * @param ip The IP address of the server to connect to. + * @param ip The IP address of the server to connect to. * @param port The port number of the server to connect to. */ public ClientService(String ip, int port) { @@ -79,24 +77,25 @@ public class ClientService { private void startReaderThread() { running.set(true); - readerThread = new Thread( - () -> { - while (running.get()) { - try { - RawPacket rp = clienttcptransport.read(); - processRawPacket(rp); - } catch (IOException e) { - if (running.get()) { - logger.warn("IO error on transport reader", e); + readerThread = + new Thread( + () -> { + while (running.get()) { + try { + RawPacket rp = clienttcptransport.read(); + processRawPacket(rp); + } catch (IOException e) { + if (running.get()) { + logger.warn("IO error on transport reader", e); + } + break; + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + break; + } } - break; - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - break; - } - } - }, - "casono-client-reader"); + }, + "casono-client-reader"); readerThread.setDaemon(true); readerThread.start(); } @@ -170,8 +169,7 @@ public class ClientService { } /** - * Constructs a ClientService in offline mode. No network connection will be - * attempted and calls + * 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 @@ -184,21 +182,19 @@ public class ClientService { this.executor = Executors.newSingleThreadExecutor(); } - /** - * Returns true if this ClientService is running in offline mode (no network). - */ + /** Returns true if this ClientService is running in offline mode (no network). */ public boolean isOffline() { return offlineMode; } // Allow primitive values to contain hyphens (UUIDs) in addition to // digits/words/colons - static Pattern responseRex = Pattern.compile( - "(?\\w+)=(('(?([^']|\\')+)')|(?[+-]?[-\\d\\w:]+))"); + static Pattern responseRex = + Pattern.compile( + "(?\\w+)=(('(?([^']|\\')+)')|(?[+-]?[-\\d\\w:]+))"); /** - * Removes escape characters from a string, specifically converting escaped - * single quotes (\') + * Removes escape characters from a string, specifically converting escaped single quotes (\') * back to regular single quotes ('). * * @param input The escaped string to process. @@ -209,10 +205,8 @@ public class ClientService { } /** - * Converts a list of raw string parameters into a list of - * {@link RequestParameter} objects. It - * uses a regex matcher to distinguish between quoted strings (which are - * unescaped) and + * Converts a list of raw string parameters into a list of {@link RequestParameter} objects. It + * uses a regex matcher to distinguish between quoted strings (which are unescaped) and * primitive values. * * @param input A list of raw strings to be parsed. @@ -237,12 +231,10 @@ public class ClientService { .toList(); } - private static record ParsedResponse(boolean success, List lines) { - } + private static record ParsedResponse(boolean success, List lines) {} /** - * Register an event listener that receives unsolicited event payload lines (no - * status prefix). + * Register an event listener that receives unsolicited event payload lines (no status prefix). */ public void addEventListener(Consumer> listener) { eventListeners.add(listener); @@ -253,18 +245,14 @@ public class ClientService { } /** - * Sends a command to the server and processes the multi-line response. It - * handles the protocol - * handshake (expecting +OK), strips leading tabs from response lines, and - * collects them until + * Sends a command to the server and processes the multi-line response. It handles the protocol + * handshake (expecting +OK), strips leading tabs from response lines, and collects them until * the "END" marker is reached. * * @param message The raw command string to be sent to the transport layer. - * @return A list of response lines received from the server (excluding protocol - * markers). - * @throws RuntimeException if the server responds with an error or if a - * communication failure - * occurs. + * @return A list of response lines received from the server (excluding protocol markers). + * @throws RuntimeException if the server responds with an error or if a communication failure + * occurs. */ protected List processCommand(String message) { if (offlineMode) { @@ -275,14 +263,15 @@ public class ClientService { ArrayBlockingQueue q = new ArrayBlockingQueue<>(1); pendingResponses.put(reqId, q); - Future writeFuture = executor.submit( - () -> { - try { - clienttcptransport.write(new RawPacket(reqId, message)); - } catch (IOException e) { - throw new RuntimeException(e); - } - }); + Future writeFuture = + executor.submit( + () -> { + try { + clienttcptransport.write(new RawPacket(reqId, message)); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); try { writeFuture.get(); @@ -314,15 +303,11 @@ public class ClientService { } /** - * Helper method to send a request to the server using the ExecutorService. It - * submits the - * request as a Runnable task and waits for its completion. If the task is - * interrupted or - * encounters an execution exception, it throws a RuntimeException with the - * appropriate cause. + * Helper method to send a request to the server using the ExecutorService. It submits the + * request as a Runnable task and waits for its completion. If the task is interrupted or + * encounters an execution exception, it throws a RuntimeException with the appropriate cause. * - * @param request The Runnable task representing the request to be sent to the - * server. + * @param request The Runnable task representing the request to be sent to the server. */ private void sendRequest(Runnable request) { Future future = executor.submit(request); @@ -336,12 +321,9 @@ public class ClientService { } /** - * Helper method to extract the cause of an exception and return it as a - * RuntimeException. If - * the cause is null, it returns the original exception as a RuntimeException. - * If the cause is - * already a RuntimeException, it returns it directly. Otherwise, it wraps the - * cause in a new + * Helper method to extract the cause of an exception and return it as a RuntimeException. If + * the cause is null, it returns the original exception as a RuntimeException. If the cause is + * already a RuntimeException, it returns it directly. Otherwise, it wraps the cause in a new * RuntimeException and returns it. * * @param e The exception from which to extract the cause. @@ -359,10 +341,8 @@ public class ClientService { } /** - * Closes the socket connection to the server and shuts down the - * ExecutorService. It also closes - * the TcpTransport used for communication. If any IOException occurs during - * this process, it + * Closes the socket connection to the server and shuts down the ExecutorService. It also closes + * the TcpTransport used for communication. If any IOException occurs during this process, it * prints the exception to the console. */ public void closeSocket() { diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/lobbyui/LobbyButtonGridManager.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/lobbyui/LobbyButtonGridManager.java index 05fb146..7c7d586 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/lobbyui/LobbyButtonGridManager.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/lobbyui/LobbyButtonGridManager.java @@ -28,6 +28,7 @@ public class LobbyButtonGridManager { private static final int REFRESH_INTERVAL_SECONDS = 5; private static final int INITIAL_DELAY_SECONDS = 5; private static final int COLS = 4; + private static final int MAX_BUTTONS = 8; private static final Logger LOGGER = LogManager.getLogger(LobbyButtonGridManager.class); @@ -66,79 +67,106 @@ public class LobbyButtonGridManager { // Subscribe to server-initiated events so closed lobbies are removed // immediately - clientService.addEventListener( - lines -> { - List params = ClientService.convertToRequestParameters(lines); - String evt = null; - String lidStr = null; - for (RequestParameter p : params) { - if ("EVENT".equalsIgnoreCase(p.key())) { - evt = p.value(); - } else if ("LOBBY_ID".equalsIgnoreCase(p.key())) { - lidStr = p.value(); - } - } - if (evt != null && "LOBBY_CLOSED".equalsIgnoreCase(evt) && lidStr != null) { - int lid; - try { - lid = Integer.parseInt(lidStr); - } catch (NumberFormatException ex) { - return; - } - LOGGER.info("LOBBY_CLOSED event for lobby {} — mapping before: {}", lid, - translationManager.getButtonIdToLobbyId()); - // find button id(s) for this lobby and remove mapping - Map mapping = translationManager.getButtonIdToLobbyId(); - Integer toRemove = null; - for (Map.Entry e : mapping.entrySet()) { - if (e.getValue() != null && e.getValue().intValue() == lid) { - toRemove = e.getKey(); - break; - } - } - if (toRemove != null) { - LOGGER.info("Removing mapping for button {} -> lobby {} (LOBBY_CLOSED event)", toRemove, - lid); - translationManager.removeLobbyButton(toRemove); - LOGGER.debug("Mapping after removal: {}", translationManager.getButtonIdToLobbyId()); - javafx.application.Platform.runLater(this::renderLobbyButtons); - } - } else if (evt != null && "LOBBY_CREATED".equalsIgnoreCase(evt) && lidStr != null) { - int lid; - try { - lid = Integer.parseInt(lidStr); - } catch (NumberFormatException ex) { - return; - } + clientService.addEventListener(this::handleClientServiceEvent); + } - LOGGER.info("LOBBY_CREATED event for lobby {} — mapping before: {}", lid, - translationManager.getButtonIdToLobbyId()); + private static record EventInfo(String event, String lobbyId) {} - Map mapping = translationManager.getButtonIdToLobbyId(); + private EventInfo extractEventInfo(List params) { + String evt = null; + String lidStr = null; + for (RequestParameter p : params) { + if ("EVENT".equalsIgnoreCase(p.key())) { + evt = p.value(); + } else if ("LOBBY_ID".equalsIgnoreCase(p.key())) { + lidStr = p.value(); + } + } + return new EventInfo(evt, lidStr); + } - // If this lobby is already known, nothing to do - if (mapping.containsValue(lid)) { - LOGGER.debug("Lobby {} already mapped, skipping", lid); - return; - } + private Integer tryParseInt(String s) { + try { + return Integer.parseInt(s); + } catch (NumberFormatException ex) { + return null; + } + } - // find next free button id (1..8) - for (int candidate = 1; candidate <= 8; candidate++) { - if (!mapping.containsKey(candidate)) { - try { - translationManager.addLobbyButton(candidate, lid); - LOGGER.info("Mapped new lobby {} to button {}", lid, candidate); - LOGGER.debug("Mapping after add: {}", translationManager.getButtonIdToLobbyId()); - javafx.application.Platform.runLater(this::renderLobbyButtons); - break; - } catch (Exception ex) { - // grid full? try next candidate - LOGGER.debug("Could not map created lobby {}: {}", lid, ex.getMessage()); - } - } - } - } - }); + private void handleLobbyClosed(int lid) { + LOGGER.info( + "LOBBY_CLOSED event for lobby {} — mapping before: {}", + lid, + translationManager.getButtonIdToLobbyId()); + // find button id(s) for this lobby and remove mapping + Map mapping = translationManager.getButtonIdToLobbyId(); + Integer toRemove = null; + for (Map.Entry e : mapping.entrySet()) { + if (e.getValue() != null && e.getValue().intValue() == lid) { + toRemove = e.getKey(); + break; + } + } + if (toRemove != null) { + LOGGER.info( + "Removing mapping for button {} -> lobby {} (LOBBY_CLOSED event)", + toRemove, + lid); + translationManager.removeLobbyButton(toRemove); + LOGGER.debug("Mapping after removal: {}", translationManager.getButtonIdToLobbyId()); + javafx.application.Platform.runLater(this::renderLobbyButtons); + } + } + + private void handleLobbyCreated(int lid) { + LOGGER.info( + "LOBBY_CREATED event for lobby {} — mapping before: {}", + lid, + translationManager.getButtonIdToLobbyId()); + + Map mapping = translationManager.getButtonIdToLobbyId(); + + // If this lobby is already known, nothing to do + if (mapping.containsValue(lid)) { + LOGGER.debug("Lobby {} already mapped, skipping", lid); + return; + } + + // find next free button id (1..MAX_BUTTONS) + for (int candidate = 1; candidate <= MAX_BUTTONS; candidate++) { + if (!mapping.containsKey(candidate)) { + try { + translationManager.addLobbyButton(candidate, lid); + LOGGER.info("Mapped new lobby {} to button {}", lid, candidate); + LOGGER.debug( + "Mapping after add: {}", translationManager.getButtonIdToLobbyId()); + javafx.application.Platform.runLater(this::renderLobbyButtons); + break; + } catch (Exception ex) { + // grid full? try next candidate + LOGGER.debug("Could not map created lobby {}: {}", lid, ex.getMessage()); + } + } + } + } + + private void handleClientServiceEvent(List lines) { + EventInfo info = extractEventInfo(ClientService.convertToRequestParameters(lines)); + + if (info.event() == null || info.lobbyId() == null) { + return; + } + + Integer lid = tryParseInt(info.lobbyId()); + if (lid == null) { + return; + } + + if ("LOBBY_CLOSED".equalsIgnoreCase(info.event())) { + handleLobbyClosed(lid); + } else if ("LOBBY_CREATED".equalsIgnoreCase(info.event())) { + handleLobbyCreated(lid); + } } private void startPeriodicRefresh(long initialDelay, long period) { @@ -185,15 +213,15 @@ public class LobbyButtonGridManager { int lobbyId = lobbyIdObj.intValue(); CompletableFuture.supplyAsync( - () -> { - try { - return lobbyClient.fetchLobbyStatusString(lobbyId); - } catch (Exception ex) { - LOGGER.info("Lobby {} missing: {}", lobbyId, ex.getMessage()); - return null; - } - }, - executor) + () -> { + try { + return lobbyClient.fetchLobbyStatusString(lobbyId); + } catch (Exception ex) { + LOGGER.info("Lobby {} missing: {}", lobbyId, ex.getMessage()); + return null; + } + }, + executor) .thenAccept( status -> { // Do not remove a mapping just because the status fetch @@ -203,10 +231,12 @@ public class LobbyButtonGridManager { if (status == null) { if (!serverIds.contains(lobbyId)) { translationManager.removeLobbyButton(buttonId); - javafx.application.Platform.runLater(this::renderLobbyButtons); + javafx.application.Platform.runLater( + this::renderLobbyButtons); } else { // treat as created/running later; keep mapping - LOGGER.debug("Temporary missing status for lobby {} - keeping mapping", + LOGGER.debug( + "Missing status for lobby {} - keeping mapping", lobbyId); } } @@ -241,7 +271,9 @@ public class LobbyButtonGridManager { } } for (Integer bid : toRemove) { - LOGGER.info("Reconciling: removing mapping for button {} (server no longer lists lobby)", bid); + LOGGER.info( + "Reconciling: removing mapping for button {} (server no longer lists lobby)", + bid); translationManager.removeLobbyButton(bid); } @@ -250,16 +282,20 @@ public class LobbyButtonGridManager { if (mapping.containsValue(li.id)) { continue; } - // find next free button id (1..8) - for (int candidate = 1; candidate <= 8; candidate++) { + // find next free button id (1..MAX_BUTTONS) + for (int candidate = 1; candidate <= MAX_BUTTONS; candidate++) { if (!mapping.containsKey(candidate)) { try { translationManager.addLobbyButton(candidate, li.id); - LOGGER.info("Reconciling: added mapping button {} -> lobby {}", candidate, li.id); + LOGGER.info( + "Reconciling: added mapping button {} -> lobby {}", + candidate, + li.id); break; } catch (Exception ex) { // grid full? try next candidate - LOGGER.debug("Could not add mapping for lobby {}: {}", li.id, ex.getMessage()); + LOGGER.debug( + "Could not add mapping for lobby {}: {}", li.id, ex.getMessage()); } } } @@ -340,8 +376,9 @@ public class LobbyButtonGridManager { statusStr -> { LobbyStatus status = parseLobbyStatus(statusStr); - String path = getImagePathForLobby( - lobbyId, status == null ? LobbyStatus.CREATED : status); + String path = + getImagePathForLobby( + lobbyId, status == null ? LobbyStatus.CREATED : status); Image img = safeLoadImage(path); @@ -428,14 +465,14 @@ public class LobbyButtonGridManager { int lobbyId = lobbyIdObj.intValue(); CompletableFuture.supplyAsync( - () -> { - String statusStr = lobbyClient.fetchLobbyStatusString(lobbyId); + () -> { + String statusStr = lobbyClient.fetchLobbyStatusString(lobbyId); - LobbyStatus status = parseLobbyStatus(statusStr); + LobbyStatus status = parseLobbyStatus(statusStr); - return status == null ? LobbyStatus.CREATED : status; - }, - executor) + return status == null ? LobbyStatus.CREATED : status; + }, + executor) .thenAccept( status -> { String path = getImagePathForLobby(lobbyId, status); @@ -445,13 +482,15 @@ public class LobbyButtonGridManager { for (Node node : gridPane.getChildren()) { boolean isButton = node instanceof Button; - boolean idMatches = ("lobbyBtn-" + buttonId) - .equals(node.getId()); + boolean idMatches = + ("lobbyBtn-" + buttonId) + .equals(node.getId()); if (isButton && idMatches) { Button btn = (Button) node; - ImageView iv = new ImageView(safeLoadImage(path)); + ImageView iv = + new ImageView(safeLoadImage(path)); iv.setPreserveRatio(true); iv.fitWidthProperty() @@ -496,7 +535,8 @@ public class LobbyButtonGridManager { javafx.application.Platform.runLater( () -> { - javafx.stage.Stage currentStage = (javafx.stage.Stage) gridPane.getScene().getWindow(); + javafx.stage.Stage currentStage = + (javafx.stage.Stage) gridPane.getScene().getWindow(); currentStage.hide(); diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/ServerApp.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/ServerApp.java index 5581892..80d2f42 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/ServerApp.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/ServerApp.java @@ -94,7 +94,12 @@ public class ServerApp { TimeUnit.SECONDS); LobbyManager lobbyManager = new LobbyManager(); - registerCommands(dispatcher, router, responseDispatcher, userRegistry, lobbyManager, sessionManager); + registerCommands( + dispatcher, + router, + responseDispatcher, + userRegistry, + new CommandContext(lobbyManager, sessionManager)); // Periodic cleanup: remove empty lobbies older than 30s and notify affected // users @@ -135,6 +140,9 @@ public class ServerApp { networkManager.start(); } + private static record CommandContext( + LobbyManager lobbyManager, SessionManager sessionManager) {} + /** * Registers command parsers and handlers. * @@ -147,8 +155,7 @@ public class ServerApp { CommandRouter commandRouter, ResponseDispatcher responseDispatcher, UserRegistry userRegistry, - LobbyManager lobbyManager, - SessionManager sessionManager) { + CommandContext context) { parserDispatcher.register("PING", new PingParser()); commandRouter.register(PingRequest.class, new PingHandler(responseDispatcher)); @@ -195,7 +202,7 @@ public class ServerApp { ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby .get_lobby_list.GetLobbyListRequest>) new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.get_lobby_list - .GetLobbyListHandler(responseDispatcher, lobbyManager)); + .GetLobbyListHandler(responseDispatcher, context.lobbyManager())); // GET_GAME_STATE registration parserDispatcher.register( @@ -210,7 +217,7 @@ public class ServerApp { .get_game_state.GetGameStateRequest>) new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.get_game_state .GetGameStateHandler( - responseDispatcher, lobbyManager, userRegistry)); + responseDispatcher, context.lobbyManager(), userRegistry)); // BET registration parserDispatcher.register( @@ -222,7 +229,8 @@ public class ServerApp { 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)); + .PlayerBetHandler( + responseDispatcher, userRegistry, context.lobbyManager())); // RAISE registration parserDispatcher.register( @@ -237,7 +245,7 @@ public class ServerApp { .PlayerRaiseRequest>) new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.raise .PlayerRaiseHandler( - responseDispatcher, userRegistry, lobbyManager)); + responseDispatcher, userRegistry, context.lobbyManager())); // CALL registration parserDispatcher.register( @@ -251,7 +259,8 @@ public class ServerApp { 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)); + .PlayerCallHandler( + responseDispatcher, userRegistry, context.lobbyManager())); // FOLD registration parserDispatcher.register( @@ -265,7 +274,8 @@ public class ServerApp { 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)); + .PlayerFoldHandler( + responseDispatcher, userRegistry, context.lobbyManager())); // GET_LOBBY_STATUS registration parserDispatcher.register( @@ -280,7 +290,7 @@ public class ServerApp { .get_lobby_status.GetLobbyStatusRequest>) new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby .get_lobby_status.GetLobbyStatusHandler( - responseDispatcher, lobbyManager, userRegistry)); + responseDispatcher, context.lobbyManager(), userRegistry)); // CREATE_LOBBY registration parserDispatcher.register( @@ -293,8 +303,11 @@ public class ServerApp { (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, sessionManager)); + new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.create_lobby + .CreateLobbyHandler( + responseDispatcher, + context.lobbyManager(), + context.sessionManager())); // JOIN_LOBBY registration parserDispatcher.register( @@ -308,7 +321,8 @@ public class ServerApp { ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.join_lobby .JoinLobbyRequest>) new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.join_lobby - .JoinLobbyHandler(responseDispatcher, lobbyManager, userRegistry)); + .JoinLobbyHandler( + responseDispatcher, context.lobbyManager(), userRegistry)); // START_GAME registration parserDispatcher.register( @@ -322,6 +336,7 @@ public class ServerApp { ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.start_game .StartGameRequest>) new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.start_game - .StartGameHandler(responseDispatcher, lobbyManager, userRegistry)); + .StartGameHandler( + responseDispatcher, context.lobbyManager(), userRegistry)); } } diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/lobby/create_lobby/CreateLobbyHandler.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/lobby/create_lobby/CreateLobbyHandler.java index 87c9764..fec3532 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/lobby/create_lobby/CreateLobbyHandler.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/lobby/create_lobby/CreateLobbyHandler.java @@ -3,20 +3,22 @@ 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.request.RequestContext; import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.ErrorResponse; import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.SuccessResponse; import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.builder.ResponseBody; import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.dispatcher.ResponseDispatcher; import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.Session; import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionManager; -import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext; public class CreateLobbyHandler extends CommandHandler { private final LobbyManager lobbyManager; private final SessionManager sessionManager; public CreateLobbyHandler( - ResponseDispatcher responseDispatcher, LobbyManager lobbyManager, SessionManager sessionManager) { + ResponseDispatcher responseDispatcher, + LobbyManager lobbyManager, + SessionManager sessionManager) { super(responseDispatcher); this.lobbyManager = lobbyManager; this.sessionManager = sessionManager; -- 2.52.0 From 06c059ce5ed458152ad520ee163a1233ca4125e5 Mon Sep 17 00:00:00 2001 From: Julian Kropff Date: Sun, 12 Apr 2026 19:31:30 +0200 Subject: [PATCH 05/21] Fix: fast Game UI communication fix: display the opponent --- .../dbis/cs108/casono/client/ClientApp.java | 65 +-- .../cs108/casono/client/game/GameService.java | 118 ++---- .../cs108/casono/client/game/GameState.java | 6 +- .../casono/client/network/ClientService.java | 49 ++- .../casono/client/network/GameClient.java | 390 +++++++----------- .../ui/gameui/CasinoGameController.java | 162 ++++++-- .../casono/client/ui/gameui/CasinoGameUI.java | 76 +++- .../ui/lobbyui/LobbyButtonGridManager.java | 15 +- 8 files changed, 474 insertions(+), 407 deletions(-) diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ClientApp.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ClientApp.java index bdb4065..23030bb 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ClientApp.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ClientApp.java @@ -10,9 +10,10 @@ import org.apache.logging.log4j.Logger; /** * Entry point and bootstrap helper for the Casono client application. * - *

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. + * Responsibilities: + * - Parse CLI args (host:port, optional username) + * - Store username centrally so UI can reuse it + * - Create a shared ClientService and do startup LOGIN (optional) */ public class ClientApp { @@ -21,16 +22,13 @@ public class ClientApp { /** Shared client connection used when a username is provided at startup. */ private static volatile ClientService sharedClientService; + private static volatile String sharedUsername; + /** 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; } @@ -39,26 +37,34 @@ public class ClientApp { 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 String getSharedUsername() { + return sharedUsername; + } + + private static void setSharedUsername(String username) { + sharedUsername = username; + LOGGER.info("sharedUsername set to '{}'", getSharedUsername()); + } + 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 :."); + throw new IllegalArgumentException("The address must be in the format :."); } String host = parts[0]; int port = Integer.parseInt(parts[1]); LOGGER.info("You've selected the client. It will connect port {} at host {}", port, host); + System.setProperty("casono.server.host", host); 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()) { + setSharedUsername(username.trim()); + } else { + setSharedUsername(null); + } + if (username != null && !username.isBlank()) { try { ClientService clientService = new ClientService(host, port); @@ -72,15 +78,21 @@ public class ClientApp { t.setName("casono-startup-login"); return t; }); + bg.submit( () -> { try { LobbyClient lobbyClient = new LobbyClient(clientService); LoginResult res = lobbyClient.login(username); + + if (res != null && res.getUsername() != null && !res.getUsername().isBlank()) { + setSharedUsername(res.getUsername().trim()); + } + LOGGER.info( "Assigned username='{}' id={}", - res.getUsername(), - res.getId()); + res != null ? res.getUsername() : null, + res != null ? res.getId() : null); } catch (RuntimeException e) { LOGGER.warn("Startup login failed: {}", e.getMessage()); } catch (Exception e) { @@ -104,19 +116,14 @@ public class ClientApp { } } - /** - * Main entry point. {@code } (e.g. {@code 127.0.0.1:1234}) - * - *

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) { if (args == null || args.length == 0) { throw new IllegalArgumentException("Address argument required: "); } - // Optional username argument - String username = args.length > 1 && args[1] != null && !args[1].isBlank() ? args[1] : null; + + String username = + args.length > 1 && args[1] != null && !args[1].isBlank() ? args[1] : null; + start(args[0], username); } } diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/game/GameService.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/game/GameService.java index 896e8bf..8a71c2b 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/game/GameService.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/game/GameService.java @@ -2,118 +2,84 @@ package ch.unibas.dmi.dbis.cs108.casono.client.game; import ch.unibas.dmi.dbis.cs108.casono.client.network.GameClient; import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; -/** - * Service class responsible for managing the game state and providing methods to interact with the - * game. - */ public class GameService { + private static final Logger LOG = Logger.getLogger(GameService.class.getName()); + private final GameClient client; private GameState state; - /** - * Constructs a GameService with the given GameClient for communication with the server. - * - * @param client The GameClient instance used to send commands and receive responses from the - * server. - */ public GameService(GameClient client) { + if (client == null) throw new IllegalArgumentException("GameClient must not be null"); this.client = client; + LOG.info("GameService created"); } - /** - * Refresh the game state by fetching the latest state from the server using the GameClient. - * - * @return The updated GameState object representing the current state of the game after - * refreshing. - */ public GameState refresh() { - state = client.fetchGameState(); + LOG.fine("Refreshing game state..."); + + GameState newState; + try { + newState = client.fetchGameState(); + } catch (Exception e) { + // Shouldn't happen with the new GameClient, but keep service stable. + LOG.log(Level.WARNING, "refresh() failed: " + e.getMessage(), e); + return state; + } + + if (newState == null) { + LOG.fine("No new state (null) -> keeping previous state"); + return state; + } + + this.state = newState; + + LOG.info(() -> "State updated: phase=" + state.phase + + " pot=" + state.pot + + " players=" + (state.players != null ? state.players.size() : 0) + + " community=" + (state.communityCards != null ? state.communityCards.size() : 0)); + return state; } - /** - * Get the current game state. - * - * @return The current GameState object representing the state of the game. - */ public int getPot() { ensureState(); return state.pot; } - /** - * Get the current game state. - * - * @return The current GameState object representing the state of the game. - */ public List getCommunityCards() { ensureState(); - return state.communityCards; + return state.communityCards != null ? state.communityCards : List.of(); } - /** - * Get the list of players in the current game state. - * - * @return A list of Player objects representing the players in the current game state. - */ public List getPlayers() { ensureState(); - return state.players; + return state.players != null ? state.players : List.of(); } - /** Send a CALL command to the server to indicate that the player wants to call. */ - public void call() { - client.sendCall(); - } - - /** Send a FOLD command to the server to indicate that the player wants to fold. */ - public void fold() { - client.sendFold(); - } - - /** - * Send a BET command to the server to indicate that the player wants to bet with the specified - * amount. - * - * @param amount The amount the player wants to bet. - */ - public void bet(int amount) { - client.sendBet(amount); - } - - /** - * Send a RAISE command to the server to indicate that the player wants to raise to the - * specified amount. - * - * @param amount The amount the player wants to raise to. - */ - public void raise(int amount) { - client.sendRaise(amount); - } - - /** - * Get the winner of the game if there is one. If the game is still ongoing or if there is no - * winner, this method returns null. - * - * @return The Player object representing the winner of the game, or null if there is no winner - * yet. - */ public Player getWinner() { ensureState(); - - if (state.winnerIndex < 0) { + if (state.winnerIndex < 0 || state.players == null || state.winnerIndex >= state.players.size()) { return null; } - return state.players.get(state.winnerIndex); } - /** Ensure that the game state has been initialized before accessing it. */ + public void call() { client.sendCall(); } + public void fold() { client.sendFold(); } + public void bet(int amount) { client.sendBet(amount); } + public void raise(int amount) { client.sendRaise(amount); } + private void ensureState() { if (state == null) { - throw new IllegalStateException("Call refresh() first"); + throw new IllegalStateException("GameService used before any successful refresh()"); } } + + public GameState peekStateOrNull() { + return state; + } } diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/game/GameState.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/game/GameState.java index e0d4c23..b01fb4b 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/game/GameState.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/game/GameState.java @@ -8,13 +8,13 @@ import java.util.List; * dealer position, active player, community cards, and player information. */ public class GameState { + public List players = new ArrayList<>(); + public List communityCards = new ArrayList<>(); + public String phase; public int pot; public int currentBet; public int dealer; public int activePlayer; public int winnerIndex = -1; - - public List communityCards = new ArrayList<>(); - public List players = new ArrayList<>(); } diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/ClientService.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/ClientService.java index 0c89317..79438c0 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/ClientService.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/ClientService.java @@ -107,11 +107,16 @@ public class ClientService { boolean hasStatus = false; boolean success = false; + List lines = new ArrayList<>(); + int depth = 0; + boolean inPlayer = false; + for (String rawLine : responseText.split("\\n")) { String line = rawLine; String trimmed = line.trim(); + if (!hasStatus) { if ("+OK".equals(trimmed)) { success = true; @@ -126,12 +131,39 @@ public class ClientService { continue; } + if (trimmed.isEmpty()) continue; + + line = line.replaceFirst("^\\s+", ""); + trimmed = line.trim(); + + if ("PLAYER".equals(trimmed)) { + inPlayer = true; + lines.add("PLAYER"); + continue; + } + + if (isContainerStart(trimmed)) { + depth++; + lines.add(trimmed); + continue; + } + if ("END".equals(trimmed)) { + if (inPlayer) { + inPlayer = false; + lines.add("END"); + continue; + } + + if (depth > 0) { + depth--; + lines.add("END"); + continue; + } + break; } - // remove leading indentation (tabs/spaces) so parameters match regex parsing - line = line.replaceFirst("^\\s+", ""); lines.add(line); } @@ -143,16 +175,17 @@ public class ClientService { success = false; hasStatus = true; } else { - // best effort: treat as success success = true; hasStatus = true; } } + logger.debug("Parsed response lines (rid={}, success={}, depthEnd={}, inPlayerEnd={}): {}", + rid, success, depth, inPlayer, lines); + if (rid == 0) { for (Consumer> l : eventListeners) { try { - logger.debug("Parsed response lines: {}", lines); l.accept(List.copyOf(lines)); } catch (Exception e) { logger.warn("Event listener threw", e); @@ -168,6 +201,14 @@ public class ClientService { } } + private boolean isContainerStart(String token) { + return token.equals("LOBBIES") + || token.equals("LOBBY") + || token.equals("PLAYERS") + || token.equals("CARDS"); + // NOTE: PLAYER deliberately excluded + } + /** * Constructs a ClientService in offline mode. No network connection will be attempted and calls * to processCommand will throw a RuntimeException. diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/GameClient.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/GameClient.java index e8e3cae..5b7e5d6 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/GameClient.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/GameClient.java @@ -5,320 +5,240 @@ import ch.unibas.dmi.dbis.cs108.casono.client.game.GameState; import ch.unibas.dmi.dbis.cs108.casono.client.game.Player; import ch.unibas.dmi.dbis.cs108.casono.client.game.PlayerId; import ch.unibas.dmi.dbis.cs108.casono.client.game.PlayerState; +import java.util.ArrayList; import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; /** - * The GameClient class is responsible for communicating with the server to retrieve the current - * game state. It sends a command to the server and parses the response into a structured GameState - * object. + * Fetches and parses game state from server. + * + * Protocol notes (based on your logs): + * - Success replies contain lines like PHASE=..., POT=..., PLAYER ... END ... END + * - Error replies contain -ERR, CODE=..., MSG=..., END */ public class GameClient { + private static final Logger LOG = Logger.getLogger(GameClient.class.getName()); + private final ClientService client; private final int gameId; - /** - * Constructs a GameClient with the given ClientService for communication. - * - * @param client The ClientService instance used to send commands and receive responses from the - * server. - */ public GameClient(ClientService client, int gameId) { + if (client == null) throw new IllegalArgumentException("ClientService must not be null"); this.client = client; this.gameId = gameId; + LOG.info(() -> "GameClient initialized for gameId=" + gameId); } - /** - * Fetch the current game state from the server by sending a "GET_GAME_STATE" - * - * @return A GameState object representing the current state of the game, as parsed - */ public GameState fetchGameState() { - List responseLines = client.processCommand("GET_GAME_STATE GAME_ID=" + gameId); + final String cmd = "GET_GAME_STATE GAME_ID=" + gameId; - if (responseLines == null || responseLines.isEmpty()) { - throw new RuntimeException("Empty server response"); + List lines; + try { + lines = client.processCommand(cmd); + } catch (Exception e) { + LOG.log(Level.SEVERE, "processCommand failed for " + cmd + ": " + e.getMessage(), e); + return null; } - if (responseLines.get(0).startsWith("-ERR")) { - throw new RuntimeException("Server Error: " + String.join(" ", responseLines)); + if (lines == null || lines.isEmpty()) { + LOG.warning("Empty server response for " + cmd); + return null; } - if (!responseLines.get(0).startsWith("+OK")) { - throw new RuntimeException("Invalid response: missing +OK"); + LOG.info(() -> "GET_GAME_STATE raw lines count=" + lines.size() + " lines=" + lines); + + String joined = String.join("\n", lines); + + if (joined.contains("-ERR")) { + String code = extractValue(joined, "CODE"); + String msg = extractValue(joined, "MSG"); + LOG.info(() -> "GET_GAME_STATE returned -ERR code=" + code + " msg=" + msg); + + if ("GAME_NOT_STARTED".equalsIgnoreCase(code)) { + return null; + } + return null; } - String fullResponse = String.join("\n", responseLines); - return parse(fullResponse); + try { + GameState state = parseGameState(joined); + LOG.info(() -> "Parsed state: phase=" + state.phase + + " pot=" + state.pot + + " players=" + (state.players != null ? state.players.size() : 0) + + " community=" + (state.communityCards != null ? state.communityCards.size() : 0)); + return state; + } catch (Exception e) { + LOG.log(Level.SEVERE, "Failed parsing game state. Payload=\n" + joined, e); + return null; + } } - /** Send a CALL command to the server to indicate that the player wants to call */ public void sendCall() { client.processCommand("CALL GAME_ID=" + gameId); } - /** Send a FOLD command to the server to indicate that the player wants to fold */ public void sendFold() { client.processCommand("FOLD GAME_ID=" + gameId); } - /** Send a BET command to the server to indicate that the player wants to bet */ public void sendBet(int amount) { client.processCommand("BET GAME_ID=" + gameId + " AMOUNT=" + amount); } - /** - * Send a RAISE command to the server to indicate that the player wants to raise - * - * @param amount The amount to raise to - */ public void sendRaise(int amount) { client.processCommand("RAISE GAME_ID=" + gameId + " AMOUNT=" + amount); } - /** - * Parses the raw server response string into a structured GameState object. - * - * @param input The raw server response string containing the game state information. - * @return A GameState object representing the current state of the game. - */ - private GameState parse(String input) { + private GameState parseGameState(String input) { + GameState s = new GameState(); - if (input.startsWith("-ERR")) { - throw new RuntimeException("Server returned Error:\n" + input); - } + if (s.players == null) s.players = new ArrayList<>(); + if (s.communityCards == null) s.communityCards = new ArrayList<>(); - GameState state = new GameState(); - ParserContext ctx = new ParserContext(state); + Player currentPlayer = null; + Card currentCard = null; + + boolean inPlayerCards = false; + boolean inCommunityCards = false; for (String raw : input.split("\n")) { String line = raw.trim(); + if (line.isEmpty()) continue; + if (line.startsWith("+OK")) continue; - if (shouldSkip(line)) { + if (line.startsWith("PHASE=")) { + s.phase = value(line); + continue; + } + if (line.startsWith("POT=")) { + s.pot = intVal(line); + continue; + } + if (line.startsWith("CURRENT_BET=")) { + s.currentBet = intVal(line); + continue; + } + if (line.startsWith("DEALER=")) { + s.dealer = intVal(line); + continue; + } + if (line.startsWith("ACTIVE_PLAYER=")) { + s.activePlayer = intVal(line); + continue; + } + if (line.startsWith("WINNER=")) { + s.winnerIndex = intVal(line); continue; } - if (handleGlobal(line, ctx)) { + if (line.equals("PLAYER")) { + currentPlayer = new Player(); + s.players.add(currentPlayer); + inPlayerCards = false; + currentCard = null; continue; } - if (handleSections(line, ctx)) { + if (line.equals("CARDS")) { + inPlayerCards = (currentPlayer != null); + inCommunityCards = (currentPlayer == null); + currentCard = null; continue; } - if (handlePlayer(line, ctx)) { + if (line.startsWith("CARD")) { + currentCard = new Card("", ""); + if (inPlayerCards && currentPlayer != null) { + currentPlayer.addCard(currentCard); + } else if (inCommunityCards) { + s.communityCards.add(currentCard); + } continue; } - if (handleCards(line, ctx)) { + if (line.startsWith("VALUE=") && currentCard != null) { + currentCard.setValue(value(line)); continue; } - } - return state; - } - - /** Holds the mutable parsing state while processing the server response. */ - private static class ParserContext { - GameState state; - Player currentPlayer; - Card currentCard; - - boolean inPlayers; - boolean inPlayerCards; - boolean inCommunityCards; - - ParserContext(GameState state) { - this.state = state; - } - } - - /** - * Checks whether a line should be ignored. - * - * @param line the current input line - * @return true if the line is empty or a status message, false otherwise - */ - private boolean shouldSkip(String line) { - return line.isEmpty() || line.startsWith("+OK"); - } - - /** - * Parses global game state properties (e.g., phase, pot, current bet). - * - * @param line the current input line - * @param ctx the parser context containing the game state - * @return true if the line was handled, false otherwise - */ - private boolean handleGlobal(String line, ParserContext ctx) { - GameState state = ctx.state; - - if (line.startsWith("PHASE=")) { - state.phase = value(line); - } else if (line.startsWith("POT=")) { - state.pot = intVal(line); - } else if (line.startsWith("CURRENT_BET=")) { - state.currentBet = intVal(line); - } else if (line.startsWith("DEALER=")) { - state.dealer = intVal(line); - } else if (line.startsWith("ACTIVE_PLAYER=")) { - state.activePlayer = intVal(line); - } else if (line.startsWith("WINNER=")) { - state.winnerIndex = intVal(line); - } else { - return false; - // throw new RuntimeException("Unknown global field: " + line); - } - return true; - } - - /** - * Processes section markers such as PLAYERS, PLAYER, CARDS, and END. Updates the parser context - * accordingly. - * - * @param line the current input line - * @param ctx the parser context - * @return true if the line was handled, false otherwise - */ - private boolean handleSections(String line, ParserContext ctx) { - - if (line.equals("END")) { - // ctx.inPlayers = false; - ctx.inPlayerCards = false; - ctx.inCommunityCards = false; - ctx.currentPlayer = null; - ctx.currentCard = null; - return true; - } - - if (line.equals("PLAYERS")) { - ctx.inPlayers = true; - return true; - } - - if (line.equals("PLAYER")) { - ctx.currentPlayer = new Player(); - ctx.state.players.add(ctx.currentPlayer); - return true; - } - - if (line.equals("CARDS")) { - if (ctx.inPlayers && ctx.currentPlayer != null) { - ctx.inPlayerCards = true; - ctx.inCommunityCards = false; - } else { - ctx.inCommunityCards = true; - ctx.inPlayerCards = false; + if (line.startsWith("SUIT=") && currentCard != null) { + currentCard.setSuit(value(line)); + continue; } - return true; - } - - return false; - } - - /** - * Parses properties of the current player (e.g., name, chips, bet, state). - * - * @param line the current input line - * @param ctx the parser context - * @return true if the line was handled, false otherwise - */ - private boolean handlePlayer(String line, ParserContext ctx) { - Player p = ctx.currentPlayer; - - if (p == null) { - return false; - } - - if (line.startsWith("NAME=")) { - p.setId(PlayerId.of(value(line))); - } else if (line.startsWith("CHIPS=")) { - p.setChips(intVal(line)); - } else if (line.startsWith("BET=")) { - p.setBet(intVal(line)); - } else if (line.startsWith("STATE=")) { - p.setState(parseState(value(line))); - } else { - return false; - } - - return true; - } - - /** - * Parses card-related lines and assigns cards to the current player or the community cards. - * - * @param line the current input line - * @param ctx the parser context - * @return true if the line was handled, false otherwise - */ - private boolean handleCards(String line, ParserContext ctx) { - - if (line.startsWith("CARD")) { - ctx.currentCard = new Card("", ""); - - if (ctx.inPlayerCards && ctx.currentPlayer != null) { - ctx.currentPlayer.addCard(ctx.currentCard); - } else if (ctx.inCommunityCards) { - ctx.state.communityCards.add(ctx.currentCard); + if (line.equals("END")) { + if (currentCard != null) { + currentCard = null; + continue; + } + if (inPlayerCards) { + inPlayerCards = false; + continue; + } + if (inCommunityCards) { + inCommunityCards = false; + continue; + } + if (currentPlayer != null) { + currentPlayer = null; + continue; + } + continue; } - return true; + if (currentPlayer != null) { + if (line.startsWith("USERNAME=") || line.startsWith("NAME=")) { + currentPlayer.setId(PlayerId.of(value(line))); + continue; + } + if (line.startsWith("CHIPS=")) { + currentPlayer.setChips(intVal(line)); + continue; + } + if (line.startsWith("BET=")) { + currentPlayer.setBet(intVal(line)); + continue; + } + if (line.startsWith("STATE=")) { + currentPlayer.setState(parseState(value(line))); + continue; + } + } + + LOG.fine(() -> "Ignored line: '" + line + "'"); } - if (line.startsWith("VALUE=") && ctx.currentCard != null) { - ctx.currentCard.setValue(value(line)); - return true; - } - - if (line.startsWith("SUIT=") && ctx.currentCard != null) { - ctx.currentCard.setSuit(value(line)); - return true; - } - - return false; + return s; } - /** - * Helper method to extract the value from a line in the format KEY=VALUE. - * - * @param line The input line from which to extract the value, expected to be in the format - * KEY=VALUE. - * @return The extracted value part of the input line, which is the substring after the first - * '=' character. - */ - private String value(String line) { + private static String value(String line) { return line.split("=", 2)[1]; } - /** - * Helper method to extract an integer value from a line in the format KEY=VALUE. - * - * @param line The input line from which to extract the integer value, expected to be in the - * format KEY=VALUE where VALUE is an integer. - * @return The extracted integer value from the input line, parsed from the substring after the - * first '=' character. - */ - private int intVal(String line) { + private static int intVal(String line) { return Integer.parseInt(value(line)); } - /** - * Helper method to parse a string representation of a player's state into the corresponding - * PlayerState enum value. - * - * @param s The input string representing the player's state, expected to be one of ACTIVE, - * FOLDED, or DEALER (case-insensitive). - * @return The corresponding PlayerState enum value based on the input string. If the input does - * not match any known state, it defaults to PlayerState.ACTIVE. - */ - private PlayerState parseState(String s) { - return switch (s.toUpperCase()) { + private static PlayerState parseState(String s) { + if (s == null) return PlayerState.ACTIVE; + return switch (s.trim().toUpperCase()) { case "ACTIVE" -> PlayerState.ACTIVE; case "FOLDED" -> PlayerState.FOLDED; case "DEALER" -> PlayerState.DEALER; default -> PlayerState.ACTIVE; }; } + + private static String extractValue(String joined, String key) { + if (joined == null) return null; + for (String raw : joined.split("\n")) { + String line = raw.trim(); + if (line.startsWith(key + "=")) { + return line.substring((key + "=").length()).replace("'", ""); + } + } + return null; + } } diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/gameui/CasinoGameController.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/gameui/CasinoGameController.java index 226b5f1..7b28b4b 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/gameui/CasinoGameController.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/gameui/CasinoGameController.java @@ -8,6 +8,7 @@ import ch.unibas.dmi.dbis.cs108.casono.client.game.PlayerId; import ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.gameuicomponents.PlayerStatusController; import ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.gameuicomponents.TaskbarController; import java.util.List; +import java.util.concurrent.CompletableFuture; import java.util.logging.Logger; import javafx.fxml.FXML; import javafx.scene.control.Label; @@ -49,6 +50,7 @@ public class CasinoGameController { private TaskbarController taskbarController; private Image dealerImage; private javafx.animation.Timeline timeline; + private boolean gameStarted = false; private static final int TOTAL_SLOTS = 5; private static final int PLAYER_SLOTS = 2; @@ -305,23 +307,37 @@ public class CasinoGameController { */ private void updateUI() { - java.util.concurrent.CompletableFuture.supplyAsync( - () -> { - try { - return gameService.refresh(); - } catch (Exception e) { - LOGGER.severe("GameService Error: " + e.getMessage()); - return null; - } - }) - .thenAccept( - s -> { - if (s == null) { - return; - } + if (gameService == null) { + LOGGER.warning("GameService is null, skipping update."); + return; + } - javafx.application.Platform.runLater(() -> applyState(s)); - }); + CompletableFuture + .supplyAsync(() -> { + try { + return gameService.refresh(); + } catch (Exception e) { + LOGGER.severe("refresh failed: " + e.getMessage()); + e.printStackTrace(); + return null; + } + }) + .thenAccept(state -> { + + if (state == null) { + LOGGER.info("No state received yet."); + return; + } + + javafx.application.Platform.runLater(() -> { + applyState(state); + }); + }) + .exceptionally(ex -> { + LOGGER.severe("UI update Error: " + ex.getMessage()); + ex.printStackTrace(); + return null; + }); } /** @@ -332,20 +348,50 @@ public class CasinoGameController { */ private void applyState(GameState s) { - renderCommunityCards(s.communityCards); + if (s == null) return; + + LOGGER.info("applyState -> phase=" + s.phase + + " pot=" + s.pot + + " players=" + (s.players != null ? s.players.size() : 0)); + + List players = (s.players != null) ? s.players : List.of(); + List community = (s.communityCards != null) ? s.communityCards : List.of(); + + renderCommunityCards(community); + renderPlayerCards(getMyCards(players)); renderPot(s.pot); - updatePlayers(s.players); - updatePlayerCards(s); - + updatePlayers(players); updateGameInfo(s); highlightDealer(s); - if (taskbarController != null) { - taskbarController.update(s, myPlayerId); + updateTaskbar(s); + + LOGGER.info("myPlayerId=" + myPlayerId); + for (int i = 0; i < players.size(); i++) { + Player p = players.get(i); + LOGGER.info("state.players[" + i + "] id=" + (p != null ? p.getId() : null) + + " chips=" + (p != null ? p.getChips() : null) + + " bet=" + (p != null ? p.getBet() : null) + + " state=" + (p != null ? p.getState() : null)); } } + private List getMyCards(List players) { + + if (myPlayerId == null || players == null) { + return List.of(); + } + + for (Player p : players) { + if (p.getId() != null && p.getId().equals(myPlayerId)) { + return p.getCards() != null ? p.getCards() : List.of(); + } + } + + return List.of(); + } + /** * Update the player's hole cards based on the active player in the game state. * @@ -398,32 +444,64 @@ public class CasinoGameController { } } - /** - * Update the player status components with the latest player information from the game state. - * - * @param p The list of players in the current game state. - */ - private void updatePlayers(List p) { - - if (p == null) { + private void updatePlayers(List players) { + if (players == null || players.isEmpty()) { + clearOpponentSlots(); return; } - if (p.size() > PLAYER_INDEX_0) { - player1Controller.setPlayer(p.get(PLAYER_INDEX_0)); + java.util.List opponents = new java.util.ArrayList<>(); + boolean meFound = false; + + for (Player p : players) { + if (p == null) continue; + + PlayerId pid = p.getId(); + boolean isMe = myPlayerId != null && pid != null && pid.equals(myPlayerId); + + if (isMe) { + meFound = true; + } else { + opponents.add(p); + } } - if (p.size() > PLAYER_INDEX_1) { - player2Controller.setPlayer(p.get(PLAYER_INDEX_1)); + if (!meFound) { + opponents.clear(); + opponents.addAll(players); } - if (p.size() > PLAYER_INDEX_2) { - player3Controller.setPlayer(p.get(PLAYER_INDEX_2)); - } + LOGGER.info("updatePlayers: total=" + players.size() + + " myPlayerId=" + myPlayerId + + " meFound=" + meFound + + " opponents=" + opponents.size()); - player1Controller.refresh(); - player2Controller.refresh(); - player3Controller.refresh(); + setOpponent(player1Controller, opponents, 0); + setOpponent(player2Controller, opponents, 1); + setOpponent(player3Controller, opponents, 2); + + safeRefresh(player1Controller); + safeRefresh(player2Controller); + safeRefresh(player3Controller); + } + + private void setOpponent(PlayerStatusController slot, java.util.List list, int index) { + if (slot == null) return; + slot.setPlayer(index < list.size() ? list.get(index) : null); + } + + private void safeRefresh(PlayerStatusController c) { + if (c == null) return; + try { c.refresh(); } catch (Exception ignored) {} + } + + private void clearOpponentSlots() { + if (player1Controller != null) player1Controller.setPlayer(null); + if (player2Controller != null) player2Controller.setPlayer(null); + if (player3Controller != null) player3Controller.setPlayer(null); + safeRefresh(player1Controller); + safeRefresh(player2Controller); + safeRefresh(player3Controller); } /** @@ -913,4 +991,8 @@ public class CasinoGameController { st.play(); } + + public void setMyPlayerId(PlayerId id) { + this.myPlayerId = id; + } } diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/gameui/CasinoGameUI.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/gameui/CasinoGameUI.java index 918d81a..1d013e2 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/gameui/CasinoGameUI.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/gameui/CasinoGameUI.java @@ -1,9 +1,15 @@ package ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui; +import ch.unibas.dmi.dbis.cs108.casono.client.game.GameService; +import ch.unibas.dmi.dbis.cs108.casono.client.game.PlayerId; import ch.unibas.dmi.dbis.cs108.casono.client.network.ClientService; +import ch.unibas.dmi.dbis.cs108.casono.client.network.GameClient; import java.io.IOException; +import java.util.UUID; +import java.util.logging.Logger; import javafx.application.Application; import javafx.fxml.FXMLLoader; +import javafx.scene.Parent; import javafx.scene.Scene; import javafx.stage.Stage; @@ -12,18 +18,18 @@ import javafx.stage.Stage; * *

Starts the JavaFX application, loads the graphical user interface from the FXML file, and * initializes the main stage for the game. - * - *

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 { + private static final Logger LOG = Logger.getLogger(CasinoGameUI.class.getName()); + private static ClientService clientService; + private static String username; + private static final int DEFAULT_WIDTH = 1200; private static final int DEFAULT_HEIGHT = 800; - /** default constructor */ public CasinoGameUI() { // default no-arg constructor } @@ -32,31 +38,69 @@ public class CasinoGameUI extends Application { CasinoGameUI.clientService = clientService; } - /** - * Starts the main stage of the application. - * - * @param stage The main stage provided by the system. - * @throws IOException If the FXML file or resources cannot be loaded. - */ + public static void setUsername(String username) { + CasinoGameUI.username = username; + } + @Override public void start(Stage stage) throws IOException { + + if (clientService == null) { + clientService = ch.unibas.dmi.dbis.cs108.casono.client.ClientApp.getSharedClientService(); + } + if (clientService == null) { + throw new IllegalStateException( + "CasinoGameUI: clientService is null. " + + "Call CasinoGameUI.setClientService(...) or start via ClientApp with a shared connection."); + } + + String effectiveUsername = normalize(username); + + if (effectiveUsername == null) { + effectiveUsername = + normalize(ch.unibas.dmi.dbis.cs108.casono.client.ClientApp.getSharedUsername()); + } + + if (effectiveUsername == null) { + effectiveUsername = "Guest-" + UUID.randomUUID().toString().substring(0, 8); + } + + LOG.info("CasinoGameUI starting: effectiveUsername='" + effectiveUsername + + "', injectedUsername='" + username + + "', sharedUsername='" + ch.unibas.dmi.dbis.cs108.casono.client.ClientApp.getSharedUsername() + + "', hasClientService=" + (clientService != null)); + FXMLLoader fxmlLoader = new FXMLLoader(CasinoGameUI.class.getResource("/ui-structure/Casinogameui.fxml")); - Scene scene = new Scene(fxmlLoader.load(), DEFAULT_WIDTH, DEFAULT_HEIGHT); + Parent root = fxmlLoader.load(); + CasinoGameController controller = fxmlLoader.getController(); + + int gameId = 1; // TODO echte gameId einsetzen + GameClient gameClient = new GameClient(clientService, gameId); + GameService gameService = new GameService(gameClient); + controller.setGameService(gameService); + + controller.setMyPlayerId(PlayerId.of(effectiveUsername)); + + Scene scene = new Scene(root, DEFAULT_WIDTH, DEFAULT_HEIGHT); stage.setTitle("Casono"); String iconPath = getClass().getResource("/images/logoinverted.png").toExternalForm(); stage.getIcons().add(new javafx.scene.image.Image(iconPath)); + stage.setScene(scene); stage.setFullScreen(true); stage.show(); + + controller.start(); + } + + private static String normalize(String s) { + if (s == null) return null; + String t = s.trim(); + return t.isBlank() ? null : t; } - /** - * Starting point of the application. - * - * @param args Command line arguments. - */ public static void main(String[] args) { launch(); } diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/lobbyui/LobbyButtonGridManager.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/lobbyui/LobbyButtonGridManager.java index 7c7d586..2736437 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/lobbyui/LobbyButtonGridManager.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/lobbyui/LobbyButtonGridManager.java @@ -2,6 +2,7 @@ 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 ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.CasinoGameUI; import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.RequestParameter; import java.util.ArrayList; import java.util.Collections; @@ -550,11 +551,17 @@ public class LobbyButtonGridManager { }); try { - ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.CasinoGameUI - .setClientService(lobbyClient.getClientService()); + var cs = lobbyClient.getClientService(); - new ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.CasinoGameUI() - .start(gameStage); + ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.CasinoGameUI.setClientService(cs); + + String username = ch.unibas.dmi.dbis.cs108.casono.client.ClientApp.getSharedUsername(); + if (username == null || username.isBlank()) { + username = "Guest-" + java.util.UUID.randomUUID().toString().substring(0, 8); + } + ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.CasinoGameUI.setUsername(username); + + new ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.CasinoGameUI().start(gameStage); } catch (Exception e) { LOGGER.error("Game UI failed: {}", e.getMessage()); -- 2.52.0 From fb593d48a66017fc36d7c43059c4863ceb29b8e6 Mon Sep 17 00:00:00 2001 From: Julian Kropff Date: Sun, 12 Apr 2026 20:11:10 +0200 Subject: [PATCH 06/21] Fix: loop Animation in Game-UI --- .../casono/client/network/GameClient.java | 46 +++++++++---------- .../ui/gameui/CasinoGameController.java | 40 ++++++++++++++-- 2 files changed, 57 insertions(+), 29 deletions(-) diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/GameClient.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/GameClient.java index 5b7e5d6..de49850 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/GameClient.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/GameClient.java @@ -13,9 +13,17 @@ import java.util.logging.Logger; /** * Fetches and parses game state from server. * - * Protocol notes (based on your logs): - * - Success replies contain lines like PHASE=..., POT=..., PLAYER ... END ... END - * - Error replies contain -ERR, CODE=..., MSG=..., END + * Protocol notes (based on your current server implementation): + * - Success replies contain: PHASE, POT, CURRENT_BET, DEALER, ACTIVE_PLAYER, then blocks: + * - CARD ... END (community cards on root level) + * - PLAYER ... (NAME/CHIPS/BET/STATE + optional CARD blocks for requesting player) ... END + * - Error replies contain: -ERR then CODE=..., MSG=..., END + * + * Important: + * - The server does NOT wrap cards in a "CARDS" container. + * - CARD blocks appear either: + * - at root level -> community cards + * - inside PLAYER -> hole cards for that player (usually only for the requesting user) */ public class GameClient { @@ -51,7 +59,7 @@ public class GameClient { String joined = String.join("\n", lines); - if (joined.contains("-ERR")) { + if (joined.contains("-ERR") || joined.contains("-ERROR")) { String code = extractValue(joined, "CODE"); String msg = extractValue(joined, "MSG"); LOG.info(() -> "GET_GAME_STATE returned -ERR code=" + code + " msg=" + msg); @@ -100,14 +108,12 @@ public class GameClient { Player currentPlayer = null; Card currentCard = null; - boolean inPlayerCards = false; - boolean inCommunityCards = false; - for (String raw : input.split("\n")) { String line = raw.trim(); if (line.isEmpty()) continue; if (line.startsWith("+OK")) continue; + // Global fields if (line.startsWith("PHASE=")) { s.phase = value(line); continue; @@ -136,24 +142,21 @@ public class GameClient { if (line.equals("PLAYER")) { currentPlayer = new Player(); s.players.add(currentPlayer); - inPlayerCards = false; currentCard = null; continue; } if (line.equals("CARDS")) { - inPlayerCards = (currentPlayer != null); - inCommunityCards = (currentPlayer == null); - currentCard = null; continue; } - if (line.startsWith("CARD")) { + if (line.equals("CARD")) { currentCard = new Card("", ""); - if (inPlayerCards && currentPlayer != null) { - currentPlayer.addCard(currentCard); - } else if (inCommunityCards) { - s.communityCards.add(currentCard); + + if (currentPlayer != null) { + currentPlayer.addCard(currentCard); // hole card + } else { + s.communityCards.add(currentCard); // community card } continue; } @@ -162,7 +165,6 @@ public class GameClient { currentCard.setValue(value(line)); continue; } - if (line.startsWith("SUIT=") && currentCard != null) { currentCard.setSuit(value(line)); continue; @@ -173,18 +175,12 @@ public class GameClient { currentCard = null; continue; } - if (inPlayerCards) { - inPlayerCards = false; - continue; - } - if (inCommunityCards) { - inCommunityCards = false; - continue; - } + if (currentPlayer != null) { currentPlayer = null; continue; } + continue; } diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/gameui/CasinoGameController.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/gameui/CasinoGameController.java index 7b28b4b..47d0e10 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/gameui/CasinoGameController.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/gameui/CasinoGameController.java @@ -51,6 +51,9 @@ public class CasinoGameController { private Image dealerImage; private javafx.animation.Timeline timeline; private boolean gameStarted = false; + private java.util.List lastCommunityKeys = java.util.List.of(); + private java.util.List lastMyCardKeys = java.util.List.of(); + private int lastPot = Integer.MIN_VALUE; private static final int TOTAL_SLOTS = 5; private static final int PLAYER_SLOTS = 2; @@ -126,6 +129,22 @@ public class CasinoGameController { // default constructor for FXML } + private String cardKey(Card c) { + if (c == null) return "null"; + String suit = (c.getSuit() == null) ? "" : c.getSuit().toLowerCase(); + String value = (c.getValue() == null) ? "" : c.getValue().toLowerCase(); + return suit + ":" + value; + } + + private java.util.List cardKeys(java.util.List cards, int slots) { + java.util.List keys = new java.util.ArrayList<>(slots); + for (int i = 0; i < slots; i++) { + Card c = (cards != null && i < cards.size()) ? cards.get(i) : null; + keys.add(cardKey(c)); + } + return java.util.List.copyOf(keys); + } + /** * Set the GameService for this controller. This method must be called before starting the UI to * ensure that the controller has access to the game logic and can update the interface @@ -356,15 +375,28 @@ public class CasinoGameController { List players = (s.players != null) ? s.players : List.of(); List community = (s.communityCards != null) ? s.communityCards : List.of(); + List myCards = getMyCards(players); - renderCommunityCards(community); - renderPlayerCards(getMyCards(players)); - renderPot(s.pot); + java.util.List newCommunityKeys = cardKeys(community, TOTAL_SLOTS); + if (!newCommunityKeys.equals(lastCommunityKeys)) { + lastCommunityKeys = newCommunityKeys; + renderCommunityCards(community); + } + + java.util.List newMyCardKeys = cardKeys(myCards, PLAYER_SLOTS); + if (!newMyCardKeys.equals(lastMyCardKeys)) { + lastMyCardKeys = newMyCardKeys; + renderPlayerCards(myCards); + } + + if (s.pot != lastPot) { + lastPot = s.pot; + renderPot(s.pot); + } updatePlayers(players); updateGameInfo(s); highlightDealer(s); - updateTaskbar(s); LOGGER.info("myPlayerId=" + myPlayerId); -- 2.52.0 From 621626865891758752240c5fc6fa534639a24358 Mon Sep 17 00:00:00 2001 From: Julian Kropff Date: Sun, 12 Apr 2026 21:39:24 +0200 Subject: [PATCH 07/21] Fix: Resolve blocking issues to enable full game loop --- .../casono/client/network/ClientService.java | 32 +- .../casono/client/network/GameClient.java | 17 +- .../ui/gameui/CasinoGameController.java | 55 ++- .../dbis/cs108/casono/server/ServerApp.java | 2 +- .../get_game_state/GetGameStateHandler.java | 17 +- .../get_game_state/GetGameStateResponse.java | 122 ++++-- .../server/domain/game/GameController.java | 5 +- .../domain/game/engine/RoundManager.java | 188 ++++++--- .../domain/game/engine/TurnManager.java | 5 +- .../game/rules/betting/ActionOrderRule.java | 5 +- .../server/domain/game/state/GameState.java | 381 +++++------------- 11 files changed, 398 insertions(+), 431 deletions(-) diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/ClientService.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/ClientService.java index 79438c0..0998fb3 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/ClientService.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/ClientService.java @@ -6,6 +6,8 @@ import ch.unibas.dmi.dbis.cs108.casono.server.network.transport.TcpTransport; import java.io.IOException; import java.net.Socket; import java.util.ArrayList; +import java.util.ArrayDeque; +import java.util.Deque; import java.util.List; import java.util.Map; import java.util.concurrent.ArrayBlockingQueue; @@ -110,8 +112,7 @@ public class ClientService { List lines = new ArrayList<>(); - int depth = 0; - boolean inPlayer = false; + Deque blockStack = new ArrayDeque<>(); for (String rawLine : responseText.split("\\n")) { String line = rawLine; @@ -136,27 +137,15 @@ public class ClientService { line = line.replaceFirst("^\\s+", ""); trimmed = line.trim(); - if ("PLAYER".equals(trimmed)) { - inPlayer = true; - lines.add("PLAYER"); - continue; - } - if (isContainerStart(trimmed)) { - depth++; + blockStack.push(trimmed); lines.add(trimmed); continue; } if ("END".equals(trimmed)) { - if (inPlayer) { - inPlayer = false; - lines.add("END"); - continue; - } - - if (depth > 0) { - depth--; + if (!blockStack.isEmpty()) { + blockStack.pop(); lines.add("END"); continue; } @@ -180,8 +169,8 @@ public class ClientService { } } - logger.debug("Parsed response lines (rid={}, success={}, depthEnd={}, inPlayerEnd={}): {}", - rid, success, depth, inPlayer, lines); + logger.debug("Parsed response lines (rid={}, success={}, openBlocks={}): {}", + rid, success, blockStack.size(), lines); if (rid == 0) { for (Consumer> l : eventListeners) { @@ -205,8 +194,9 @@ public class ClientService { return token.equals("LOBBIES") || token.equals("LOBBY") || token.equals("PLAYERS") - || token.equals("CARDS"); - // NOTE: PLAYER deliberately excluded + || token.equals("PLAYER") + || token.equals("CARDS") + || token.equals("CARD"); } /** diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/GameClient.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/GameClient.java index de49850..456bd2f 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/GameClient.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/GameClient.java @@ -107,6 +107,7 @@ public class GameClient { Player currentPlayer = null; Card currentCard = null; + boolean insidePlayer = false; for (String raw : input.split("\n")) { String line = raw.trim(); @@ -143,6 +144,7 @@ public class GameClient { currentPlayer = new Player(); s.players.add(currentPlayer); currentCard = null; + insidePlayer = true; continue; } @@ -153,10 +155,10 @@ public class GameClient { if (line.equals("CARD")) { currentCard = new Card("", ""); - if (currentPlayer != null) { - currentPlayer.addCard(currentCard); // hole card + if (insidePlayer && currentPlayer != null) { + currentPlayer.addCard(currentCard); // hole card } else { - s.communityCards.add(currentCard); // community card + s.communityCards.add(currentCard); // community card } continue; } @@ -171,17 +173,16 @@ public class GameClient { } if (line.equals("END")) { - if (currentCard != null) { + if (currentCard != null) { // schließt CARD currentCard = null; continue; } - - if (currentPlayer != null) { + if (insidePlayer) { // schließt PLAYER + insidePlayer = false; currentPlayer = null; continue; } - - continue; + continue; // root END } if (currentPlayer != null) { diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/gameui/CasinoGameController.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/gameui/CasinoGameController.java index 47d0e10..e674082 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/gameui/CasinoGameController.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/gameui/CasinoGameController.java @@ -47,6 +47,7 @@ public class CasinoGameController { private GameService gameService; private PlayerId myPlayerId; + @FXML private TaskbarController taskbarIncludeController; private TaskbarController taskbarController; private Image dealerImage; private javafx.animation.Timeline timeline; @@ -154,6 +155,10 @@ public class CasinoGameController { */ public void setGameService(GameService gameService) { this.gameService = gameService; + TaskbarController controller = resolveTaskbarController(); + if (controller != null && myPlayerId != null) { + controller.setGameService(gameService, myPlayerId); + } } /** Set the PlayerId of the current player. */ @@ -161,6 +166,11 @@ public class CasinoGameController { public void initialize() { LOGGER.info("INIT UI"); + taskbarController = resolveTaskbarController(); + if (taskbarController != null && gameService != null && myPlayerId != null) { + taskbarController.setGameService(gameService, myPlayerId); + } + if (communityCardsBox == null) { LOGGER.warning("communityCardsBox is NULL"); return; @@ -471,8 +481,12 @@ public class CasinoGameController { * @param s The current game state. */ private void updateTaskbar(GameState s) { - if (taskbarController != null) { - taskbarController.update(s, myPlayerId); + TaskbarController controller = resolveTaskbarController(); + if (controller != null) { + if (gameService != null && myPlayerId != null) { + controller.setGameService(gameService, myPlayerId); + } + controller.update(s, myPlayerId); } } @@ -868,23 +882,29 @@ public class CasinoGameController { return BACKSIDE; } + String rawSuit = card.getSuit(); + String rawValue = card.getValue(); + if (rawSuit == null || rawValue == null) { + return BACKSIDE; + } + String suit = - switch (card.getSuit().toLowerCase()) { - case "hearts" -> "heart"; - case "diamonds" -> "diamond"; - case "clubs", "cross" -> "cross"; - case "spades", "pik" -> "pik"; - default -> card.getSuit().toLowerCase(); + switch (rawSuit.trim().toLowerCase()) { + case "h", "hearts", "heart" -> "heart"; + case "d", "diamonds", "diamond" -> "diamond"; + case "c", "clubs", "club", "cross" -> "cross"; + case "s", "spades", "spade", "pik" -> "pik"; + default -> rawSuit.trim().toLowerCase(); }; String value = - switch (card.getValue().toLowerCase()) { + switch (rawValue.trim().toLowerCase()) { case "a", "ace" -> "ace"; case "k", "king" -> "king"; case "q", "queen" -> "queen"; case "j", "jack" -> "jack"; case "10", "t" -> "10"; - default -> card.getValue().toLowerCase(); + default -> rawValue.trim().toLowerCase(); }; String path = "/images/card-" + suit + "-" + value + "-3.png"; @@ -1026,5 +1046,20 @@ public class CasinoGameController { public void setMyPlayerId(PlayerId id) { this.myPlayerId = id; + TaskbarController controller = resolveTaskbarController(); + if (controller != null && gameService != null && myPlayerId != null) { + controller.setGameService(gameService, myPlayerId); + } + } + + private TaskbarController resolveTaskbarController() { + if (taskbarController != null) { + return taskbarController; + } + if (taskbarIncludeController != null) { + taskbarController = taskbarIncludeController; + return taskbarController; + } + return null; } } diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/ServerApp.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/ServerApp.java index 80d2f42..ff3d672 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/ServerApp.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/ServerApp.java @@ -54,7 +54,7 @@ public class ServerApp { private static final int USER_CLEANUP_JOB_RECONNECT_THRESHOLD = 10; private static final int SESSION_DISCONNECT_JOB_DELAY = 0; private static final int SESSION_DISCONNECT_JOB_PERIOD = 2; - private static final int SESSION_DISCONNECT_JOB_TIMEOUT = 30000; + private static final int SESSION_DISCONNECT_JOB_TIMEOUT = 30; private static final int LOBBY_EXPIRY_SECONDS = 30; private static final int LOBBY_CLEANUP_INITIAL_DELAY_SECONDS = 5; private static final int LOBBY_CLEANUP_PERIOD_SECONDS = 5; diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/game/get_game_state/GetGameStateHandler.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/game/get_game_state/GetGameStateHandler.java index 17e6a96..4ea677a 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/game/get_game_state/GetGameStateHandler.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/game/get_game_state/GetGameStateHandler.java @@ -28,9 +28,9 @@ public class GetGameStateHandler extends CommandHandler { @Override public void execute(GetGameStateRequest request) { Integer gameId = request.getGameId(); - String username = request.getUsername(); + String username = resolveUsername(request); - Lobby lobby = null; + Lobby lobby; if (gameId != null) { lobby = lobbyManager.getLobby(LobbyId.of(gameId)); if (lobby == null) { @@ -71,4 +71,17 @@ public class GetGameStateHandler extends CommandHandler { responseDispatcher.dispatch(new GetGameStateResponse(request.getContext(), game, username)); } + + private String resolveUsername(GetGameStateRequest request) { + String username = request.getUsername(); + if (username != null && !username.isBlank()) { + return username.trim(); + } + + return userRegistry.getBySessionId(request.getSessionId()) + .map(User::getName) + .map(String::trim) + .filter(name -> !name.isEmpty()) + .orElse(null); + } } diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/game/get_game_state/GetGameStateResponse.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/game/get_game_state/GetGameStateResponse.java index 455abe8..8fee488 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/game/get_game_state/GetGameStateResponse.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/game/get_game_state/GetGameStateResponse.java @@ -12,25 +12,58 @@ import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.SuccessR import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.builder.ResponseBody; import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.builder.ResponseBodyBuilder; import java.util.List; +import java.util.Objects; -/** Response carrying a snapshot of the current game state using the project's wire format. */ +/** + * Response carrying a snapshot of the current game state using the project's wire format. + * + * Wire format (relevant parts): + * + * +OK + * PHASE=... + * POT=... + * CURRENT_BET=... + * DEALER=... + * ACTIVE_PLAYER=... + * CARD + * VALUE=... + * SUIT=... + * END + * PLAYER + * NAME=... + * CHIPS=... + * BET=... + * STATE=... + * CARD (only for requesting user) + * VALUE=... + * SUIT=... + * END + * END + * END + * + * Notes: + * - Community cards are always emitted at top-level (0..5). If none exist (e.g. PREFLOP), none are emitted. + * - Hole cards are only emitted for the requesting player (privacy). + */ public class GetGameStateResponse extends SuccessResponse { - public GetGameStateResponse( - RequestContext context, GameController game, String requestingUsername) { - super(context, buildBody(game.getState(), requestingUsername)); + + public GetGameStateResponse(RequestContext context, GameController game, String requestingUsername) { + super(context, buildBody(Objects.requireNonNull(game, "game must not be null").getState(), requestingUsername)); } private static ResponseBody buildBody(GameState state, String requestingUsername) { - int globalCurrentBet = computeGlobalCurrentBet(state); + Objects.requireNonNull(state, "state must not be null"); ResponseBodyBuilder builder = ResponseBody.builder(); + builder.param("PHASE", state.getPhase() == null ? "UNKNOWN" : state.getPhase().name()); - builder.param("POT", state.getPot().getAmount()); - builder.param("CURRENT_BET", globalCurrentBet); + builder.param("POT", state.getPot() == null ? 0 : state.getPot().getAmount()); + builder.param("CURRENT_BET", computeGlobalCurrentBet(state)); builder.param("DEALER", state.getDealerIndex()); builder.param("ACTIVE_PLAYER", state.getCurrentPlayerIndex()); appendCommunityCards(builder, state); + appendPlayers(builder, state, requestingUsername); return builder.build(); @@ -39,53 +72,63 @@ public class GetGameStateResponse extends SuccessResponse { private static int computeGlobalCurrentBet(GameState state) { int globalCurrentBet = 0; for (Player p : state.getPlayers()) { + if (p == null) continue; int b = state.getCurrentBet(p.getId()); - if (b > globalCurrentBet) { - globalCurrentBet = b; - } + if (b > globalCurrentBet) globalCurrentBet = b; } return globalCurrentBet; } private static void appendCommunityCards(ResponseBodyBuilder builder, GameState state) { - for (Card c : state.getCommunityCards()) { - builder.block( - "CARD", - card -> { - card.param("VALUE", rankToWire(c.getRank())); - card.param("SUIT", suitToWire(c.getSuit())); - }); + List community = state.getCommunityCards(); + + if (community == null || community.isEmpty()) { + return; + } + + for (Card c : community) { + if (c == null) continue; + builder.block("CARD", card -> { + card.param("VALUE", rankToWire(c.getRank())); + card.param("SUIT", suitToWire(c.getSuit())); + }); } } - private static void appendPlayers( - ResponseBodyBuilder builder, GameState state, String requestingUsername) { - for (Player p : state.getPlayers()) { - PlayerId pid = p.getId(); - builder.block( - "PLAYER", - pblock -> { - pblock.param("NAME", pid.value()); - pblock.param("CHIPS", p.getChips()); - pblock.param("BET", state.getCurrentBet(pid)); - pblock.param("STATE", p.isFolded() ? "FOLDED" : "ACTIVE"); + private static void appendPlayers(ResponseBodyBuilder builder, GameState state, String requestingUsername) { + String req = (requestingUsername == null) ? null : requestingUsername.trim(); - if (requestingUsername != null && requestingUsername.equals(pid.value())) { - List hole = state.getHoleCards(pid); - for (Card hc : hole) { - pblock.block( - "CARD", - cblock -> { - cblock.param("VALUE", rankToWire(hc.getRank())); - cblock.param("SUIT", suitToWire(hc.getSuit())); - }); - } + for (Player p : state.getPlayers()) { + if (p == null) continue; + + PlayerId pid = p.getId(); + String name = (pid == null || pid.value() == null) ? "" : pid.value().trim(); + + builder.block("PLAYER", pblock -> { + pblock.param("NAME", name); + pblock.param("CHIPS", p.getChips()); + pblock.param("BET", pid == null ? 0 : state.getCurrentBet(pid)); + pblock.param("STATE", p.isFolded() ? "FOLDED" : "ACTIVE"); + + if (req != null && pid != null && req.equals(name)) { + List hole = state.getHoleCards(pid); + + if (hole != null) { + for (Card hc : hole) { + if (hc == null) continue; + pblock.block("CARD", cblock -> { + cblock.param("VALUE", rankToWire(hc.getRank())); + cblock.param("SUIT", suitToWire(hc.getSuit())); + }); } - }); + } + } + }); } } private static String rankToWire(Rank r) { + if (r == null) return ""; return switch (r) { case TWO -> "2"; case THREE -> "3"; @@ -104,6 +147,7 @@ public class GetGameStateResponse extends SuccessResponse { } private static String suitToWire(Suit s) { + if (s == null) return ""; return switch (s) { case HEARTS -> "H"; case DIAMONDS -> "D"; diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/game/GameController.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/game/GameController.java index 02254ba..1199630 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/game/GameController.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/game/GameController.java @@ -29,7 +29,6 @@ public class GameController { private int dealerIndex = 0; - private static final int NEXT_PLAYER_OFFSET = 3; private static final int DEALER_OFFSET = 1; private static final int SMALL_BLIND_OFFSET = 1; private static final int BIG_BLIND_OFFSET = 2; @@ -81,13 +80,13 @@ public class GameController { dealHoleCards(); postBlinds(); - int nextPlayerIndex = (dealerIndex + NEXT_PLAYER_OFFSET) % players.size(); - engine.getState().setCurrentPlayerIndex(nextPlayerIndex); + engine.getState().setCurrentPlayerToPreflopFirstToAct(); } /** Rotates the dealer position to the next player in the list. */ private void rotateDealer() { dealerIndex = (dealerIndex + DEALER_OFFSET) % players.size(); + engine.getState().setDealerIndex(dealerIndex); } /** diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/game/engine/RoundManager.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/game/engine/RoundManager.java index 0fdec45..5faa01f 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/game/engine/RoundManager.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/game/engine/RoundManager.java @@ -1,133 +1,189 @@ package ch.unibas.dmi.dbis.cs108.casono.server.domain.game.engine; +import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.deck.Card; +import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.deck.Deck; import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.player.Player; +import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.player.PlayerId; import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.player.PlayerStatus; +import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.state.GamePhase; import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.state.GameState; import java.util.ArrayList; import java.util.List; /** - * RoundManager is responsible for managing the flow of a poker game round. It handles the - * progression of the game through its various phases (pre-flop, flop, turn, river) and manages - * player actions such as posting blinds and dealing cards. The RoundManager ensures that the game - * state is updated correctly based on player actions and the current phase of the game. + * RoundManager manages hand lifecycle and phase progression (PREFLOP -> FLOP -> TURN -> RIVER -> SHOWDOWN). + * + *

This implementation: + *

    + *
  • starts a new hand (reset + hole cards + blinds)
  • + *
  • progresses phases once betting round is finished
  • + *
  • deals community cards (3/1/1)
  • + *
*/ public class RoundManager { public static final int SMALL_BLIND = 100; public static final int BIG_BLIND = 200; - /** - * Starts a new hand by initializing the game state, dealing cards to players, and posting - * blinds. This method sets the hand as active and resets any necessary state variables to - * prepare for a new round of play. - * - * @param state The game state to be used for starting the new hand. - */ public void startNewHand(GameState state) { - state.setHandActive(true); - state.resetBets(); + // Use GameState's canonical reset + state.startNewHand(); - dealCards(state); + // Ensure deck exists + ensureDeck(state); + // Deal hole cards + dealHoleCards(state); + + // Post blinds postBlinds(state); + + // Preflop always starts left of BB (or dealer in heads-up). + state.setCurrentPlayerToPreflopFirstToAct(); } - /** - * Checks if the betting round is finished and advances the game phase if necessary. This method - * evaluates the current bets of all active players and determines if the betting round can be - * concluded. If all players have met the current bet or are all-in, the game phase is advanced - * to the next stage. - * - * @param state The current game state to be evaluated for betting round progression. - */ public void progressIfNeeded(GameState state) { + if (state.getPhase() == null) { + state.setPhase(GamePhase.PREFLOP); + } if (isBettingRoundFinished(state)) { advancePhase(state); } } - /** - * Determines if the betting round is finished by checking if all active players have met the - * current bet or are all-in. This method iterates through all players in the game state and - * evaluates their bets against the current bet on the table. - * - * @param state The current game state to be evaluated for betting round completion. - * @return true if the betting round is finished, false otherwise. - */ private boolean isBettingRoundFinished(GameState state) { - int target = state.getTableState().getCurrentBet(); for (Player p : state.getPlayers()) { + if (p == null) continue; - if (p.getStatus() == PlayerStatus.FOLDED) { - continue; - } + // be tolerant if codebase mixes status + boolean flags + if (p.getStatus() == PlayerStatus.FOLDED || p.isFolded()) continue; int bet = state.getCurrentBet(p.getId()); - - // The player must have placed at least one bet if (bet < target && !p.isAllIn()) { return false; } } - return true; } - /** - * Advances the game phase to the next stage (flop, turn, river, or showdown) based on the - * current phase of the game. This method is called when the betting round is finished and - * updates the game state accordingly to reflect the new phase of play. - * - * @param state The current game state to be updated with the new phase. - */ private void advancePhase(GameState state) { - switch (state.getPhase()) { case PREFLOP -> dealFlop(state); case FLOP -> dealTurn(state); case TURN -> dealRiver(state); case RIVER -> showdown(state); + case SHOWDOWN -> { /* nothing */ } + } + } + + private void ensureDeck(GameState state) { + Deck deck = state.getDeck(); + if (deck == null) { + deck = new Deck(); + deck.shuffle(); + state.setDeck(deck); + } + } + + private void dealHoleCards(GameState state) { + Deck deck = state.getDeck(); + + for (Player p : state.getPlayers()) { + if (p == null) continue; + + PlayerId pid = p.getId(); + Card c1 = deck.draw(); + Card c2 = deck.draw(); + + state.giveHoleCards(pid, c1, c2); } } /** - * Handles the posting of blinds at the start of a new hand. This method identifies the players - * responsible for posting the small and big blinds, updates their chip counts, adds the blind - * amounts to the pot, and updates the current bets for those players in the game state. - * - * @param state The current game state to be updated with the posted blinds. + * NOTE: Blind assignment here is intentionally minimal because GameState does not expose seating order. + * If you want correct dealer-relative blinds, add helpers in GameState (e.g. getPlayerIdAt(int)). */ private void postBlinds(GameState state) { List playerList = new ArrayList<>(state.getPlayers()); + if (playerList.size() < 2) return; - Player sb = playerList.get(0); - Player bb = playerList.get(1); + // pick first two non-folded players as SB/BB + Player sb = null; + Player bb = null; - int smallBlind = SMALL_BLIND; - int bigBlind = BIG_BLIND; + for (Player p : playerList) { + if (p == null) continue; + if (p.isFolded()) continue; - sb.removeChips(smallBlind); - bb.removeChips(bigBlind); + if (sb == null) { + sb = p; + } else { + bb = p; + break; + } + } - state.addToPot(smallBlind + bigBlind); + if (sb == null || bb == null) return; - state.setCurrentBet(sb.getId(), smallBlind); - state.setCurrentBet(bb.getId(), bigBlind); + sb.removeChips(SMALL_BLIND); + bb.removeChips(BIG_BLIND); - state.getTableState().setCurrentBet(bigBlind); + state.addToPot(SMALL_BLIND + BIG_BLIND); + + state.setCurrentBet(sb.getId(), SMALL_BLIND); + state.setCurrentBet(bb.getId(), BIG_BLIND); + + state.getTableState().setCurrentBet(BIG_BLIND); } - private void dealCards(GameState state) {} + private void dealFlop(GameState state) { + ensureDeck(state); + Deck deck = state.getDeck(); - private void dealFlop(GameState state) {} + state.addCommunityCard(deck.draw()); + state.addCommunityCard(deck.draw()); + state.addCommunityCard(deck.draw()); - private void dealTurn(GameState state) {} + state.setPhase(GamePhase.FLOP); - private void dealRiver(GameState state) {} + // new betting round + state.resetBets(); + state.setCurrentPlayerToPostflopFirstToAct(); + } - private void showdown(GameState state) {} -} + private void dealTurn(GameState state) { + ensureDeck(state); + Deck deck = state.getDeck(); + + state.addCommunityCard(deck.draw()); + + state.setPhase(GamePhase.TURN); + + // new betting round + state.resetBets(); + state.setCurrentPlayerToPostflopFirstToAct(); + } + + private void dealRiver(GameState state) { + ensureDeck(state); + Deck deck = state.getDeck(); + + state.addCommunityCard(deck.draw()); + + state.setPhase(GamePhase.RIVER); + + // new betting round + state.resetBets(); + state.setCurrentPlayerToPostflopFirstToAct(); + } + + private void showdown(GameState state) { + state.setPhase(GamePhase.SHOWDOWN); + + // winner evaluation is elsewhere (rules/controller) + // do NOT reset cards here; clients still need board visible during showdown + } +} \ No newline at end of file diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/game/engine/TurnManager.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/game/engine/TurnManager.java index 397303d..98c5346 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/game/engine/TurnManager.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/game/engine/TurnManager.java @@ -18,9 +18,6 @@ public class TurnManager { * @param state The current game state that will be updated to reflect the next player's turn. */ public void nextPlayer(GameState state) { - - int next = (state.getCurrentPlayerIndex() + 1) % state.getPlayers().size(); - - state.setCurrentPlayerIndex(next); + state.nextPlayer(); } } diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/game/rules/betting/ActionOrderRule.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/game/rules/betting/ActionOrderRule.java index 110a8d1..8db3a61 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/game/rules/betting/ActionOrderRule.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/game/rules/betting/ActionOrderRule.java @@ -6,8 +6,6 @@ import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.player.PlayerId; import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.rules.Rule; import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.rules.RuleViolationException; import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.state.GameState; -import java.util.ArrayList; -import java.util.List; /** * The ActionOrderRule class implements the Rule interface and defines the validation logic for @@ -31,8 +29,7 @@ public class ActionOrderRule implements Rule { PlayerId actingPlayerId = action.getPlayerId(); - List playerList = new ArrayList<>(state.getPlayers()); - Player current = playerList.get(state.getCurrentPlayerIndex()); + Player current = state.getCurrentPlayer(); if (!current.getId().equals(actingPlayerId)) { throw new RuleViolationException("Not your turn"); diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/game/state/GameState.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/game/state/GameState.java index 040bcec..9b614e5 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/game/state/GameState.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/game/state/GameState.java @@ -11,225 +11,154 @@ import java.util.List; import java.util.Map; /** - * The GameState class encapsulates the entire state of a poker game at any given moment. It - * maintains information about the players, the pot, the current phase of the game, the dealer - * position, and the cards in play. This class is central to managing the flow of the game and - * ensuring that all actions and decisions are based on an accurate representation of the current - * game state. + * The GameState class encapsulates the entire state of a poker game at any given moment. + * + *

Important invariants this implementation maintains: + *

    + *
  • {@code playerOrder} defines the stable seating/turn order.
  • + *
  • {@code currentBets} always contains an entry for every player in {@code playerOrder}.
  • + *
  • {@code holeCards} maps every player to a mutable list; if not present, it is created on demand.
  • + *
*/ public class GameState { - private Map players = new HashMap<>(); - - private List playerOrder = new ArrayList<>(); + private final Map players = new HashMap<>(); + private final List playerOrder = new ArrayList<>(); private Pot pot = new Pot(); - - private TableState tableState = new TableState(); + private final TableState tableState = new TableState(); private int currentPlayerIndex; - private boolean handActive; - private boolean allowOutOfTurn; - private GamePhase phase; - private int dealerIndex; - private Map currentBets = new HashMap<>(); + private final Map currentBets = new HashMap<>(); + private final Map playerBetCommitments = new HashMap<>(); - private Map playerBetCommitments = new HashMap<>(); - - private Map> holeCards = new HashMap<>(); - - private List communityCards = new ArrayList<>(); - - // private final Set foldedPlayers = new HashSet<>(); + private final Map> holeCards = new HashMap<>(); + private final List communityCards = new ArrayList<>(); private Deck deck; - // Getter - - /** - * Returns the list of players currently in the game. - * - * @return A list of Player objects representing the players in the game. - */ + // Getters public Collection getPlayers() { - return players.values(); + List ordered = new ArrayList<>(playerOrder.size()); + for (PlayerId id : playerOrder) { + Player p = players.get(id); + if (p != null) { + ordered.add(p); + } + } + return ordered; } - /** - * Returns the current player whose turn it is to act. - * - * @return The Player object representing the current player. - */ public Player getCurrentPlayer() { PlayerId id = playerOrder.get(currentPlayerIndex); return players.get(id); } - /** - * Returns the index of the current player in the players list. - * - * @return An integer representing the index of the current player. - */ public int getCurrentPlayerIndex() { return currentPlayerIndex; } - /** - * Indicates whether a hand is currently active in the game. - * - * @return true if a hand is active, false otherwise. - */ public boolean isHandActive() { return handActive; } - /** - * Returns the current phase of the game (e.g., PREFLOP, FLOP, TURN, RIVER). - * - * @return The GamePhase enum value representing the current phase of the game. - */ public GamePhase getPhase() { return phase; } - /** - * Returns the current state of the table, including player statuses and positions. - * - * @return A TableState object representing the current state of the table. - */ public TableState getTableState() { return tableState; } - /** - * Returns the current pot, which contains the total amount of chips bet by players in the - * current hand. - * - * @return A Pot object representing the current pot. - */ public Pot getPot() { return pot; } - /** - * Returns the current deck of cards being used in the game. - * - * @return A Deck object representing the current deck of cards. - */ public Deck getDeck() { return deck; } - /** - * Returns the list of community cards currently on the table. - * - * @return A list of Card objects representing the community cards. - */ public List getCommunityCards() { return communityCards; } - /** - * Returns the hole cards for a specific player based on their ID. - * - * @param playerId The ID of the player whose hole cards are being requested. - * @return A list of Card objects representing the player's hole cards, or an empty list if the - * player has no hole cards. - */ + /** Always returns a mutable list (never null). */ public List getHoleCards(PlayerId playerId) { - return holeCards.getOrDefault(playerId, new ArrayList<>()); + return holeCards.computeIfAbsent(playerId, k -> new ArrayList<>()); } - /** - * Returns the index of the dealer in the players list. - * - * @return An integer representing the index of the dealer. - */ public int getDealerIndex() { return dealerIndex; } - /** - * Returns a map of player IDs to their current hole cards. - * - * @return A map where the key is the player ID and the value is a list of Card objects - * representing the player's hole cards. - */ public Map> getPlayerCards() { return holeCards; } - /** - * Returns the number of players currently in the game. - * - * @return An integer representing the number of players in the game. - */ public int getPlayerCount() { return players.size(); } - // SETTER - - /** - * Sets the index of the current player in the players list. - * - * @param index An integer representing the index of the current player. - */ + // Setters / Mutators public void setCurrentPlayerIndex(int index) { + if (playerOrder.isEmpty()) { + this.currentPlayerIndex = 0; + return; + } this.currentPlayerIndex = index; } - /** - * Sets whether a hand is currently active in the game. - * - * @param handActive A boolean value indicating whether a hand is active. - */ + public void setCurrentPlayerToPreflopFirstToAct() { + int size = playerOrder.size(); + if (size == 0) { + currentPlayerIndex = 0; + return; + } + + // Heads-up: dealer (small blind) acts first preflop. + int first = (size == 2) ? dealerIndex : (dealerIndex + 3) % size; + currentPlayerIndex = first; + if (!canPlayerAct(currentPlayerIndex)) { + nextPlayer(); + } + } + + public void setCurrentPlayerToPostflopFirstToAct() { + int size = playerOrder.size(); + if (size == 0) { + currentPlayerIndex = 0; + return; + } + + int first = (dealerIndex + 1) % size; + currentPlayerIndex = first; + if (!canPlayerAct(currentPlayerIndex)) { + nextPlayer(); + } + } + public void setHandActive(boolean handActive) { this.handActive = handActive; } - /** - * Sets the current phase of the game. - * - * @param phase The GamePhase enum value representing the new phase of the game. - */ public void setPhase(GamePhase phase) { this.phase = phase; } - /** - * Sets the index of the dealer in the players list. - * - * @param dealerIndex An integer representing the index of the dealer. - */ public void setDealerIndex(int dealerIndex) { this.dealerIndex = dealerIndex; } - /** - * Sets the current deck of cards being used in the game. - * - * @param deck A Deck object representing the new deck of cards to be used in the game. - */ public void setDeck(Deck deck) { this.deck = deck; } - /** - * Adds a player to the game with the specified ID and initial chip count. This method creates a - * new Player object, adds it to the list of players, and initializes the player's current bet - * and bet commitment in the game state. - * - * @param id The ID of the player to add. - * @param chips The initial number of chips the player has. - */ public void addPlayer(PlayerId id, int chips) { - Player player = new Player(id, chips); players.put(id, player); @@ -237,213 +166,130 @@ public class GameState { currentBets.put(id, 0); playerBetCommitments.put(id, 0); + + holeCards.computeIfAbsent(id, k -> new ArrayList<>()); } - // BETTING LOGIC - - /** - * Retrieves the current bet amount for a specific player based on their ID. - * - * @param playerId The ID of the player whose current bet is being requested. - * @return An integer representing the current bet amount for the specified player, or 0 if the - * player has not placed any bets. - */ + // Betting public int getCurrentBet(PlayerId playerId) { return currentBets.getOrDefault(playerId, 0); } - /** - * Sets the current bet amount for a specific player based on their ID. This method updates the - * currentBets map with the new bet amount for the specified player. - * - * @param playerId The ID of the player whose current bet is being set. - * @param amount The new bet amount to be set for the specified player. - */ public void setCurrentBet(PlayerId playerId, int amount) { currentBets.put(playerId, amount); } /** - * Resets the current bets for all players by clearing the currentBets map. This method is - * typically called at the start of a new hand to ensure that all players' bets are reset to - * zero. + * Reset bets to 0 for ALL players (do not clear the map). + * Also resets table current bet to 0. */ public void resetBets() { - currentBets.clear(); + for (PlayerId id : playerOrder) { + currentBets.put(id, 0); + } + tableState.setCurrentBet(0); } - /** - * Adds a specified amount to the pot. This method updates the total amount in the pot by adding - * the given amount to it. - * - * @param amount The amount of chips to be added to the pot. - */ public void addToPot(int amount) { pot.add(amount); } - /** - * Returns whether out-of-turn actions are allowed in the game. Out-of-turn actions refer to - * players being able to act when it is not their turn, which can be a feature in some poker - * variants or game modes. - * - * @return true if out-of-turn actions are allowed, false otherwise. - */ public boolean isAllowOutOfTurn() { return allowOutOfTurn; } - /** - * Sets whether out-of-turn actions are allowed in the game. This method updates the - * allowOutOfTurn flag, which determines if players can act when it is not their turn. - * - * @param allowOutOfTurn A boolean value indicating whether out-of-turn actions should be - * allowed in the game. - */ public void setAllowOutOfTurn(boolean allowOutOfTurn) { this.allowOutOfTurn = allowOutOfTurn; } - // PLAYER HELPERS - - /** - * Retrieves a player from the game based on their ID. This method searches the list of players - * for a player with the specified ID and returns it. If no player with the given ID is found, a - * RuntimeException is thrown. - * - * @param id The ID of the player to retrieve. - * @return The Player object corresponding to the specified ID. - * @throws RuntimeException if no player with the given ID is found in the game. - */ + // Player helpers public Player getPlayer(PlayerId id) { Player player = players.get(id); - - if (player == null) { - throw new RuntimeException("Player not found: " + id); - } - + if (player == null) throw new RuntimeException("Player not found: " + id); return player; } - // BET COMMITMENTS - - /** - * Retrieves the current bet commitment for a specific player based on their ID. A bet - * commitment represents the total amount a player has committed to the pot in the current hand, - * including all bets, raises, and calls they have made. - * - * @param playerId The ID of the player whose current bet commitment is being requested. - * @return An integer representing the current bet commitment for the specified player, or 0 if - * the player has not made any bet commitments. - */ + // Bet commitments public int getCurrentBetCommitment(PlayerId playerId) { return playerBetCommitments.getOrDefault(playerId, 0); } - /** - * Sets the current bet commitment for a specific player based on their ID. This method updates - * the playerBetCommitments map with the new bet commitment amount for the specified player. - * - * @param playerId The ID of the player whose current bet commitment is being set. - * @param amount The new bet commitment amount to be set for the specified player. - */ public void setCurrentBetCommitment(PlayerId playerId, int amount) { playerBetCommitments.put(playerId, amount); } - // CARDS + // Cards /** - * Gives hole cards to a specific player based on their ID. This method takes two Card objects - * representing the player's hole cards and adds them to the holeCards map under the player's - * ID. - * - * @param playerId The ID of the player to whom the hole cards are being given. - * @param c1 The first Card object representing one of the player's hole cards. - * @param c2 The second Card object representing the other hole card for the player. + * Gives (overwrites) the two hole cards for the given player. + * Ensures stable list identity (important if other code holds references). */ public void giveHoleCards(PlayerId playerId, Card c1, Card c2) { - - List cards = new ArrayList<>(); + List cards = holeCards.computeIfAbsent(playerId, k -> new ArrayList<>()); + cards.clear(); cards.add(c1); cards.add(c2); - - holeCards.put(playerId, cards); } - /** - * Adds a community card to the game state. This method takes a Card object representing a - * community card and adds it to the list of community cards on the table. - * - * @param card The Card object representing the community card to be added to the game state. - */ public void addCommunityCard(Card card) { communityCards.add(card); } - /** - * Resets the community cards by clearing the list of community cards. This method is typically - * called at the start of a new hand to ensure that all community cards from the previous hand - * are removed from the game state. - */ public void resetCommunityCards() { communityCards.clear(); } - // TURN MANAGEMENT - - /** - * Advances the turn to the next player in the players list. This method updates the - * currentPlayerIndex by incrementing it and wrapping around to the start of the list if - * necessary, ensuring that the turn order is maintained correctly throughout the game. - */ + // Turn / dealer management public void nextPlayer() { + if (playerOrder.isEmpty()) { + currentPlayerIndex = 0; + return; + } + int start = currentPlayerIndex; do { currentPlayerIndex = (currentPlayerIndex + 1) % playerOrder.size(); - Player p = getCurrentPlayer(); - - if (!p.isFolded() && !p.isAllIn()) { + if (canPlayerAct(currentPlayerIndex)) { return; } - } while (currentPlayerIndex != start); } - // Dealer Rotation + private boolean canPlayerAct(int index) { + if (index < 0 || index >= playerOrder.size()) { + return false; + } + + PlayerId id = playerOrder.get(index); + Player p = players.get(id); + return p != null && !p.isFolded() && !p.isAllIn(); + } - /** - * Rotates the dealer position to the next player in the players list. This method updates the - * dealerIndex by incrementing it and wrapping around to the start of the list if necessary, - * ensuring that the dealer position rotates correctly after each hand. - */ public void rotateDealer() { dealerIndex = (dealerIndex + 1) % playerOrder.size(); } - /** - * Retrieves the current dealer based on the dealerIndex. This method returns the Player object - * corresponding to the current dealer position in the players list. - * - * @return The Player object representing the current dealer. - */ public Player getDealer() { PlayerId id = playerOrder.get(dealerIndex); return players.get(id); } - // HAND RESET + // Hand reset / lifecycle /** - * Starts a new hand by resetting the game state for the next round of poker. This method sets - * the handActive flag to true, resets the game phase to PREFLOP, clears the pot, resets all - * player bets and bet commitments, clears the community cards, and initializes a new shuffled - * deck of cards for the new hand. + * Starts a new hand by resetting the game state for the next round of poker. + * + *

This: + *

    + *
  • sets {@code phase=PREFLOP}
  • + *
  • resets pot/bets/commitments
  • + *
  • clears community + hole cards
  • + *
  • creates & shuffles a new deck
  • + *
*/ public void startNewHand() { - handActive = true; phase = GamePhase.PREFLOP; @@ -455,6 +301,10 @@ public class GameState { resetCommunityCards(); holeCards.clear(); + for (PlayerId id : playerOrder) { + holeCards.put(id, new ArrayList<>()); + } + for (Player player : players.values()) { player.setFolded(false); } @@ -462,28 +312,13 @@ public class GameState { deck = new Deck(); deck.shuffle(); - currentPlayerIndex = (dealerIndex + 1) % playerOrder.size(); + setCurrentPlayerToPreflopFirstToAct(); } - /** - * Folds a player in the current hand. This method adds the specified player's ID to the set of - * folded players, indicating that the player has folded and is no longer active in the current - * hand. - * - * @param playerId The ID of the player who is folding. - */ public void foldPlayer(PlayerId playerId) { getPlayer(playerId).setFolded(true); } - /** - * Checks if a specific player has folded in the current hand. This method checks if the - * specified player's ID is present in the set of folded players, indicating that the player has - * folded. - * - * @param playerId The ID of the player to check for folding status. - * @return true if the player has folded, false otherwise. - */ public boolean isFolded(PlayerId playerId) { return getPlayer(playerId).isFolded(); } -- 2.52.0 From b208f1fffdbce741c893b164b2a77445d2c6b061 Mon Sep 17 00:00:00 2001 From: Julian Kropff Date: Sun, 12 Apr 2026 21:47:13 +0200 Subject: [PATCH 08/21] Fix: Dealer button display issue --- .../ui/gameui/CasinoGameController.java | 71 ++++++++++++------- 1 file changed, 47 insertions(+), 24 deletions(-) diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/gameui/CasinoGameController.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/gameui/CasinoGameController.java index e674082..a250344 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/gameui/CasinoGameController.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/gameui/CasinoGameController.java @@ -496,6 +496,24 @@ public class CasinoGameController { return; } + java.util.List opponents = buildOpponents(players); + boolean meFound = opponents.size() != players.size(); + + LOGGER.info("updatePlayers: total=" + players.size() + + " myPlayerId=" + myPlayerId + + " meFound=" + meFound + + " opponents=" + opponents.size()); + + setOpponent(player1Controller, opponents, 0); + setOpponent(player2Controller, opponents, 1); + setOpponent(player3Controller, opponents, 2); + + safeRefresh(player1Controller); + safeRefresh(player2Controller); + safeRefresh(player3Controller); + } + + private java.util.List buildOpponents(List players) { java.util.List opponents = new java.util.ArrayList<>(); boolean meFound = false; @@ -517,18 +535,7 @@ public class CasinoGameController { opponents.addAll(players); } - LOGGER.info("updatePlayers: total=" + players.size() - + " myPlayerId=" + myPlayerId - + " meFound=" + meFound - + " opponents=" + opponents.size()); - - setOpponent(player1Controller, opponents, 0); - setOpponent(player2Controller, opponents, 1); - setOpponent(player3Controller, opponents, 2); - - safeRefresh(player1Controller); - safeRefresh(player2Controller); - safeRefresh(player3Controller); + return opponents; } private void setOpponent(PlayerStatusController slot, java.util.List list, int index) { @@ -556,30 +563,46 @@ public class CasinoGameController { * @param s The current game state containing the dealer index. */ private void highlightDealer(GameState s) { - - player1Controller.setDealer(false); - player2Controller.setDealer(false); - player3Controller.setDealer(false); + if (player1Controller != null) player1Controller.setDealer(false); + if (player2Controller != null) player2Controller.setDealer(false); + if (player3Controller != null) player3Controller.setDealer(false); myDealerIcon.setVisible(false); - int dealer = s.dealer; + if (s == null || s.players == null || s.players.isEmpty()) { + return; + } + if (s.dealer < 0 || s.dealer >= s.players.size()) { + return; + } - if (dealer == DEALER_PLAYER_1) { + Player dealerPlayer = s.players.get(s.dealer); + if (dealerPlayer == null || dealerPlayer.getId() == null) { + return; + } + + if (myPlayerId != null && myPlayerId.equals(dealerPlayer.getId())) { + myDealerIcon.setVisible(true); + return; + } + + java.util.List opponents = buildOpponents(s.players); + if (opponents.size() > 0 && samePlayer(opponents.get(0), dealerPlayer) && player1Controller != null) { player1Controller.setDealer(true); } - - if (dealer == DEALER_PLAYER_2) { + if (opponents.size() > 1 && samePlayer(opponents.get(1), dealerPlayer) && player2Controller != null) { player2Controller.setDealer(true); } - - if (dealer == DEALER_PLAYER_3) { + if (opponents.size() > 2 && samePlayer(opponents.get(2), dealerPlayer) && player3Controller != null) { player3Controller.setDealer(true); } + } - if (dealer == DEALER_MYSELF) { - myDealerIcon.setVisible(true); + private boolean samePlayer(Player a, Player b) { + if (a == null || b == null || a.getId() == null || b.getId() == null) { + return false; } + return a.getId().equals(b.getId()); } /** -- 2.52.0 From 4557dc2f5a045fea0d4e1af2595af152b23afb81 Mon Sep 17 00:00:00 2001 From: Julian Kropff Date: Sun, 12 Apr 2026 21:52:40 +0200 Subject: [PATCH 09/21] Fix: comment out unused input box and button --- .../resources/ui-structure/Casinomainui.fxml | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/src/main/resources/ui-structure/Casinomainui.fxml b/src/main/resources/ui-structure/Casinomainui.fxml index d16edc2..8a4e214 100644 --- a/src/main/resources/ui-structure/Casinomainui.fxml +++ b/src/main/resources/ui-structure/Casinomainui.fxml @@ -82,24 +82,24 @@ - - - - - - -