Feat: Improving the Game UI and implementing new Features:

- Improve dealer chip Rendering.
- Improve pot Rendering.
- Uses names instead of IDs as identifiers.
- Users are displayed one after another as they enter the Game UI.
- Correct order of opponents <- B <-> S <-> D <-> O -> in the Game UI.
- The boxes for the opponents should have a working hover effect.
- Improve the rendering of the table text.
- Remove hover animations from cards and the pot for improved handling and a more stable Game UI.
This commit is contained in:
Julian Kropff
2026-05-08 16:27:42 +02:00
parent b63ce096fa
commit 2f19fff618
7 changed files with 686 additions and 81 deletions
@@ -18,4 +18,6 @@ public class GameState {
public int dealer;
public int activePlayer;
public int winnerIndex = -1;
public List<String> winnerNames = new ArrayList<>();
public int potPerWinner;
}
@@ -111,6 +111,7 @@ public class GameClient {
GameState s = new GameState();
s.players = new ArrayList<>();
s.communityCards = new ArrayList<>();
s.winnerNames = new ArrayList<>();
ParseState state = new ParseState();
for (String raw : input.split("\n")) {
@@ -155,6 +156,14 @@ public class GameClient {
s.winnerIndex = intVal(l);
yield true;
}
case String l when l.startsWith("WINNER_NAME=") -> {
s.winnerNames.add(value(l));
yield true;
}
case String l when l.startsWith("POT_PER_WINNER=") -> {
s.potPerWinner = intVal(l);
yield true;
}
case String l when l.startsWith("HIGHSCORE=") -> {
s.highscoreEntries.add(value(l));
yield true;
@@ -224,6 +233,13 @@ public class GameClient {
return true;
}
if (line.startsWith("ID=")) {
if (state.currentPlayer.getId() == null) {
state.currentPlayer.setId(PlayerId.of(value(line)));
}
return true;
}
if (line.startsWith("CHIPS=")) {
state.currentPlayer.setChips(intVal(line));
return true;
@@ -244,6 +244,33 @@ public class LobbyClient {
return result;
}
/**
* Fetches the current lobby members from GET_LOBBY_STATUS and returns their usernames in order.
*
* @param lobbyId id of the lobby to inspect
* @return list of usernames currently known in the lobby
*/
public List<String> fetchLobbyPlayerNames(int lobbyId) {
List<String> lines = client.processCommand("GET_LOBBY_STATUS ID=" + lobbyId);
List<RequestParameter> params = ClientService.convertToRequestParameters(lines);
List<String> names = new ArrayList<>();
for (RequestParameter parameter : params) {
if (!"USERNAME".equalsIgnoreCase(parameter.key())) {
continue;
}
String name = parameter.value();
if (name != null) {
String trimmed = name.trim();
if (!trimmed.isEmpty() && !names.contains(trimmed)) {
names.add(trimmed);
}
}
}
return names;
}
/** Simple data holder for lobby metadata returned by the server. */
public static final class LobbyInfo {
public final int id;
@@ -10,7 +10,10 @@ 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.network.LobbyClient;
import ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.gameuicomponents.NotebookController;
import ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.gameuicomponents.PlayerStatusController;
import ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.gameuicomponents.SettingsController;
import ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.gameuicomponents.TaskbarController;
import java.io.IOException;
import java.net.URL;
@@ -21,6 +24,7 @@ import java.util.concurrent.CompletableFuture;
import java.util.logging.Logger;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.image.Image;
@@ -60,8 +64,13 @@ public class CasinoGameController {
private GameService gameService;
private PlayerId myPlayerId;
private String myPlayerName;
@FXML private TaskbarController taskbarIncludeController;
private TaskbarController taskbarController;
@FXML private NotebookController notebookIncludeController;
private NotebookController notebookController;
@FXML private SettingsController settingsIncludeController;
// private SettingsController settingsController;
private Image dealerImage;
private javafx.animation.Timeline timeline;
private boolean gameStarted = false;
@@ -73,6 +82,7 @@ public class CasinoGameController {
public static final double CHAT_CONTAINER_CONSTANT = 6.0;
private static final int TOTAL_SLOTS = 5;
private static final int PLAYER_SLOTS = 2;
private static final int MAX_OPPONENTS_TO_RENDER = 3;
private int pot = 0;
private static final String BACKSIDE = "/images/card-background-3.png";
private static final String DEALER_IMAGE_PATH = "/images/chip-dealer-blue-3.png";
@@ -115,6 +125,8 @@ public class CasinoGameController {
private static final double COMMUNITY_CARD_WIDTH_RATIO = 0.11;
private static final double PLAYER_CARD_HEIGHT_RATIO = 0.30;
private static final double PLAYER_CARD_WIDTH_RATIO = 0.15;
private static final double DEALER_HEIGHT_RATIO = 0.13;
private static final double DEALER_WIDTH_RATIO = 0.13;
private static final double PLAYER_CARDS_Y_OFFSET_FACTOR = 0.10;
private static final int[] CHIP_VALUES = {
100000, 50000, 20000, 10000,
@@ -139,6 +151,12 @@ public class CasinoGameController {
private static final long CHIP_SCALE_DURATION_MS = 220;
private static final long CHIP_DROP_DURATION_MS = 250;
private static final long CHIP_STAGGER_DELAY_MULTIPLIER = 35L;
private static final int MAX_POT_ROWS = 2;
private static final double POT_CHIP_H_GAP = 6.0;
private static final double POT_CHIP_V_GAP = 6.0;
private static final double POT_WIDTH_FACTOR = 0.45;
private static final double FALLBACK_POT_WIDTH = 500.0;
private static final double FALLBACK_CHIP_WIDTH = 36.0;
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";
@@ -156,6 +174,8 @@ public class CasinoGameController {
private static final int SMALL_BLIND = 100;
private static final int BIG_BLIND = 200;
private static final int NO_BET = 0;
private boolean myDealerIconSizeBound;
private java.util.List<String> lobbyPlayerNames = java.util.List.of();
/** Standard constructor. Used by FXML. */
public CasinoGameController() {
@@ -230,6 +250,16 @@ public class CasinoGameController {
if (taskbarController != null && gameService != null && myPlayerId != null) {
taskbarController.setGameService(gameService, myPlayerId);
}
notebookController = resolveNotebookController();
if (taskbarController != null && notebookController != null) {
taskbarController.setNotebookController(notebookController);
}
if (taskbarController != null && settingsIncludeController != null) {
taskbarController.setSettingsController(settingsIncludeController);
settingsIncludeController.setThemeChangeListener(this::applyTheme);
}
configureTaskbarLobbyActionAnnouncements();
if (communityCardsBox == null) {
@@ -251,6 +281,7 @@ public class CasinoGameController {
}
myDealerIcon.setVisible(false);
styleMyDealerIcon();
tableText.setWrapText(true);
javafx.stage.Screen screen = javafx.stage.Screen.getPrimary();
@@ -267,6 +298,8 @@ public class CasinoGameController {
renderPlayerCards(List.of());
chatContainer.toFront();
javafx.application.Platform.runLater(() -> applyTheme("standard"));
}
/**
@@ -475,6 +508,7 @@ public class CasinoGameController {
.thenAccept(
state -> {
if (state == null) {
refreshLobbyOpponentsBeforeGameStart();
LOGGER.info("No state received yet.");
return;
}
@@ -492,6 +526,35 @@ public class CasinoGameController {
});
}
/**
* Refresh the list of opponents in the lobby before the game starts by fetching the latest player.
*/
private void refreshLobbyOpponentsBeforeGameStart() {
if (gameService != null && gameService.peekStateOrNull() != null) {
return;
}
if (chatClientService == null || chatLobbyId < 0) {
return;
}
try {
LobbyClient lobbyClient = new LobbyClient(chatClientService);
List<String> latestNames = lobbyClient.fetchLobbyPlayerNames(chatLobbyId);
if (latestNames == null) {
latestNames = List.of();
}
if (latestNames.equals(lobbyPlayerNames)) {
return;
}
List<String> namesToRender = List.copyOf(latestNames);
javafx.application.Platform.runLater(() -> setLobbyPlayerNames(namesToRender));
} catch (Exception e) {
String msg = "Could not refresh lobby player names: " + e.getMessage();
LOGGER.fine(msg);
}
}
/**
* Updates all visual UI components with the data from the provided GameState. This includes
* community cards, pot, player information, and the taskbar.
@@ -621,12 +684,12 @@ public class CasinoGameController {
*/
private List<Card> getMyCards(List<Player> players) {
if (myPlayerId == null || players == null) {
if (players == null) {
return List.of();
}
for (Player p : players) {
if (p.getId() != null && p.getId().equals(myPlayerId)) {
if (isCurrentPlayer(p)) {
return p.getCards() != null ? p.getCards() : List.of();
}
}
@@ -641,13 +704,13 @@ public class CasinoGameController {
*/
private void updatePlayerCards(GameState s) {
if (s.players == null || myPlayerId == null) {
if (s.players == null) {
renderPlayerCards(List.of());
return;
}
for (Player p : s.players) {
if (p.getId().equals(myPlayerId)) {
if (isCurrentPlayer(p)) {
renderPlayerCards(p.getCards());
return;
}
@@ -672,7 +735,18 @@ public class CasinoGameController {
String winnerName = resolveWinnerName(s);
if (winnerName != null && !winnerName.isBlank()) {
tableText.setText("Winner: " + winnerName);
StringBuilder winnerText = new StringBuilder("Winner");
if (s.winnerNames != null && s.winnerNames.size() > 1) {
winnerText.append("s: ").append(String.join(", ", s.winnerNames));
if (s.potPerWinner > 0) {
winnerText.append(" (").append(s.potPerWinner).append(" each)");
}
} else {
winnerText.append(": ").append(winnerName);
}
tableText.setText(winnerText.toString());
return;
}
@@ -770,13 +844,13 @@ public class CasinoGameController {
*/
private void updateTaskbar(GameState s) {
TaskbarController controller = resolveTaskbarController();
if (controller == null || myPlayerId == null) {
if (controller == null) {
return;
}
if (gameService != null) {
controller.setGameService(gameService, myPlayerId);
}
controller.update(s, myPlayerId);
controller.update(s, myPlayerName);
}
/**
@@ -832,30 +906,50 @@ public class CasinoGameController {
* opponents.
*/
private java.util.List<Player> buildOpponents(List<Player> players) {
java.util.List<Player> opponents = new java.util.ArrayList<>();
boolean meFound = false;
for (Player p : players) {
if (p == null) {
continue;
}
PlayerId pid = p.getId();
boolean isMe = myPlayerId != null && myPlayerId.equals(pid);
if (isMe) {
meFound = true;
} else {
opponents.add(p);
}
if (players == null || players.isEmpty()) {
return java.util.List.of();
}
if (!meFound) {
opponents.clear();
opponents.addAll(players);
int myIndex = findCurrentPlayerIndex(players);
if (myIndex < 0) {
java.util.List<Player> fallback = new java.util.ArrayList<>();
for (Player player : players) {
if (player != null) {
fallback.add(player);
}
}
return fallback;
}
return opponents;
java.util.List<Player> ordered = new java.util.ArrayList<>(Math.max(0, players.size() - 1));
for (int offset = 1; offset < players.size(); offset++) {
int index = (myIndex + offset) % players.size();
Player player = players.get(index);
if (player != null) {
ordered.add(player);
}
}
return ordered;
}
/**
* Find the index of the current player in the provided list of players by comparing their PlayerId
* with myPlayerId.
*
* @param players The list of players to search through, which may include the current player and their opponents.
* @return The index of the current player in the list if found, or -1 if the current player is not present in the
* list or if the list is null or empty.
*/
private int findCurrentPlayerIndex(List<Player> players) {
if (players == null || players.isEmpty()) {
return -1;
}
for (int i = 0; i < players.size(); i++) {
if (isCurrentPlayer(players.get(i))) {
return i;
}
}
return -1;
}
/**
@@ -970,12 +1064,12 @@ public class CasinoGameController {
clearActiveTurnHighlights();
Player activePlayer = getPlayerAtIndex(players, activePlayerIndex);
if (activePlayer == null || activePlayer.getId() == null) {
if (activePlayer == null) {
setTaskbarTurnHighlight(false);
return;
}
if (myPlayerId != null && myPlayerId.equals(activePlayer.getId())) {
if (isCurrentPlayer(activePlayer)) {
setPlayerCardsTurnHighlighted(true);
setTaskbarTurnHighlight(true);
return;
@@ -1104,11 +1198,7 @@ public class CasinoGameController {
}
Player winner = resolveWinner(s);
boolean isLocalWinner =
winner != null
&& winner.getId() != null
&& myPlayerId != null
&& myPlayerId.equals(winner.getId());
boolean isLocalWinner = winner != null && isCurrentPlayer(winner);
if (!isLocalWinner && lastWinnerName != null && !lastWinnerName.isBlank()) {
String localName =
@@ -1255,11 +1345,11 @@ public class CasinoGameController {
}
Player dealerPlayer = s.players.get(s.dealer);
if (dealerPlayer == null || dealerPlayer.getId() == null) {
if (dealerPlayer == null) {
return;
}
if (myPlayerId != null && myPlayerId.equals(dealerPlayer.getId())) {
if (isCurrentPlayer(dealerPlayer)) {
myDealerIcon.setVisible(true);
return;
}
@@ -1292,10 +1382,17 @@ public class CasinoGameController {
* otherwise.
*/
private boolean samePlayer(Player a, Player b) {
if (a == null || b == null || a.getId() == null || b.getId() == null) {
if (a == null || b == null) {
return false;
}
return a.getId().equals(b.getId());
String aName = normalizeIdentifier(a.getName());
String bName = normalizeIdentifier(b.getName());
if (aName != null && bName != null) {
return aName.equals(bName);
}
return a.getId() != null && b.getId() != null && a.getId().equals(b.getId());
}
/**
@@ -1326,8 +1423,8 @@ public class CasinoGameController {
view.setImage(image);
view.setOnMouseEntered(e -> animateCardHover(view, true));
view.setOnMouseExited(e -> animateCardHover(view, false));
// view.setOnMouseEntered(e -> animateCardHover(view, true));
// view.setOnMouseExited(e -> animateCardHover(view, false));
view.setOpacity(CARD_START_OPACITY);
@@ -1370,8 +1467,8 @@ public class CasinoGameController {
view.setImage(image);
view.setOnMouseEntered(e -> animateCardHover(view, true));
view.setOnMouseExited(e -> animateCardHover(view, false));
// view.setOnMouseEntered(e -> animateCardHover(view, true));
// view.setOnMouseExited(e -> animateCardHover(view, false));
view.setOpacity(CARD_START_OPACITY);
@@ -1384,6 +1481,47 @@ public class CasinoGameController {
}
}
/**
* Apply styling to the myDealerIcon ImageView.
*/
private void styleMyDealerIcon() {
if (myDealerIcon == null || playerCardsBox == null) {
return;
}
myDealerIcon.setPreserveRatio(true);
myDealerIcon.setSmooth(true);
javafx.scene.Scene scene = playerCardsBox.getScene();
if (scene != null) {
bindMyDealerIconSize(scene);
} else {
playerCardsBox
.sceneProperty()
.addListener(
(obs, oldScene, newScene) -> {
if (newScene != null) {
bindMyDealerIconSize(newScene);
}
});
}
}
/**
* Bind the size of the myDealerIcon ImageView to the dimensions of the scene using predefined ratios.
*
* @param scene The JavaFX Scene to which the myDealerIcon belongs, used for binding the size properties.
*/
private void bindMyDealerIconSize(javafx.scene.Scene scene) {
if (myDealerIconSizeBound || scene == null || myDealerIcon == null) {
return;
}
myDealerIcon.fitHeightProperty().bind(scene.heightProperty().multiply(DEALER_HEIGHT_RATIO));
myDealerIcon.fitWidthProperty().bind(scene.widthProperty().multiply(DEALER_WIDTH_RATIO));
myDealerIconSizeBound = true;
}
/**
* Load an image from the given path safely, falling back to a default backside image if the
* specified image cannot be found.
@@ -1551,28 +1689,30 @@ public class CasinoGameController {
* Animate a card hover effect by scaling and lifting the card when hovered and resetting it
* when not hovered.
*
* However, we have removed the hover animations from the cards and the pot for improved handling and a more stable Game UI.
*
* @param view The ImageView representing the card to animate on hover.
* @param hover A boolean indicating whether the card is being hovered (true) or not (false).
*/
private void animateCardHover(ImageView view, boolean hover) {
double scale = hover ? HOVER_SCALE_ON : HOVER_SCALE_OFF;
double lift = hover ? HOVER_LIFT_ON : HOVER_LIFT_OFF;
javafx.animation.ScaleTransition st =
new javafx.animation.ScaleTransition(
javafx.util.Duration.millis(HOVER_DURATION_MS), view);
st.setToX(scale);
st.setToY(scale);
javafx.animation.TranslateTransition tt =
new javafx.animation.TranslateTransition(
javafx.util.Duration.millis(HOVER_DURATION_MS), view);
tt.setToY(lift);
st.play();
tt.play();
}
// private void animateCardHover(ImageView view, boolean hover) {
//
// double scale = hover ? HOVER_SCALE_ON : HOVER_SCALE_OFF;
// double lift = hover ? HOVER_LIFT_ON : HOVER_LIFT_OFF;
//
// javafx.animation.ScaleTransition st =
// new javafx.animation.ScaleTransition(
// javafx.util.Duration.millis(HOVER_DURATION_MS), view);
// st.setToX(scale);
// st.setToY(scale);
//
// javafx.animation.TranslateTransition tt =
// new javafx.animation.TranslateTransition(
// javafx.util.Duration.millis(HOVER_DURATION_MS), view);
// tt.setToY(lift);
//
// st.play();
// tt.play();
// }
/**
* Position the player's hole cards box on the screen based on a predefined offset factor
@@ -1640,13 +1780,26 @@ public class CasinoGameController {
potBox.getChildren().clear();
int chipsPerRow = resolveChipsPerPotRow();
int maxVisibleChips = chipsPerRow * MAX_POT_ROWS;
HBox topRow = createPotRow();
HBox bottomRow = createPotRow();
VBox rows = new VBox(POT_CHIP_V_GAP, topRow, bottomRow);
rows.setAlignment(Pos.CENTER);
potBox.getChildren().add(rows);
int remaining = pot;
int index = 0;
boolean limitReached = false;
for (int chipValue : CHIP_VALUES) {
while (remaining >= chipValue) {
if (index >= maxVisibleChips) {
limitReached = true;
break;
}
ImageView chip =
new ImageView(loadImageSafe("/images/chip-" + chipValue + "-5.png"));
@@ -1658,18 +1811,66 @@ public class CasinoGameController {
styleChip(chip);
potBox.getChildren().add(chip);
if (index < chipsPerRow) {
topRow.getChildren().add(chip);
} else {
bottomRow.getChildren().add(chip);
}
animateChipAppear(chip, index);
remaining -= chipValue;
index++;
}
if (limitReached) {
break;
}
}
// LOGGER.info("POT RENDERED: " + pot + " -> remaining: " + remaining);
}
/**
* Create a new HBox to represent a row of chips in the pot display, with predefined horizontal.
*
* @return A new HBox instance configured for displaying a row of chips in the pot,
* with appropriate spacing and alignment.
*/
private HBox createPotRow() {
HBox row = new HBox(POT_CHIP_H_GAP);
row.setAlignment(Pos.CENTER);
return row;
}
/**
* Calculate the number of chips that can be displayed per row in the pot based on the available width.
*
* @return The calculated number of chips that can fit in a single row of the pot display.
*/
private int resolveChipsPerPotRow() {
double availableWidth = potBox != null ? potBox.getWidth() : 0.0;
if (availableWidth <= 0 && potBox != null && potBox.getScene() != null) {
availableWidth = potBox.getScene().getWidth() * POT_WIDTH_FACTOR;
}
if (availableWidth <= 0) {
availableWidth = FALLBACK_POT_WIDTH;
}
double estimatedChipWidth = FALLBACK_CHIP_WIDTH;
if (potBox != null && potBox.getScene() != null) {
estimatedChipWidth =
Math.max(FALLBACK_CHIP_WIDTH, potBox.getScene().getWidth() * CHIP_WIDTH_RATIO);
}
int chipsPerRow =
(int)
Math.floor(
(availableWidth + POT_CHIP_H_GAP)
/ (estimatedChipWidth + POT_CHIP_H_GAP));
return Math.max(1, chipsPerRow);
}
/**
* Apply styling to the given ImageView for chips, including CSS classes and size bindings.
*
@@ -1769,13 +1970,126 @@ public class CasinoGameController {
*/
public void setMyPlayerId(PlayerId id) {
this.myPlayerId = id;
this.myPlayerName = id != null ? normalizeIdentifier(id.value()) : null;
TaskbarController controller = resolveTaskbarController();
if (controller != null && gameService != null && myPlayerId != null) {
controller.setGameService(gameService, myPlayerId);
}
renderLobbyOpponentsIfNeeded();
configureTaskbarLobbyActionAnnouncements();
}
/**
* Sets lobby player names to be shown before the first game-state update is available.
*
* @param names player names currently present in the lobby
*/
public void setLobbyPlayerNames(List<String> names) {
if (names == null || names.isEmpty()) {
this.lobbyPlayerNames = java.util.List.of();
} else {
this.lobbyPlayerNames = java.util.List.copyOf(names);
}
renderLobbyOpponentsIfNeeded();
}
/**
* Render the opponents in the lobby based on the list of lobby player names, but only if the game
* service is not yet initialized or if there is no game state available.
*/
private void renderLobbyOpponentsIfNeeded() {
if (gameService != null && gameService.peekStateOrNull() != null) {
return;
}
if (lobbyPlayerNames == null || lobbyPlayerNames.isEmpty()) {
return;
}
java.util.List<String> opponents = new java.util.ArrayList<>();
for (String rawName : lobbyPlayerNames) {
String normalized = normalizeIdentifier(rawName);
if (normalized == null) {
continue;
}
if (myPlayerName != null && myPlayerName.equals(normalized)) {
continue;
}
if (!opponents.contains(rawName.trim())) {
opponents.add(rawName.trim());
}
if (opponents.size() >= MAX_OPPONENTS_TO_RENDER) {
break;
}
}
setLobbyOpponent(player1Controller, opponents, 0);
setLobbyOpponent(player2Controller, opponents, 1);
setLobbyOpponent(player3Controller, opponents, 2);
}
/**
* Set the lobby opponent information in the given PlayerStatusController slot based on the list
* of opponent names and the specified index.
*
* @param slot The PlayerStatusController instance representing the UI slot for an opponent,
* which will be updated with the opponent's name if available.
* @param opponents The list of opponent names currently in the lobby, which may be null or
* contain fewer entries than the index.
* @param index The index of the opponent in the opponents list to be displayed in the given slot, where
* 0 corresponds to the first opponent, 1 to the second, and so on.
*/
private void setLobbyOpponent(PlayerStatusController slot, List<String> opponents, int index) {
if (slot == null) {
return;
}
if (opponents == null || index >= opponents.size()) {
slot.setPlayer(null);
safeRefresh(slot);
return;
}
slot.setPlayer(null);
slot.updatePlayer(opponents.get(index), 0);
slot.setDealer(false);
slot.setTurnHighlighted(false);
}
/**
* Determine if the given Player object represents the current player based on the
* player's name and ID compared to the stored myPlayerName and myPlayerId values.
*
* @param player The Player object to check, which may be null or contain name and ID information.
* @return true if the given Player is identified as the current player based on name or ID matching, false otherwise.
*/
private boolean isCurrentPlayer(Player player) {
if (player == null) {
return false;
}
String candidateName = normalizeIdentifier(player.getName());
if (myPlayerName != null && candidateName != null && myPlayerName.equals(candidateName)) {
return true;
}
return myPlayerId != null && player.getId() != null && myPlayerId.equals(player.getId());
}
/**
* Normalize a player name or identifier by trimming whitespace and converting to lowercase, returning null if the result is empty.
*
* @param value The raw player name or identifier to normalize, which may be null or contain leading/trailing whitespace.
* @return The normalized identifier in lowercase without leading/trailing whitespace, or null if the input is null or empty after trimming.
*/
private String normalizeIdentifier(String value) {
if (value == null) {
return null;
}
String trimmed = value.trim();
return trimmed.isEmpty() ? null : trimmed.toLowerCase();
}
/**
* Configure the TaskbarController to announce lobby actions by sending chat messages when
* certain actions occur in the game, such as winning a hand.
@@ -1833,4 +2147,82 @@ public class CasinoGameController {
}
return null;
}
/**
* Resolve the NotebookController for displaying tips and game information.
*
* @return The NotebookController instance, which may be the directly injected
* notebookController or the one obtained from the notebookIncludeController.
*/
private NotebookController resolveNotebookController() {
if (notebookController != null) {
return notebookController;
}
if (notebookIncludeController != null) {
notebookController = notebookIncludeController;
return notebookController;
}
return null;
}
/**
* Resolve the SettingsController for theme switching.
*
* @return The SettingsController instance, which may be the directly injected
* settingsController or the one obtained from the settingsIncludeController.
*/
public SettingsController getSettingsController() {
return settingsIncludeController;
}
/**
* Applies a UI theme by switching the application's stylesheet.
*
* @param theme The theme name: "standard", "blackwhite", or "glass"
*/
private void applyTheme(String theme) {
if (theme == null || theme.isBlank()) {
LOGGER.warning("Theme is null or empty -> ignoring theme change");
return;
}
javafx.scene.Scene scene = casinoTable != null ? casinoTable.getScene() : null;
if (scene == null) {
LOGGER.warning("Scene not ready yet -> cannot apply theme: " + theme);
return;
}
String cssPath =
switch (theme.toLowerCase()) {
case "blackwhite" -> "/ui-structure/Casinogameui-blackwhite.css";
case "glass" -> "/ui-structure/Casinogameui-glass.css";
default -> "/ui-structure/Casinogameui.css";
};
java.net.URL cssUrl = getClass().getResource(cssPath);
if (cssUrl == null) {
LOGGER.severe("CSS resource not found: " + cssPath);
return;
}
try {
String cssExternalForm = cssUrl.toExternalForm();
scene.getStylesheets()
.removeIf(
s ->
s.contains("Casinogameui.css")
|| s.contains("Casinogameui-blackwhite.css")
|| s.contains("Casinogameui-glass.css"));
scene.getStylesheets().add(cssExternalForm);
LOGGER.info("Theme switched successfully to: " + theme);
} catch (Exception e) {
LOGGER.severe("Failed to apply theme '" + theme + "': " + e.getMessage());
}
}
}
@@ -6,6 +6,7 @@ import ch.unibas.dmi.dbis.cs108.casono.client.game.GameService;
import ch.unibas.dmi.dbis.cs108.casono.client.game.PlayerId;
import ch.unibas.dmi.dbis.cs108.casono.client.network.ClientService;
import ch.unibas.dmi.dbis.cs108.casono.client.network.GameClient;
import ch.unibas.dmi.dbis.cs108.casono.client.network.LobbyClient;
import java.io.IOException;
import java.util.UUID;
import java.util.logging.Logger;
@@ -130,6 +131,13 @@ public class CasinoGameUI extends Application {
controller.setMyPlayerId(PlayerId.of(effectiveUsername));
controller.setChatContext(effectiveUsername, clientService, lobbyId);
try {
LobbyClient lobbyClient = new LobbyClient(clientService);
controller.setLobbyPlayerNames(lobbyClient.fetchLobbyPlayerNames(lobbyId));
} catch (Exception e) {
LOG.fine("Could not preload lobby player names: " + e.getMessage());
}
controller.startChat(chatController);
Scene scene = new Scene(root, DEFAULT_WIDTH, DEFAULT_HEIGHT);
@@ -115,14 +115,27 @@ public class PlayerStatusController {
* @return true if both represent the same player.
*/
public boolean hasPlayer(Player candidate) {
if (player == null
|| candidate == null
|| player.getId() == null
|| candidate.getId() == null) {
if (player == null || candidate == null) {
return false;
}
return player.getId().equals(candidate.getId());
String currentName = normalizeIdentifier(player.getName());
String candidateName = normalizeIdentifier(candidate.getName());
if (currentName != null && candidateName != null) {
return currentName.equals(candidateName);
}
return player.getId() != null
&& candidate.getId() != null
&& player.getId().equals(candidate.getId());
}
private String normalizeIdentifier(String value) {
if (value == null) {
return null;
}
String trimmed = value.trim();
return trimmed.isEmpty() ? null : trimmed.toLowerCase();
}
/**
@@ -51,15 +51,19 @@ public class TaskbarController {
private GameService gameService;
private PlayerId myPlayerId;
private String myPlayerName;
private GameState lastState;
private double xOffset = 0;
private double yOffset = 0;
private NotebookController notebookController;
private SettingsController settingsController;
private static final double TASKBAR_SCALE = 0.95;
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 SMALLEST_VALUE = 5;
private static final int SMALL_BLIND = 100;
private static final int BIG_BLIND = 200;
private static final String STYLE_YELLOW_BUTTON = "yellow-button";
@@ -230,6 +234,17 @@ public class TaskbarController {
public void setGameService(GameService gameService, PlayerId myPlayerId) {
this.gameService = gameService;
this.myPlayerId = myPlayerId;
this.myPlayerName = myPlayerId != null ? normalizeIdentifier(myPlayerId.value()) : null;
}
/** Sets the NotebookController reference for showing/hiding tips. */
public void setNotebookController(NotebookController notebookController) {
this.notebookController = notebookController;
}
/** Sets the SettingsController reference for theme switching. */
public void setSettingsController(SettingsController settingsController) {
this.settingsController = settingsController;
}
/** Sets an optional callback that publishes simple action labels to lobby chat. */
@@ -340,6 +355,37 @@ public class TaskbarController {
return "RAISE +" + raiseBy + " TO " + raiseamout;
}
/**
* Resolves the typed input as the additional amount the player wants to contribute.
*
* <p>If the player has already invested chips in the current betting round, the typed value is
* interpreted as an additional amount on top of that investment. This lets the user enter the
* amount they still need to put in directly (for example: already invested 200, type 400 ->
* total target 600).
*
* @param state current game state
* @param rawInput text from the input field
* @return total target bet, or null if the input is invalid
*/
private Integer resolveTypedContributionTarget(GameState state, String rawInput) {
Integer input = parseInputTarget(rawInput);
if (input == null) {
return null;
}
if (state == null || state.players == null) {
return input;
}
Player me = findCurrentPlayer(state);
if (me == null) {
return input;
}
int alreadyInvested = Math.max(0, me.getBet());
return alreadyInvested > 0 ? alreadyInvested + input : input;
}
/**
* Updates the taskbar based on the current game state and the player's status.
*
@@ -352,8 +398,30 @@ public class TaskbarController {
* and whether they are out of the game.
*/
public void update(GameState state, PlayerId myPlayerId) {
this.myPlayerId = myPlayerId;
this.myPlayerName = myPlayerId != null ? normalizeIdentifier(myPlayerId.value()) : null;
update(state);
}
if (state == null || state.players == null || myPlayerId == null) {
/**
* Updates the taskbar based on the current game state and the player's name.
*
* @param state current game state
* @param myPlayerName local player name
*/
public void update(GameState state, String myPlayerName) {
this.myPlayerName = normalizeIdentifier(myPlayerName);
update(state);
}
/**
* Updates the taskbar based on the current game state.
*
* @param state current game state
*/
private void update(GameState state) {
if (state == null || state.players == null) {
LOGGER.warn("Cannot update taskbar: invalid input");
return;
}
@@ -364,11 +432,7 @@ public class TaskbarController {
lastReferenceBet = state.currentBet;
}
Player me =
state.players.stream()
.filter(p -> myPlayerId.equals(p.getId()))
.findFirst()
.orElse(null);
Player me = findCurrentPlayer(state);
if (me == null) {
LOGGER.error("Player not found in GameState!");
@@ -876,7 +940,8 @@ public class TaskbarController {
return input != null ? input : callTarget + BIG_BLIND;
}
return parseInputTarget(taskbarInput.getText());
return resolveTypedContributionTarget(
state, taskbarInput != null ? taskbarInput.getText() : null);
}
/**
@@ -1011,7 +1076,7 @@ public class TaskbarController {
try {
int value = Integer.parseInt(text.trim());
if (value % SMALL_BLIND != 0) {
if (value % SMALLEST_VALUE != 0) {
return null;
}
@@ -1033,7 +1098,7 @@ public class TaskbarController {
* current game state.
*/
private ValidationResult validateTypedAmount(GameState state, String text) {
Integer target = parseInputTarget(text);
Integer target = resolveTypedContributionTarget(state, text);
if (target == null || state == null) {
return ValidationResult.blocked("Invalid input");
}
@@ -1491,15 +1556,53 @@ public class TaskbarController {
submitAction(ActionType.INPUT);
}
/**
* Finds the current player in the given game state based on the player's name or ID.
*
* @param state The current GameState object representing the state of the game,
* which includes a list of players and their details.
* @return The Player object representing the current player if found in the game state.
*/
private Player findCurrentPlayer(GameState state) {
if (state == null || state.players == null || myPlayerId == null) {
if (state == null || state.players == null) {
return null;
}
return state.players.stream()
.filter(player -> player != null && myPlayerId.equals(player.getId()))
.findFirst()
.orElse(null);
return state.players.stream().filter(this::isCurrentPlayer).findFirst().orElse(null);
}
/**
* Checks if the given player matches the current player's identity based on name or ID.
*
* @param player The Player object to check against the current player's identity,
* which includes the player's name and ID.
* @return A boolean value indicating whether the given player matches the current player's identity.
*/
private boolean isCurrentPlayer(Player player) {
if (player == null) {
return false;
}
String playerName = normalizeIdentifier(player.getName());
if (myPlayerName != null && playerName != null && myPlayerName.equals(playerName)) {
return true;
}
return myPlayerId != null && myPlayerId.equals(player.getId());
}
/**
* Normalizes a player identifier (name or ID) by trimming whitespace and converting to lowercase.
*
* @param value The string value representing a player identifier, such as a name or ID.
* @return A normalized version of the player identifier, where leading and trailing whitespace is removed.
*/
private String normalizeIdentifier(String value) {
if (value == null) {
return null;
}
String trimmed = value.trim();
return trimmed.isEmpty() ? null : trimmed.toLowerCase();
}
/**
@@ -1550,6 +1653,50 @@ public class TaskbarController {
}
}
/**
* Opens the integrated Casono Web Browser.
*/
@FXML
private void onBrowserButtonClickWiki() {
try {
CasinoBrowserController.open("https://www.wikipedia.org/");
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* Opens the integrated Casono Web Browser.
*/
@FXML
private void onBrowserButtonClickBrave() {
try {
CasinoBrowserController.open("https://search.brave.com");
} catch (Exception e) {
e.printStackTrace();
}
}
/** Opens the tips notebook if it's currently closed. */
@FXML
private void onShowTipsButtonClick() {
if (notebookController != null) {
notebookController.showTips();
}
}
/** Opens the settings box for theme selection. */
@FXML
private void onSettingsButtonClick() {
if (settingsController != null) {
settingsController.show();
}
}
/** Opens the highscore popup window from the taskbar. */
@FXML
private void onHighscoreButtonClick() {