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 f3ab44d..6307aff 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 @@ -6,6 +6,7 @@ import ch.unibas.dmi.dbis.cs108.casono.client.game.GameService; import ch.unibas.dmi.dbis.cs108.casono.client.game.GameState; import ch.unibas.dmi.dbis.cs108.casono.client.game.Player; import ch.unibas.dmi.dbis.cs108.casono.client.game.PlayerId; +import ch.unibas.dmi.dbis.cs108.casono.client.game.PlayerState; import ch.unibas.dmi.dbis.cs108.casono.client.network.ClientService; import ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.gameuicomponents.PlayerStatusController; import ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.gameuicomponents.TaskbarController; @@ -138,11 +139,14 @@ public class CasinoGameController { private static final long CHIP_STAGGER_DELAY_MULTIPLIER = 35L; private static final double CHAT_WIDTH = 400; private static final double CHAT_HEIGHT = 600; + private static final String ACTIVE_CARD_BOX_STYLE_CLASS = "player-cards-active-turn"; private String chatUsername; private ClientService chatClientService; private int chatLobbyId = -1; private boolean chatInitialized; + private boolean gameFinished; + private String lastWinnerName; /** Standard constructor. Used by FXML. */ public CasinoGameController() { @@ -230,6 +234,7 @@ public class CasinoGameController { } myDealerIcon.setVisible(false); + tableText.setWrapText(true); javafx.stage.Screen screen = javafx.stage.Screen.getPrimary(); double screenHeight = screen.getBounds().getHeight(); @@ -477,7 +482,7 @@ public class CasinoGameController { */ private void applyState(GameState s) { - if (s == null) { + if (s == null || gameFinished) { return; } @@ -490,6 +495,24 @@ public class CasinoGameController { + (s.players != null ? s.players.size() : 0)); List players = (s.players != null) ? s.players : List.of(); + String winnerName = resolveWinnerName(s); + if (winnerName != null && !winnerName.isBlank()) { + lastWinnerName = winnerName; + } else if (!isTerminalPhase(s.phase)) { + lastWinnerName = null; + } + + if (isGameFinishedState(s, winnerName)) { + updatePlayers(players, -1); + syncWhisperTargetsFromPlayers(players); + updateGameInfo(s); + highlightDealer(s); + updateTaskbar(s); + clearActiveTurnHighlights(); + finishGameUiLoop(); + return; + } + List community = (s.communityCards != null) ? s.communityCards : List.of(); List myCards = getMyCards(players); @@ -510,7 +533,7 @@ public class CasinoGameController { renderPot(s.pot); } - updatePlayers(players); + updatePlayers(players, s.activePlayer); syncWhisperTargetsFromPlayers(players); updateGameInfo(s); highlightDealer(s); @@ -610,14 +633,28 @@ public class CasinoGameController { */ private void updateGameInfo(GameState s) { - String text = "Phase: " + s.phase; - - if (s.winnerIndex >= 0 && s.winnerIndex < s.players.size()) { - Player winner = s.players.get(s.winnerIndex); - text = "Winner: " + winner.getName(); + if (s == null) { + tableText.setText("Phase: -"); + return; } - tableText.setText(text); + String winnerName = resolveWinnerName(s); + if (winnerName != null && !winnerName.isBlank()) { + tableText.setText("Winner: " + winnerName); + return; + } + + String phaseText = formatPhaseText(s.phase); + StringBuilder text = new StringBuilder("Phase: ").append(phaseText); + + tableText.setText(text.toString()); + } + + private String formatPhaseText(String phase) { + if (phase == null || phase.isBlank()) { + return "-"; + } + return phase.toUpperCase().replace("_", " "); } /** @@ -645,7 +682,7 @@ public class CasinoGameController { * opponents and update their display in the UI accordingly. If the list is null or empty, * all opponent slots will be cleared. */ - private void updatePlayers(List players) { + private void updatePlayers(List players, int activePlayerIndex) { if (players == null || players.isEmpty()) { clearOpponentSlots(); return; @@ -671,6 +708,7 @@ public class CasinoGameController { safeRefresh(player1Controller); safeRefresh(player2Controller); safeRefresh(player3Controller); + updateActiveTurnHighlights(players, activePlayerIndex); } /** @@ -695,7 +733,7 @@ public class CasinoGameController { } PlayerId pid = p.getId(); - boolean isMe = myPlayerId != null && pid != null && pid.equals(myPlayerId); + boolean isMe = myPlayerId != null && myPlayerId.equals(pid); if (isMe) { meFound = true; @@ -767,6 +805,286 @@ public class CasinoGameController { safeRefresh(player1Controller); safeRefresh(player2Controller); safeRefresh(player3Controller); + clearActiveTurnHighlights(); + setTaskbarTurnHighlight(false); + } + + /** + * Clear all visual highlights in the UI that indicate the active player's turn. + */ + private void clearActiveTurnHighlights() { + if (player1Controller != null) { + player1Controller.setTurnHighlighted(false); + } + + if (player2Controller != null) { + player2Controller.setTurnHighlighted(false); + } + + if (player3Controller != null) { + player3Controller.setTurnHighlighted(false); + } + + if (playerStatusController != null) { + playerStatusController.setTurnHighlighted(false); + } + + setPlayerCardsTurnHighlighted(false); + } + + /** + * Set the visual highlight state of the player's hole cards to indicate whether it is + * currently the player's turn in the game. + * + * @param highlighted true to highlight the player's hole cards, indicating that it is their turn, + * or false to remove the highlight when it is not their turn. + */ + private void setPlayerCardsTurnHighlighted(boolean highlighted) { + if (playerCardsBox == null) { + return; + } + + for (Node child : playerCardsBox.getChildren()) { + if (child instanceof ImageView) { + toggleStyleClass(child, ACTIVE_CARD_BOX_STYLE_CLASS, highlighted); + } + } + } + + /** + * Update the visual highlights in the UI to indicate which player's turn it is based on the active. + * + * @param players The list of players currently in the game, used to identify the active + * player and update the turn highlights accordingly. + * @param activePlayerIndex The index of the active player in the players list, which is used to determine + * whose turn it is and update the UI highlights to reflect that. + */ + private void updateActiveTurnHighlights(List players, int activePlayerIndex) { + clearActiveTurnHighlights(); + + Player activePlayer = getPlayerAtIndex(players, activePlayerIndex); + if (activePlayer == null || activePlayer.getId() == null) { + setTaskbarTurnHighlight(false); + return; + } + + if (myPlayerId != null && myPlayerId.equals(activePlayer.getId())) { + setPlayerCardsTurnHighlighted(true); + setTaskbarTurnHighlight(true); + return; + } + + setTaskbarTurnHighlight(false); + + if (player1Controller != null && player1Controller.hasPlayer(activePlayer)) { + player1Controller.setTurnHighlighted(true); + return; + } + + if (player2Controller != null && player2Controller.hasPlayer(activePlayer)) { + player2Controller.setTurnHighlighted(true); + return; + } + + if (player3Controller != null && player3Controller.hasPlayer(activePlayer)) { + player3Controller.setTurnHighlighted(true); + } + } + + /** + * Set the turn highlight state in the taskbar controller, which visually indicates whether + * it is the current player's turn in the game. + * + * @param highlighted true to highlight the turn in the taskbar, false to remove the highlight. + */ + private void setTaskbarTurnHighlight(boolean highlighted) { + TaskbarController taskbarCtrl = resolveTaskbarController(); + if (taskbarCtrl != null) { + taskbarCtrl.setTurnHighlighted(highlighted); + } + } + + /** + * Retrieve the Player object at the specified index from the list of players. + * + * @param players The list of players from which to retrieve the player at the specified index. + * @param index The index of the player to retrieve from the list. + * @return The Player object at the specified index if it exists and is valid, or null if the + * index is out of bounds or if the players list is null or empty. + */ + private Player getPlayerAtIndex(List players, int index) { + if (players == null || index < 0 || index >= players.size()) { + return null; + } + + return players.get(index); + } + + /** + * Resolve the winner of the hand based on the game state. + * + * @param s The current game state, which may contain information about the players, + * their states, and the winner index. + * @return The Player object representing the winner if it can be determined from the + * game state, or null if the winner cannot be resolved or if the game is still in progress. + */ + private Player resolveWinner(GameState s) { + if (s == null || s.players == null || s.players.isEmpty()) { + return null; + } + + if (s.winnerIndex >= 0 && s.winnerIndex < s.players.size()) { + return s.players.get(s.winnerIndex); + } + + Player remaining = null; + int remainingCount = 0; + for (Player player : s.players) { + if (player == null) { + continue; + } + if (player.getState() == PlayerState.FOLDED) { + continue; + } + if (remainingCount > 0) { + return null; + } + remaining = player; + remainingCount++; + } + + if (remaining != null && remainingCount == 1) { + return remaining; + } + + return null; + } + + /** + * Resolve the name of the winner based on the game state. + * + * @param s The current game state, which may contain information about the players, + * their states, and the winner index. + * @return The name of the winner if it can be determined from the game state, + * or null if the winner cannot be resolved or if the game is still in progress. + */ + private String resolveWinnerName(GameState s) { + Player winner = resolveWinner(s); + if (winner != null) { + String name = winner.getName(); + if (name != null && !name.isBlank()) { + return name; + } + } + + if (isTerminalPhase(s != null ? s.phase : null) + && lastWinnerName != null + && !lastWinnerName.isBlank()) { + return lastWinnerName; + } + + return null; + } + + /** + * Determine if the game is in a finished state based on the game state and the presence of a winner name. + * + * @param s The current game state, which may be null or contain information about the phase, players, + * and winner index. + * @param winnerName The name of the winner, which may be null or blank if the winner is not yet determined + * or if the game is still in progress. + * @return true if the game is considered finished based on the provided state and winner information, + * false otherwise. + */ + private boolean isGameFinishedState(GameState s, String winnerName) { + if (s == null) { + return false; + } + + boolean winnerKnown = winnerName != null && !winnerName.isBlank(); + if (!winnerKnown) { + return false; + } + + if (isTerminalPhase(s.phase)) { + return true; + } + + if (s.winnerIndex >= 0 && s.players != null && s.winnerIndex < s.players.size()) { + return true; + } + + return hasSingleRemainingPlayer(s); + } + + /** + * Determine if there is only one remaining active player in the game state. + * + * @param s The current game state, which may be null or contain a list of players. + * @return true if there is exactly one active player remaining (not folded), false otherwise. + */ + private boolean hasSingleRemainingPlayer(GameState s) { + if (s == null || s.players == null || s.players.isEmpty()) { + return false; + } + + int remaining = 0; + for (Player player : s.players) { + if (player == null || player.getState() == PlayerState.FOLDED) { + continue; + } + remaining++; + if (remaining > 1) { + return false; + } + } + return remaining == 1; + } + + /** + * Determine if the current phase of the game is a terminal phase, such as "FINISHED" or "SHOWDOWN". + * + * @param phase The current phase of the game, which may be null or blank. + * @return true if the phase is considered terminal, indicating that the hand has ended + * and a winner can be declared, false otherwise. + */ + private boolean isTerminalPhase(String phase) { + return "FINISHED".equalsIgnoreCase(phase) || "SHOWDOWN".equalsIgnoreCase(phase); + } + + /** + * Finish the game UI loop by stopping the timeline that updates the UI. + */ + private void finishGameUiLoop() { + if (gameFinished) { + return; + } + gameFinished = true; + if (timeline != null) { + timeline.stop(); + } + } + + /** + * Toggle a CSS style class on a JavaFX Node based on the active state. + * + * @param node The JavaFX Node on which to toggle the style class. + * @param styleClass The name of the CSS style class to toggle. + * @param active A boolean indicating whether to add (true) + * or remove (false) the style class from the node. + */ + private void toggleStyleClass(Node node, String styleClass, boolean active) { + if (node == null || styleClass == null || styleClass.isBlank()) { + return; + } + + if (active) { + if (!node.getStyleClass().contains(styleClass)) { + node.getStyleClass().add(styleClass); + } + } else { + node.getStyleClass().remove(styleClass); + } } /** diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/gameui/gameuicomponents/PlayerStatusController.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/gameui/gameuicomponents/PlayerStatusController.java index cfa2aad..19cd340 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/gameui/gameuicomponents/PlayerStatusController.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/gameui/gameuicomponents/PlayerStatusController.java @@ -2,12 +2,15 @@ package ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.gameuicomponents; import ch.unibas.dmi.dbis.cs108.casono.client.game.Player; import ch.unibas.dmi.dbis.cs108.casono.client.game.PlayerState; +import java.util.logging.Level; import java.util.logging.Logger; import javafx.fxml.FXML; import javafx.scene.control.Label; import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.layout.Pane; +import javafx.scene.layout.VBox; +import javafx.scene.layout.HBox; /** * Controller for displaying a player's status in the poker game, including their name, chip count @@ -21,11 +24,16 @@ public class PlayerStatusController { @FXML private Label playerMoney; @FXML private ImageView dealerIcon; @FXML private Pane parent; + @FXML private VBox playerStatusBox; + @FXML private HBox statusInnerBoxTop; + @FXML private HBox statusInnerBoxBottom; private Image dealerImage; private Player player; + private boolean turnHighlighted; private static final String DEALER_IMAGE_PATH = "/images/chip-dealer-blue-3.png"; private static final double DEALER_ICON_X_FACTOR = 0.8; + private static final String TURN_HIGHLIGHT_STYLE_CLASS = "player-status-active-turn"; /** Initialize the controller, load dealer image, and set up bindings. */ @FXML @@ -42,8 +50,7 @@ public class PlayerStatusController { } } catch (Exception e) { - LOGGER.severe("Error: loading dealer image:"); - e.printStackTrace(); + LOGGER.log(Level.SEVERE, "Error loading dealer image", e); } dealerIcon.layoutXProperty().bind(parent.widthProperty().multiply(DEALER_ICON_X_FACTOR)); @@ -57,13 +64,20 @@ public class PlayerStatusController { */ public void setPlayer(Player player) { this.player = player; + if (player == null) { + turnHighlighted = false; + } refresh(); + updateTurnHighlightStyle(); } /** Refresh UI safely */ public void refresh() { if (player == null) { + playerName.setText("-"); + playerMoney.setText("0 $"); + dealerIcon.setVisible(false); return; } @@ -84,6 +98,30 @@ public class PlayerStatusController { } } + /** + * Highlight or unhighlight this player slot as the currently active turn. + * + * @param highlighted true if this slot should be highlighted, false otherwise. + */ + public void setTurnHighlighted(boolean highlighted) { + this.turnHighlighted = highlighted; + updateTurnHighlightStyle(); + } + + /** + * Check whether the controller is currently bound to the provided player. + * + * @param candidate the player to compare against. + * @return true if both represent the same player. + */ + public boolean hasPlayer(Player candidate) { + if (player == null || candidate == null || player.getId() == null || candidate.getId() == null) { + return false; + } + + return player.getId().equals(candidate.getId()); + } + /** * External quick update * @@ -107,4 +145,25 @@ public class PlayerStatusController { dealerIcon.setImage(dealerImage); } } + + /** + * Update the visual style of the player status box to indicate whether it's currently this player's turn. + */ + private void updateTurnHighlightStyle() { + if (statusInnerBoxTop == null || statusInnerBoxBottom == null) { + return; + } + + if (turnHighlighted) { + if (!statusInnerBoxTop.getStyleClass().contains(TURN_HIGHLIGHT_STYLE_CLASS)) { + statusInnerBoxTop.getStyleClass().add(TURN_HIGHLIGHT_STYLE_CLASS); + } + if (!statusInnerBoxBottom.getStyleClass().contains(TURN_HIGHLIGHT_STYLE_CLASS)) { + statusInnerBoxBottom.getStyleClass().add(TURN_HIGHLIGHT_STYLE_CLASS); + } + } else { + statusInnerBoxTop.getStyleClass().remove(TURN_HIGHLIGHT_STYLE_CLASS); + statusInnerBoxBottom.getStyleClass().remove(TURN_HIGHLIGHT_STYLE_CLASS); + } + } }