Fix: fast Game UI communication fix: display the opponent
This commit is contained in:
@@ -10,9 +10,10 @@ import org.apache.logging.log4j.Logger;
|
||||
/**
|
||||
* Entry point and bootstrap helper for the Casono client application.
|
||||
*
|
||||
* <p>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 <ip>:<port>.");
|
||||
throw new IllegalArgumentException("The address must be in the format <ip>:<port>.");
|
||||
}
|
||||
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 <host:port>} (e.g. {@code 127.0.0.1:1234})
|
||||
*
|
||||
* <p>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: <host:port>");
|
||||
}
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Card> 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<Player> 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,13 +8,13 @@ import java.util.List;
|
||||
* dealer position, active player, community cards, and player information.
|
||||
*/
|
||||
public class GameState {
|
||||
public List<Player> players = new ArrayList<>();
|
||||
public List<Card> communityCards = new ArrayList<>();
|
||||
|
||||
public String phase;
|
||||
public int pot;
|
||||
public int currentBet;
|
||||
public int dealer;
|
||||
public int activePlayer;
|
||||
public int winnerIndex = -1;
|
||||
|
||||
public List<Card> communityCards = new ArrayList<>();
|
||||
public List<Player> players = new ArrayList<>();
|
||||
}
|
||||
|
||||
@@ -107,11 +107,16 @@ public class ClientService {
|
||||
|
||||
boolean hasStatus = false;
|
||||
boolean success = false;
|
||||
|
||||
List<String> 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<List<String>> 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.
|
||||
|
||||
@@ -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<String> 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<String> 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 (shouldSkip(line)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (handleGlobal(line, ctx)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (handleSections(line, ctx)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (handlePlayer(line, ctx)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (handleCards(line, ctx)) {
|
||||
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.isEmpty()) continue;
|
||||
if (line.startsWith("+OK")) continue;
|
||||
|
||||
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);
|
||||
s.phase = value(line);
|
||||
continue;
|
||||
}
|
||||
return true;
|
||||
if (line.startsWith("POT=")) {
|
||||
s.pot = intVal(line);
|
||||
continue;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.startsWith("CURRENT_BET=")) {
|
||||
s.currentBet = intVal(line);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (line.equals("PLAYERS")) {
|
||||
ctx.inPlayers = true;
|
||||
return true;
|
||||
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")) {
|
||||
ctx.currentPlayer = new Player();
|
||||
ctx.state.players.add(ctx.currentPlayer);
|
||||
return true;
|
||||
currentPlayer = new Player();
|
||||
s.players.add(currentPlayer);
|
||||
inPlayerCards = false;
|
||||
currentCard = null;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (line.equals("CARDS")) {
|
||||
if (ctx.inPlayers && ctx.currentPlayer != null) {
|
||||
ctx.inPlayerCards = true;
|
||||
ctx.inCommunityCards = false;
|
||||
} else {
|
||||
ctx.inCommunityCards = true;
|
||||
ctx.inPlayerCards = false;
|
||||
inPlayerCards = (currentPlayer != null);
|
||||
inCommunityCards = (currentPlayer == null);
|
||||
currentCard = null;
|
||||
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);
|
||||
currentCard = new Card("", "");
|
||||
if (inPlayerCards && currentPlayer != null) {
|
||||
currentPlayer.addCard(currentCard);
|
||||
} else if (inCommunityCards) {
|
||||
s.communityCards.add(currentCard);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
return true;
|
||||
if (line.startsWith("VALUE=") && currentCard != null) {
|
||||
currentCard.setValue(value(line));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (line.startsWith("VALUE=") && ctx.currentCard != null) {
|
||||
ctx.currentCard.setValue(value(line));
|
||||
return true;
|
||||
if (line.startsWith("SUIT=") && currentCard != null) {
|
||||
currentCard.setSuit(value(line));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (line.startsWith("SUIT=") && ctx.currentCard != null) {
|
||||
ctx.currentCard.setSuit(value(line));
|
||||
return true;
|
||||
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 false;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
LOG.fine(() -> "Ignored line: '" + line + "'");
|
||||
}
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
+118
-36
@@ -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,22 +307,36 @@ 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) {
|
||||
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<Player> players = (s.players != null) ? s.players : List.of();
|
||||
List<Card> 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<Card> getMyCards(List<Player> 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<Player> p) {
|
||||
|
||||
if (p == null) {
|
||||
private void updatePlayers(List<Player> players) {
|
||||
if (players == null || players.isEmpty()) {
|
||||
clearOpponentSlots();
|
||||
return;
|
||||
}
|
||||
|
||||
if (p.size() > PLAYER_INDEX_0) {
|
||||
player1Controller.setPlayer(p.get(PLAYER_INDEX_0));
|
||||
java.util.List<Player> 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());
|
||||
|
||||
setOpponent(player1Controller, opponents, 0);
|
||||
setOpponent(player2Controller, opponents, 1);
|
||||
setOpponent(player3Controller, opponents, 2);
|
||||
|
||||
safeRefresh(player1Controller);
|
||||
safeRefresh(player2Controller);
|
||||
safeRefresh(player3Controller);
|
||||
}
|
||||
|
||||
player1Controller.refresh();
|
||||
player2Controller.refresh();
|
||||
player3Controller.refresh();
|
||||
private void setOpponent(PlayerStatusController slot, java.util.List<Player> 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
*
|
||||
* <p>Starts the JavaFX application, loads the graphical user interface from the FXML file, and
|
||||
* initializes the main stage for the game.
|
||||
*
|
||||
* <p>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();
|
||||
}
|
||||
|
||||
+11
-4
@@ -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());
|
||||
|
||||
Reference in New Issue
Block a user