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
8 changed files with 474 additions and 407 deletions
Showing only changes of commit 06c059ce5e - Show all commits
@@ -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 (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;
}
}
@@ -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<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());
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<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();
}
@@ -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());