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());