Add: visual indicator for current player in Game UI

This commit is contained in:
Julian Kropff
2026-04-25 14:54:46 +02:00
parent 6e640f7fde
commit c11201479b
2 changed files with 389 additions and 12 deletions
@@ -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.GameState;
import ch.unibas.dmi.dbis.cs108.casono.client.game.Player; import ch.unibas.dmi.dbis.cs108.casono.client.game.Player;
import ch.unibas.dmi.dbis.cs108.casono.client.game.PlayerId; import ch.unibas.dmi.dbis.cs108.casono.client.game.PlayerId;
import ch.unibas.dmi.dbis.cs108.casono.client.game.PlayerState;
import ch.unibas.dmi.dbis.cs108.casono.client.network.ClientService; 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.PlayerStatusController;
import ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.gameuicomponents.TaskbarController; 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 long CHIP_STAGGER_DELAY_MULTIPLIER = 35L;
private static final double CHAT_WIDTH = 400; private static final double CHAT_WIDTH = 400;
private static final double CHAT_HEIGHT = 600; private static final double CHAT_HEIGHT = 600;
private static final String ACTIVE_CARD_BOX_STYLE_CLASS = "player-cards-active-turn";
private String chatUsername; private String chatUsername;
private ClientService chatClientService; private ClientService chatClientService;
private int chatLobbyId = -1; private int chatLobbyId = -1;
private boolean chatInitialized; private boolean chatInitialized;
private boolean gameFinished;
private String lastWinnerName;
/** Standard constructor. Used by FXML. */ /** Standard constructor. Used by FXML. */
public CasinoGameController() { public CasinoGameController() {
@@ -230,6 +234,7 @@ public class CasinoGameController {
} }
myDealerIcon.setVisible(false); myDealerIcon.setVisible(false);
tableText.setWrapText(true);
javafx.stage.Screen screen = javafx.stage.Screen.getPrimary(); javafx.stage.Screen screen = javafx.stage.Screen.getPrimary();
double screenHeight = screen.getBounds().getHeight(); double screenHeight = screen.getBounds().getHeight();
@@ -477,7 +482,7 @@ public class CasinoGameController {
*/ */
private void applyState(GameState s) { private void applyState(GameState s) {
if (s == null) { if (s == null || gameFinished) {
return; return;
} }
@@ -490,6 +495,24 @@ public class CasinoGameController {
+ (s.players != null ? s.players.size() : 0)); + (s.players != null ? s.players.size() : 0));
List<Player> players = (s.players != null) ? s.players : List.of(); List<Player> 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<Card> community = (s.communityCards != null) ? s.communityCards : List.of(); List<Card> community = (s.communityCards != null) ? s.communityCards : List.of();
List<Card> myCards = getMyCards(players); List<Card> myCards = getMyCards(players);
@@ -510,7 +533,7 @@ public class CasinoGameController {
renderPot(s.pot); renderPot(s.pot);
} }
updatePlayers(players); updatePlayers(players, s.activePlayer);
syncWhisperTargetsFromPlayers(players); syncWhisperTargetsFromPlayers(players);
updateGameInfo(s); updateGameInfo(s);
highlightDealer(s); highlightDealer(s);
@@ -610,14 +633,28 @@ public class CasinoGameController {
*/ */
private void updateGameInfo(GameState s) { private void updateGameInfo(GameState s) {
String text = "Phase: " + s.phase; if (s == null) {
tableText.setText("Phase: -");
if (s.winnerIndex >= 0 && s.winnerIndex < s.players.size()) { return;
Player winner = s.players.get(s.winnerIndex);
text = "Winner: " + winner.getName();
} }
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, * opponents and update their display in the UI accordingly. If the list is null or empty,
* all opponent slots will be cleared. * all opponent slots will be cleared.
*/ */
private void updatePlayers(List<Player> players) { private void updatePlayers(List<Player> players, int activePlayerIndex) {
if (players == null || players.isEmpty()) { if (players == null || players.isEmpty()) {
clearOpponentSlots(); clearOpponentSlots();
return; return;
@@ -671,6 +708,7 @@ public class CasinoGameController {
safeRefresh(player1Controller); safeRefresh(player1Controller);
safeRefresh(player2Controller); safeRefresh(player2Controller);
safeRefresh(player3Controller); safeRefresh(player3Controller);
updateActiveTurnHighlights(players, activePlayerIndex);
} }
/** /**
@@ -695,7 +733,7 @@ public class CasinoGameController {
} }
PlayerId pid = p.getId(); PlayerId pid = p.getId();
boolean isMe = myPlayerId != null && pid != null && pid.equals(myPlayerId); boolean isMe = myPlayerId != null && myPlayerId.equals(pid);
if (isMe) { if (isMe) {
meFound = true; meFound = true;
@@ -767,6 +805,286 @@ public class CasinoGameController {
safeRefresh(player1Controller); safeRefresh(player1Controller);
safeRefresh(player2Controller); safeRefresh(player2Controller);
safeRefresh(player3Controller); 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<Player> 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<Player> 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);
}
} }
/** /**
@@ -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.Player;
import ch.unibas.dmi.dbis.cs108.casono.client.game.PlayerState; import ch.unibas.dmi.dbis.cs108.casono.client.game.PlayerState;
import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.scene.control.Label; import javafx.scene.control.Label;
import javafx.scene.image.Image; import javafx.scene.image.Image;
import javafx.scene.image.ImageView; import javafx.scene.image.ImageView;
import javafx.scene.layout.Pane; 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 * 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 Label playerMoney;
@FXML private ImageView dealerIcon; @FXML private ImageView dealerIcon;
@FXML private Pane parent; @FXML private Pane parent;
@FXML private VBox playerStatusBox;
@FXML private HBox statusInnerBoxTop;
@FXML private HBox statusInnerBoxBottom;
private Image dealerImage; private Image dealerImage;
private Player player; private Player player;
private boolean turnHighlighted;
private static final String DEALER_IMAGE_PATH = "/images/chip-dealer-blue-3.png"; private static final String DEALER_IMAGE_PATH = "/images/chip-dealer-blue-3.png";
private static final double DEALER_ICON_X_FACTOR = 0.8; private static final double DEALER_ICON_X_FACTOR = 0.8;
private static final String TURN_HIGHLIGHT_STYLE_CLASS = "player-status-active-turn";
/** Initialize the controller, load dealer image, and set up bindings. */ /** Initialize the controller, load dealer image, and set up bindings. */
@FXML @FXML
@@ -42,8 +50,7 @@ public class PlayerStatusController {
} }
} catch (Exception e) { } catch (Exception e) {
LOGGER.severe("Error: loading dealer image:"); LOGGER.log(Level.SEVERE, "Error loading dealer image", e);
e.printStackTrace();
} }
dealerIcon.layoutXProperty().bind(parent.widthProperty().multiply(DEALER_ICON_X_FACTOR)); dealerIcon.layoutXProperty().bind(parent.widthProperty().multiply(DEALER_ICON_X_FACTOR));
@@ -57,13 +64,20 @@ public class PlayerStatusController {
*/ */
public void setPlayer(Player player) { public void setPlayer(Player player) {
this.player = player; this.player = player;
if (player == null) {
turnHighlighted = false;
}
refresh(); refresh();
updateTurnHighlightStyle();
} }
/** Refresh UI safely */ /** Refresh UI safely */
public void refresh() { public void refresh() {
if (player == null) { if (player == null) {
playerName.setText("-");
playerMoney.setText("0 $");
dealerIcon.setVisible(false);
return; 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 * External quick update
* *
@@ -107,4 +145,25 @@ public class PlayerStatusController {
dealerIcon.setImage(dealerImage); 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);
}
}
} }