From 181f538038080b3d19f5dc03b455795182d7fa7a Mon Sep 17 00:00:00 2001 From: Jona Walpert Date: Wed, 15 Apr 2026 00:11:23 +0200 Subject: [PATCH 01/12] fix: disable post-game buttons Disable action buttons when game reaches FINISHED phase in UI --- .../client/ui/gameui/gameuicomponents/TaskbarController.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/gameui/gameuicomponents/TaskbarController.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/gameui/gameuicomponents/TaskbarController.java index 9fef1e4..6e28c20 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/gameui/gameuicomponents/TaskbarController.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/gameui/gameuicomponents/TaskbarController.java @@ -243,8 +243,9 @@ public class TaskbarController { boolean isMyTurn = state.activePlayer == myIndex; boolean isOut = me.getState() == PlayerState.FOLDED; + boolean isGameFinished = state.phase != null && state.phase.equalsIgnoreCase("FINISHED"); - updateBasicButtons(isMyTurn, isOut); + updateBasicButtons(isMyTurn, isOut || isGameFinished); setMoney(me.getChips()); } -- 2.52.0 From d23a00e148bd94224d4a2c77ed90ee3948479083 Mon Sep 17 00:00:00 2001 From: Julian Kropff Date: Thu, 16 Apr 2026 21:18:42 +0200 Subject: [PATCH 02/12] Fix: disable context-dependent buttons in taskbar --- .../gameuicomponents/TaskbarController.java | 306 +++++++++++++++++- 1 file changed, 292 insertions(+), 14 deletions(-) diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/gameui/gameuicomponents/TaskbarController.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/gameui/gameuicomponents/TaskbarController.java index 6e28c20..8b80356 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/gameui/gameuicomponents/TaskbarController.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/gameui/gameuicomponents/TaskbarController.java @@ -7,6 +7,7 @@ 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.ui.lobbyui.Casinomainui; import javafx.fxml.FXML; +import javafx.scene.control.Alert; import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.control.TextField; @@ -44,6 +45,10 @@ public class TaskbarController { private static final int MIN_CREDITS = 5; private static final int MAX_CREDITS = 100000; private static final int CREDIT_STEP = 5; + private static final double PREFLOP_WARN_RATIO = 0.30; + private static final double PREFLOP_BLOCK_RATIO = 0.60; + private static final double POSTFLOP_WARN_RATIO = 0.50; + private static final double POSTFLOP_BLOCK_RATIO = 0.80; private static final String STYLE_YELLOW_BUTTON = "yellow-button"; private static final String STYLE_GRAY_BUTTON = "gray-button"; private static final String STYLE_RED_BUTTON = "red-button"; @@ -246,9 +251,177 @@ public class TaskbarController { boolean isGameFinished = state.phase != null && state.phase.equalsIgnoreCase("FINISHED"); updateBasicButtons(isMyTurn, isOut || isGameFinished); + applyActionAvailability(state, me, isMyTurn, isOut || isGameFinished); setMoney(me.getChips()); } + private void applyActionAvailability( + GameState state, Player me, boolean isMyTurn, boolean isOutOrFinished) { + if (!isMyTurn || isOutOrFinished || state == null || me == null) { + return; + } + + int chips = Math.max(0, me.getChips()); + int toCall = getToCall(state, me); + int minBetAmount = getMinimumBetAmount(state); + + boolean canBet = chips >= minBetAmount; + boolean canCall = chips > 0 && (toCall == 0 || chips >= toCall); + int raiseAmount = Math.max(0, state.currentBet); + boolean canRaise = chips > 0 && raiseAmount > 0 && chips >= toCall + raiseAmount; + + if (!canBet && !canCall && !canRaise) { + setActionEnabled(betButton, false); + setActionEnabled(callButton, false); + setActionEnabled(raiseButton, false); + setActionEnabled(foldButton, true); + deactivateInputField(taskbarInput); + return; + } + + if (isOnlyBlindsInPot(state)) { + setActionEnabled(betButton, canBet); + setActionEnabled(callButton, false); + setActionEnabled(foldButton, false); + setActionEnabled(raiseButton, false); + if (canBet) { + activateInputField(taskbarInput); + } else { + deactivateInputField(taskbarInput); + } + return; + } + + setActionEnabled(betButton, canBet); + setActionEnabled(callButton, canCall); + setActionEnabled(raiseButton, canRaise); + setActionEnabled(foldButton, true); + + if (canBet) { + activateInputField(taskbarInput); + } else { + deactivateInputField(taskbarInput); + } + } + + private void setActionEnabled(Button button, boolean enabled) { + if (enabled) { + activateButton(button); + } else { + deactivateButton(button); + } + } + + private int getToCall(GameState state, Player me) { + int current = Math.max(0, state.currentBet); + int alreadyInvested = Math.max(0, me.getBet()); + return Math.max(0, current - alreadyInvested); + } + + private int getMinimumBetAmount(GameState state) { + int bigBlind = estimateBigBlind(state); + return Math.max(MIN_CREDITS, Math.max(state.currentBet, bigBlind)); + } + + private int estimateBigBlind(GameState state) { + if (state == null) { + return MIN_CREDITS * 2; + } + + if (isPreflop(state.phase) && state.currentBet > 0) { + return state.currentBet; + } + + int maxCommitted = 0; + for (Player player : state.players) { + if (player != null) { + maxCommitted = Math.max(maxCommitted, player.getBet()); + } + } + + return maxCommitted > 0 ? maxCommitted : MIN_CREDITS * 2; + } + + private boolean isOnlyBlindsInPot(GameState state) { + if (state == null || !isPreflop(state.phase) || state.players == null || state.players.isEmpty()) { + return false; + } + + int nonZeroBets = 0; + int maxBetCount = 0; + int lowerBlindCount = 0; + int maxBet = Math.max(0, state.currentBet); + int totalCommitted = 0; + + for (Player player : state.players) { + if (player == null) { + continue; + } + + int bet = Math.max(0, player.getBet()); + totalCommitted += bet; + + if (bet > 0) { + nonZeroBets++; + if (bet == maxBet) { + maxBetCount++; + } else if (bet < maxBet) { + lowerBlindCount++; + } + } + } + + return maxBet > 0 + && nonZeroBets == 2 + && maxBetCount == 1 + && lowerBlindCount == 1 + && totalCommitted == state.pot; + } + + private boolean isPreflop(String phase) { + return "PREFLOP".equalsIgnoreCase(phase); + } + + private enum BetRisk { + ALLOWED, + WARNING, + BLOCKED + } + + private BetRisk evaluateBetRisk(GameState state, int amount, int chips) { + if (chips <= 0 || amount <= 0) { + return BetRisk.BLOCKED; + } + + double ratio = (double) amount / chips; + + if (isPreflop(state != null ? state.phase : null)) { + if (ratio > PREFLOP_BLOCK_RATIO) { + return BetRisk.BLOCKED; + } + if (ratio > PREFLOP_WARN_RATIO) { + return BetRisk.WARNING; + } + return BetRisk.ALLOWED; + } + + if (ratio > POSTFLOP_BLOCK_RATIO) { + return BetRisk.BLOCKED; + } + if (ratio > POSTFLOP_WARN_RATIO) { + return BetRisk.WARNING; + } + return BetRisk.ALLOWED; + } + + private void showWarningDialog(String title, String content) { + Alert alert = new Alert(Alert.AlertType.WARNING); + alert.setTitle(title); + alert.setHeaderText(null); + alert.setContentText(content); + alert.showAndWait(); + } + /** * Called when the taskbar is clicked with the mouse. Saves the relative position for later, * correct repositioning. @@ -320,6 +493,23 @@ public class TaskbarController { return; } + GameState state = ensureLatestStateForAction("call"); + if (state == null) { + return; + } + + Player me = findCurrentPlayer(state); + if (me == null) { + LOGGER.error("Cannot CALL: player missing in state"); + return; + } + + int toCall = getToCall(state, me); + if (me.getChips() <= 0 || (toCall > 0 && me.getChips() < toCall)) { + LOGGER.warn("CALL not possible: insufficient chips (chips={}, toCall={})", me.getChips(), toCall); + return; + } + gameService.call(); LOGGER.info("Player CALL"); @@ -350,25 +540,32 @@ public class TaskbarController { return; } - GameState state = lastState; + GameState state = ensureLatestStateForAction("raise"); if (state == null) { - try { - state = gameService.refresh(); - lastState = state; - } catch (Exception e) { - LOGGER.error("Failed to refresh game state for raise: {}", e.getMessage()); - return; - } - } - - if (state == null || state.currentBet <= 0) { - LOGGER.warn( - "Raise not possible: current bet is {}", - state != null ? state.currentBet : null); return; } + Player me = findCurrentPlayer(state); + if (me == null) { + LOGGER.error("Cannot RAISE: player missing in state"); + return; + } + + if (state.currentBet <= 0) { + LOGGER.warn("Raise not possible: current bet is {}", state.currentBet); + return; + } + + int toCall = getToCall(state, me); int raiseAmount = state.currentBet; + if (me.getChips() <= 0 || me.getChips() < toCall + raiseAmount) { + LOGGER.warn( + "Raise not possible: insufficient chips (chips={}, required={})", + me.getChips(), + toCall + raiseAmount); + return; + } + gameService.raise(raiseAmount); LOGGER.info("Player RAISE by {} (target: double table bet)", raiseAmount); @@ -446,8 +643,59 @@ public class TaskbarController { int credits = Integer.parseInt(input.trim()); + GameState state = ensureLatestStateForAction("bet"); + if (state == null) { + return; + } + + Player me = findCurrentPlayer(state); + if (me == null) { + LOGGER.error("Cannot BET: player missing in state"); + return; + } + + int minBetAmount = getMinimumBetAmount(state); + if (credits >= MIN_CREDITS && credits <= MAX_CREDITS && credits % CREDIT_STEP == 0) { + if (credits < minBetAmount) { + LOGGER.error( + "Error: Bet must be at least {} (current table minimum)", minBetAmount); + return; + } + + if (credits > me.getChips()) { + LOGGER.error( + "Error: Bet {} exceeds available chips {}", credits, me.getChips()); + return; + } + + BetRisk risk = evaluateBetRisk(state, credits, me.getChips()); + if (risk == BetRisk.BLOCKED) { + if (isPreflop(state.phase)) { + showWarningDialog( + "Bet blocked", + "In the preflop, bets exceeding 60% of your stack are blocked."); + } else { + showWarningDialog( + "Bet blocked", + "After the flop, bets exceeding 80% of your stack are blocked."); + } + return; + } + + if (risk == BetRisk.WARNING) { + if (isPreflop(state.phase)) { + showWarningDialog( + "High preflop bet", + "Warning: You're betting more than 30% of your stack preflop."); + } else { + showWarningDialog( + "High stakes", + "Warning: You are betting more than 50% of your stack in this betting round."); + } + } + gameService.bet(credits); LOGGER.info("Bet placed: {}", credits); @@ -465,6 +713,36 @@ public class TaskbarController { } } + private Player findCurrentPlayer(GameState state) { + if (state == null || state.players == null || myPlayerId == null) { + return null; + } + + return state.players.stream() + .filter(player -> player != null && myPlayerId.equals(player.getId())) + .findFirst() + .orElse(null); + } + + private GameState ensureLatestStateForAction(String actionName) { + GameState state = lastState; + + if (state == null) { + try { + state = gameService.refresh(); + lastState = state; + } catch (Exception e) { + LOGGER.error( + "Failed to refresh game state for {}: {}", + actionName, + e.getMessage()); + return null; + } + } + + return state; + } + /** * Opens the integrated Casono web browser. * -- 2.52.0 From 03daf9c3b84b6705bcd3b476d1e4fb24e61c7a80 Mon Sep 17 00:00:00 2001 From: Julian Kropff Date: Sun, 19 Apr 2026 18:51:25 +0200 Subject: [PATCH 03/12] Fix: updateTaskbar in CasinoGameController Refs #118 --- .../casono/client/ui/gameui/CasinoGameController.java | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/gameui/CasinoGameController.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/gameui/CasinoGameController.java index c43b059..1a65be2 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/gameui/CasinoGameController.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/gameui/CasinoGameController.java @@ -633,12 +633,13 @@ public class CasinoGameController { */ private void updateTaskbar(GameState s) { TaskbarController controller = resolveTaskbarController(); - if (controller != null) { - if (gameService != null && myPlayerId != null) { - controller.setGameService(gameService, myPlayerId); - } - controller.update(s, myPlayerId); + if (controller == null || myPlayerId == null) { + return; } + if (gameService != null) { + controller.setGameService(gameService, myPlayerId); + } + controller.update(s, myPlayerId); } /** -- 2.52.0 From 215e7e2caeae4559d38eaf502f9991a342a1e3a8 Mon Sep 17 00:00:00 2001 From: Julian Kropff Date: Sun, 19 Apr 2026 19:19:11 +0200 Subject: [PATCH 04/12] Fix: Improved validation for betting, calling, raising and folding in TaskbarController. Prevents incorrect user input during gameplay. Refs #118 --- .../gameuicomponents/TaskbarController.java | 783 ++++++++++++------ 1 file changed, 539 insertions(+), 244 deletions(-) diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/gameui/gameuicomponents/TaskbarController.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/gameui/gameuicomponents/TaskbarController.java index 8b80356..e25528a 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/gameui/gameuicomponents/TaskbarController.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/gameui/gameuicomponents/TaskbarController.java @@ -26,7 +26,7 @@ import org.apache.logging.log4j.Logger; */ public class TaskbarController { - private static final Logger LOGGER = LogManager.getLogger(CasinoBrowserController.class); + private static final Logger LOGGER = LogManager.getLogger(TaskbarController.class); @FXML private HBox taskbar; @FXML private TextField taskbarInput; @@ -42,13 +42,13 @@ public class TaskbarController { private double xOffset = 0; private double yOffset = 0; private static final double TASKBAR_SCALE = 0.95; - private static final int MIN_CREDITS = 5; - private static final int MAX_CREDITS = 100000; - private static final int CREDIT_STEP = 5; - private static final double PREFLOP_WARN_RATIO = 0.30; - private static final double PREFLOP_BLOCK_RATIO = 0.60; - private static final double POSTFLOP_WARN_RATIO = 0.50; - private static final double POSTFLOP_BLOCK_RATIO = 0.80; + private static final double PREFLOP_BLOCK_RATIO = 0.50; + private static final double FLOP_WARN_RATIO = 0.50; + private static final double FLOP_BLOCK_RATIO = 0.80; + private static final double TURN_RIVER_WARN_RATIO = 0.80; + private static final double TURN_RIVER_BLOCK_RATIO = 1.00; + private static final int SMALL_BLIND = 100; + private static final int BIG_BLIND = 200; private static final String STYLE_YELLOW_BUTTON = "yellow-button"; private static final String STYLE_GRAY_BUTTON = "gray-button"; private static final String STYLE_RED_BUTTON = "red-button"; @@ -56,6 +56,8 @@ public class TaskbarController { private static final String STYLE_GRAY_INPUT = "gray-input-field"; private static final String STYLE_RED_INPUT = "red-input-field"; private static final double SCALE_NORMAL = 1.0; + private boolean inputActionAllowed; + private int lastReferenceBet = BIG_BLIND; /** Standard constructor. Used by FXML. */ public TaskbarController() { @@ -63,7 +65,7 @@ public class TaskbarController { } /** - * Updates the state of the basic action buttons (Bet, Call, Fold, Raise) and the input field + * Updates the state of the basic action buttons (Bet, Call, Fold, Raise) and the input field. * * @param isMyTurn Indicates whether it is currently the player's turn. * @param isOut Indicates whether the player is currently out of the game (folded or all-in). @@ -95,9 +97,9 @@ public class TaskbarController { } /** - * Sets the given button to a disabled state with red styling, indicating that the player is out + * Sets the given button to a disabled state with red styling, indicating that the player is out. * - * @param b The button to be styled as red and disabled + * @param b The button to be styled as red and disabled. */ private void setRedButton(Button b) { b.setDisable(true); @@ -112,9 +114,9 @@ public class TaskbarController { /** * Sets the given input field to a disabled state with red styling, indicating that the player - * is out + * is out. * - * @param t The text field to be styled as red and disabled + * @param t The text field to be styled as red and disabled. */ private void setRedInputField(TextField t) { t.setDisable(true); @@ -129,9 +131,9 @@ public class TaskbarController { /** * Activates the given button by enabling it and applying yellow styling, indicating that it is - * the player's turn + * the player's turn. * - * @param b The button to be activated and styled for the player's turn + * @param b The button to be activated and styled for the player's turn. */ private void activateButton(Button b) { b.setDisable(false); @@ -145,9 +147,9 @@ public class TaskbarController { /** * Activates the given input field by enabling it and applying yellow styling, indicating that - * it is the player's turn + * it is the player's turn. * - * @param t The text field to be activated and styled for the player's turn + * @param t The text field to be activated and styled for the player's turn. */ private void activateInputField(TextField t) { t.setDisable(false); @@ -161,9 +163,9 @@ public class TaskbarController { /** * Deactivates the given button by disabling it and applying gray styling, indicating that it is - * not the player's turn + * not the player's turn. * - * @param b The button to be deactivated and styled for non-active state + * @param b The button to be deactivated and styled for non-active state. */ private void deactivateButton(Button b) { b.setDisable(true); @@ -178,9 +180,9 @@ public class TaskbarController { /** * Deactivates the given input field by disabling it and applying gray styling, indicating that - * it is not the player's turn + * it is not the player's turn. * - * @param t The text field to be deactivated and styled for non-active state + * @param t The text field to be deactivated and styled for non-active state. */ private void deactivateInputField(TextField t) { t.setDisable(true); @@ -214,15 +216,24 @@ public class TaskbarController { @FXML public void initialize() { updateBasicButtons(false, false); + if (taskbarInput != null) { + taskbarInput.textProperty().addListener((obs, oldValue, newValue) + -> refreshBetInputUi()); + } + + setBetButtonVisible(false); } /** - * Updates the taskbar based on the current game state and the player's status. It checks if - * it's the player's turn and whether they are out of the game (folded or all-in) to adjust the - * button states and displayed money accordingly. + * Updates the taskbar based on the current game state and the player's status. * - * @param state - * @param myPlayerId + * @param state The current GameState object representing the state of the game, which includes + * information about the players, their bets, the current phase and other relevant + * details needed to update the taskbar UI accurately based on the player's status + * and the game context. + * @param myPlayerId The PlayerId object representing the current player, used to identify the + * player's status and update the taskbar UI accordingly based on whether + * it is their turn and whether they are out of the game. */ public void update(GameState state, PlayerId myPlayerId) { @@ -232,6 +243,9 @@ public class TaskbarController { } this.lastState = state; + if (state.currentBet > 0) { + lastReferenceBet = state.currentBet; + } Player me = state.players.stream() @@ -253,57 +267,132 @@ public class TaskbarController { updateBasicButtons(isMyTurn, isOut || isGameFinished); applyActionAvailability(state, me, isMyTurn, isOut || isGameFinished); setMoney(me.getChips()); + refreshBetInputUi(); } + /** + * Applies the availability of actions (Bet, Call, Raise, Fold) based on the current game state. + * + * @param state The current game state to evaluate action availability. + * @param me The player object representing the current player, used to determine their chips + * and bet status. + * @param isMyTurn Indicates whether it is currently the player's turn, which affects whether + * actions can be taken. + * @param isOutOrFinished Indicates whether the player is out of the game (folded or all-in) + * or if the game is finished, which disables actions. + */ private void applyActionAvailability( GameState state, Player me, boolean isMyTurn, boolean isOutOrFinished) { + inputActionAllowed = false; if (!isMyTurn || isOutOrFinished || state == null || me == null) { + setBetButtonVisible(false); return; } int chips = Math.max(0, me.getChips()); int toCall = getToCall(state, me); - int minBetAmount = getMinimumBetAmount(state); - boolean canBet = chips >= minBetAmount; - boolean canCall = chips > 0 && (toCall == 0 || chips >= toCall); - int raiseAmount = Math.max(0, state.currentBet); - boolean canRaise = chips > 0 && raiseAmount > 0 && chips >= toCall + raiseAmount; - - if (!canBet && !canCall && !canRaise) { + boolean forcedFoldOnly = chips <= 0 || toCall > chips; + if (forcedFoldOnly) { setActionEnabled(betButton, false); setActionEnabled(callButton, false); setActionEnabled(raiseButton, false); setActionEnabled(foldButton, true); deactivateInputField(taskbarInput); + setBetButtonVisible(false); return; } - if (isOnlyBlindsInPot(state)) { - setActionEnabled(betButton, canBet); + if (isFirstPreflopPlayerInputOnly(state, me)) { + setActionEnabled(betButton, true); setActionEnabled(callButton, false); setActionEnabled(foldButton, false); setActionEnabled(raiseButton, false); - if (canBet) { - activateInputField(taskbarInput); - } else { - deactivateInputField(taskbarInput); - } + activateInputField(taskbarInput); + inputActionAllowed = true; return; } - setActionEnabled(betButton, canBet); + int callTarget = effectiveCallTarget(state); + int raiseTarget = safeDouble(callTarget); + ValidationResult callValidation = validateTarget(state, me, callTarget); + ValidationResult raiseValidation = validateTarget(state, me, raiseTarget); + boolean canCall = callValidation.valid; + boolean canRaise = raiseValidation.valid; + boolean canInput = canCall || canRaise; + + setActionEnabled(betButton, canInput); setActionEnabled(callButton, canCall); setActionEnabled(raiseButton, canRaise); setActionEnabled(foldButton, true); - if (canBet) { + if (canInput) { activateInputField(taskbarInput); + inputActionAllowed = true; } else { deactivateInputField(taskbarInput); } } + /** + * Safely doubles the given value while preventing integer overflow. + * + * @param value The integer value to be safely doubled. + * @return The safely doubled value, or 0 if the input is non-positive, or Integer.MAX_VALUE + * if doubling would overflow. + */ + private int safeDouble(int value) { + if (value <= 0) { + return 0; + } + if (value > Integer.MAX_VALUE / 2) { + return Integer.MAX_VALUE; + } + return value * 2; + } + + /** + * Sets the visibility of the Bet button in the taskbar. + * + * @param visible A boolean indicating whether the Bet button should be visible (true) + * or hidden (false). + */ + private void setBetButtonVisible(boolean visible) { + if (betButton == null) { + return; + } + betButton.setVisible(visible); + betButton.setManaged(visible); + } + + /** + * Refreshes the user interface of the bet input field and the Bet button based on + * the current game state and the validity of the input. + */ + private void refreshBetInputUi() { + if (!inputActionAllowed || taskbarInput == null || taskbarInput.isDisabled()) { + setBetButtonVisible(false); + return; + } + + ValidationResult validation = validateTypedAmount(lastState, taskbarInput.getText()); + boolean valid = validation.valid; + setBetButtonVisible(valid); + + if (valid) { + activateButton(betButton); + } else { + deactivateButton(betButton); + } + } + + /** + * Enables or disables the given button based on the provided boolean value. + * + * @param button The Button object to be enabled or disabled based on the provided boolean value. + * @param enabled A boolean value indicating whether the button should be enabled (true) + * or disabled (false). + */ private void setActionEnabled(Button button, boolean enabled) { if (enabled) { activateButton(button); @@ -312,46 +401,84 @@ public class TaskbarController { } } + /** + * Calculates the amount needed to call the current bet in the game. + * + * @param state The current GameState object representing the state of the game, which + * includes information about the current bet and the player's bet status. + * @param me The Player object representing the current player, used to determine how much + * they have already invested in the current bet. + * @return An integer representing the amount needed for the player to call the current bet. + */ private int getToCall(GameState state, Player me) { - int current = Math.max(0, state.currentBet); + int current = effectiveCallTarget(state); int alreadyInvested = Math.max(0, me.getBet()); return Math.max(0, current - alreadyInvested); } - private int getMinimumBetAmount(GameState state) { - int bigBlind = estimateBigBlind(state); - return Math.max(MIN_CREDITS, Math.max(state.currentBet, bigBlind)); - } - - private int estimateBigBlind(GameState state) { - if (state == null) { - return MIN_CREDITS * 2; - } - - if (isPreflop(state.phase) && state.currentBet > 0) { + /** + * Determines the effective call target for the current game state. If there is an active current + * bet, it returns that as the call target. + * + * @param state The current GameState object representing the state of the game, which + * includes information about the current bet and the last reference bet. + * @return An integer representing the effective call target for the current game state. + */ + private int effectiveCallTarget(GameState state) { + if (state != null && state.currentBet > 0) { return state.currentBet; } - - int maxCommitted = 0; - for (Player player : state.players) { - if (player != null) { - maxCommitted = Math.max(maxCommitted, player.getBet()); - } - } - - return maxCommitted > 0 ? maxCommitted : MIN_CREDITS * 2; + return Math.max(0, lastReferenceBet); } - private boolean isOnlyBlindsInPot(GameState state) { - if (state == null || !isPreflop(state.phase) || state.players == null || state.players.isEmpty()) { + /** + * Checks if the current player is the first to act in the pre-flop phase + * and if the initial blind layout is still in place. + * + * @param state The current GameState object representing the state of the game, + * which includes information about the phase of the game, the players, + * the dealer position, and the active player. + * @param me The Player object representing the current player, used to determine + * their position in the player list and whether they are the active player. + * @return A boolean value indicating whether the current player is the first to act + * in the pre-flop phase with only the initial blind layout in place. + */ + private boolean isFirstPreflopPlayerInputOnly(GameState state, Player me) { + if (state == null || me == null || !isPreflop(state.phase) || state.players == null) { return false; } - int nonZeroBets = 0; - int maxBetCount = 0; - int lowerBlindCount = 0; - int maxBet = Math.max(0, state.currentBet); - int totalCommitted = 0; + int size = state.players.size(); + if (size < 2) { + return false; + } + + int myIndex = state.players.indexOf(me); + if (myIndex < 0) { + return false; + } + + int dealer = state.dealer; + int firstIndex = (size == 2) ? dealer : (dealer + 3) % size; + if (state.activePlayer != firstIndex || myIndex != firstIndex) { + return false; + } + + return isInitialPreflopBlindLayout(state); + } + + /** + * Checks if the initial blind layout is still in place during the pre-flop phase. + * + * @param state The current GameState object representing the state of the game, + * which includes information about the players and their bets. + * @return A boolean value indicating whether the initial blind layout is still in + * place during the pre-flop phase. + */ + private boolean isInitialPreflopBlindLayout(GameState state) { + int sbCount = 0; + int bbCount = 0; + int zeroCount = 0; for (Player player : state.players) { if (player == null) { @@ -359,61 +486,343 @@ public class TaskbarController { } int bet = Math.max(0, player.getBet()); - totalCommitted += bet; - - if (bet > 0) { - nonZeroBets++; - if (bet == maxBet) { - maxBetCount++; - } else if (bet < maxBet) { - lowerBlindCount++; - } + if (bet == SMALL_BLIND) { + sbCount++; + } else if (bet == BIG_BLIND) { + bbCount++; + } else if (bet == 0) { + zeroCount++; } } - return maxBet > 0 - && nonZeroBets == 2 - && maxBetCount == 1 - && lowerBlindCount == 1 - && totalCommitted == state.pot; + int playerCount = state.players.size(); + return playerCount >= 2 && sbCount == 1 && bbCount == 1 && zeroCount == playerCount - 2; } + /** + * Checks if the given phase string corresponds to the pre-flop phase of the game. + * + * @param phase The string representing the current phase of the game, which is expected + * to be compared against "PREFLOP" to determine if it is the pre-flop phase. + * @return A boolean value indicating whether the given phase string corresponds to the + * pre-flop phase. + */ private boolean isPreflop(String phase) { return "PREFLOP".equalsIgnoreCase(phase); } + /** + * Checks if the given phase string corresponds to the flop phase of the game. + * + * @param phase The string representing the current phase of the game, which is + * expected to be compared against "FLOP" to determine if it is the flop phase. + * @return A boolean value indicating whether the given phase string corresponds to the flop phase. + */ + private boolean isFlop(String phase) { + return "FLOP".equalsIgnoreCase(phase); + } + + /** + * Checks if the given phase string corresponds to either the turn or river phase of the game. + * + * @param phase The string representing the current phase of the game, which is expected to + * be compared against "TURN" and "RIVER" to determine if it is either the turn + * or river phase. + * @return A boolean value indicating whether the given phase string corresponds to either + * the turn or river phase. + */ + private boolean isTurnOrRiver(String phase) { + return "TURN".equalsIgnoreCase(phase) || "RIVER".equalsIgnoreCase(phase); + } + + /** + * Evaluates the risk level of a proposed bet based on the current game state, the amount of + * the bet and the player's available chips. + */ private enum BetRisk { ALLOWED, WARNING, BLOCKED } + /** + * Enum representing the types of actions that can be submitted from the taskbar. + */ + private enum ActionType { + INPUT, + CALL_BUTTON, + RAISE_BUTTON + } + + /** + * Class representing the result of validating a proposed bet or action. + */ + private static final class ValidationResult { + private final boolean valid; + private final boolean warning; + private final String message; + + private ValidationResult(boolean valid, boolean warning, String message) { + this.valid = valid; + this.warning = warning; + this.message = message; + } + + private static ValidationResult ok() { + return new ValidationResult(true, false, null); + } + + private static ValidationResult warning(String message) { + return new ValidationResult(true, true, message); + } + + private static ValidationResult blocked(String message) { + return new ValidationResult(false, false, message); + } + } + + /** + * Submits a player action based on the specified ActionType. + * + * @param actionType The type of action being submitted, which can be an input-based action + * (where the player types an amount) or a specific button action for + * calling or raising. + */ + private void submitAction(ActionType actionType) { + if (gameService == null) { + LOGGER.error("GameService not initialized"); + return; + } + + GameState state = ensureLatestStateForAction(actionType.name().toLowerCase()); + if (state == null) { + return; + } + + Player me = findCurrentPlayer(state); + if (me == null) { + LOGGER.error("Action {} blocked: player missing in state", actionType); + return; + } + + int callTarget = effectiveCallTarget(state); + int targetBet; + if (actionType == ActionType.CALL_BUTTON) { + targetBet = callTarget; + } else if (actionType == ActionType.RAISE_BUTTON) { + targetBet = safeDouble(callTarget); + } else { + Integer fromInput = parseInputTarget(taskbarInput.getText()); + if (fromInput == null) { + return; + } + targetBet = fromInput; + } + + ValidationResult validation = validateTarget(state, me, targetBet); + if (!validation.valid) { + refreshBetInputUi(); + return; + } + + if (validation.warning) { + showWarningDialog("WARNING", validation.message); + } + + if (state.currentBet <= 0) { + gameService.bet(targetBet); + LOGGER.info("Player BET (target={})", targetBet); + } else if (targetBet == callTarget) { + gameService.call(); + LOGGER.info("Player CALL (target={})", targetBet); + } else { + int raiseBy = Math.max(0, targetBet - state.currentBet); + gameService.raise(raiseBy); + LOGGER.info("Player RAISE by {} (target={})", raiseBy, targetBet); + } + + if (targetBet > 0) { + lastReferenceBet = targetBet; + } + + taskbarInput.clear(); + refreshGame(); + } + + /** + * Ensures that the latest game state is available for processing a player action. + * If the last known state is null, it attempts to fetch the current state from the GameService. + * + * @param text The text associated with the action being processed, used for logging purposes + * to indicate which action is being attempted when the state is not available. + * @return The latest GameState object if available, or null if the state cannot be retrieved, + * indicating that the action cannot be processed. + */ + private Integer parseInputTarget(String text) { + if (text == null || text.trim().isEmpty()) { + return null; + } + try { + return Integer.parseInt(text.trim()); + } catch (NumberFormatException e) { + return null; + } + } + + /** + * Finds the current player in the given game state based on the player's ID. + * + * @param state The current GameState object representing the state of the game, + * which includes a list of players. + * @param text The text associated with the action being processed, used for logging + * purposes to indicate which action is being attempted when the player + * cannot be found in the state. + * @return The Player object representing the current player if found in the game state, + * or null if no matching player is found, indicating that the player cannot be identified + * in the current game state. + */ + private ValidationResult validateTypedAmount(GameState state, String text) { + Integer target = parseInputTarget(text); + if (target == null || state == null) { + return ValidationResult.blocked("Invalid input"); + } + Player me = findCurrentPlayer(state); + if (me == null) { + return ValidationResult.blocked("Player not found"); + } + return validateTarget(state, me, target); + } + + /** + * Validates a proposed target bet against the current game state and the player's status. + * It checks if the target bet is either a valid call or a valid raise + * (specifically, exactly double the call target). + * + * @param state The current GameState object representing the state of the game, + * which includes information about the current bet, the phase of the game, + * and the players. + * @param me The Player object representing the current player, used to determine their + * current bet, available chips, and to evaluate whether the proposed target + * bet is valid for this player based on their status in the game. + * @param targetBet The integer value representing the proposed target bet that the + * player intends to make. + * @return A ValidationResult object indicating whether the proposed target bet is valid, + * if it triggers a warning, or if it is blocked due to being invalid or too risky. + */ + private ValidationResult validateTarget(GameState state, Player me, int targetBet) { + if (state == null || me == null) { + return ValidationResult.blocked("Invalid game state"); + } + + int callTarget = effectiveCallTarget(state); + int raiseTarget = safeDouble(callTarget); + if (targetBet != callTarget && targetBet != raiseTarget) { + return ValidationResult.blocked("Only calls or exactly two raises allowed"); + } + + int alreadyInvested = Math.max(0, me.getBet()); + int required = Math.max(0, targetBet - alreadyInvested); + int chips = Math.max(0, me.getChips()); + + if (targetBet == callTarget && required == 0) { + return ValidationResult.ok(); + } + + if (required <= 0) { + return ValidationResult.blocked("Invalid bet"); + } + + if (required > chips) { + return ValidationResult.blocked("Not enough chips for the next bet"); + } + + BetRisk risk = evaluateBetRisk(state, required, chips); + if (risk == BetRisk.BLOCKED) { + return ValidationResult.blocked("Assignment for this phase is blocked"); + } + if (risk == BetRisk.WARNING) { + return ValidationResult.warning(warningTextForPhase(state)); + } + return ValidationResult.ok(); + } + + /** + * Generates a warning message based on the current phase of the game. + * + * @param state The current GameState object representing the state of the game, + * which includes information about the phase of the game. + * @return A string containing the warning message appropriate for the current phase + * of the game when a player's bet triggers a warning due to being at least + * 50% of their stack. + */ + private String warningTextForPhase(GameState state) { + String phase = state != null ? state.phase : null; + if (isFlop(phase)) { + return "WARNING: On the flop, you're betting at least 50% of your stack."; + } + if (isTurnOrRiver(phase)) { + return "WARNING: On the flop, you're betting at least 50% of your stack."; + } + return "WARNING: High stakes."; + } + + /** + * Evaluates the risk level of a proposed bet based on the current game state, + * the amount of the bet, and the player's available chips. + * + * @param state The current GameState object representing the state of the game, + * which includes information about the phase of the game. + * @param amount The integer value representing the amount of the proposed bet + * that the player intends to make. + * @param chips The integer value representing the player's available chips. + * @return A BetRisk enum value indicating the risk level of the proposed bet. + */ private BetRisk evaluateBetRisk(GameState state, int amount, int chips) { if (chips <= 0 || amount <= 0) { return BetRisk.BLOCKED; } double ratio = (double) amount / chips; + String phase = state != null ? state.phase : null; - if (isPreflop(state != null ? state.phase : null)) { + if (isPreflop(phase)) { if (ratio > PREFLOP_BLOCK_RATIO) { return BetRisk.BLOCKED; } - if (ratio > PREFLOP_WARN_RATIO) { + return BetRisk.ALLOWED; + } + + if (isFlop(phase)) { + if (ratio > FLOP_BLOCK_RATIO) { + return BetRisk.BLOCKED; + } + if (ratio >= FLOP_WARN_RATIO) { return BetRisk.WARNING; } return BetRisk.ALLOWED; } - if (ratio > POSTFLOP_BLOCK_RATIO) { - return BetRisk.BLOCKED; - } - if (ratio > POSTFLOP_WARN_RATIO) { - return BetRisk.WARNING; + if (isTurnOrRiver(phase)) { + if (ratio >= TURN_RIVER_BLOCK_RATIO) { + return BetRisk.BLOCKED; + } + if (ratio > TURN_RIVER_WARN_RATIO) { + return BetRisk.WARNING; + } + return BetRisk.ALLOWED; } + return BetRisk.ALLOWED; } + /** + * Displays a warning dialog with the specified title and content. + * + * @param title The string representing the title of the warning dialog, which is displayed + * in the title bar of the dialog window. + * @param content The string representing the content of the warning message, which is displayed + * in the body of the dialog. + */ private void showWarningDialog(String title, String content) { Alert alert = new Alert(Alert.AlertType.WARNING); alert.setTitle(title); @@ -471,6 +880,12 @@ public class TaskbarController { @FXML private void onInputSubmitted(KeyEvent event) { if (event.getCode() == KeyCode.ENTER) { + ValidationResult validation = validateTypedAmount(lastState, taskbarInput.getText()); + if (!validation.valid) { + event.consume(); + setBetButtonVisible(false); + return; + } processBet(); } } @@ -487,33 +902,7 @@ public class TaskbarController { /** Called when the Call button is clicked. */ @FXML private void onInputPlayerCall() { - - if (gameService == null) { - LOGGER.error("GameService not initialized"); - return; - } - - GameState state = ensureLatestStateForAction("call"); - if (state == null) { - return; - } - - Player me = findCurrentPlayer(state); - if (me == null) { - LOGGER.error("Cannot CALL: player missing in state"); - return; - } - - int toCall = getToCall(state, me); - if (me.getChips() <= 0 || (toCall > 0 && me.getChips() < toCall)) { - LOGGER.warn("CALL not possible: insufficient chips (chips={}, toCall={})", me.getChips(), toCall); - return; - } - - gameService.call(); - LOGGER.info("Player CALL"); - - refreshGame(); + submitPresetInputAndProcess("call"); } /** Called when the Fold button is clicked. */ @@ -534,43 +923,25 @@ public class TaskbarController { /** Called when the Raise button is clicked. */ @FXML private void onInputPlayerRaise() { + submitPresetInputAndProcess("raise"); + } - if (gameService == null) { - LOGGER.error("GameService not initialized"); + /** + * Submits a preset input for either calling or raising based on the specified mode. + * + * @param mode A string indicating the mode of the action, which can be "call" + * for calling the current bet or "raise" for raising to double the current bet. + */ + private void submitPresetInputAndProcess(String mode) { + GameState state = ensureLatestStateForAction(mode); + if (state == null || taskbarInput == null) { return; } - GameState state = ensureLatestStateForAction("raise"); - if (state == null) { - return; - } - - Player me = findCurrentPlayer(state); - if (me == null) { - LOGGER.error("Cannot RAISE: player missing in state"); - return; - } - - if (state.currentBet <= 0) { - LOGGER.warn("Raise not possible: current bet is {}", state.currentBet); - return; - } - - int toCall = getToCall(state, me); - int raiseAmount = state.currentBet; - if (me.getChips() <= 0 || me.getChips() < toCall + raiseAmount) { - LOGGER.warn( - "Raise not possible: insufficient chips (chips={}, required={})", - me.getChips(), - toCall + raiseAmount); - return; - } - - gameService.raise(raiseAmount); - LOGGER.info("Player RAISE by {} (target: double table bet)", raiseAmount); - - taskbarInput.clear(); - refreshGame(); + int callTarget = effectiveCallTarget(state); + int target = "raise".equals(mode) ? safeDouble(callTarget) : callTarget; + taskbarInput.setText(String.valueOf(target)); + processBet(); } /** @@ -631,86 +1002,7 @@ public class TaskbarController { * displayed on the console. */ private void processBet() { - - if (gameService == null) { - LOGGER.error("Error: GameService not initialized"); - return; - } - - String input = taskbarInput.getText(); - - try { - - int credits = Integer.parseInt(input.trim()); - - GameState state = ensureLatestStateForAction("bet"); - if (state == null) { - return; - } - - Player me = findCurrentPlayer(state); - if (me == null) { - LOGGER.error("Cannot BET: player missing in state"); - return; - } - - int minBetAmount = getMinimumBetAmount(state); - - if (credits >= MIN_CREDITS && credits <= MAX_CREDITS && credits % CREDIT_STEP == 0) { - - if (credits < minBetAmount) { - LOGGER.error( - "Error: Bet must be at least {} (current table minimum)", minBetAmount); - return; - } - - if (credits > me.getChips()) { - LOGGER.error( - "Error: Bet {} exceeds available chips {}", credits, me.getChips()); - return; - } - - BetRisk risk = evaluateBetRisk(state, credits, me.getChips()); - if (risk == BetRisk.BLOCKED) { - if (isPreflop(state.phase)) { - showWarningDialog( - "Bet blocked", - "In the preflop, bets exceeding 60% of your stack are blocked."); - } else { - showWarningDialog( - "Bet blocked", - "After the flop, bets exceeding 80% of your stack are blocked."); - } - return; - } - - if (risk == BetRisk.WARNING) { - if (isPreflop(state.phase)) { - showWarningDialog( - "High preflop bet", - "Warning: You're betting more than 30% of your stack preflop."); - } else { - showWarningDialog( - "High stakes", - "Warning: You are betting more than 50% of your stack in this betting round."); - } - } - - gameService.bet(credits); - - LOGGER.info("Bet placed: {}", credits); - - taskbarInput.clear(); - - refreshGame(); - - } else { - LOGGER.error("Error: Bet must be between 5 and 100000 and multiple of 5"); - } - - } catch (NumberFormatException e) { - LOGGER.error("Error: Invalid number entered"); - } + submitAction(ActionType.INPUT); } private Player findCurrentPlayer(GameState state) { @@ -724,23 +1016,26 @@ public class TaskbarController { .orElse(null); } + /** + * Ensures that the latest game state is available for processing a player action. + * + * @param actionName The name of the action being processed, used for logging purposes to indicate + * which action is being attempted when the state is not available. + * @return The latest GameState object if available, or null if the state cannot be retrieved, + * indicating that the action cannot be processed due to the lack of a valid game state. + */ private GameState ensureLatestStateForAction(String actionName) { - GameState state = lastState; - - if (state == null) { - try { - state = gameService.refresh(); - lastState = state; - } catch (Exception e) { - LOGGER.error( - "Failed to refresh game state for {}: {}", - actionName, - e.getMessage()); - return null; + try { + GameState refreshed = gameService.refresh(); + if (refreshed != null) { + lastState = refreshed; + return refreshed; } + } catch (Exception e) { + LOGGER.error("Failed to refresh game state for {}: {}", actionName, e.getMessage()); } - return state; + return lastState; } /** -- 2.52.0 From 621843167e8a0f39449b44c4c64e625175aee229 Mon Sep 17 00:00:00 2001 From: Julian Kropff Date: Sun, 19 Apr 2026 19:21:44 +0200 Subject: [PATCH 05/12] Fix: Improve handleAction logic in GameEngine Refs #118 --- .../server/domain/game/engine/GameEngine.java | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/game/engine/GameEngine.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/game/engine/GameEngine.java index 76c2233..4ea817e 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/game/engine/GameEngine.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/game/engine/GameEngine.java @@ -1,6 +1,9 @@ package ch.unibas.dmi.dbis.cs108.casono.server.domain.game.engine; import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.action.Action; +import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.action.ActionType; +import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.player.Player; +import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.player.PlayerId; import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.rules.RuleEngine; import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.state.GameState; @@ -80,12 +83,38 @@ public class GameEngine { */ public void handleAction(GameState state, Action action) { + if (action.getType() != ActionType.BLIND) { + + Player currentPlayer = state.getCurrentPlayer(); + PlayerId actingPlayerId = action.getPlayerId(); + + if (currentPlayer == null || actingPlayerId == null) { + throw new RuntimeException("Invalid turn state"); + } + + if (!state.isAllowOutOfTurn() && !currentPlayer.getId().equals(actingPlayerId)) { + throw new RuntimeException("Not your turn"); + } + + Player actingPlayer = state.getPlayer(actingPlayerId); + if (actingPlayer.isFolded()) { + throw new RuntimeException("Player already folded"); + } + if (actingPlayer.isAllIn()) { + throw new RuntimeException("Player is all-in"); + } + } + // 1.Check the rules ruleEngine.validate(state, action); // 2. Perform action action.execute(state); + if (action.getType() != ActionType.BLIND) { + state.markActedThisRound(action.getPlayerId()); + } + // 3. Next turn turnManager.nextPlayer(state); -- 2.52.0 From 238a156d9106e3c150c26aef24669407d6c743ac Mon Sep 17 00:00:00 2001 From: Julian Kropff Date: Sun, 19 Apr 2026 19:28:13 +0200 Subject: [PATCH 06/12] Fix: round transition logic in RoundManager Refs #118 --- .../domain/game/engine/RoundManager.java | 151 ++++++++++++++++++ 1 file changed, 151 insertions(+) diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/game/engine/RoundManager.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/game/engine/RoundManager.java index 677c1b4..9602398 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/game/engine/RoundManager.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/game/engine/RoundManager.java @@ -27,6 +27,12 @@ public class RoundManager { public static final int SMALL_BLIND = 100; public static final int BIG_BLIND = 200; + /** + * Starts a new hand by resetting the game state, ensuring a deck is available, dealing hole cards + * to players, posting blinds, and setting the first player to act for the preflop phase. + * + * @param state The GameState object representing the current state of the game, which will be modified to + */ public void startNewHand(GameState state) { // Use GameState's canonical reset state.startNewHand(); @@ -44,12 +50,19 @@ public class RoundManager { state.setCurrentPlayerToPreflopFirstToAct(); } + /** + * Checks if the current betting round is finished and advances the game phase if necessary. + * + * @param state The GameState object representing the current state of the game, + * which may be modified to advance the phase or end the hand. + */ public void progressIfNeeded(GameState state) { if (state.getPhase() == null) { state.setPhase(GamePhase.PREFLOP); } if (state.countNonFoldedPlayers() == 1) { + state.setHandActive(false); state.setPhase(GamePhase.FINISHED); return; } @@ -59,9 +72,27 @@ public class RoundManager { } } + /** + * Determines if the current betting round is finished by checking if all active + * players have met the current bet or are all-in and by handling special cases for + * preflop betting rounds. + * + * @param state The GameState object representing the current state of the game, + * which is used to evaluate the betting round status. + * @return true if the betting round is finished and the game can progress to the + * next phase, false otherwise. + */ private boolean isBettingRoundFinished(GameState state) { int target = state.getTableState().getCurrentBet(); + if (target == 0 && isPostflopStreet(state.getPhase()) && !allActivePlayersActedThisRound(state)) { + return false; + } + + if (isPendingBigBlindOption(state, target)) { + return false; + } + for (Player p : state.getPlayers()) { if (p == null) { continue; @@ -80,6 +111,79 @@ public class RoundManager { return true; } + /** + * Helper method to determine if the current game phase is a postflop street (FLOP, TURN, RIVER). + * + * @param phase The GamePhase to check, which is used to determine if the current street is postflop. + * @return true if the phase is FLOP, TURN, or RIVER, indicating a postflop street, false otherwise. + */ + private boolean isPostflopStreet(GamePhase phase) { + return phase == GamePhase.FLOP || phase == GamePhase.TURN || phase == GamePhase.RIVER; + } + + /** + * Helper method to check if all active players have acted in the current betting round. + * This is used to handle the special case of preflop rounds where no bets have been made yet, + * but players still need to have the opportunity to act. + * + * @param state The GameState object representing the current state of the game, which is + * used to check if all active players have acted in the current round. + * @return true if all active players have acted in the current round, false if there are + * still active players who have not acted yet. + */ + private boolean allActivePlayersActedThisRound(GameState state) { + for (Player p : state.getPlayers()) { + if (p == null || p.isFolded() || p.isAllIn()) { + continue; + } + + if (!state.hasActedThisRound(p.getId())) { + return false; + } + } + return true; + } + + /** + * Helper method to determine if the current player has a pending big blind option + * in the preflop betting round. + * + * @param state The GameState object representing the current state of the game, + * which is used to determine if the big blind has a pending option + * in the preflop round. + * @param target The current target bet that players need to meet, which is used to + * check if the big blind has a pending option when the target is + * equal to the big blind amount. + * @return true if the big blind has a pending option to act in the preflop round, + * false otherwise. + */ + private boolean isPendingBigBlindOption(GameState state, int target) { + if (state == null || state.getPhase() != GamePhase.PREFLOP) { + return false; + } + + if (target != BIG_BLIND) { + return false; + } + + int size = state.getPlayerCount(); + if (size < 2) { + return false; + } + + int dealer = state.getDealerIndex(); + int bigBlindIndex = (size == 2) ? (dealer + 1) % size : (dealer + 2) % size; + + return state.getCurrentPlayerIndex() == bigBlindIndex; + } + + /** + * Advances the game phase to the next stage (FLOP, TURN, RIVER, SHOWDOWN) based on the current phase. + * + * @param state The GameState object representing the current state of the game, which will be modified + * to advance the phase and deal community cards as needed when progressing to the + * next stage of the hand. + */ private void advancePhase(GameState state) { switch (state.getPhase()) { case PREFLOP -> dealFlop(state); @@ -93,6 +197,13 @@ public class RoundManager { } } + /** + * Ensures that a deck of cards is available in the game state. + * + * @param state The GameState object representing the current state of the game, + * which will be modified to include a new shuffled deck if one + * does not already exist. + */ private void ensureDeck(GameState state) { Deck deck = state.getDeck(); if (deck == null) { @@ -102,6 +213,13 @@ public class RoundManager { } } + /** + * Deals hole cards to each player in the game state by drawing two cards from the + * deck for each player and assigning them as their hole cards. + * + * @param state The GameState object representing the current state of the game, + * which will be modified to assign hole cards to each active player. + */ private void dealHoleCards(GameState state) { Deck deck = state.getDeck(); @@ -165,6 +283,15 @@ public class RoundManager { state.getTableState().setCurrentBet(BIG_BLIND); } + /** + * Deals the flop by drawing three community cards from the deck and adding them + * to the game state, then setting the game phase to FLOP and resetting bets for + * the new betting round. + * + * @param state The GameState object representing the current state of the game, + * which will be modified to add three community cards for the flop, + * set the phase to FLOP, and reset bets for the new betting round. + */ private void dealFlop(GameState state) { ensureDeck(state); Deck deck = state.getDeck(); @@ -180,6 +307,15 @@ public class RoundManager { state.setCurrentPlayerToPostflopFirstToAct(); } + /** + * Deals the turn by drawing one community card from the deck and adding it to + * the game state, then setting the game phase to TURN and resetting bets for + * the new betting round. + * + * @param state The GameState object representing the current state of the game, + * which will be modified to add one community card for the turn, + * set the phase to TURN, and reset bets for the new betting round. + */ private void dealTurn(GameState state) { ensureDeck(state); Deck deck = state.getDeck(); @@ -193,6 +329,15 @@ public class RoundManager { state.setCurrentPlayerToPostflopFirstToAct(); } + /** + * Deals the river by drawing one community card from the deck and adding it + * to the game state, then setting the game phase to RIVER and resetting bets + * for the new betting round. + * + * @param state The GameState object representing the current state of the game, + * which will be modified to add one community card for the river, + * set the phase to RIVER, and reset bets for the new betting round. + */ private void dealRiver(GameState state) { ensureDeck(state); Deck deck = state.getDeck(); @@ -206,6 +351,12 @@ public class RoundManager { state.setCurrentPlayerToPostflopFirstToAct(); } + /** + * Handles the showdown phase by setting the game phase to SHOWDOWN. + * + * @param state The GameState object representing the current state of the game, + * which will be modified to set the phase to SHOWDOWN. + */ private void showdown(GameState state) { state.setPhase(GamePhase.SHOWDOWN); -- 2.52.0 From e39cbb4357db64016992542cb885973d58b53cb9 Mon Sep 17 00:00:00 2001 From: Julian Kropff Date: Sun, 19 Apr 2026 19:35:57 +0200 Subject: [PATCH 07/12] Fix: adjust GameState logic Refs #118 --- .../server/domain/game/state/GameState.java | 243 +++++++++++++++++- 1 file changed, 233 insertions(+), 10 deletions(-) diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/game/state/GameState.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/game/state/GameState.java index 1732072..92d18b5 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/game/state/GameState.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/game/state/GameState.java @@ -7,8 +7,10 @@ import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.player.PlayerId; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; /** * The GameState class encapsulates the entire state of a poker game at any given moment. @@ -38,6 +40,7 @@ public class GameState { private final Map currentBets = new HashMap<>(); private final Map playerBetCommitments = new HashMap<>(); + private final Set actedThisRound = new HashSet<>(); private final Map> holeCards = new HashMap<>(); private final List communityCards = new ArrayList<>(); @@ -46,7 +49,11 @@ public class GameState { private static final int DEALER_OFFSET = 3; - // Getters + /** + * Returns the players in seating/turn order. + * + * @return a collection of players in the order they are seated/act. + */ public Collection getPlayers() { List ordered = new ArrayList<>(playerOrder.size()); for (PlayerId id : playerOrder) { @@ -58,57 +65,124 @@ public class GameState { return ordered; } + /** + * Returns the current player whose turn it is to act. + * + * @return the current player, or null if there are no players or the index is out of bounds. + */ public Player getCurrentPlayer() { PlayerId id = playerOrder.get(currentPlayerIndex); return players.get(id); } + /** + * Returns the index of the current player in the player order list. + * + * @return the index of the current player, or 0 if there are no players. + */ public int getCurrentPlayerIndex() { return currentPlayerIndex; } + /** + * Returns whether a hand is currently active (i.e., in progress). + * + * @return true if a hand is active, false otherwise. + */ public boolean isHandActive() { return handActive; } + /** + * Returns the current phase of the game (e.g., PREFLOP, FLOP, TURN, RIVER). + * + * @return the current game phase. + */ public GamePhase getPhase() { return phase; } + /** + * Returns the current state of the table, including information such as the current bet + * and pot size. + * + * @return the current table state. + */ public TableState getTableState() { return tableState; } + /** + * Returns the current pot, which includes the total amount of chips in the pot + * and the contributions from each player. + * + * @return the current pot. + */ public Pot getPot() { return pot; } + /** + * Returns the current deck of cards being used in the game. + * + * @return the current deck of cards. + */ public Deck getDeck() { return deck; } + /** + * Returns the list of community cards currently on the table. + * + * @return a list of community cards, which may be empty if no cards have been dealt yet. + */ public List getCommunityCards() { return communityCards; } - /** Always returns a mutable list (never null). */ + /** + * Returns the hole cards for the specified player. If the player does not have + * hole cards yet, an empty list is returned and stored in the map for future reference. + * + * @param playerId the ID of the player whose hole cards are being requested. + * @return a list of hole cards for the specified player, which may be empty if + * the player has not been dealt cards yet. + */ public List getHoleCards(PlayerId playerId) { return holeCards.computeIfAbsent(playerId, k -> new ArrayList<>()); } + /** + * Returns the index of the dealer in the player order list. + * + * @return the index of the dealer, or 0 if there are no players. + */ public int getDealerIndex() { return dealerIndex; } + /** + * Returns a map of player IDs to their respective hole cards. Each player's + * hole cards are represented as a list of Card objects. If a player does not + * have hole cards yet, they will be associated with an empty list. + * + * @return a map where the key is the player's ID and the value is a list of + * their hole cards. + */ public Map> getPlayerCards() { return holeCards; } + /** + * Returns the number of players currently in the game, based on the size of the + * player order list. + * + * @return the number of players in the game. + */ public int getPlayerCount() { return players.size(); } - // Setters / Mutators public void setCurrentPlayerIndex(int index) { if (playerOrder.isEmpty()) { this.currentPlayerIndex = 0; @@ -117,6 +191,10 @@ public class GameState { this.currentPlayerIndex = index; } + /** + * Sets the current player index to the first player who should act preflop, + * which is determined based on the dealer index and the number of players. + */ public void setCurrentPlayerToPreflopFirstToAct() { int size = playerOrder.size(); if (size == 0) { @@ -127,11 +205,16 @@ public class GameState { // Heads-up: dealer (small blind) acts first preflop. int first = (size == 2) ? dealerIndex : (dealerIndex + DEALER_OFFSET) % size; currentPlayerIndex = first; + resetRoundActions(); if (!canPlayerAct(currentPlayerIndex)) { nextPlayer(); } } + /** + * Sets the current player index to the first player who should act postflop, + * which is the player immediately to the left of the dealer (i.e., dealerIndex + 1). + */ public void setCurrentPlayerToPostflopFirstToAct() { int size = playerOrder.size(); if (size == 0) { @@ -141,27 +224,87 @@ public class GameState { int first = (dealerIndex + 1) % size; currentPlayerIndex = first; + resetRoundActions(); if (!canPlayerAct(currentPlayerIndex)) { nextPlayer(); } } + /** + * Marks the specified player as having acted in the current round. + * + * @param playerId the ID of the player to mark as having acted this round. + */ + public void markActedThisRound(PlayerId playerId) { + if (playerId != null) { + actedThisRound.add(playerId); + } + } + + /** + * Checks if the specified player has already acted in the current round by checking + * if their ID is present in the set of players who have acted this round. + * + * @param playerId the ID of the player to check for having acted this round. + * @return true if the player has acted this round, false otherwise. + */ + public boolean hasActedThisRound(PlayerId playerId) { + return playerId != null && actedThisRound.contains(playerId); + } + + /** + * Resets the tracking of which players have acted this round by clearing the set of player IDs. + */ + public void resetRoundActions() { + actedThisRound.clear(); + } + + /** + * Sets whether a hand is currently active (i.e., in progress). + * + * @param handActive a boolean value indicating whether a hand is active (true) or not (false). + */ public void setHandActive(boolean handActive) { this.handActive = handActive; } + /** + * Sets the current phase of the game (e.g., PREFLOP, FLOP, TURN, RIVER). + * + * @param phase the GamePhase to set as the current phase of the game. + */ public void setPhase(GamePhase phase) { this.phase = phase; } + /** + * Sets the index of the dealer in the player order list. + * + * @param dealerIndex the index to set as the dealer index, which should + * be within the bounds of the player order list if it is not empty. + */ public void setDealerIndex(int dealerIndex) { this.dealerIndex = dealerIndex; } + /** + * Sets the current deck of cards being used in the game. + * + * @param deck the Deck object to set as the current deck of cards for the game. + * This should be a valid Deck instance that can be used for dealing + * cards during the game. + */ public void setDeck(Deck deck) { this.deck = deck; } + /** + * Adds a new player to the game with the specified ID and initial chip count. + * + * @param id the PlayerId of the new player to add to the game. + * @param chips the initial number of chips the player has, which can be used + * for betting during the game. + */ public void addPlayer(PlayerId id, int chips) { Player player = new Player(id, chips); @@ -213,23 +356,49 @@ public class GameState { return true; } + /** + * Helper method to move an entry in a map from an old key to a new key, + * with a fallback value if the old key is not present. + * + * @param map the map in which to move the entry, where the key is a PlayerId + * and the value is of type T. + * @param oldId the existing PlayerId key that is being renamed. + * @param newId the new PlayerId key to which the entry should be moved. + * @param fallback the value to use if the oldId is not present in the map. + * @param the type of the values in the map, which can be any type that is + * used for player-related data in the game state. + */ private void moveMapEntry( Map map, PlayerId oldId, PlayerId newId, T fallback) { T value = map.remove(oldId); map.put(newId, value != null ? value : fallback); } - // Betting + // BETTING + + /** + * Returns the current bet amount for the specified player. + * + * @param playerId the ID of the player whose current bet is being requested. + * @return the current bet amount for the specified player, + * or 0 if the player does not have a current bet. + */ public int getCurrentBet(PlayerId playerId) { return currentBets.getOrDefault(playerId, 0); } + /** + * Sets the current bet amount for the specified player. + * + * @param playerId the ID of the player whose current bet is being set. + * @param amount the new bet amount to set for the specified player. + */ public void setCurrentBet(PlayerId playerId, int amount) { currentBets.put(playerId, amount); } /** - * Reset bets to 0 for ALL players (do not clear the map). Also resets table current bet to 0. + * Reset bets to 0 for ALL players (do not clear the map). */ public void resetBets() { for (PlayerId id : playerOrder) { @@ -238,10 +407,20 @@ public class GameState { tableState.setCurrentBet(0); } + /** + * Adds the specified amount to the pot. + * + * @param amount the amount to add to the pot. + */ public void addToPot(int amount) { pot.add(amount); } + /** + * Returns whether out-of-turn actions are allowed in the current game state. + * + * @return true if out-of-turn actions are allowed, false otherwise. + */ public boolean isAllowOutOfTurn() { return allowOutOfTurn; } @@ -250,7 +429,13 @@ public class GameState { this.allowOutOfTurn = allowOutOfTurn; } - // Player helpers + /** + * Retrieves the Player object associated with the given PlayerId. + * + * @param id the PlayerId of the player to retrieve. + * @return the Player object associated with the specified PlayerId, or a RuntimeException + * if the player is not found. + */ public Player getPlayer(PlayerId id) { Player player = players.get(id); if (player == null) { @@ -260,7 +445,14 @@ public class GameState { return player; } - // Bet commitments + /** + * Retrieves the current bet commitment for the specified player. + * + * @param playerId the ID of the player whose current bet commitment + * is being requested. + * @return the current bet commitment for the specified player, + * or 0 if the player does not have a bet commitment. + */ public int getCurrentBetCommitment(PlayerId playerId) { return playerBetCommitments.getOrDefault(playerId, 0); } @@ -269,7 +461,7 @@ public class GameState { playerBetCommitments.put(playerId, amount); } - // Cards + // CARDS /** * Gives (overwrites) the two hole cards for the given player. Ensures stable list identity @@ -282,15 +474,25 @@ public class GameState { cards.add(c2); } + /** + * Adds a community card to the list of community cards on the table. + * + * @param card the Card object representing the community card to be added to the table. + */ public void addCommunityCard(Card card) { communityCards.add(card); } + /** + * Resets the community cards by clearing the list of community cards on the table. + */ public void resetCommunityCards() { communityCards.clear(); } - // Turn / dealer management + /** + * Advances the current player index to the next player who can act. + */ public void nextPlayer() { if (playerOrder.isEmpty()) { currentPlayerIndex = 0; @@ -308,6 +510,13 @@ public class GameState { } while (currentPlayerIndex != start); } + /** + * Checks if the player at the specified index in the player order list is able to take an action. + * + * @param index the index of the player in the player order list to check for actionability. + * @return true if the player at the specified index can act (i.e., is not folded and not all-in), + * false otherwise. + */ private boolean canPlayerAct(int index) { if (index < 0 || index >= playerOrder.size()) { return false; @@ -322,12 +531,19 @@ public class GameState { dealerIndex = (dealerIndex + 1) % playerOrder.size(); } + /** + * Returns the Player object representing the current dealer based on the dealer index in + * the player order list. + * + * @return the Player object representing the current dealer, + * or null if there are no players or the dealer index is out of bounds. + */ public Player getDealer() { PlayerId id = playerOrder.get(dealerIndex); return players.get(id); } - // Hand reset / lifecycle + // HAND RESET / LIFECYCLE /** * Starts a new hand by resetting the game state for the next round of poker. @@ -371,6 +587,13 @@ public class GameState { getPlayer(playerId).setFolded(true); } + /** + * Checks if the specified player has folded by retrieving their Player object + * and checking their folded status. + * + * @param playerId the ID of the player to check for folded status. + * @return true if the player has folded, false otherwise. + */ public boolean isFolded(PlayerId playerId) { return getPlayer(playerId).isFolded(); } -- 2.52.0 From ad7086e5a5c4c8a9a09b4b0ebb6f632ead8e31d8 Mon Sep 17 00:00:00 2001 From: Julian Kropff Date: Sun, 19 Apr 2026 19:38:37 +0200 Subject: [PATCH 08/12] Fix: implement folding in GameController Refs #118 --- .../server/domain/game/GameController.java | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/game/GameController.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/game/GameController.java index 361879d..4230385 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/game/GameController.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/game/GameController.java @@ -10,6 +10,7 @@ import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.deck.Deck; import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.engine.GameEngine; import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.evaluator.HandEvaluator; import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.evaluator.HandRank; +import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.player.Player; import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.player.PlayerId; import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.state.GamePhase; import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.state.GameState; @@ -17,6 +18,8 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; +import static com.sun.media.jfxmedia.MediaManager.getPlayer; + /** * GameController is responsible for managing the flow of the poker game. It interacts with the * GameEngine to process player actions and update the game state accordingly. @@ -183,9 +186,22 @@ public class GameController { * * @param playerId The ID of the player who is folding. */ - public void playerFold(PlayerId playerId) { - // engine.processAction(new FoldAction(PlayerId.of(playerId))); + public PlayerId playerFold(PlayerId playerId) { + engine.processAction(new FoldAction(playerId)); + + GameState state = engine.getState(); + + if (state.countNonFoldedPlayers() == 1) { + + for (Player p : state.getPlayers()) { + if (!p.isFolded()) { + return p.getId(); + } + } + } + + return null; } /** -- 2.52.0 From 3443b5e8f3e29b70e5f0c8e283448364bad92416 Mon Sep 17 00:00:00 2001 From: Julian Kropff Date: Sun, 19 Apr 2026 20:02:02 +0200 Subject: [PATCH 09/12] Fix: align TaskbarController with Round logic fixes Refs #118 --- .../gameuicomponents/TaskbarController.java | 37 ++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/gameui/gameuicomponents/TaskbarController.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/gameui/gameuicomponents/TaskbarController.java index e25528a..0b2f398 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/gameui/gameuicomponents/TaskbarController.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/gameui/gameuicomponents/TaskbarController.java @@ -262,7 +262,7 @@ public class TaskbarController { boolean isMyTurn = state.activePlayer == myIndex; boolean isOut = me.getState() == PlayerState.FOLDED; - boolean isGameFinished = state.phase != null && state.phase.equalsIgnoreCase("FINISHED"); + boolean isGameFinished = isHandFinished(state); updateBasicButtons(isMyTurn, isOut || isGameFinished); applyActionAvailability(state, me, isMyTurn, isOut || isGameFinished); @@ -535,6 +535,28 @@ public class TaskbarController { return "TURN".equalsIgnoreCase(phase) || "RIVER".equalsIgnoreCase(phase); } + /** + * Checks if the hand is finished based on the current game state. + * + * @param state The current GameState object representing the state of the game, which includes information + * @return A boolean value indicating whether the hand is finished, which can be determined by + * checking if the phase is "FINISHED" or "SHOWDOWN", + */ + private boolean isHandFinished(GameState state) { + if (state == null) { + return false; + } + + if ("FINISHED".equalsIgnoreCase(state.phase) + || "SHOWDOWN".equalsIgnoreCase(state.phase)) { + return true; + } + + return state.players != null + && state.winnerIndex >= 0 + && state.winnerIndex < state.players.size(); + } + /** * Evaluates the risk level of a proposed bet based on the current game state, the amount of * the bet and the player's available chips. @@ -599,6 +621,12 @@ public class TaskbarController { return; } + if (isHandFinished(state)) { + LOGGER.info("Action {} ignored: hand is already finished", actionType); + update(state, myPlayerId); + return; + } + Player me = findCurrentPlayer(state); if (me == null) { LOGGER.error("Action {} blocked: player missing in state", actionType); @@ -914,6 +942,13 @@ public class TaskbarController { return; } + GameState state = ensureLatestStateForAction("fold"); + if (isHandFinished(state)) { + LOGGER.info("Fold ignored: hand is already finished"); + update(state, myPlayerId); + return; + } + gameService.fold(); LOGGER.info("Player FOLD"); -- 2.52.0 From ac83d63ea1d0e6333e8138dfde8e8106acddda10 Mon Sep 17 00:00:00 2001 From: Julian Kropff Date: Sun, 19 Apr 2026 20:02:43 +0200 Subject: [PATCH 10/12] Fix: align RoundManager with Round logic fixes Refs #118 --- .../domain/game/engine/RoundManager.java | 71 +------------------ 1 file changed, 2 insertions(+), 69 deletions(-) diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/game/engine/RoundManager.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/game/engine/RoundManager.java index 9602398..11db660 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/game/engine/RoundManager.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/game/engine/RoundManager.java @@ -4,7 +4,6 @@ import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.deck.Card; import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.deck.Deck; import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.player.Player; import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.player.PlayerId; -import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.player.PlayerStatus; import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.state.GamePhase; import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.state.GameState; import java.util.ArrayList; @@ -83,42 +82,8 @@ public class RoundManager { * next phase, false otherwise. */ private boolean isBettingRoundFinished(GameState state) { - int target = state.getTableState().getCurrentBet(); - - if (target == 0 && isPostflopStreet(state.getPhase()) && !allActivePlayersActedThisRound(state)) { - return false; - } - - if (isPendingBigBlindOption(state, target)) { - return false; - } - - for (Player p : state.getPlayers()) { - if (p == null) { - continue; - } - - // be tolerant if codebase mixes status + boolean flags - if (p.getStatus() == PlayerStatus.FOLDED || p.isFolded()) { - continue; - } - - int bet = state.getCurrentBet(p.getId()); - if (bet < target && !p.isAllIn()) { - return false; - } - } - return true; - } - - /** - * Helper method to determine if the current game phase is a postflop street (FLOP, TURN, RIVER). - * - * @param phase The GamePhase to check, which is used to determine if the current street is postflop. - * @return true if the phase is FLOP, TURN, or RIVER, indicating a postflop street, false otherwise. - */ - private boolean isPostflopStreet(GamePhase phase) { - return phase == GamePhase.FLOP || phase == GamePhase.TURN || phase == GamePhase.RIVER; + // A betting round only ends after every active player had at least one chance to act. + return allActivePlayersActedThisRound(state); } /** @@ -144,38 +109,6 @@ public class RoundManager { return true; } - /** - * Helper method to determine if the current player has a pending big blind option - * in the preflop betting round. - * - * @param state The GameState object representing the current state of the game, - * which is used to determine if the big blind has a pending option - * in the preflop round. - * @param target The current target bet that players need to meet, which is used to - * check if the big blind has a pending option when the target is - * equal to the big blind amount. - * @return true if the big blind has a pending option to act in the preflop round, - * false otherwise. - */ - private boolean isPendingBigBlindOption(GameState state, int target) { - if (state == null || state.getPhase() != GamePhase.PREFLOP) { - return false; - } - - if (target != BIG_BLIND) { - return false; - } - - int size = state.getPlayerCount(); - if (size < 2) { - return false; - } - - int dealer = state.getDealerIndex(); - int bigBlindIndex = (size == 2) ? (dealer + 1) % size : (dealer + 2) % size; - - return state.getCurrentPlayerIndex() == bigBlindIndex; - } /** * Advances the game phase to the next stage (FLOP, TURN, RIVER, SHOWDOWN) based on the current phase. -- 2.52.0 From 8099228d4feb637ae5a2dbc972f59d0ed9acc20c Mon Sep 17 00:00:00 2001 From: Julian Kropff Date: Sun, 19 Apr 2026 20:11:21 +0200 Subject: [PATCH 11/12] Test: improve GameEngine tests and add testPreflopEndsAfterOneActionPerActivePlayer and testPreflopCountsOnlyNonFoldedPlayersForProgress Refs #118 --- .../domain/game/GameControllerTest.java | 171 +++++++++++++++--- 1 file changed, 150 insertions(+), 21 deletions(-) diff --git a/src/test/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/game/GameControllerTest.java b/src/test/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/game/GameControllerTest.java index e85aba8..2f2536b 100644 --- a/src/test/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/game/GameControllerTest.java +++ b/src/test/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/game/GameControllerTest.java @@ -37,6 +37,125 @@ public class GameControllerTest { private static final Logger LOGGER = Logger.getLogger(GameControllerTest.class.getName()); + /** + * Helper method to retrieve the current player's ID from the game state. + * + * @param game The GameController instance from which to retrieve the current player's ID. + * @return The PlayerId of the current player in the game. + */ + private static PlayerId currentPlayerId(GameController game) { + return game.getState().getCurrentPlayer().getId(); + } + + /** + * Helper method to simulate a call action for the current player in the game. + * + * @param game The GameController instance on which to perform the call action for the current player. + */ + private static void callCurrent(GameController game) { + game.playerCall(currentPlayerId(game)); + } + + /** + * Helper method to simulate a fold action for the current player in the game. + * + * @param game The GameController instance on which to perform the fold action for the current player. + */ + private static void foldCurrent(GameController game) { + game.playerFold(currentPlayerId(game)); + } + + /** + * Verifies that the preflop phase ends once all active (non-folded) players + * have completed their required actions for the current betting round. + */ + @Test + public void testPreflopEndsAfterOneActionPerActivePlayer() { + + GameState state = new GameState(); + GameEngine engine = + new GameEngine( + state, + new RuleEngine(new ArrayList<>()), + new RoundManager(), + new TurnManager()); + + GameController game = new GameController(engine); + game.addPlayer(PlayerId.of("P1"), 10000); + game.addPlayer(PlayerId.of("P2"), 10000); + game.addPlayer(PlayerId.of("P3"), 10000); + game.addPlayer(PlayerId.of("P4"), 10000); + + game.startGame(); + + assertEquals(GamePhase.PREFLOP, game.getState().getPhase()); + assertEquals(0, game.getCommunityCards().size()); + + callCurrent(game); + callCurrent(game); + callCurrent(game); + + assertEquals(GamePhase.PREFLOP, game.getState().getPhase()); + assertEquals(0, game.getCommunityCards().size()); + + callCurrent(game); + + assertEquals(GamePhase.FLOP, game.getState().getPhase()); + assertEquals(3, game.getCommunityCards().size()); + + callCurrent(game); + callCurrent(game); + callCurrent(game); + callCurrent(game); + + assertEquals(GamePhase.TURN, game.getState().getPhase()); + assertEquals(4, game.getCommunityCards().size()); + + callCurrent(game); + callCurrent(game); + callCurrent(game); + callCurrent(game); + + assertEquals(GamePhase.RIVER, game.getState().getPhase()); + assertEquals(5, game.getCommunityCards().size()); + } + + /** + * Ensures that during the preflop phase, folded players are excluded + * from progression checks and do not block phase advancement. + */ + @Test + public void testPreflopCountsOnlyNonFoldedPlayersForProgress() { + + GameState state = new GameState(); + GameEngine engine = + new GameEngine( + state, + new RuleEngine(new ArrayList<>()), + new RoundManager(), + new TurnManager()); + + GameController game = new GameController(engine); + game.addPlayer(PlayerId.of("A"), 10000); + game.addPlayer(PlayerId.of("B"), 10000); + game.addPlayer(PlayerId.of("C"), 10000); + game.addPlayer(PlayerId.of("D"), 10000); + + game.startGame(); + assertEquals(GamePhase.PREFLOP, game.getState().getPhase()); + + callCurrent(game); + foldCurrent(game); + callCurrent(game); + + assertEquals(GamePhase.PREFLOP, game.getState().getPhase()); + + callCurrent(game); + + assertEquals(GamePhase.FLOP, game.getState().getPhase()); + assertEquals(3, game.getCommunityCards().size()); + } + /** * This test simulates a specific game scenario where Julian is expected to win with a Two Pair * hand. It sets up a fixed deck to ensure that the desired cards are dealt to the players and @@ -83,11 +202,14 @@ public class GameControllerTest { } } - game.playerCall(PlayerId.of("Julian")); + callCurrent(game); game.playerFold(PlayerId.of("Mathis")); - game.playerCall(PlayerId.of("Jona")); + callCurrent(game); game.playerRaise(PlayerId.of("Lars"), 1200); + // New street model: once all non-folded players acted, preflop is complete. + assertEquals(GamePhase.FLOP, game.getState().getPhase()); + List board = game.getCommunityCards(); assertTrue(board.size() >= 3 && board.size() <= 5); @@ -170,10 +292,10 @@ public class GameControllerTest { int potBefore = game.getState().getPot().getAmount(); - game.playerRaise(PlayerId.of("Lars"), 1200); - game.playerCall(PlayerId.of("Julian")); - game.playerFold(PlayerId.of("Mathis")); - game.playerCall(PlayerId.of("Jona")); + game.playerRaise(currentPlayerId(game), 1200); + callCurrent(game); + foldCurrent(game); + callCurrent(game); System.out.println("Pot after actions: " + game.getState().getPot().getAmount()); @@ -183,11 +305,15 @@ public class GameControllerTest { assertEquals(3, game.getCommunityCards().size(), "The flop must have 3 cards"); - game.playerCall(PlayerId.of("Julian")); + for (int i = 0; i < 10 && game.getCommunityCards().size() < 4; i++) { + callCurrent(game); + } assertEquals(4, game.getCommunityCards().size(), "A turn must result in 4 cards"); - game.playerCall(PlayerId.of("Jona")); + for (int i = 0; i < 10 && game.getCommunityCards().size() < 5; i++) { + callCurrent(game); + } assertEquals(5, game.getCommunityCards().size(), "The river must consist of 5 cards"); @@ -278,13 +404,12 @@ public class GameControllerTest { game.playerCall(PlayerId.of("Jona")); game.playerRaise(PlayerId.of("Lars"), 1200); - game.playerCall(PlayerId.of("Julian")); - game.playerCall(PlayerId.of("Jona")); + assertEquals(GamePhase.FLOP, game.getState().getPhase()); assertTrue(game.getCommunityCards().size() >= 3); for (int i = 0; i < 10 && game.getCommunityCards().size() < 5; i++) { - game.playerCall(PlayerId.of("Julian")); + callCurrent(game); } assertEquals(5, game.getCommunityCards().size()); @@ -367,10 +492,14 @@ public class GameControllerTest { assertEquals(3, game.getCommunityCards().size()); - game.playerCall(PlayerId.of("Julian")); + for (int i = 0; i < 10 && game.getCommunityCards().size() < 4; i++) { + callCurrent(game); + } assertEquals(4, game.getCommunityCards().size()); - game.playerCall(PlayerId.of("Mathis")); + for (int i = 0; i < 10 && game.getCommunityCards().size() < 5; i++) { + callCurrent(game); + } assertEquals(5, game.getCommunityCards().size()); Set boardSeen = new HashSet<>(); @@ -424,10 +553,10 @@ public class GameControllerTest { game.startGame(); - game.playerRaise(PlayerId.of("BigStack"), 1000); - game.playerCall(PlayerId.of("Caller")); - game.playerCall(PlayerId.of("MidStack")); - game.playerCall(PlayerId.of("ShortStack")); + game.playerRaise(currentPlayerId(game), 1000); + callCurrent(game); + callCurrent(game); + callCurrent(game); for (Player p : game.getState().getPlayers()) { assertTrue(p.getChips() >= 0, "Negative chips detected for " + p.getId()); @@ -435,13 +564,13 @@ public class GameControllerTest { assertTrue(game.getState().getPot().getAmount() > 0); - game.playerRaise(PlayerId.of("BigStack"), 100); + game.playerRaise(currentPlayerId(game), 100); int pot = game.getState().getPot().getAmount(); assertTrue(pot > 0, "Pot must remain valid after re-raises"); - game.playerFold(PlayerId.of("Caller")); - game.playerFold(PlayerId.of("MidStack")); + foldCurrent(game); + foldCurrent(game); long activePlayers = game.getState().getPlayers().stream().filter(p -> p.getChips() > 0).count(); @@ -451,7 +580,7 @@ public class GameControllerTest { assertTrue(game.getCommunityCards().size() >= 3); for (int i = 0; i < 10 && game.getCommunityCards().size() < 5; i++) { - game.playerCall(PlayerId.of("BigStack")); + callCurrent(game); } assertEquals(5, game.getCommunityCards().size()); -- 2.52.0 From ab2adbf3119a21a2487b2ec38b1330724c54c2f6 Mon Sep 17 00:00:00 2001 From: Julian Kropff Date: Sun, 19 Apr 2026 20:20:57 +0200 Subject: [PATCH 12/12] Fix: resolve checkstyle magic number and apply spotless formatting --- .../gameuicomponents/TaskbarController.java | 224 +++++++++--------- .../server/domain/game/GameController.java | 2 - .../domain/game/engine/RoundManager.java | 98 ++++---- .../server/domain/game/state/GameState.java | 112 ++++----- .../domain/game/GameControllerTest.java | 14 +- 5 files changed, 217 insertions(+), 233 deletions(-) diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/gameui/gameuicomponents/TaskbarController.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/gameui/gameuicomponents/TaskbarController.java index 0b2f398..cd3a34e 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/gameui/gameuicomponents/TaskbarController.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/gameui/gameuicomponents/TaskbarController.java @@ -56,6 +56,7 @@ public class TaskbarController { private static final String STYLE_GRAY_INPUT = "gray-input-field"; private static final String STYLE_RED_INPUT = "red-input-field"; private static final double SCALE_NORMAL = 1.0; + private static final int DEALER_OFFSET = 3; private boolean inputActionAllowed; private int lastReferenceBet = BIG_BLIND; @@ -97,7 +98,8 @@ public class TaskbarController { } /** - * Sets the given button to a disabled state with red styling, indicating that the player is out. + * Sets the given button to a disabled state with red styling, indicating that the player is + * out. * * @param b The button to be styled as red and disabled. */ @@ -217,8 +219,9 @@ public class TaskbarController { public void initialize() { updateBasicButtons(false, false); if (taskbarInput != null) { - taskbarInput.textProperty().addListener((obs, oldValue, newValue) - -> refreshBetInputUi()); + taskbarInput + .textProperty() + .addListener((obs, oldValue, newValue) -> refreshBetInputUi()); } setBetButtonVisible(false); @@ -228,12 +231,12 @@ public class TaskbarController { * Updates the taskbar based on the current game state and the player's status. * * @param state The current GameState object representing the state of the game, which includes - * information about the players, their bets, the current phase and other relevant - * details needed to update the taskbar UI accurately based on the player's status - * and the game context. + * information about the players, their bets, the current phase and other relevant details + * needed to update the taskbar UI accurately based on the player's status and the game + * context. * @param myPlayerId The PlayerId object representing the current player, used to identify the - * player's status and update the taskbar UI accordingly based on whether - * it is their turn and whether they are out of the game. + * player's status and update the taskbar UI accordingly based on whether it is their turn + * and whether they are out of the game. */ public void update(GameState state, PlayerId myPlayerId) { @@ -275,11 +278,11 @@ public class TaskbarController { * * @param state The current game state to evaluate action availability. * @param me The player object representing the current player, used to determine their chips - * and bet status. + * and bet status. * @param isMyTurn Indicates whether it is currently the player's turn, which affects whether - * actions can be taken. - * @param isOutOrFinished Indicates whether the player is out of the game (folded or all-in) - * or if the game is finished, which disables actions. + * actions can be taken. + * @param isOutOrFinished Indicates whether the player is out of the game (folded or all-in) or + * if the game is finished, which disables actions. */ private void applyActionAvailability( GameState state, Player me, boolean isMyTurn, boolean isOutOrFinished) { @@ -338,8 +341,8 @@ public class TaskbarController { * Safely doubles the given value while preventing integer overflow. * * @param value The integer value to be safely doubled. - * @return The safely doubled value, or 0 if the input is non-positive, or Integer.MAX_VALUE - * if doubling would overflow. + * @return The safely doubled value, or 0 if the input is non-positive, or Integer.MAX_VALUE if + * doubling would overflow. */ private int safeDouble(int value) { if (value <= 0) { @@ -354,8 +357,8 @@ public class TaskbarController { /** * Sets the visibility of the Bet button in the taskbar. * - * @param visible A boolean indicating whether the Bet button should be visible (true) - * or hidden (false). + * @param visible A boolean indicating whether the Bet button should be visible (true) or hidden + * (false). */ private void setBetButtonVisible(boolean visible) { if (betButton == null) { @@ -366,8 +369,8 @@ public class TaskbarController { } /** - * Refreshes the user interface of the bet input field and the Bet button based on - * the current game state and the validity of the input. + * Refreshes the user interface of the bet input field and the Bet button based on the current + * game state and the validity of the input. */ private void refreshBetInputUi() { if (!inputActionAllowed || taskbarInput == null || taskbarInput.isDisabled()) { @@ -389,9 +392,10 @@ public class TaskbarController { /** * Enables or disables the given button based on the provided boolean value. * - * @param button The Button object to be enabled or disabled based on the provided boolean value. - * @param enabled A boolean value indicating whether the button should be enabled (true) - * or disabled (false). + * @param button The Button object to be enabled or disabled based on the provided boolean + * value. + * @param enabled A boolean value indicating whether the button should be enabled (true) or + * disabled (false). */ private void setActionEnabled(Button button, boolean enabled) { if (enabled) { @@ -404,10 +408,10 @@ public class TaskbarController { /** * Calculates the amount needed to call the current bet in the game. * - * @param state The current GameState object representing the state of the game, which - * includes information about the current bet and the player's bet status. - * @param me The Player object representing the current player, used to determine how much - * they have already invested in the current bet. + * @param state The current GameState object representing the state of the game, which includes + * information about the current bet and the player's bet status. + * @param me The Player object representing the current player, used to determine how much they + * have already invested in the current bet. * @return An integer representing the amount needed for the player to call the current bet. */ private int getToCall(GameState state, Player me) { @@ -417,11 +421,11 @@ public class TaskbarController { } /** - * Determines the effective call target for the current game state. If there is an active current - * bet, it returns that as the call target. + * Determines the effective call target for the current game state. If there is an active + * current bet, it returns that as the call target. * - * @param state The current GameState object representing the state of the game, which - * includes information about the current bet and the last reference bet. + * @param state The current GameState object representing the state of the game, which includes + * information about the current bet and the last reference bet. * @return An integer representing the effective call target for the current game state. */ private int effectiveCallTarget(GameState state) { @@ -432,16 +436,16 @@ public class TaskbarController { } /** - * Checks if the current player is the first to act in the pre-flop phase - * and if the initial blind layout is still in place. + * Checks if the current player is the first to act in the pre-flop phase and if the initial + * blind layout is still in place. * - * @param state The current GameState object representing the state of the game, - * which includes information about the phase of the game, the players, - * the dealer position, and the active player. - * @param me The Player object representing the current player, used to determine - * their position in the player list and whether they are the active player. - * @return A boolean value indicating whether the current player is the first to act - * in the pre-flop phase with only the initial blind layout in place. + * @param state The current GameState object representing the state of the game, which includes + * information about the phase of the game, the players, the dealer position, and the active + * player. + * @param me The Player object representing the current player, used to determine their position + * in the player list and whether they are the active player. + * @return A boolean value indicating whether the current player is the first to act in the + * pre-flop phase with only the initial blind layout in place. */ private boolean isFirstPreflopPlayerInputOnly(GameState state, Player me) { if (state == null || me == null || !isPreflop(state.phase) || state.players == null) { @@ -459,7 +463,7 @@ public class TaskbarController { } int dealer = state.dealer; - int firstIndex = (size == 2) ? dealer : (dealer + 3) % size; + int firstIndex = (size == 2) ? dealer : (dealer + DEALER_OFFSET) % size; if (state.activePlayer != firstIndex || myIndex != firstIndex) { return false; } @@ -470,10 +474,10 @@ public class TaskbarController { /** * Checks if the initial blind layout is still in place during the pre-flop phase. * - * @param state The current GameState object representing the state of the game, - * which includes information about the players and their bets. - * @return A boolean value indicating whether the initial blind layout is still in - * place during the pre-flop phase. + * @param state The current GameState object representing the state of the game, which includes + * information about the players and their bets. + * @return A boolean value indicating whether the initial blind layout is still in place during + * the pre-flop phase. */ private boolean isInitialPreflopBlindLayout(GameState state) { int sbCount = 0; @@ -502,10 +506,10 @@ public class TaskbarController { /** * Checks if the given phase string corresponds to the pre-flop phase of the game. * - * @param phase The string representing the current phase of the game, which is expected - * to be compared against "PREFLOP" to determine if it is the pre-flop phase. - * @return A boolean value indicating whether the given phase string corresponds to the - * pre-flop phase. + * @param phase The string representing the current phase of the game, which is expected to be + * compared against "PREFLOP" to determine if it is the pre-flop phase. + * @return A boolean value indicating whether the given phase string corresponds to the pre-flop + * phase. */ private boolean isPreflop(String phase) { return "PREFLOP".equalsIgnoreCase(phase); @@ -514,9 +518,10 @@ public class TaskbarController { /** * Checks if the given phase string corresponds to the flop phase of the game. * - * @param phase The string representing the current phase of the game, which is - * expected to be compared against "FLOP" to determine if it is the flop phase. - * @return A boolean value indicating whether the given phase string corresponds to the flop phase. + * @param phase The string representing the current phase of the game, which is expected to be + * compared against "FLOP" to determine if it is the flop phase. + * @return A boolean value indicating whether the given phase string corresponds to the flop + * phase. */ private boolean isFlop(String phase) { return "FLOP".equalsIgnoreCase(phase); @@ -525,11 +530,10 @@ public class TaskbarController { /** * Checks if the given phase string corresponds to either the turn or river phase of the game. * - * @param phase The string representing the current phase of the game, which is expected to - * be compared against "TURN" and "RIVER" to determine if it is either the turn - * or river phase. - * @return A boolean value indicating whether the given phase string corresponds to either - * the turn or river phase. + * @param phase The string representing the current phase of the game, which is expected to be + * compared against "TURN" and "RIVER" to determine if it is either the turn or river phase. + * @return A boolean value indicating whether the given phase string corresponds to either the + * turn or river phase. */ private boolean isTurnOrRiver(String phase) { return "TURN".equalsIgnoreCase(phase) || "RIVER".equalsIgnoreCase(phase); @@ -538,17 +542,17 @@ public class TaskbarController { /** * Checks if the hand is finished based on the current game state. * - * @param state The current GameState object representing the state of the game, which includes information + * @param state The current GameState object representing the state of the game, which includes + * information * @return A boolean value indicating whether the hand is finished, which can be determined by - * checking if the phase is "FINISHED" or "SHOWDOWN", + * checking if the phase is "FINISHED" or "SHOWDOWN", */ private boolean isHandFinished(GameState state) { if (state == null) { return false; } - if ("FINISHED".equalsIgnoreCase(state.phase) - || "SHOWDOWN".equalsIgnoreCase(state.phase)) { + if ("FINISHED".equalsIgnoreCase(state.phase) || "SHOWDOWN".equalsIgnoreCase(state.phase)) { return true; } @@ -558,8 +562,8 @@ public class TaskbarController { } /** - * Evaluates the risk level of a proposed bet based on the current game state, the amount of - * the bet and the player's available chips. + * Evaluates the risk level of a proposed bet based on the current game state, the amount of the + * bet and the player's available chips. */ private enum BetRisk { ALLOWED, @@ -567,18 +571,14 @@ public class TaskbarController { BLOCKED } - /** - * Enum representing the types of actions that can be submitted from the taskbar. - */ + /** Enum representing the types of actions that can be submitted from the taskbar. */ private enum ActionType { INPUT, CALL_BUTTON, RAISE_BUTTON } - /** - * Class representing the result of validating a proposed bet or action. - */ + /** Class representing the result of validating a proposed bet or action. */ private static final class ValidationResult { private final boolean valid; private final boolean warning; @@ -607,8 +607,7 @@ public class TaskbarController { * Submits a player action based on the specified ActionType. * * @param actionType The type of action being submitted, which can be an input-based action - * (where the player types an amount) or a specific button action for - * calling or raising. + * (where the player types an amount) or a specific button action for calling or raising. */ private void submitAction(ActionType actionType) { if (gameService == null) { @@ -678,13 +677,13 @@ public class TaskbarController { } /** - * Ensures that the latest game state is available for processing a player action. - * If the last known state is null, it attempts to fetch the current state from the GameService. + * Ensures that the latest game state is available for processing a player action. If the last + * known state is null, it attempts to fetch the current state from the GameService. * - * @param text The text associated with the action being processed, used for logging purposes - * to indicate which action is being attempted when the state is not available. + * @param text The text associated with the action being processed, used for logging purposes to + * indicate which action is being attempted when the state is not available. * @return The latest GameState object if available, or null if the state cannot be retrieved, - * indicating that the action cannot be processed. + * indicating that the action cannot be processed. */ private Integer parseInputTarget(String text) { if (text == null || text.trim().isEmpty()) { @@ -700,14 +699,13 @@ public class TaskbarController { /** * Finds the current player in the given game state based on the player's ID. * - * @param state The current GameState object representing the state of the game, - * which includes a list of players. - * @param text The text associated with the action being processed, used for logging - * purposes to indicate which action is being attempted when the player - * cannot be found in the state. - * @return The Player object representing the current player if found in the game state, - * or null if no matching player is found, indicating that the player cannot be identified - * in the current game state. + * @param state The current GameState object representing the state of the game, which includes + * a list of players. + * @param text The text associated with the action being processed, used for logging purposes to + * indicate which action is being attempted when the player cannot be found in the state. + * @return The Player object representing the current player if found in the game state, or null + * if no matching player is found, indicating that the player cannot be identified in the + * current game state. */ private ValidationResult validateTypedAmount(GameState state, String text) { Integer target = parseInputTarget(text); @@ -722,20 +720,19 @@ public class TaskbarController { } /** - * Validates a proposed target bet against the current game state and the player's status. - * It checks if the target bet is either a valid call or a valid raise - * (specifically, exactly double the call target). + * Validates a proposed target bet against the current game state and the player's status. It + * checks if the target bet is either a valid call or a valid raise (specifically, exactly + * double the call target). * - * @param state The current GameState object representing the state of the game, - * which includes information about the current bet, the phase of the game, - * and the players. - * @param me The Player object representing the current player, used to determine their - * current bet, available chips, and to evaluate whether the proposed target - * bet is valid for this player based on their status in the game. - * @param targetBet The integer value representing the proposed target bet that the - * player intends to make. - * @return A ValidationResult object indicating whether the proposed target bet is valid, - * if it triggers a warning, or if it is blocked due to being invalid or too risky. + * @param state The current GameState object representing the state of the game, which includes + * information about the current bet, the phase of the game, and the players. + * @param me The Player object representing the current player, used to determine their current + * bet, available chips, and to evaluate whether the proposed target bet is valid for this + * player based on their status in the game. + * @param targetBet The integer value representing the proposed target bet that the player + * intends to make. + * @return A ValidationResult object indicating whether the proposed target bet is valid, if it + * triggers a warning, or if it is blocked due to being invalid or too risky. */ private ValidationResult validateTarget(GameState state, Player me, int targetBet) { if (state == null || me == null) { @@ -777,11 +774,10 @@ public class TaskbarController { /** * Generates a warning message based on the current phase of the game. * - * @param state The current GameState object representing the state of the game, - * which includes information about the phase of the game. - * @return A string containing the warning message appropriate for the current phase - * of the game when a player's bet triggers a warning due to being at least - * 50% of their stack. + * @param state The current GameState object representing the state of the game, which includes + * information about the phase of the game. + * @return A string containing the warning message appropriate for the current phase of the game + * when a player's bet triggers a warning due to being at least 50% of their stack. */ private String warningTextForPhase(GameState state) { String phase = state != null ? state.phase : null; @@ -795,13 +791,13 @@ public class TaskbarController { } /** - * Evaluates the risk level of a proposed bet based on the current game state, - * the amount of the bet, and the player's available chips. + * Evaluates the risk level of a proposed bet based on the current game state, the amount of the + * bet, and the player's available chips. * - * @param state The current GameState object representing the state of the game, - * which includes information about the phase of the game. - * @param amount The integer value representing the amount of the proposed bet - * that the player intends to make. + * @param state The current GameState object representing the state of the game, which includes + * information about the phase of the game. + * @param amount The integer value representing the amount of the proposed bet that the player + * intends to make. * @param chips The integer value representing the player's available chips. * @return A BetRisk enum value indicating the risk level of the proposed bet. */ @@ -846,10 +842,10 @@ public class TaskbarController { /** * Displays a warning dialog with the specified title and content. * - * @param title The string representing the title of the warning dialog, which is displayed - * in the title bar of the dialog window. + * @param title The string representing the title of the warning dialog, which is displayed in + * the title bar of the dialog window. * @param content The string representing the content of the warning message, which is displayed - * in the body of the dialog. + * in the body of the dialog. */ private void showWarningDialog(String title, String content) { Alert alert = new Alert(Alert.AlertType.WARNING); @@ -964,8 +960,8 @@ public class TaskbarController { /** * Submits a preset input for either calling or raising based on the specified mode. * - * @param mode A string indicating the mode of the action, which can be "call" - * for calling the current bet or "raise" for raising to double the current bet. + * @param mode A string indicating the mode of the action, which can be "call" for calling the + * current bet or "raise" for raising to double the current bet. */ private void submitPresetInputAndProcess(String mode) { GameState state = ensureLatestStateForAction(mode); @@ -1054,10 +1050,10 @@ public class TaskbarController { /** * Ensures that the latest game state is available for processing a player action. * - * @param actionName The name of the action being processed, used for logging purposes to indicate - * which action is being attempted when the state is not available. + * @param actionName The name of the action being processed, used for logging purposes to + * indicate which action is being attempted when the state is not available. * @return The latest GameState object if available, or null if the state cannot be retrieved, - * indicating that the action cannot be processed due to the lack of a valid game state. + * indicating that the action cannot be processed due to the lack of a valid game state. */ private GameState ensureLatestStateForAction(String actionName) { try { diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/game/GameController.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/game/GameController.java index 4230385..a1c2017 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/game/GameController.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/game/GameController.java @@ -18,8 +18,6 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; -import static com.sun.media.jfxmedia.MediaManager.getPlayer; - /** * GameController is responsible for managing the flow of the poker game. It interacts with the * GameEngine to process player actions and update the game state accordingly. diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/game/engine/RoundManager.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/game/engine/RoundManager.java index 11db660..50716ec 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/game/engine/RoundManager.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/game/engine/RoundManager.java @@ -27,10 +27,11 @@ public class RoundManager { public static final int BIG_BLIND = 200; /** - * Starts a new hand by resetting the game state, ensuring a deck is available, dealing hole cards - * to players, posting blinds, and setting the first player to act for the preflop phase. + * Starts a new hand by resetting the game state, ensuring a deck is available, dealing hole + * cards to players, posting blinds, and setting the first player to act for the preflop phase. * - * @param state The GameState object representing the current state of the game, which will be modified to + * @param state The GameState object representing the current state of the game, which will be + * modified to */ public void startNewHand(GameState state) { // Use GameState's canonical reset @@ -52,8 +53,8 @@ public class RoundManager { /** * Checks if the current betting round is finished and advances the game phase if necessary. * - * @param state The GameState object representing the current state of the game, - * which may be modified to advance the phase or end the hand. + * @param state The GameState object representing the current state of the game, which may be + * modified to advance the phase or end the hand. */ public void progressIfNeeded(GameState state) { if (state.getPhase() == null) { @@ -72,14 +73,13 @@ public class RoundManager { } /** - * Determines if the current betting round is finished by checking if all active - * players have met the current bet or are all-in and by handling special cases for - * preflop betting rounds. + * Determines if the current betting round is finished by checking if all active players have + * met the current bet or are all-in and by handling special cases for preflop betting rounds. * - * @param state The GameState object representing the current state of the game, - * which is used to evaluate the betting round status. - * @return true if the betting round is finished and the game can progress to the - * next phase, false otherwise. + * @param state The GameState object representing the current state of the game, which is used + * to evaluate the betting round status. + * @return true if the betting round is finished and the game can progress to the next phase, + * false otherwise. */ private boolean isBettingRoundFinished(GameState state) { // A betting round only ends after every active player had at least one chance to act. @@ -87,14 +87,14 @@ public class RoundManager { } /** - * Helper method to check if all active players have acted in the current betting round. - * This is used to handle the special case of preflop rounds where no bets have been made yet, - * but players still need to have the opportunity to act. + * Helper method to check if all active players have acted in the current betting round. This is + * used to handle the special case of preflop rounds where no bets have been made yet, but + * players still need to have the opportunity to act. * - * @param state The GameState object representing the current state of the game, which is - * used to check if all active players have acted in the current round. - * @return true if all active players have acted in the current round, false if there are - * still active players who have not acted yet. + * @param state The GameState object representing the current state of the game, which is used + * to check if all active players have acted in the current round. + * @return true if all active players have acted in the current round, false if there are still + * active players who have not acted yet. */ private boolean allActivePlayersActedThisRound(GameState state) { for (Player p : state.getPlayers()) { @@ -109,13 +109,13 @@ public class RoundManager { return true; } - /** - * Advances the game phase to the next stage (FLOP, TURN, RIVER, SHOWDOWN) based on the current phase. + * Advances the game phase to the next stage (FLOP, TURN, RIVER, SHOWDOWN) based on the current + * phase. * - * @param state The GameState object representing the current state of the game, which will be modified - * to advance the phase and deal community cards as needed when progressing to the - * next stage of the hand. + * @param state The GameState object representing the current state of the game, which will be + * modified to advance the phase and deal community cards as needed when progressing to the + * next stage of the hand. */ private void advancePhase(GameState state) { switch (state.getPhase()) { @@ -133,9 +133,8 @@ public class RoundManager { /** * Ensures that a deck of cards is available in the game state. * - * @param state The GameState object representing the current state of the game, - * which will be modified to include a new shuffled deck if one - * does not already exist. + * @param state The GameState object representing the current state of the game, which will be + * modified to include a new shuffled deck if one does not already exist. */ private void ensureDeck(GameState state) { Deck deck = state.getDeck(); @@ -147,11 +146,11 @@ public class RoundManager { } /** - * Deals hole cards to each player in the game state by drawing two cards from the - * deck for each player and assigning them as their hole cards. + * Deals hole cards to each player in the game state by drawing two cards from the deck for each + * player and assigning them as their hole cards. * - * @param state The GameState object representing the current state of the game, - * which will be modified to assign hole cards to each active player. + * @param state The GameState object representing the current state of the game, which will be + * modified to assign hole cards to each active player. */ private void dealHoleCards(GameState state) { Deck deck = state.getDeck(); @@ -217,13 +216,12 @@ public class RoundManager { } /** - * Deals the flop by drawing three community cards from the deck and adding them - * to the game state, then setting the game phase to FLOP and resetting bets for - * the new betting round. + * Deals the flop by drawing three community cards from the deck and adding them to the game + * state, then setting the game phase to FLOP and resetting bets for the new betting round. * - * @param state The GameState object representing the current state of the game, - * which will be modified to add three community cards for the flop, - * set the phase to FLOP, and reset bets for the new betting round. + * @param state The GameState object representing the current state of the game, which will be + * modified to add three community cards for the flop, set the phase to FLOP, and reset bets + * for the new betting round. */ private void dealFlop(GameState state) { ensureDeck(state); @@ -241,13 +239,12 @@ public class RoundManager { } /** - * Deals the turn by drawing one community card from the deck and adding it to - * the game state, then setting the game phase to TURN and resetting bets for - * the new betting round. + * Deals the turn by drawing one community card from the deck and adding it to the game state, + * then setting the game phase to TURN and resetting bets for the new betting round. * - * @param state The GameState object representing the current state of the game, - * which will be modified to add one community card for the turn, - * set the phase to TURN, and reset bets for the new betting round. + * @param state The GameState object representing the current state of the game, which will be + * modified to add one community card for the turn, set the phase to TURN, and reset bets + * for the new betting round. */ private void dealTurn(GameState state) { ensureDeck(state); @@ -263,13 +260,12 @@ public class RoundManager { } /** - * Deals the river by drawing one community card from the deck and adding it - * to the game state, then setting the game phase to RIVER and resetting bets - * for the new betting round. + * Deals the river by drawing one community card from the deck and adding it to the game state, + * then setting the game phase to RIVER and resetting bets for the new betting round. * - * @param state The GameState object representing the current state of the game, - * which will be modified to add one community card for the river, - * set the phase to RIVER, and reset bets for the new betting round. + * @param state The GameState object representing the current state of the game, which will be + * modified to add one community card for the river, set the phase to RIVER, and reset bets + * for the new betting round. */ private void dealRiver(GameState state) { ensureDeck(state); @@ -287,8 +283,8 @@ public class RoundManager { /** * Handles the showdown phase by setting the game phase to SHOWDOWN. * - * @param state The GameState object representing the current state of the game, - * which will be modified to set the phase to SHOWDOWN. + * @param state The GameState object representing the current state of the game, which will be + * modified to set the phase to SHOWDOWN. */ private void showdown(GameState state) { state.setPhase(GamePhase.SHOWDOWN); diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/game/state/GameState.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/game/state/GameState.java index 92d18b5..4cff068 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/game/state/GameState.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/game/state/GameState.java @@ -103,8 +103,8 @@ public class GameState { } /** - * Returns the current state of the table, including information such as the current bet - * and pot size. + * Returns the current state of the table, including information such as the current bet and pot + * size. * * @return the current table state. */ @@ -113,8 +113,8 @@ public class GameState { } /** - * Returns the current pot, which includes the total amount of chips in the pot - * and the contributions from each player. + * Returns the current pot, which includes the total amount of chips in the pot and the + * contributions from each player. * * @return the current pot. */ @@ -141,12 +141,12 @@ public class GameState { } /** - * Returns the hole cards for the specified player. If the player does not have - * hole cards yet, an empty list is returned and stored in the map for future reference. + * Returns the hole cards for the specified player. If the player does not have hole cards yet, + * an empty list is returned and stored in the map for future reference. * * @param playerId the ID of the player whose hole cards are being requested. - * @return a list of hole cards for the specified player, which may be empty if - * the player has not been dealt cards yet. + * @return a list of hole cards for the specified player, which may be empty if the player has + * not been dealt cards yet. */ public List getHoleCards(PlayerId playerId) { return holeCards.computeIfAbsent(playerId, k -> new ArrayList<>()); @@ -162,20 +162,19 @@ public class GameState { } /** - * Returns a map of player IDs to their respective hole cards. Each player's - * hole cards are represented as a list of Card objects. If a player does not - * have hole cards yet, they will be associated with an empty list. + * Returns a map of player IDs to their respective hole cards. Each player's hole cards are + * represented as a list of Card objects. If a player does not have hole cards yet, they will be + * associated with an empty list. * - * @return a map where the key is the player's ID and the value is a list of - * their hole cards. + * @return a map where the key is the player's ID and the value is a list of their hole cards. */ public Map> getPlayerCards() { return holeCards; } /** - * Returns the number of players currently in the game, based on the size of the - * player order list. + * Returns the number of players currently in the game, based on the size of the player order + * list. * * @return the number of players in the game. */ @@ -192,8 +191,8 @@ public class GameState { } /** - * Sets the current player index to the first player who should act preflop, - * which is determined based on the dealer index and the number of players. + * Sets the current player index to the first player who should act preflop, which is determined + * based on the dealer index and the number of players. */ public void setCurrentPlayerToPreflopFirstToAct() { int size = playerOrder.size(); @@ -212,8 +211,8 @@ public class GameState { } /** - * Sets the current player index to the first player who should act postflop, - * which is the player immediately to the left of the dealer (i.e., dealerIndex + 1). + * Sets the current player index to the first player who should act postflop, which is the + * player immediately to the left of the dealer (i.e., dealerIndex + 1). */ public void setCurrentPlayerToPostflopFirstToAct() { int size = playerOrder.size(); @@ -242,8 +241,8 @@ public class GameState { } /** - * Checks if the specified player has already acted in the current round by checking - * if their ID is present in the set of players who have acted this round. + * Checks if the specified player has already acted in the current round by checking if their ID + * is present in the set of players who have acted this round. * * @param playerId the ID of the player to check for having acted this round. * @return true if the player has acted this round, false otherwise. @@ -280,8 +279,8 @@ public class GameState { /** * Sets the index of the dealer in the player order list. * - * @param dealerIndex the index to set as the dealer index, which should - * be within the bounds of the player order list if it is not empty. + * @param dealerIndex the index to set as the dealer index, which should be within the bounds of + * the player order list if it is not empty. */ public void setDealerIndex(int dealerIndex) { this.dealerIndex = dealerIndex; @@ -290,9 +289,8 @@ public class GameState { /** * Sets the current deck of cards being used in the game. * - * @param deck the Deck object to set as the current deck of cards for the game. - * This should be a valid Deck instance that can be used for dealing - * cards during the game. + * @param deck the Deck object to set as the current deck of cards for the game. This should be + * a valid Deck instance that can be used for dealing cards during the game. */ public void setDeck(Deck deck) { this.deck = deck; @@ -302,8 +300,8 @@ public class GameState { * Adds a new player to the game with the specified ID and initial chip count. * * @param id the PlayerId of the new player to add to the game. - * @param chips the initial number of chips the player has, which can be used - * for betting during the game. + * @param chips the initial number of chips the player has, which can be used for betting during + * the game. */ public void addPlayer(PlayerId id, int chips) { Player player = new Player(id, chips); @@ -357,16 +355,16 @@ public class GameState { } /** - * Helper method to move an entry in a map from an old key to a new key, - * with a fallback value if the old key is not present. + * Helper method to move an entry in a map from an old key to a new key, with a fallback value + * if the old key is not present. * - * @param map the map in which to move the entry, where the key is a PlayerId - * and the value is of type T. + * @param map the map in which to move the entry, where the key is a PlayerId and the value is + * of type T. * @param oldId the existing PlayerId key that is being renamed. * @param newId the new PlayerId key to which the entry should be moved. * @param fallback the value to use if the oldId is not present in the map. - * @param the type of the values in the map, which can be any type that is - * used for player-related data in the game state. + * @param the type of the values in the map, which can be any type that is used for + * player-related data in the game state. */ private void moveMapEntry( Map map, PlayerId oldId, PlayerId newId, T fallback) { @@ -380,8 +378,8 @@ public class GameState { * Returns the current bet amount for the specified player. * * @param playerId the ID of the player whose current bet is being requested. - * @return the current bet amount for the specified player, - * or 0 if the player does not have a current bet. + * @return the current bet amount for the specified player, or 0 if the player does not have a + * current bet. */ public int getCurrentBet(PlayerId playerId) { return currentBets.getOrDefault(playerId, 0); @@ -397,9 +395,7 @@ public class GameState { currentBets.put(playerId, amount); } - /** - * Reset bets to 0 for ALL players (do not clear the map). - */ + /** Reset bets to 0 for ALL players (do not clear the map). */ public void resetBets() { for (PlayerId id : playerOrder) { currentBets.put(id, 0); @@ -433,8 +429,8 @@ public class GameState { * Retrieves the Player object associated with the given PlayerId. * * @param id the PlayerId of the player to retrieve. - * @return the Player object associated with the specified PlayerId, or a RuntimeException - * if the player is not found. + * @return the Player object associated with the specified PlayerId, or a RuntimeException if + * the player is not found. */ public Player getPlayer(PlayerId id) { Player player = players.get(id); @@ -448,10 +444,9 @@ public class GameState { /** * Retrieves the current bet commitment for the specified player. * - * @param playerId the ID of the player whose current bet commitment - * is being requested. - * @return the current bet commitment for the specified player, - * or 0 if the player does not have a bet commitment. + * @param playerId the ID of the player whose current bet commitment is being requested. + * @return the current bet commitment for the specified player, or 0 if the player does not have + * a bet commitment. */ public int getCurrentBetCommitment(PlayerId playerId) { return playerBetCommitments.getOrDefault(playerId, 0); @@ -483,16 +478,12 @@ public class GameState { communityCards.add(card); } - /** - * Resets the community cards by clearing the list of community cards on the table. - */ + /** Resets the community cards by clearing the list of community cards on the table. */ public void resetCommunityCards() { communityCards.clear(); } - /** - * Advances the current player index to the next player who can act. - */ + /** Advances the current player index to the next player who can act. */ public void nextPlayer() { if (playerOrder.isEmpty()) { currentPlayerIndex = 0; @@ -511,11 +502,12 @@ public class GameState { } /** - * Checks if the player at the specified index in the player order list is able to take an action. + * Checks if the player at the specified index in the player order list is able to take an + * action. * * @param index the index of the player in the player order list to check for actionability. - * @return true if the player at the specified index can act (i.e., is not folded and not all-in), - * false otherwise. + * @return true if the player at the specified index can act (i.e., is not folded and not + * all-in), false otherwise. */ private boolean canPlayerAct(int index) { if (index < 0 || index >= playerOrder.size()) { @@ -532,11 +524,11 @@ public class GameState { } /** - * Returns the Player object representing the current dealer based on the dealer index in - * the player order list. + * Returns the Player object representing the current dealer based on the dealer index in the + * player order list. * - * @return the Player object representing the current dealer, - * or null if there are no players or the dealer index is out of bounds. + * @return the Player object representing the current dealer, or null if there are no players or + * the dealer index is out of bounds. */ public Player getDealer() { PlayerId id = playerOrder.get(dealerIndex); @@ -588,8 +580,8 @@ public class GameState { } /** - * Checks if the specified player has folded by retrieving their Player object - * and checking their folded status. + * Checks if the specified player has folded by retrieving their Player object and checking + * their folded status. * * @param playerId the ID of the player to check for folded status. * @return true if the player has folded, false otherwise. diff --git a/src/test/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/game/GameControllerTest.java b/src/test/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/game/GameControllerTest.java index 2f2536b..f926806 100644 --- a/src/test/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/game/GameControllerTest.java +++ b/src/test/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/game/GameControllerTest.java @@ -50,7 +50,8 @@ public class GameControllerTest { /** * Helper method to simulate a call action for the current player in the game. * - * @param game The GameController instance on which to perform the call action for the current player. + * @param game The GameController instance on which to perform the call action for the current + * player. */ private static void callCurrent(GameController game) { game.playerCall(currentPlayerId(game)); @@ -59,15 +60,16 @@ public class GameControllerTest { /** * Helper method to simulate a fold action for the current player in the game. * - * @param game The GameController instance on which to perform the fold action for the current player. + * @param game The GameController instance on which to perform the fold action for the current + * player. */ private static void foldCurrent(GameController game) { game.playerFold(currentPlayerId(game)); } /** - * Verifies that the preflop phase ends once all active (non-folded) players - * have completed their required actions for the current betting round. + * Verifies that the preflop phase ends once all active (non-folded) players have completed + * their required actions for the current betting round. */ @Test public void testPreflopEndsAfterOneActionPerActivePlayer() { @@ -121,8 +123,8 @@ public class GameControllerTest { } /** - * Ensures that during the preflop phase, folded players are excluded - * from progression checks and do not block phase advancement. + * Ensures that during the preflop phase, folded players are excluded from progression checks + * and do not block phase advancement. */ @Test public void testPreflopCountsOnlyNonFoldedPlayersForProgress() { -- 2.52.0