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