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 23030bb..db72bfa 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,10 +10,11 @@ import org.apache.logging.log4j.Logger; /** * Entry point and bootstrap helper for the Casono client application. * - * 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) + *
+ * 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 {
@@ -47,72 +48,83 @@ public class ClientApp {
}
public static void start(String arg, String username) {
+ String[] hostPort = parseHostAndPort(arg);
+ String host = hostPort[0];
+ int port = Integer.parseInt(hostPort[1]);
+
+ configureServerProperties(host, port);
+ setSharedUsername(username != null && !username.isBlank() ? username.trim() : null);
+
+ if (getSharedUsername() != null) {
+ performStartupLogin(host, port, username);
+ }
+
+ launchUI(arg, username);
+ }
+
+ private static String[] parseHostAndPort(String arg) {
String[] parts = arg.split(":", 2);
if (parts.length != 2) {
throw new IllegalArgumentException("The address must be in the format
+ * 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)
+ *
+ * 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 {
@@ -33,7 +37,8 @@ public class GameClient {
private final int gameId;
public GameClient(ClientService client, int gameId) {
- if (client == null) throw new IllegalArgumentException("ClientService must not be null");
+ if (client == null)
+ throw new IllegalArgumentException("ClientService must not be null");
this.client = client;
this.gameId = gameId;
LOG.info(() -> "GameClient initialized for gameId=" + gameId);
@@ -72,10 +77,17 @@ public class GameClient {
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));
+ 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);
@@ -101,115 +113,128 @@ public class GameClient {
private GameState parseGameState(String input) {
GameState s = new GameState();
+ s.players = new ArrayList<>();
+ s.communityCards = new ArrayList<>();
- if (s.players == null) s.players = new ArrayList<>();
- if (s.communityCards == null) s.communityCards = new ArrayList<>();
-
- Player currentPlayer = null;
- Card currentCard = null;
- boolean insidePlayer = false;
-
+ ParseState state = new ParseState();
for (String raw : input.split("\n")) {
String line = raw.trim();
- if (line.isEmpty()) continue;
- if (line.startsWith("+OK")) continue;
+ if (line.isEmpty() || line.startsWith("+OK"))
+ continue;
- // Global fields
- if (line.startsWith("PHASE=")) {
- s.phase = value(line);
- continue;
+ if (!parseGlobalField(s, line)
+ && !parsePlayerStructure(s, state, line)
+ && !parseCardData(state, line)
+ && !parsePlayerData(state, line)) {
+ LOG.fine(() -> "Ignored line: '" + line + "'");
}
- 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 (line.equals("PLAYER")) {
- currentPlayer = new Player();
- s.players.add(currentPlayer);
- currentCard = null;
- insidePlayer = true;
- continue;
- }
-
- if (line.equals("CARDS")) {
- continue;
- }
-
- if (line.equals("CARD")) {
- currentCard = new Card("", "");
-
- if (insidePlayer && currentPlayer != null) {
- currentPlayer.addCard(currentCard); // hole card
- } else {
- s.communityCards.add(currentCard); // community card
- }
- continue;
- }
-
- if (line.startsWith("VALUE=") && currentCard != null) {
- currentCard.setValue(value(line));
- continue;
- }
- if (line.startsWith("SUIT=") && currentCard != null) {
- currentCard.setSuit(value(line));
- continue;
- }
-
- if (line.equals("END")) {
- if (currentCard != null) { // schließt CARD
- currentCard = null;
- continue;
- }
- if (insidePlayer) { // schließt PLAYER
- insidePlayer = false;
- currentPlayer = null;
- continue;
- }
- continue; // root END
- }
-
- 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 + "'");
}
-
return s;
}
+ private boolean parseGlobalField(GameState s, String line) {
+ return switch (line) {
+ case String l when l.startsWith("PHASE=") -> {
+ s.phase = value(l);
+ yield true;
+ }
+ case String l when l.startsWith("POT=") -> {
+ s.pot = intVal(l);
+ yield true;
+ }
+ case String l when l.startsWith("CURRENT_BET=") -> {
+ s.currentBet = intVal(l);
+ yield true;
+ }
+ case String l when l.startsWith("DEALER=") -> {
+ s.dealer = intVal(l);
+ yield true;
+ }
+ case String l when l.startsWith("ACTIVE_PLAYER=") -> {
+ s.activePlayer = intVal(l);
+ yield true;
+ }
+ case String l when l.startsWith("WINNER=") -> {
+ s.winnerIndex = intVal(l);
+ yield true;
+ }
+ default -> false;
+ };
+ }
+
+ private boolean parsePlayerStructure(GameState s, ParseState state, String line) {
+ if (line.equals("PLAYER")) {
+ state.currentPlayer = new Player();
+ s.players.add(state.currentPlayer);
+ state.currentCard = null;
+ state.insidePlayer = true;
+ return true;
+ }
+ if (line.equals("CARDS"))
+ return true;
+ if (line.equals("CARD")) {
+ state.currentCard = new Card("", "");
+ if (state.insidePlayer && state.currentPlayer != null) {
+ state.currentPlayer.addCard(state.currentCard);
+ } else {
+ s.communityCards.add(state.currentCard);
+ }
+ return true;
+ }
+ if (line.equals("END")) {
+ if (state.currentCard != null) {
+ state.currentCard = null;
+ } else if (state.insidePlayer) {
+ state.insidePlayer = false;
+ state.currentPlayer = null;
+ }
+ return true;
+ }
+ return false;
+ }
+
+ private boolean parseCardData(ParseState state, String line) {
+ if (state.currentCard == null)
+ return false;
+ if (line.startsWith("VALUE=")) {
+ state.currentCard.setValue(value(line));
+ return true;
+ }
+ if (line.startsWith("SUIT=")) {
+ state.currentCard.setSuit(value(line));
+ return true;
+ }
+ return false;
+ }
+
+ private boolean parsePlayerData(ParseState state, String line) {
+ if (state.currentPlayer == null)
+ return false;
+ if (line.startsWith("USERNAME=") || line.startsWith("NAME=")) {
+ state.currentPlayer.setId(PlayerId.of(value(line)));
+ return true;
+ }
+ if (line.startsWith("CHIPS=")) {
+ state.currentPlayer.setChips(intVal(line));
+ return true;
+ }
+ if (line.startsWith("BET=")) {
+ state.currentPlayer.setBet(intVal(line));
+ return true;
+ }
+ if (line.startsWith("STATE=")) {
+ state.currentPlayer.setState(parseState(value(line)));
+ return true;
+ }
+ return false;
+ }
+
+ private static class ParseState {
+ Player currentPlayer;
+ Card currentCard;
+ boolean insidePlayer;
+ }
+
private static String value(String line) {
return line.split("=", 2)[1];
}
@@ -219,7 +244,8 @@ public class GameClient {
}
private static PlayerState parseState(String s) {
- if (s == null) return PlayerState.ACTIVE;
+ if (s == null)
+ return PlayerState.ACTIVE;
return switch (s.trim().toUpperCase()) {
case "ACTIVE" -> PlayerState.ACTIVE;
case "FOLDED" -> PlayerState.FOLDED;
@@ -229,7 +255,8 @@ public class GameClient {
}
private static String extractValue(String joined, String key) {
- if (joined == null) return null;
+ if (joined == null)
+ return null;
for (String raw : joined.split("\n")) {
String line = raw.trim();
if (line.startsWith(key + "=")) {
> l : eventListeners) {
try {
@@ -190,6 +210,12 @@ public class ClientService {
}
}
+ private record ParsedPacket(boolean success, List
> listener) {
eventListeners.add(listener);
@@ -278,14 +311,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