Add Game UI logic #255

Merged
j.kropff merged 28 commits from feat/game-ui into main 2026-04-11 16:59:57 +02:00
9 changed files with 202 additions and 121 deletions
Showing only changes of commit 3c7773e7fb - Show all commits
@@ -60,16 +60,12 @@ public class GameService {
return state.players; return state.players;
} }
/** /** Send a CALL command to the server to indicate that the player wants to call. */
* Send a CALL command to the server to indicate that the player wants to call.
*/
public void call() { public void call() {
client.sendCall(); client.sendCall();
} }
/** /** Send a FOLD command to the server to indicate that the player wants to fold. */
* Send a FOLD command to the server to indicate that the player wants to fold.
*/
public void fold() { public void fold() {
client.sendFold(); client.sendFold();
} }
@@ -183,9 +183,7 @@ public class Player {
this.chips -= amount; this.chips -= amount;
} }
/** /** Sets the player's state to FOLDED, indicating that they have folded in the current hand. */
* Sets the player's state to FOLDED, indicating that they have folded in the current hand.
*/
public void fall() { public void fall() {
this.state = PlayerState.FOLDED; this.state = PlayerState.FOLDED;
if (this.id != null) { if (this.id != null) {
@@ -2,9 +2,7 @@ package ch.unibas.dmi.dbis.cs108.casono.client.game;
import java.util.Objects; import java.util.Objects;
/** /** Represents a unique identifier for a player in the client domain. */
* Represents a unique identifier for a player in the client domain.
*/
public record PlayerId(String value) { public record PlayerId(String value) {
/** /**
@@ -21,9 +19,7 @@ public record PlayerId(String value) {
} }
} }
/** /** Factory method to create a PlayerId. */
* Factory method to create a PlayerId.
*/
public static PlayerId of(String value) { public static PlayerId of(String value) {
return new PlayerId(value); return new PlayerId(value);
} }
@@ -1,8 +1,6 @@
package ch.unibas.dmi.dbis.cs108.casono.client.game; package ch.unibas.dmi.dbis.cs108.casono.client.game;
/** /** Enumeration representing the possible states of a player in the poker game. */
* Enumeration representing the possible states of a player in the poker game.
*/
public enum PlayerState { public enum PlayerState {
ACTIVE, ACTIVE,
FOLDED, FOLDED,
@@ -5,6 +5,7 @@ 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.Player;
import ch.unibas.dmi.dbis.cs108.casono.client.game.PlayerId; import ch.unibas.dmi.dbis.cs108.casono.client.game.PlayerId;
import ch.unibas.dmi.dbis.cs108.casono.client.game.PlayerState; import ch.unibas.dmi.dbis.cs108.casono.client.game.PlayerState;
import java.util.List;
/** /**
* The GameClient class is responsible for communicating with the server to retrieve the current * The GameClient class is responsible for communicating with the server to retrieve the current
@@ -33,14 +34,15 @@ public class GameClient {
* @return A GameState object representing the current state of the game, as parsed * @return A GameState object representing the current state of the game, as parsed
*/ */
public GameState fetchGameState() { public GameState fetchGameState() {
List<String> responseLines = client.processCommand("GET_GAME_STATE\nGAME_ID=" + gameId);
String response = client.processCommand("GET_GAME_STATE\nGAME_ID=" + gameId); if (responseLines == null || responseLines.isEmpty()) {
if (response == null || response.isBlank()) {
throw new RuntimeException("Empty server response"); throw new RuntimeException("Empty server response");
} }
return parse(response); String fullResponse = String.join("\n", responseLines);
return parse(fullResponse);
} }
/** Send a CALL command to the server to indicate that the player wants to call */ /** Send a CALL command to the server to indicate that the player wants to call */
@@ -68,30 +70,78 @@ public class GameClient {
} }
/** /**
* Parse the raw string response from the server into a structured GameState object. * Parses the raw server response string into a structured GameState object.
* *
* @param input The raw string response from the server representing the game state. * @param input The raw server response string containing the game state information.
* @return A GameState object representing the parsed game state. * @return A GameState object representing the current state of the game.
*/ */
private GameState parse(String input) { private GameState parse(String input) {
GameState state = new GameState(); GameState state = new GameState();
Player currentPlayer = null; ParserContext ctx = new ParserContext(state);
Card currentCard = null;
boolean inPlayers = false;
boolean inPlayerCards = false;
boolean inCommunityCards = false;
for (String raw : input.split("\n")) { for (String raw : input.split("\n")) {
String line = raw.trim(); String line = raw.trim();
if (line.isEmpty() || line.startsWith("+OK")) { if (shouldSkip(line)) {
continue; 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.startsWith("PHASE=")) { if (line.startsWith("PHASE=")) {
state.phase = value(line); state.phase = value(line);
} else if (line.startsWith("POT=")) { } else if (line.startsWith("POT=")) {
@@ -104,57 +154,118 @@ public class GameClient {
state.activePlayer = intVal(line); state.activePlayer = intVal(line);
} else if (line.startsWith("WINNER=")) { } else if (line.startsWith("WINNER=")) {
state.winnerIndex = intVal(line); state.winnerIndex = intVal(line);
} else if (line.equals("END")) {
inPlayers = false;
inPlayerCards = false;
inCommunityCards = false;
currentPlayer = null;
currentCard = null;
continue;
} else if (line.equals("PLAYERS")) {
inPlayers = true;
inPlayerCards = false;
inCommunityCards = false;
continue;
} else if (line.equals("PLAYER")) {
currentPlayer = new Player();
state.players.add(currentPlayer);
continue;
} else if (line.startsWith("NAME=") && currentPlayer != null) {
currentPlayer.setId(PlayerId.of(value(line)));
} else if (line.startsWith("CHIPS=") && currentPlayer != null) {
currentPlayer.setChips(intVal(line));
} else if (line.startsWith("BET=") && currentPlayer != null) {
currentPlayer.setBet(intVal(line));
} else if (line.startsWith("STATE=") && currentPlayer != null) {
currentPlayer.setState(parseState(value(line)));
} else if (line.equals("CARDS")) {
if (inPlayers && currentPlayer != null) {
inPlayerCards = true;
inCommunityCards = false;
} else { } else {
inCommunityCards = true; return false;
inPlayerCards = false; }
return true;
} }
continue; /**
} else if (line.startsWith("CARD")) { * Processes section markers such as PLAYERS, PLAYER, CARDS, and END. Updates the parser context
currentCard = new Card("", ""); * 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 (inPlayerCards && currentPlayer != null) { if (line.equals("END")) {
currentPlayer.addCard(currentCard); ctx.inPlayers = false;
} else if (inCommunityCards) { ctx.inPlayerCards = false;
state.communityCards.add(currentCard); ctx.inCommunityCards = false;
} ctx.currentPlayer = null;
} else if (line.startsWith("VALUE=") && currentCard != null) { ctx.currentCard = null;
currentCard.setValue(value(line)); return true;
} else if (line.startsWith("SUIT=") && currentCard != null) {
currentCard.setSuit(value(line));
}
} }
return state; 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;
}
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);
}
return true;
}
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;
} }
/** /**
@@ -19,10 +19,10 @@ import javafx.scene.layout.VBox;
/** /**
* Controller for the casino gaming area. * Controller for the casino gaming area.
* *
* Responsible for: - the display of the poker table and the player interface, - the processing * <p>Responsible for: - the display of the poker table and the player interface, - the processing
* of user input, - the interface to the game engine and the network protocol. * of user input, - the interface to the game engine and the network protocol.
* *
* Notes: - The `onTableClick()` method currently serves only as test logic. It may no longer be * <p>Notes: - The `onTableClick()` method currently serves only as test logic. It may no longer be
* functional and will be replaced by the final game interaction in the future. * functional and will be replaced by the final game interaction in the future.
*/ */
public class CasinoGameController { public class CasinoGameController {
@@ -118,9 +118,7 @@ public class CasinoGameController {
private static final long CHIP_DROP_DURATION_MS = 250; private static final long CHIP_DROP_DURATION_MS = 250;
private static final long CHIP_STAGGER_DELAY_MULTIPLIER = 35L; private static final long CHIP_STAGGER_DELAY_MULTIPLIER = 35L;
/** /** Standard constructor. Used by FXML. */
* Standard constructor. Used by FXML.
*/
public CasinoGameController() { public CasinoGameController() {
// default constructor for FXML // default constructor for FXML
} }
@@ -136,9 +134,7 @@ public class CasinoGameController {
this.gameService = gameService; this.gameService = gameService;
} }
/** /** Set the PlayerId of the current player. */
* Set the PlayerId of the current player.
*/
@FXML @FXML
public void initialize() { public void initialize() {
LOGGER.info("INIT UI"); LOGGER.info("INIT UI");
@@ -265,9 +261,7 @@ public class CasinoGameController {
// updateGameInfo(s); // updateGameInfo(s);
// } // }
/** /** Start the UI update loop. */
* Start the UI update loop.
*/
public void start() { public void start() {
if (gameService == null) { if (gameService == null) {
@@ -10,10 +10,10 @@ import javafx.stage.Stage;
/** /**
* Main class for the Casono Game UI. * Main class for the Casono Game UI.
* *
* Starts the JavaFX application, loads the graphical user interface from the FXML file, and * <p>Starts the JavaFX application, loads the graphical user interface from the FXML file, and
* initializes the main stage for the game. * initializes the main stage for the game.
* *
* Tasks: - Loads the FXML interface "/ui-structure/Casinogameui.fxml". - Loads the application * <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. * icon from "/images/logoinverted.png". - Starts the application in full-screen mode.
*/ */
public class CasinoGameUI extends Application { public class CasinoGameUI extends Application {
@@ -27,9 +27,7 @@ public class PlayerStatusController {
private static final String DEALER_IMAGE_PATH = "/images/chip-dealer-blue-3.png"; private static final String DEALER_IMAGE_PATH = "/images/chip-dealer-blue-3.png";
private static final double DEALER_ICON_X_FACTOR = 0.8; private static final double DEALER_ICON_X_FACTOR = 0.8;
/** /** Initialize the controller, load dealer image, and set up bindings. */
* Initialize the controller, load dealer image, and set up bindings.
*/
@FXML @FXML
public void initialize() { public void initialize() {
@@ -62,9 +60,7 @@ public class PlayerStatusController {
refresh(); refresh();
} }
/** /** Refresh UI safely */
* Refresh UI safely
*/
public void refresh() { public void refresh() {
if (player == null) { if (player == null) {
@@ -51,9 +51,7 @@ public class TaskbarController {
private static final String STYLE_RED_INPUT = "red-input-field"; private static final String STYLE_RED_INPUT = "red-input-field";
private static final double SCALE_NORMAL = 1.0; private static final double SCALE_NORMAL = 1.0;
/** /** Standard constructor. Used by FXML. */
* Standard constructor. Used by FXML.
*/
public TaskbarController() { public TaskbarController() {
// default constructor for FXML // default constructor for FXML
} }
@@ -309,9 +307,7 @@ public class TaskbarController {
processBet(); processBet();
} }
/** /** Called when the Call button is clicked. */
* Called when the Call button is clicked.
*/
@FXML @FXML
private void onInputPlayerCall() { private void onInputPlayerCall() {
@@ -326,9 +322,7 @@ public class TaskbarController {
refreshGame(); refreshGame();
} }
/** /** Called when the Fold button is clicked. */
* Called when the Fold button is clicked.
*/
@FXML @FXML
private void onInputPlayerFold() { private void onInputPlayerFold() {
@@ -343,9 +337,7 @@ public class TaskbarController {
refreshGame(); refreshGame();
} }
/** /** Called when the Raise button is clicked. */
* Called when the Raise button is clicked.
*/
@FXML @FXML
private void onInputPlayerRaise() { private void onInputPlayerRaise() {
@@ -442,7 +434,7 @@ public class TaskbarController {
/** /**
* Opens the integrated Casono web browser. * Opens the integrated Casono web browser.
* *
* TODO: Replace the start URL with the official project website (e.g., Tips & Tricks page) * <p>TODO: Replace the start URL with the official project website (e.g., Tips & Tricks page)
* once the content for strategies and support is available. * once the content for strategies and support is available.
*/ */
@FXML @FXML