Feat/ms 4 integration #282

Merged
jona.walpert merged 23 commits from feat/ms-4-integration into main 2026-04-13 03:07:52 +02:00
3 changed files with 341 additions and 258 deletions
Showing only changes of commit 52b5d5b8c2 - Show all commits
@@ -10,10 +10,11 @@ import org.apache.logging.log4j.Logger;
/** /**
* Entry point and bootstrap helper for the Casono client application. * Entry point and bootstrap helper for the Casono client application.
* *
* Responsibilities: * <p>
* - Parse CLI args (host:port, optional username) * Responsibilities: - Parse CLI args (host:port, optional username) - Store
* - Store username centrally so UI can reuse it * username centrally
* - Create a shared ClientService and do startup LOGIN (optional) * so UI can reuse it - Create a shared ClientService and do startup LOGIN
* (optional)
*/ */
public class ClientApp { public class ClientApp {
@@ -47,31 +48,40 @@ public class ClientApp {
} }
public static void start(String arg, String username) { 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); String[] parts = arg.split(":", 2);
if (parts.length != 2) { if (parts.length != 2) {
throw new IllegalArgumentException("The address must be in the format <ip>:<port>."); throw new IllegalArgumentException("The address must be in the format <ip>:<port>.");
} }
String host = parts[0]; return parts;
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 (username != null && !username.isBlank()) {
setSharedUsername(username.trim());
} else {
setSharedUsername(null);
} }
if (username != null && !username.isBlank()) { private static void configureServerProperties(String host, int port) {
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));
}
private static void performStartupLogin(String host, int port, String username) {
try { try {
ClientService clientService = new ClientService(host, port); ClientService clientService = new ClientService(host, port);
setSharedClientService(clientService); setSharedClientService(clientService);
java.util.concurrent.ExecutorService bg = java.util.concurrent.ExecutorService bg = java.util.concurrent.Executors.newSingleThreadExecutor(
java.util.concurrent.Executors.newSingleThreadExecutor(
r -> { r -> {
Thread t = new Thread(r); Thread t = new Thread(r);
t.setDaemon(true); t.setDaemon(true);
@@ -85,7 +95,9 @@ public class ClientApp {
LobbyClient lobbyClient = new LobbyClient(clientService); LobbyClient lobbyClient = new LobbyClient(clientService);
LoginResult res = lobbyClient.login(username); LoginResult res = lobbyClient.login(username);
if (res != null && res.getUsername() != null && !res.getUsername().isBlank()) { if (res != null
&& res.getUsername() != null
&& !res.getUsername().isBlank()) {
setSharedUsername(res.getUsername().trim()); setSharedUsername(res.getUsername().trim());
} }
@@ -105,14 +117,14 @@ public class ClientApp {
LOGGER.warn( LOGGER.warn(
"Could not establish initial connection for startup login: {}", "Could not establish initial connection for startup login: {}",
e.getMessage()); e.getMessage());
// UI will create its own connection when it initializes
} }
} }
private static void launchUI(String arg, String username) {
if (username != null && !username.isBlank()) { if (username != null && !username.isBlank()) {
Launcher.main(new String[] {arg, username}); Launcher.main(new String[] { arg, username });
} else { } else {
Launcher.main(new String[] {arg}); Launcher.main(new String[] { arg });
} }
} }
@@ -121,8 +133,7 @@ public class ClientApp {
throw new IllegalArgumentException("Address argument required: <host:port>"); throw new IllegalArgumentException("Address argument required: <host:port>");
} }
String username = String username = args.length > 1 && args[1] != null && !args[1].isBlank() ? args[1] : null;
args.length > 1 && args[1] != null && !args[1].isBlank() ? args[1] : null;
start(args[0], username); start(args[0], username);
} }
@@ -5,8 +5,8 @@ import ch.unibas.dmi.dbis.cs108.casono.server.network.transport.RawPacket;
import ch.unibas.dmi.dbis.cs108.casono.server.network.transport.TcpTransport; import ch.unibas.dmi.dbis.cs108.casono.server.network.transport.TcpTransport;
import java.io.IOException; import java.io.IOException;
import java.net.Socket; import java.net.Socket;
import java.util.ArrayList;
import java.util.ArrayDeque; import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque; import java.util.Deque;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@@ -26,8 +26,10 @@ import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
/** /**
* The ClientService class is responsible for managing the connection to the server, sending * The ClientService class is responsible for managing the connection to the
* commands, and receiving responses. It uses a TcpTransport to communicate with the server and an * server, sending
* commands, and receiving responses. It uses a TcpTransport to communicate with
* the server and an
* ExecutorService to handle asynchronous requests. * ExecutorService to handle asynchronous requests.
*/ */
public class ClientService { public class ClientService {
@@ -41,17 +43,17 @@ public class ClientService {
private final AtomicInteger idGenerator; private final AtomicInteger idGenerator;
private Logger logger; private Logger logger;
private final Map<Integer, ArrayBlockingQueue<ParsedResponse>> pendingResponses = private final Map<Integer, ArrayBlockingQueue<ParsedResponse>> pendingResponses = new ConcurrentHashMap<>();
new ConcurrentHashMap<>(); private final CopyOnWriteArrayList<Consumer<List<String>>> eventListeners = new CopyOnWriteArrayList<>();
private final CopyOnWriteArrayList<Consumer<List<String>>> eventListeners =
new CopyOnWriteArrayList<>();
private Thread readerThread = null; private Thread readerThread = null;
private final AtomicBoolean running = new AtomicBoolean(false); private final AtomicBoolean running = new AtomicBoolean(false);
private static final int READER_JOIN_TIMEOUT_MS = 500; private static final int READER_JOIN_TIMEOUT_MS = 500;
/** /**
* Constructs a ClientService with the given server IP and port. It establishes a socket * Constructs a ClientService with the given server IP and port. It establishes
* connection to the server and initializes the TcpTransport and ExecutorService for * a socket
* connection to the server and initializes the TcpTransport and ExecutorService
* for
* communication. * communication.
* *
* @param ip The IP address of the server to connect to. * @param ip The IP address of the server to connect to.
@@ -79,8 +81,7 @@ public class ClientService {
private void startReaderThread() { private void startReaderThread() {
running.set(true); running.set(true);
readerThread = readerThread = new Thread(
new Thread(
() -> { () -> {
while (running.get()) { while (running.get()) {
try { try {
@@ -107,11 +108,22 @@ public class ClientService {
String responseText = rp.payload(); String responseText = rp.payload();
logger.info("Raw message '{}'", responseText); logger.info("Raw message '{}'", responseText);
ParsedPacket parsed = parsePacketContent(responseText);
logger.debug(
"Parsed response lines (rid={}, success={}, openBlocks={}): {}",
rid,
parsed.success,
parsed.blockStack.size(),
parsed.lines);
handleParsedResponse(rid, parsed.success, parsed.lines);
}
private ParsedPacket parsePacketContent(String responseText) {
boolean hasStatus = false; boolean hasStatus = false;
boolean success = false; boolean success = false;
List<String> lines = new ArrayList<>(); List<String> lines = new ArrayList<>();
Deque<String> blockStack = new ArrayDeque<>(); Deque<String> blockStack = new ArrayDeque<>();
for (String rawLine : responseText.split("\\n")) { for (String rawLine : responseText.split("\\n")) {
@@ -119,20 +131,16 @@ public class ClientService {
String trimmed = line.trim(); String trimmed = line.trim();
if (!hasStatus) { if (!hasStatus) {
if ("+OK".equals(trimmed)) { StatusCheckResult status = checkStatus(trimmed);
success = true; if (status != null) {
success = status.success;
hasStatus = true; hasStatus = true;
continue;
}
if (trimmed.startsWith("-ERR") || trimmed.startsWith("-ERROR")) {
success = false;
hasStatus = true;
continue;
} }
continue; continue;
} }
if (trimmed.isEmpty()) continue; if (trimmed.isEmpty())
continue;
line = line.replaceFirst("^\\s+", ""); line = line.replaceFirst("^\\s+", "");
trimmed = line.trim(); trimmed = line.trim();
@@ -149,7 +157,6 @@ public class ClientService {
lines.add("END"); lines.add("END");
continue; continue;
} }
break; break;
} }
@@ -157,21 +164,34 @@ public class ClientService {
} }
if (!hasStatus) { if (!hasStatus) {
success = inferStatus(responseText);
}
return new ParsedPacket(success, lines, blockStack);
}
private StatusCheckResult checkStatus(String trimmed) {
if ("+OK".equals(trimmed)) {
return new StatusCheckResult(true);
}
if (trimmed.startsWith("-ERR") || trimmed.startsWith("-ERROR")) {
return new StatusCheckResult(false);
}
return null;
}
private boolean inferStatus(String responseText) {
if (responseText.contains("+OK")) { if (responseText.contains("+OK")) {
success = true; return true;
hasStatus = true;
} else if (responseText.contains("-ERR") || responseText.contains("-ERROR")) {
success = false;
hasStatus = true;
} else {
success = true;
hasStatus = true;
} }
if (responseText.contains("-ERR") || responseText.contains("-ERROR")) {
return false;
}
return true;
} }
logger.debug("Parsed response lines (rid={}, success={}, openBlocks={}): {}", private void handleParsedResponse(int rid, boolean success, List<String> lines)
rid, success, blockStack.size(), lines); throws InterruptedException {
if (rid == 0) { if (rid == 0) {
for (Consumer<List<String>> l : eventListeners) { for (Consumer<List<String>> l : eventListeners) {
try { try {
@@ -190,6 +210,12 @@ public class ClientService {
} }
} }
private record ParsedPacket(boolean success, List<String> lines, Deque<String> blockStack) {
}
private record StatusCheckResult(boolean success) {
}
private boolean isContainerStart(String token) { private boolean isContainerStart(String token) {
return token.equals("LOBBIES") return token.equals("LOBBIES")
|| token.equals("LOBBY") || token.equals("LOBBY")
@@ -202,7 +228,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. * to processCommand will throw a RuntimeException.
* *
* @param offline true to create an offline (no-network) client service * @param offline true to create an offline (no-network) client service
@@ -215,19 +242,21 @@ public class ClientService {
this.executor = Executors.newSingleThreadExecutor(); 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() { public boolean isOffline() {
return offlineMode; return offlineMode;
} }
// Allow primitive values to contain hyphens (UUIDs) in addition to // Allow primitive values to contain hyphens (UUIDs) in addition to
// digits/words/colons // digits/words/colons
static Pattern responseRex = static Pattern responseRex = Pattern.compile(
Pattern.compile(
"(?<key>\\w+)=(('(?<string>([^']|\\')+)')|(?<primVal>[+-]?[-\\d\\w:]+))"); "(?<key>\\w+)=(('(?<string>([^']|\\')+)')|(?<primVal>[+-]?[-\\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 ('). * back to regular single quotes (').
* *
* @param input The escaped string to process. * @param input The escaped string to process.
@@ -238,8 +267,10 @@ public class ClientService {
} }
/** /**
* Converts a list of raw string parameters into a list of {@link RequestParameter} objects. It * Converts a list of raw string parameters into a list of
* uses a regex matcher to distinguish between quoted strings (which are unescaped) and * {@link RequestParameter} objects. It
* uses a regex matcher to distinguish between quoted strings (which are
* unescaped) and
* primitive values. * primitive values.
* *
* @param input A list of raw strings to be parsed. * @param input A list of raw strings to be parsed.
@@ -264,10 +295,12 @@ public class ClientService {
.toList(); .toList();
} }
private static record ParsedResponse(boolean success, List<String> lines) {} private static record ParsedResponse(boolean success, List<String> 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<List<String>> listener) { public void addEventListener(Consumer<List<String>> listener) {
eventListeners.add(listener); eventListeners.add(listener);
@@ -278,13 +311,17 @@ public class ClientService {
} }
/** /**
* Sends a command to the server and processes the multi-line response. It handles the protocol * Sends a command to the server and processes the multi-line response. It
* handshake (expecting +OK), strips leading tabs from response lines, and collects them until * handles the protocol
* handshake (expecting +OK), strips leading tabs from response lines, and
* collects them until
* the "END" marker is reached. * the "END" marker is reached.
* *
* @param message The raw command string to be sent to the transport layer. * @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). * @return A list of response lines received from the server (excluding protocol
* @throws RuntimeException if the server responds with an error or if a communication failure * markers).
* @throws RuntimeException if the server responds with an error or if a
* communication failure
* occurs. * occurs.
*/ */
protected List<String> processCommand(String message) { protected List<String> processCommand(String message) {
@@ -296,8 +333,7 @@ public class ClientService {
ArrayBlockingQueue<ParsedResponse> q = new ArrayBlockingQueue<>(1); ArrayBlockingQueue<ParsedResponse> q = new ArrayBlockingQueue<>(1);
pendingResponses.put(reqId, q); pendingResponses.put(reqId, q);
Future<?> writeFuture = Future<?> writeFuture = executor.submit(
executor.submit(
() -> { () -> {
try { try {
clienttcptransport.write(new RawPacket(reqId, message)); clienttcptransport.write(new RawPacket(reqId, message));
@@ -336,11 +372,15 @@ public class ClientService {
} }
/** /**
* Helper method to send a request to the server using the ExecutorService. It submits the * Helper method to send a request to the server using the ExecutorService. It
* request as a Runnable task and waits for its completion. If the task is interrupted or * submits the
* encounters an execution exception, it throws a RuntimeException with the appropriate cause. * 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) { private void sendRequest(Runnable request) {
Future<?> future = executor.submit(request); Future<?> future = executor.submit(request);
@@ -354,9 +394,12 @@ public class ClientService {
} }
/** /**
* Helper method to extract the cause of an exception and return it as a RuntimeException. If * Helper method to extract the cause of an exception and return it as a
* the cause is null, it returns the original exception as a RuntimeException. If the cause is * RuntimeException. If
* already a RuntimeException, it returns it directly. Otherwise, it wraps the cause in a new * 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. * RuntimeException and returns it.
* *
* @param e The exception from which to extract the cause. * @param e The exception from which to extract the cause.
@@ -374,8 +417,10 @@ public class ClientService {
} }
/** /**
* Closes the socket connection to the server and shuts down the ExecutorService. It also closes * Closes the socket connection to the server and shuts down the
* the TcpTransport used for communication. If any IOException occurs during this process, it * ExecutorService. It also closes
* the TcpTransport used for communication. If any IOException occurs during
* this process, it
* prints the exception to the console. * prints the exception to the console.
*/ */
public void closeSocket() { public void closeSocket() {
@@ -13,17 +13,21 @@ import java.util.logging.Logger;
/** /**
* Fetches and parses game state from server. * Fetches and parses game state from server.
* *
* Protocol notes (based on your current server implementation): * <p>
* - Success replies contain: PHASE, POT, CURRENT_BET, DEALER, ACTIVE_PLAYER, then blocks: * Protocol notes (based on your current server implementation): - Success
* - CARD ... END (community cards on root level) * replies contain:
* - PLAYER ... (NAME/CHIPS/BET/STATE + optional CARD blocks for requesting player) ... END * PHASE, POT, CURRENT_BET, DEALER, ACTIVE_PLAYER, then blocks: - CARD ... END
* - Error replies contain: -ERR then CODE=..., MSG=..., 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: * <p>
* - The server does NOT wrap cards in a "CARDS" container. * Important: - The server does NOT wrap cards in a "CARDS" container. - CARD
* - CARD blocks appear either: * blocks appear
* - at root level -> community cards * either: - at root level -> community cards - inside PLAYER -> hole cards for
* - inside PLAYER -> hole cards for that player (usually only for the requesting user) * that player (usually
* only for the requesting user)
*/ */
public class GameClient { public class GameClient {
@@ -33,7 +37,8 @@ public class GameClient {
private final int gameId; private final int gameId;
public GameClient(ClientService client, 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.client = client;
this.gameId = gameId; this.gameId = gameId;
LOG.info(() -> "GameClient initialized for gameId=" + gameId); LOG.info(() -> "GameClient initialized for gameId=" + gameId);
@@ -72,10 +77,17 @@ public class GameClient {
try { try {
GameState state = parseGameState(joined); GameState state = parseGameState(joined);
LOG.info(() -> "Parsed state: phase=" + state.phase LOG.info(
+ " pot=" + state.pot () -> "Parsed state: phase="
+ " players=" + (state.players != null ? state.players.size() : 0) + state.phase
+ " community=" + (state.communityCards != null ? state.communityCards.size() : 0)); + " pot="
+ state.pot
+ " players="
+ (state.players != null ? state.players.size() : 0)
+ " community="
+ (state.communityCards != null
? state.communityCards.size()
: 0));
return state; return state;
} catch (Exception e) { } catch (Exception e) {
LOG.log(Level.SEVERE, "Failed parsing game state. Payload=\n" + joined, 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) { private GameState parseGameState(String input) {
GameState s = new GameState(); GameState s = new GameState();
s.players = new ArrayList<>();
s.communityCards = new ArrayList<>();
if (s.players == null) s.players = new ArrayList<>(); ParseState state = new ParseState();
if (s.communityCards == null) s.communityCards = new ArrayList<>();
Player currentPlayer = null;
Card currentCard = null;
boolean insidePlayer = false;
for (String raw : input.split("\n")) { for (String raw : input.split("\n")) {
String line = raw.trim(); String line = raw.trim();
if (line.isEmpty()) continue; if (line.isEmpty() || line.startsWith("+OK"))
if (line.startsWith("+OK")) continue; continue;
// Global fields
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 (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;
}
}
if (!parseGlobalField(s, line)
&& !parsePlayerStructure(s, state, line)
&& !parseCardData(state, line)
&& !parsePlayerData(state, line)) {
LOG.fine(() -> "Ignored line: '" + line + "'"); LOG.fine(() -> "Ignored line: '" + line + "'");
} }
}
return s; 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) { private static String value(String line) {
return line.split("=", 2)[1]; return line.split("=", 2)[1];
} }
@@ -219,7 +244,8 @@ public class GameClient {
} }
private static PlayerState parseState(String s) { private static PlayerState parseState(String s) {
if (s == null) return PlayerState.ACTIVE; if (s == null)
return PlayerState.ACTIVE;
return switch (s.trim().toUpperCase()) { return switch (s.trim().toUpperCase()) {
case "ACTIVE" -> PlayerState.ACTIVE; case "ACTIVE" -> PlayerState.ACTIVE;
case "FOLDED" -> PlayerState.FOLDED; case "FOLDED" -> PlayerState.FOLDED;
@@ -229,7 +255,8 @@ public class GameClient {
} }
private static String extractValue(String joined, String key) { private static String extractValue(String joined, String key) {
if (joined == null) return null; if (joined == null)
return null;
for (String raw : joined.split("\n")) { for (String raw : joined.split("\n")) {
String line = raw.trim(); String line = raw.trim();
if (line.startsWith(key + "=")) { if (line.startsWith(key + "=")) {