Merge branch 'main' into 'feat/133-add-sounds-to-game'

This commit is contained in:
Jona Walpert
2026-05-13 21:21:38 +02:00
52 changed files with 5316 additions and 168 deletions
+1 -1
View File
@@ -26,7 +26,7 @@ repositories {
javafx {
version = "25.0.2"
modules = ['javafx.controls', 'javafx.fxml', 'javafx.base', 'javafx.graphics', 'javafx.web']
modules = ['javafx.controls', 'javafx.fxml', 'javafx.base', 'javafx.graphics', 'javafx.web', 'javafx.media']
}
dependencies {
@@ -52,7 +52,7 @@ public final class Main {
}
private static void printUsage() {
Logger logger = LogManager.getLogger(Main.class);
Logger logger = LogManager.getLogger(Main.class.getSimpleName());
logger.fatal(
"""
Usage:
@@ -15,7 +15,7 @@ import org.apache.logging.log4j.Logger;
*/
public class ClientApp {
private static final Logger LOGGER = LogManager.getLogger(ClientApp.class);
private static final Logger LOGGER = LogManager.getLogger(ClientApp.class.getSimpleName());
/** Shared client connection used when a username is provided at startup. */
private static volatile ClientService sharedClientService;
@@ -88,7 +88,7 @@ public class ChatController {
chatModelMap = new LinkedHashMap<>();
localUserList = new ArrayList<>();
this.chatBoxController = new ChatBoxController(username, this);
this.logger = LogManager.getLogger(ChatController.class);
this.logger = LogManager.getLogger(ChatController.class.getSimpleName());
this.serverEventListener = this::handleServerEvent;
this.activeChatControllers = new HashMap<>();
@@ -124,7 +124,7 @@ public class ChatController {
chatModelMap = new LinkedHashMap<>();
localUserList = new ArrayList<>();
this.chatBoxController = new ChatBoxController(username, this);
this.logger = LogManager.getLogger(ChatController.class);
this.logger = LogManager.getLogger(ChatController.class.getSimpleName());
this.serverEventListener = this::handleServerEvent;
this.activeChatControllers = new HashMap<>();
@@ -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;
}
@@ -26,7 +26,7 @@ public class ChatClient implements ChatClientInterface {
*/
public ChatClient(ClientService clientService) {
this.clientService = clientService;
this.logger = LogManager.getLogger(ChatClient.class);
this.logger = LogManager.getLogger(ChatClient.class.getSimpleName());
}
/**
@@ -60,7 +60,7 @@ public class ClientService {
public ClientService(String ip, int port) {
this.idGenerator = new AtomicInteger(0);
this.logger = LogManager.getLogger(ClientService.class);
this.logger = LogManager.getLogger(ClientService.class.getSimpleName());
this.offlineMode = false;
@@ -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 ch.unibas.dmi.dbis.cs108.casono.ui.sound.SoundManager;
import java.io.IOException;
@@ -22,6 +25,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;
@@ -61,8 +65,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;
@@ -75,6 +84,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";
@@ -117,6 +127,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,
@@ -141,6 +153,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";
@@ -158,6 +176,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() {
@@ -264,6 +284,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) {
@@ -285,6 +315,7 @@ public class CasinoGameController {
}
myDealerIcon.setVisible(false);
styleMyDealerIcon();
tableText.setWrapText(true);
javafx.stage.Screen screen = javafx.stage.Screen.getPrimary();
@@ -301,6 +332,8 @@ public class CasinoGameController {
renderPlayerCards(List.of());
chatContainer.toFront();
javafx.application.Platform.runLater(() -> applyTheme("standard"));
}
/**
@@ -509,6 +542,7 @@ public class CasinoGameController {
.thenAccept(
state -> {
if (state == null) {
refreshLobbyOpponentsBeforeGameStart();
LOGGER.info("No state received yet.");
return;
}
@@ -526,6 +560,36 @@ 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.
@@ -669,12 +733,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();
}
}
@@ -689,13 +753,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;
}
@@ -720,7 +784,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;
}
@@ -818,13 +893,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);
}
/**
@@ -880,30 +955,51 @@ 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;
}
/**
@@ -1018,12 +1114,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;
@@ -1152,11 +1248,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 =
@@ -1303,11 +1395,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;
}
@@ -1340,10 +1432,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());
}
/**
@@ -1374,8 +1473,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);
@@ -1418,8 +1517,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);
@@ -1432,6 +1531,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.
@@ -1599,28 +1739,31 @@ public class CasinoGameController {
* Animate a card hover effect by scaling and lifting the card when hovered and resetting it
* when not hovered.
*
* <p>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
@@ -1688,13 +1831,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"));
@@ -1706,18 +1862,67 @@ 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.
*
@@ -1817,13 +2022,131 @@ 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.
@@ -1881,4 +2204,86 @@ 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 "mainuiparquet" -> "/ui-structure/Casinogameui-parquet.css";
case "blackwhite" -> "/ui-structure/Casinogameui-blackwhite.css";
case "glass" -> "/ui-structure/Casinogameui-glass.css";
case "lightglass" -> "/ui-structure/Casinogameui-light-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-parquet.css")
|| s.contains("Casinogameui-blackwhite.css")
|| s.contains("Casinogameui-glass.css")
|| s.contains("Casinogameui-light-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);
@@ -68,7 +68,8 @@ public class CasinoBrowserController {
private static final CookieManager COOKIE_MANAGER =
new CookieManager(null, CookiePolicy.ACCEPT_ORIGINAL_SERVER);
private static final Logger LOGGER = LogManager.getLogger(CasinoBrowserController.class);
private static final Logger LOGGER =
LogManager.getLogger(CasinoBrowserController.class.getSimpleName());
private static final ObservableList<String> URL_SUGGESTIONS =
FXCollections.observableArrayList(TRUSTED_DOMAINS);
@@ -14,7 +14,8 @@ import org.apache.logging.log4j.Logger;
/** Controller for the highscore popup window. */
public class HighscoreViewController {
private static final Logger LOGGER = LogManager.getLogger(HighscoreViewController.class);
private static final Logger LOGGER =
LogManager.getLogger(HighscoreViewController.class.getSimpleName());
@FXML private ListView<String> highscoreList;
@FXML private Label statusLabel;
@@ -0,0 +1,269 @@
package ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.gameuicomponents;
import javafx.fxml.FXML;
import javafx.scene.control.Button;
import javafx.scene.control.ScrollPane;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.VBox;
import javafx.scene.text.Text;
import javafx.scene.text.TextFlow;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/**
* Controller for the movable notebook (Tips) display within the Casono Game UI.
*
* <p>Responsible for: - Drag-and-drop movement of the notebook, - Display of general gaming tips, -
* Toggle visibility of the notebook.
*/
public class NotebookController {
private static final Logger LOGGER =
LogManager.getLogger(NotebookController.class.getSimpleName());
@FXML private VBox notebook;
@FXML private ScrollPane tipsContent;
@FXML private VBox tipsList;
@FXML private Button toggleButton;
private double xOffset = 0;
private double yOffset = 0;
private static final double NOTEBOOK_SCALE = 0.95;
private static final double SCALE_NORMAL = 1.0;
private boolean isVisible = true;
/** Standard constructor. Used by FXML. */
public NotebookController() {
// default constructor for FXML
}
/** Initialize the notebook with default tips. */
@FXML
private void initialize() {
if (tipsList != null) {
loadDefaultTips();
}
}
/** Load default poker tips into the notebook. */
private void loadDefaultTips() {
tipsList.getChildren().clear();
String[] tips = {
"TEXAS HOLD'EM GRUNDLAGEN\n\nJeder Spieler erhält 2 verdeckte Karten. "
+ "Zusätzlich gibt es 5 Gemeinschaftskarten.\n"
+ "Ziel: Beste 5-Karten-Hand bilden oder alle Gegner zum Fold bringen.\n"
+ "Startstack: 20000 Chips ($)",
"BLINDS & DEALER\n\nSmall Blind: 100 Chips | Big Blind: 200 Chips\n"
+ "Die Blinds erzeugen den Startpot und "
+ "müssen vor jeder Runde gezahlt werden.\n"
+ "Der Dealer-Button bestimmt die Positionen "
+ "sowie die Reihenfolge der Aktionen.",
"SPIELABLAUF\n\nPreflop → Flop (3 Karten) → Turn (4. Karte) → River (5. Karte)\n"
+ "Preflop beginnt links vom Big Blind.\n"
+ "Nach dem Flop beginnt die Action links vom Dealer (aktive Spieler).",
"AKTIONEN\n\nFold = aussteigen\nCall = mitgehen\nRaise = erhöhen\n"
+ "Alle Aktionen müssen in korrekter Reihenfolge (Acting in Turn) erfolgen.",
"POSITION IST ENTSCHEIDEND\n\nEarly Position: nur starke Hände spielen\n"
+ "Middle Position: etwas breiter spielen\n"
+ "Late Position: größter Vorteil durch mehr Informationen",
"SHOWDOWN\n\nWenn nach der letzten Setzrunde mehrere Spieler übrig sind:\n"
+ "Beste 5-Karten-Kombination aus Handkarten + Gemeinschaftskarten gewinnt.",
"STRATEGIE & DENKEN\n\nPoker ist Strategie + Psychologie + Mathematik\n"
+ "Entscheidungen basieren auf Wahrscheinlichkeiten, "
+ "Position und Gegnerverhalten.",
"WICHTIGE REGELN\n\n- Acting in Turn ist Pflicht\n"
+ "- Jeder Spieler spielt nur seine eigene Hand (One Player One Hand)\n"
+ "- Ungültige Einsätze werden blockiert oder korrigiert\n"
+ "- Reihenfolge am Tisch muss immer eingehalten werden",
"HÄUFIGE FEHLER\n\n❌ Zu viele Hände spielen\n"
+ "❌ Position ignorieren\n❌ Zu oft callen statt folden\n"
+ "❌ Stärke der Gegner unterschätzen\n❌ Tilt (emotional spielen)",
"TOP 10 POKERHÄNDE\n\n"
+ "1. 👑 Royal Flush\n"
+ "A♠ K♠ Q♠ J♠ 10♠\n\n"
+ "2. 🔥 Straight Flush\n"
+ "9♥ 8♥ 7♥ 6♥ 5♥\n\n"
+ "3. 💥 Four of a Kind (Poker)\n"
+ "A♣ A♦ A♥ A♠ K♠\n\n"
+ "4. 🏠 Full House\n"
+ "K♣ K♦ K♥ 9♠ 9♦\n\n"
+ "5. 🌊 Flush\n"
+ "A♥ J♥ 8♥ 5♥ 2♥\n\n"
+ "6. ➡ Straight\n"
+ "10♣ 9♦ 8♠ 7♥ 6♣\n\n"
+ "7. 🎯 Three of a Kind (Drilling)\n"
+ "Q♣ Q♦ Q♥ 7♠ 2♦\n\n"
+ "8. 👥 Two Pair\n"
+ "J♣ J♦ 4♠ 4♥ A♣\n\n"
+ "9. 👍 One Pair\n"
+ "8♣ 8♦ K♠ J♥ 3♣\n\n"
+ "10. 🃏 High Card\n"
+ "A♠ J♦ 9♣ 5♥ 2♠"
};
for (String tip : tips) {
TextFlow tipBox = createTipElement(tip);
tipsList.getChildren().add(tipBox);
}
}
private static final int LINE_SPACING = 4;
private static final int TIP_WIDTH = 280;
/**
* Create a styled text element for a single tip.
*
* @param tipText The text of the tip to display.
* @return A TextFlow element containing the formatted tip.
*/
private TextFlow createTipElement(String tipText) {
Text text = new Text(tipText);
text.setStyle("-fx-font-size: 12; -fx-font-family: 'Segoe UI', Arial;");
TextFlow flow = new TextFlow(text);
flow.setStyle(
"-fx-padding: 10; -fx-background-color: #f5f5f5; "
+ "-fx-border-radius: 5; -fx-margin: 5;");
flow.setLineSpacing(LINE_SPACING);
flow.setPrefWidth(TIP_WIDTH);
return flow;
}
/**
* Called when the notebook header is pressed with the mouse. Saves the relative position for
* later correct repositioning.
*
* @param event The mouse event
*/
@FXML
private void onNotebookPressed(MouseEvent event) {
if (notebook == null || event == null) {
return;
}
xOffset = event.getSceneX() - notebook.getLayoutX();
yOffset = event.getSceneY() - notebook.getLayoutY();
}
/**
* Called while dragging the notebook with the mouse. Updates the position and slightly scales
* the notebook for visual feedback.
*
* @param event The mouse event
*/
@FXML
private void onNotebookDragged(MouseEvent event) {
if (notebook == null || event == null || notebook.getScene() == null) {
return;
}
notebook.setScaleX(NOTEBOOK_SCALE);
notebook.setScaleY(NOTEBOOK_SCALE);
double targetX = event.getSceneX() - xOffset;
double targetY = event.getSceneY() - yOffset;
double maxX = Math.max(0, notebook.getScene().getWidth() - scaledNodeWidth());
double maxY = Math.max(0, notebook.getScene().getHeight() - scaledNodeHeight());
notebook.setLayoutX(clamp(targetX, 0, maxX));
notebook.setLayoutY(clamp(targetY, 0, maxY));
}
/**
* Called when the mouse cursor is released over the notebook. Resets the notebook scaling to
* normal size.
*
* @param event The mouse event
*/
@FXML
private void onNotebookReleased(MouseEvent event) {
if (notebook == null) {
return;
}
notebook.setScaleX(SCALE_NORMAL);
notebook.setScaleY(SCALE_NORMAL);
if (notebook.getScene() == null) {
return;
}
double maxX = Math.max(0, notebook.getScene().getWidth() - scaledNodeWidth());
double maxY = Math.max(0, notebook.getScene().getHeight() - scaledNodeHeight());
notebook.setLayoutX(clamp(notebook.getLayoutX(), 0, maxX));
notebook.setLayoutY(clamp(notebook.getLayoutY(), 0, maxY));
}
/** Close the notebook window from the header minus button. */
@FXML
private void onToggleNotebook() {
if (notebook == null) {
return;
}
isVisible = false;
notebook.setVisible(false);
notebook.setManaged(false);
}
/** Shows the tips notebook (makes it visible if currently hidden). */
public void showTips() {
if (notebook == null) {
return;
}
if (!isVisible) {
isVisible = true;
notebook.setVisible(true);
notebook.setManaged(true);
}
if (tipsContent != null) {
tipsContent.setVisible(true);
tipsContent.setManaged(true);
}
if (toggleButton != null) {
toggleButton.setText("");
}
}
/**
* Calculates the scaled width of the notebook node based on its current bounds and scale
* factor.
*
* @return The scaled width of the notebook node.
*/
private double scaledNodeWidth() {
double width = notebook.getBoundsInLocal().getWidth();
if (width <= 0) {
width = notebook.prefWidth(-1);
}
return Math.max(0, width * notebook.getScaleX());
}
/**
* Calculates the scaled height of the notebook node based on its current bounds and scale
* factor.
*
* @return The scaled height of the notebook node.
*/
private double scaledNodeHeight() {
double height = notebook.getBoundsInLocal().getHeight();
if (height <= 0) {
height = notebook.prefHeight(-1);
}
return Math.max(0, height * notebook.getScaleY());
}
/**
* Clamps a value between a minimum and maximum bound.
*
* @param value the value to clamp
* @param min the lower bound
* @param max the upper bound
* @return the clamped value in the range [min, max]
*/
private double clamp(double value, double min, double max) {
return Math.max(min, Math.min(value, max));
}
}
@@ -20,15 +20,19 @@ public class PlayerStatusController {
private static final Logger LOGGER = Logger.getLogger(PlayerStatusController.class.getName());
@FXML private Label playerName;
@FXML private Label playerMoney;
@FXML private ImageView dealerIcon;
@FXML private Pane parent;
@FXML private VBox playerStatusBox;
@FXML private HBox statusInnerBoxTop;
@FXML private HBox statusInnerBoxBottom;
@FXML Label playerName;
@FXML Label playerMoney;
@FXML ImageView dealerIcon;
@FXML ImageView playerProfileImage;
@FXML Pane parent;
@FXML VBox playerStatusBox;
@FXML HBox statusInnerBoxTop;
@FXML HBox statusInnerBoxBottom;
private Image dealerImage;
// Profile images (loaded from resources/images/profile-picture)
private Image profileUserImage;
private Image profileDealerImage;
private Player player;
private boolean turnHighlighted;
private static final String DEALER_IMAGE_PATH = "/images/chip-dealer-blue-5.png";
@@ -53,6 +57,26 @@ public class PlayerStatusController {
LOGGER.log(Level.SEVERE, "Error loading dealer image", e);
}
try {
var base = "/images/profile-picture/";
var uUser = getClass().getResource(base + "poker_user_491.png");
var uDealer = getClass().getResource(base + "poker_dealer_471.png");
if (uUser != null) {
profileUserImage = new Image(uUser.toExternalForm(), true);
}
if (uDealer != null) {
profileDealerImage = new Image(uDealer.toExternalForm(), true);
}
if (playerProfileImage != null && profileUserImage != null) {
playerProfileImage.setImage(profileUserImage);
}
} catch (Exception e) {
LOGGER.log(Level.FINE, "Could not load profile images", e);
}
dealerIcon.layoutXProperty().bind(parent.widthProperty().multiply(DEALER_ICON_X_FACTOR));
dealerIcon.setVisible(false);
}
@@ -115,14 +139,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();
}
/**
@@ -147,6 +184,14 @@ public class PlayerStatusController {
if (isDealer && dealerImage != null) {
dealerIcon.setImage(dealerImage);
}
if (playerProfileImage != null) {
if (isDealer && profileDealerImage != null) {
playerProfileImage.setImage(profileDealerImage);
} else if (profileUserImage != null) {
playerProfileImage.setImage(profileUserImage);
}
}
}
/**
@@ -0,0 +1,209 @@
package ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.gameuicomponents;
import java.util.function.Consumer;
import javafx.fxml.FXML;
import javafx.scene.control.RadioButton;
import javafx.scene.control.ToggleGroup;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.VBox;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/**
* Controller for the settings display within the poker UI. Handles theme selection and drag
* movement.
*/
public class SettingsController {
private static final Logger LOGGER =
LogManager.getLogger(SettingsController.class.getSimpleName());
@FXML private VBox settingsBox;
@FXML private RadioButton themeStandard;
@FXML private RadioButton themeStandardParquet;
@FXML private RadioButton themeBlackWhite;
@FXML private RadioButton themeGlass;
@FXML private RadioButton themeLightGlass;
private boolean visible = true;
private double xOffset;
private double yOffset;
private static final double SCALE_DRAG = 0.95;
private static final double SCALE_NORMAL = 1.0;
private String currentTheme = "standard";
private Consumer<String> themeChangeListener;
/** Initialize the settings controller, set up theme toggle group and listeners. */
@FXML
private void initialize() {
setupThemeToggle();
}
/** Set up the theme toggle group and listeners for the theme selection radio buttons. */
private void setupThemeToggle() {
if (themeStandard == null
|| themeStandardParquet == null
|| themeBlackWhite == null
|| themeGlass == null
|| themeLightGlass == null) {
LOGGER.warn("Theme buttons not injected from FXML");
return;
}
ToggleGroup group = new ToggleGroup();
themeStandard.setToggleGroup(group);
themeStandardParquet.setToggleGroup(group);
themeBlackWhite.setToggleGroup(group);
themeGlass.setToggleGroup(group);
themeLightGlass.setToggleGroup(group);
themeStandard.setSelected(true);
group.selectedToggleProperty()
.addListener(
(obs, oldVal, newVal) -> {
if (newVal == themeStandard) {
setTheme("standard");
} else if (newVal == themeStandardParquet) {
setTheme("mainuiparquet");
} else if (newVal == themeBlackWhite) {
setTheme("blackwhite");
} else if (newVal == themeGlass) {
setTheme("glass");
} else if (newVal == themeLightGlass) {
setTheme("lightglass");
}
});
}
/**
* Set the current theme and notify the listener of the change.
*
* @param theme The new theme to set (e.g., "standard", "blackwhite", "glass").
*/
private void setTheme(String theme) {
currentTheme = theme;
if (themeChangeListener != null) {
themeChangeListener.accept(theme);
}
LOGGER.info("Theme changed to: " + theme);
}
/**
* Handle the mouse press event on the settings box to prepare for dragging.
*
* @param event The MouseEvent triggered when the user presses the mouse button on the settings
* box.
*/
@FXML
private void onSettingsPressed(MouseEvent event) {
if (settingsBox == null) {
return;
}
xOffset = event.getSceneX() - settingsBox.getLayoutX();
yOffset = event.getSceneY() - settingsBox.getLayoutY();
}
/**
* Handle the mouse drag event on the settings box to allow dragging it around the scene.
*
* @param event The MouseEvent triggered when the user drags the mouse while pressing on the
* settings box.
*/
@FXML
private void onSettingsDragged(MouseEvent event) {
if (settingsBox == null || settingsBox.getScene() == null) {
return;
}
settingsBox.setScaleX(SCALE_DRAG);
settingsBox.setScaleY(SCALE_DRAG);
double x = event.getSceneX() - xOffset;
double y = event.getSceneY() - yOffset;
double maxX = settingsBox.getScene().getWidth() - settingsBox.getWidth();
double maxY = settingsBox.getScene().getHeight() - settingsBox.getHeight();
settingsBox.setLayoutX(clamp(x, 0, maxX));
settingsBox.setLayoutY(clamp(y, 0, maxY));
}
/** Close the settings box from the header minus button. */
@FXML
private void onToggleSettings() {
if (settingsBox == null) {
return;
}
visible = !visible;
settingsBox.setVisible(visible);
settingsBox.setManaged(visible);
}
/**
* Handle the mouse release event on the settings box to reset its scale after dragging.
*
* @param event The MouseEvent triggered when the user releases the mouse button after dragging
* the settings box.
*/
@FXML
private void onSettingsReleased(MouseEvent event) {
if (settingsBox == null || settingsBox.getScene() == null) {
return;
}
settingsBox.setScaleX(SCALE_NORMAL);
settingsBox.setScaleY(SCALE_NORMAL);
}
/**
* Set a listener to be notified when the theme changes. The listener will receive the new theme
* as a string.
*
* @param listener A Consumer that accepts a String representing the new theme (e.g.,
* "standard", "blackwhite", "glass").
*/
public void setThemeChangeListener(Consumer<String> listener) {
this.themeChangeListener = listener;
}
/** Show the settings box by setting its visibility and managed properties to true. */
public void show() {
if (settingsBox == null) {
return;
}
settingsBox.setVisible(true);
settingsBox.setManaged(true);
}
/** Hide the settings box by setting its visibility and managed properties to false. */
public void hide() {
if (settingsBox == null) {
return;
}
settingsBox.setVisible(false);
settingsBox.setManaged(false);
}
/**
* Utility method to clamp a value between a minimum and maximum range.
*
* @param value The value to clamp.
* @param min The minimum allowed value.
* @param max The maximum allowed value.
* @return The clamped value, guaranteed to be between min and max.
*/
private double clamp(double value, double min, double max) {
return Math.max(min, Math.min(value, max));
}
}
@@ -40,7 +40,8 @@ import org.apache.logging.log4j.Logger;
*/
public class TaskbarController {
private static final Logger LOGGER = LogManager.getLogger(TaskbarController.class);
private static final Logger LOGGER =
LogManager.getLogger(TaskbarController.class.getSimpleName());
@FXML private HBox taskbar;
@FXML private TextField taskbarInput;
@@ -52,15 +53,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";
@@ -81,6 +86,9 @@ public class TaskbarController {
private static final int FIRST_PLAYER_MAX_BET = 3000;
private static final int MAX_INPUT_LENGTH = 6;
private static final double MAX_CHIP_PERCENT = 0.30;
private int lastPotSnapshot = 0;
private int lastObservedIncrease = 0;
private int lastRaiseIncrement = 0;
/** Standard constructor. Used by FXML. */
public TaskbarController() {
@@ -231,6 +239,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. */
@@ -341,6 +360,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.
*
@@ -353,24 +403,44 @@ 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;
}
this.lastState = state;
Player me = findCurrentPlayer(state);
synchronizeCurrentBetIfNeeded(state, me);
if (state.currentBet > 0) {
lastReferenceBet = state.currentBet;
}
Player me =
state.players.stream()
.filter(p -> myPlayerId.equals(p.getId()))
.findFirst()
.orElse(null);
if (me == null) {
LOGGER.error("Player not found in GameState!");
return;
@@ -394,6 +464,52 @@ public class TaskbarController {
refreshBetInputUi();
}
/**
* Synchronizes the current bet in the game state if there has been a change in the pot size
* that is not yet reflected in the current bet.
*
* @param state The current GameState object representing the state of the game.
* @param me The Player object representing the current player, used to determine their bet
* status and whether they are the first to act in the current phase.
*/
private void synchronizeCurrentBetIfNeeded(GameState state, Player me) {
if (state == null || me == null) {
return;
}
int currentPot = Math.max(0, state.pot);
int potDiff = currentPot - lastPotSnapshot;
lastPotSnapshot = currentPot;
if (potDiff <= 0) {
return;
}
LOGGER.debug("Pot changed: +" + potDiff);
lastRaiseIncrement = Math.max(lastRaiseIncrement, potDiff);
boolean isTurnOrRiver = isTurnOrRiver(state.phase);
boolean isSpecialActor = isFirstPlayerOfPhase(state, me) && isTurnOrRiver;
if (isSpecialActor) {
LOGGER.info("SPECIAL TURN/RIVER LOGIC ACTIVE, potDiff=" + potDiff);
if (potDiff > state.currentBet) {
LOGGER.info("Updating currentBet -> potDiff: " + potDiff);
state.currentBet = potDiff;
lastReferenceBet = potDiff;
}
}
}
/**
* Applies the availability of actions (Bet, Call, Raise, Fold) based on the current game state.
*
@@ -444,7 +560,7 @@ public class TaskbarController {
return;
}
if (isFirstPlayerOfPhase(state, me)) {
if (isFirstFlopPlayer(state, me)) {
setActionEnabled(betButton, true);
setActionEnabled(callButton, false);
// setActionEnabled(foldButton, false);
@@ -630,6 +746,53 @@ public class TaskbarController {
return isInitialPreflopBlindLayout(state);
}
/**
* Checks if the current player is the first to act on the flop.
*
* @param state The current GameState containing phase, dealer and active player information.
* @param me The current player.
* @return true if the player is the first player to act on the flop.
*/
private boolean isFirstFlopPlayer(GameState state, Player me) {
if (state == null || me == null || state.players == null) {
return false;
}
if (!isFlop(state.phase)) {
return false;
}
int size = state.players.size();
if (size < 2) {
return false;
}
int myIndex = state.players.indexOf(me);
if (myIndex < 0) {
return false;
}
int firstIndex = (state.dealer + 1) % size;
for (int i = 0; i < size; i++) {
int candidate = (firstIndex + i) % size;
Player p = state.players.get(candidate);
if (p != null && p.getState() != PlayerState.FOLDED && p.getChips() > 0) {
firstIndex = candidate;
break;
}
}
return state.activePlayer == firstIndex && myIndex == firstIndex;
}
/**
* Checks if the current player is the first to act in the current phase of the game, based on
* the dealer position and the active player index.
@@ -877,7 +1040,8 @@ public class TaskbarController {
return input != null ? input : callTarget + BIG_BLIND;
}
return parseInputTarget(taskbarInput.getText());
return resolveTypedContributionTarget(
state, taskbarInput != null ? taskbarInput.getText() : null);
}
/**
@@ -1012,7 +1176,7 @@ public class TaskbarController {
try {
int value = Integer.parseInt(text.trim());
if (value % SMALL_BLIND != 0) {
if (value % SMALLEST_VALUE != 0) {
return null;
}
@@ -1034,7 +1198,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");
}
@@ -1497,15 +1661,56 @@ 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();
}
/**
@@ -1557,6 +1762,46 @@ 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() {
@@ -31,7 +31,8 @@ import org.apache.logging.log4j.Logger;
/** Controller for the Casono main UI lobby. Handles UI initialization and user actions. */
public class CasinomainuiController {
private static final Logger LOGGER = LogManager.getLogger(CasinomainuiController.class);
private static final Logger LOGGER =
LogManager.getLogger(CasinomainuiController.class.getSimpleName());
@FXML private AnchorPane rootPane;
@FXML private Label titleLabel;
@@ -35,7 +35,8 @@ public class LobbyButtonGridManager {
private static final int MAX_BUTTONS = 8;
private static final int GUEST_ID_LENGTH = 8;
private static final Logger LOGGER = LogManager.getLogger(LobbyButtonGridManager.class);
private static final Logger LOGGER =
LogManager.getLogger(LobbyButtonGridManager.class.getSimpleName());
private static final String BUTTON_FALLBACK_IMAGE = "/images/lobbypictures/error.png";
@@ -99,7 +99,7 @@ public class ServerApp {
public static void start(String arg) {
int port = Integer.parseInt(arg);
Logger logger = LogManager.getLogger(ServerApp.class);
Logger logger = LogManager.getLogger(ServerApp.class.getSimpleName());
logger.info("Starting server at port {}", port);
EventBus eventBus = new EventBus();
@@ -6,6 +6,7 @@ import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.deck.Rank;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.deck.Suit;
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.showdown.CardsSpeakRule;
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 ch.unibas.dmi.dbis.cs108.casono.server.domain.highscore.HighscoreService;
@@ -13,8 +14,11 @@ import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestCo
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.SuccessResponse;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.builder.ResponseBody;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.builder.ResponseBodyBuilder;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
/**
* Response carrying a snapshot of the current game state using the project's wire format.
@@ -51,10 +55,16 @@ public class GetGameStateResponse extends SuccessResponse {
builder.param("CURRENT_BET", computeGlobalCurrentBet(state));
builder.param("DEALER", state.getDealerIndex());
builder.param("ACTIVE_PLAYER", state.getCurrentPlayerIndex());
int winnerIndex = computeWinnerIndex(state, game);
builder.param("WINNER", winnerIndex);
appendWinnerToHighscoresIfFinished(state, winnerIndex);
List<String> winnerNames = computeWinnerNames(state, game);
for (String name : winnerNames) {
builder.param("WINNER_NAME", name);
}
appendWinnerToHighscoresIfFinished(state, winnerNames);
appendHighscoreEntries(builder);
appendCommunityCards(builder, state);
@@ -64,8 +74,14 @@ public class GetGameStateResponse extends SuccessResponse {
return builder.build();
}
private static void appendWinnerToHighscoresIfFinished(GameState state, int winnerIndex) {
if (winnerIndex < 0 || state.getPhase() != GamePhase.FINISHED) {
private static void appendWinnerToHighscoresIfFinished(
GameState state, List<String> winnerNames) {
if (winnerNames == null || winnerNames.isEmpty()) {
return;
}
GamePhase phase = state.getPhase();
if (phase != GamePhase.SHOWDOWN && phase != GamePhase.FINISHED) {
return;
}
@@ -73,12 +89,22 @@ public class GetGameStateResponse extends SuccessResponse {
return;
}
Player winner = findPlayerByIndex(state, winnerIndex);
if (winner == null || winner.getId() == null || winner.getId().value() == null) {
return;
// Keep ordering stable and persist tied winners as a single shared highscore
// entry. Compute the per-winner share here (pot is split equally among
// winners)
int potPerWinner = computePotPerWinner(state, winnerNames.size());
Set<String> uniqueWinners = new LinkedHashSet<>(winnerNames);
List<String> formattedWinners = new ArrayList<>();
for (String name : uniqueWinners) {
if (name != null && !name.isBlank()) {
formattedWinners.add(name + " (" + Math.max(0, potPerWinner) + ")");
}
}
HighscoreService.getInstance().appendWinner(winner.getId().value());
if (!formattedWinners.isEmpty()) {
HighscoreService.getInstance()
.appendFormattedEntry(String.join(", ", formattedWinners));
}
}
private static Player findPlayerByIndex(GameState state, int winnerIndex) {
@@ -120,6 +146,33 @@ public class GetGameStateResponse extends SuccessResponse {
return -1;
}
/** Computes the list of all winner names at showdown/finished phase. */
private static List<String> computeWinnerNames(GameState state, GameController game) {
GamePhase phase = state.getPhase();
if (phase != GamePhase.SHOWDOWN && phase != GamePhase.FINISHED) {
return new ArrayList<>();
}
CardsSpeakRule showdown = new CardsSpeakRule();
List<Player> winners = showdown.determineWinners(state);
List<String> winnerNames = new ArrayList<>();
for (Player winner : winners) {
if (winner != null && winner.getId() != null && winner.getId().value() != null) {
winnerNames.add(winner.getId().value());
}
}
return winnerNames;
}
/** Computes the pot share per winner. */
private static int computePotPerWinner(GameState state, int numWinners) {
if (numWinners <= 0 || state.getPot() == null) {
return 0;
}
return state.getPot().getAmount() / numWinners;
}
private static int computeGlobalCurrentBet(GameState state) {
int globalCurrentBet = 0;
for (Player p : state.getPlayers()) {
@@ -23,7 +23,8 @@ import org.apache.logging.log4j.Logger;
public class JoinLobbyHandler extends CommandHandler<JoinLobbyRequest> {
private final LobbyManager lobbyManager;
private final UserRegistry userRegistry;
private static final Logger LOGGER = LogManager.getLogger(JoinLobbyHandler.class);
private static final Logger LOGGER =
LogManager.getLogger(JoinLobbyHandler.class.getSimpleName());
/**
* Create a new {@link JoinLobbyHandler}.
@@ -2,6 +2,7 @@ package ch.unibas.dmi.dbis.cs108.casono.server.domain.game.evaluator;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.deck.Card;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.deck.Suit;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
@@ -40,6 +41,9 @@ public class HandEvaluator {
private static final int START_INDEX = 1;
private static final int CARD_DIFFERENCE = 1;
private static final int PREVIOUS_INDEX_OFFSET = -1;
private static final int TWO_KICKERS = 2;
private static final int ONE_KICKER = 1;
private static final int THREE_KICKERS = 3;
/**
* Evaluates a list of cards and determines the best possible hand rank.
@@ -62,7 +66,7 @@ public class HandEvaluator {
return straightFlush;
}
HandRank fourKind = checkFourOfAKind(rankCount);
HandRank fourKind = checkFourOfAKind(rankCount, ranks);
if (fourKind != null) {
return fourKind;
}
@@ -80,17 +84,17 @@ public class HandEvaluator {
return new HandRank(HandRank.Type.STRAIGHT, ranks);
}
HandRank threeKind = checkThreeOfAKind(rankCount);
HandRank threeKind = checkThreeOfAKind(rankCount, ranks);
if (threeKind != null) {
return threeKind;
}
HandRank twoPair = checkTwoPair(rankCount);
HandRank twoPair = checkTwoPair(rankCount, ranks);
if (twoPair != null) {
return twoPair;
}
HandRank onePair = checkOnePair(rankCount);
HandRank onePair = checkOnePair(rankCount, ranks);
if (onePair != null) {
return onePair;
}
@@ -200,20 +204,26 @@ public class HandEvaluator {
}
/**
* Helper method to check for a four of a kind hand rank.
* Helper method to check for a four of a kind hand rank. Stores the quad rank and 1 kicker for
* tie-breaking.
*
* @param rankCount A map where the key is the card rank and the value is the count of
* occurrences.
* @param allRanks A sorted list of all card ranks in the hand.
* @return A HandRank object representing the four of a kind hand rank, or null if not found.
*/
private static HandRank checkFourOfAKind(Map<Integer, Long> rankCount) {
private static HandRank checkFourOfAKind(Map<Integer, Long> rankCount, List<Integer> allRanks) {
if (!rankCount.containsValue(FOUR_OF_A_KIND_COUNT)) {
return null;
}
int quad = getRank(rankCount, FOUR_OF_A_KIND);
List<Integer> kickers = getKickers(allRanks, List.of(quad), ONE_KICKER);
return new HandRank(HandRank.Type.FOUR_OF_A_KIND, List.of(quad));
List<Integer> result = new ArrayList<>(List.of(quad));
result.addAll(kickers);
return new HandRank(HandRank.Type.FOUR_OF_A_KIND, result);
}
/**
@@ -255,30 +265,39 @@ public class HandEvaluator {
}
/**
* Helper method to check for a three of a kind hand rank.
* Helper method to check for a three of a kind hand rank. Stores the trips rank and 2 kickers
* for tie-breaking.
*
* @param rankCount A map where the key is the card rank and the value is the count of
* occurrences.
* @param allRanks A sorted list of all card ranks in the hand.
* @return A HandRank object representing the three of a kind hand rank, or null if not found.
*/
private static HandRank checkThreeOfAKind(Map<Integer, Long> rankCount) {
private static HandRank checkThreeOfAKind(
Map<Integer, Long> rankCount, List<Integer> allRanks) {
if (!rankCount.containsValue(THREE_OF_A_KIND_COUNT)) {
return null;
}
int tripsRank = getRank(rankCount, THREE_OF_A_KIND_RANK);
List<Integer> kickers = getKickers(allRanks, List.of(tripsRank), TWO_KICKERS);
return new HandRank(HandRank.Type.THREE_OF_A_KIND, List.of(tripsRank));
List<Integer> result = new ArrayList<>(List.of(tripsRank));
result.addAll(kickers);
return new HandRank(HandRank.Type.THREE_OF_A_KIND, result);
}
/**
* Helper method to check for a two pair hand rank.
* Helper method to check for a two pair hand rank. Stores the two pair ranks and 1 kicker for
* tie-breaking. The kicker is the highest remaining card that is not part of either pair.
*
* @param rankCount A map where the key is the card rank and the value is the count of
* occurrences.
* @param allRanks A sorted list of all card ranks in the hand.
* @return A HandRank object representing the two pair hand rank, or null if not found.
*/
private static HandRank checkTwoPair(Map<Integer, Long> rankCount) {
private static HandRank checkTwoPair(Map<Integer, Long> rankCount, List<Integer> allRanks) {
long pairCount = rankCount.values().stream().filter(v -> v == PAIR).count();
@@ -294,17 +313,24 @@ public class HandEvaluator {
.limit(PAIR)
.toList();
return new HandRank(HandRank.Type.TWO_PAIR, pairRanks);
List<Integer> kickers = getKickers(allRanks, pairRanks, ONE_KICKER);
List<Integer> result = new ArrayList<>(pairRanks);
result.addAll(kickers);
return new HandRank(HandRank.Type.TWO_PAIR, result);
}
/**
* Helper method to check for a one pair hand rank.
* Helper method to check for a one pair hand rank. Stores the pair rank and 3 kickers for
* tie-breaking.
*
* @param rankCount A map where the key is the card rank and the value is the count of
* occurrences.
* @param allRanks A sorted list of all card ranks in the hand.
* @return A HandRank object representing the one pair hand rank, or null if not found.
*/
private static HandRank checkOnePair(Map<Integer, Long> rankCount) {
private static HandRank checkOnePair(Map<Integer, Long> rankCount, List<Integer> allRanks) {
long pairCount = rankCount.values().stream().filter(v -> v == PAIR).count();
@@ -313,8 +339,12 @@ public class HandEvaluator {
}
int pair = getRank(rankCount, PAIR);
List<Integer> kickers = getKickers(allRanks, List.of(pair), THREE_KICKERS);
return new HandRank(HandRank.Type.ONE_PAIR, List.of(pair));
List<Integer> result = new ArrayList<>(List.of(pair));
result.addAll(kickers);
return new HandRank(HandRank.Type.ONE_PAIR, result);
}
/**
@@ -333,6 +363,26 @@ public class HandEvaluator {
.orElse(DEFAULT_VALUE);
}
/**
* Helper method to extract kickers from the list of all card ranks. Excludes the ranks already
* used in the main hand combination.
*
* @param allRanks A sorted list of all card ranks in the hand.
* @param excludeRanks A list of ranks to exclude (e.g., ranks used in pairs, trips, etc.).
* @param numKickers The number of kickers to extract.
* @return A list of kicker ranks, sorted in descending order.
*/
private static List<Integer> getKickers(
List<Integer> allRanks, List<Integer> excludeRanks, int numKickers) {
Set<Integer> excludeSet = new HashSet<>(excludeRanks);
return allRanks.stream()
.filter(rank -> !excludeSet.contains(rank))
.limit(numKickers)
.toList();
}
/**
* Helper method to determine if a list of card ranks forms a straight.
*
@@ -5,7 +5,6 @@ import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.deck.Card;
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.PlayerStatus;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.rules.Rule;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.state.GameState;
import java.util.ArrayList;
@@ -43,14 +42,28 @@ public class CardsSpeakRule implements Rule {
*/
public Player determineWinner(GameState state) {
List<Player> winners = determineWinners(state);
return winners.isEmpty() ? null : winners.get(0);
}
/**
* Determines all players that share the best hand rank.
*
* @param state The current state of the game.
* @return The list of winners, ordered by appearance in the state.
*/
public List<Player> determineWinners(GameState state) {
List<Player> players = new ArrayList<>(state.getPlayers());
Player bestPlayer = null;
HandRank bestRank = null;
List<Player> winners = new ArrayList<>();
for (Player player : players) {
if (player.getStatus() == PlayerStatus.FOLDED) {
// Use folded flag from game flow to stay consistent with winner calculation in
// controller.
if (player.isFolded()) {
continue;
}
@@ -62,13 +75,15 @@ public class CardsSpeakRule implements Rule {
HandRank rank = HandEvaluator.evaluate(cards);
if (bestRank == null || rank.compareTo(bestRank) > 0) {
bestRank = rank;
bestPlayer = player;
winners.clear();
winners.add(player);
} else if (rank.compareTo(bestRank) == 0) {
winners.add(player);
}
}
return bestPlayer;
return winners;
}
/**
@@ -81,15 +96,23 @@ public class CardsSpeakRule implements Rule {
*/
public void awardPot(GameState state) {
Player winner = determineWinner(state);
List<Player> winners = determineWinners(state);
if (winner == null) {
if (winners.isEmpty()) {
return;
}
int pot = state.getPot().getAmount();
int share = winners.isEmpty() ? 0 : pot / winners.size();
int remainder = winners.isEmpty() ? 0 : pot % winners.size();
winner.addChips(pot);
for (int i = 0; i < winners.size(); i++) {
Player winner = winners.get(i);
if (winner == null) {
continue;
}
winner.addChips(share + (i < remainder ? 1 : 0));
}
state.getPot().reset();
}
@@ -18,6 +18,7 @@ public final class HighscoreService {
private static final DateTimeFormatter DISPLAY_FORMATTER =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").withZone(ZoneId.systemDefault());
private static final int MAX_RETURNED_ENTRIES = 100;
private static final int MIN_MONEY_COLUMN_COUNT = 3;
private static final String LINE_SEPARATOR = "\t";
private final Path storagePath;
@@ -32,6 +33,11 @@ public final class HighscoreService {
}
public synchronized void appendWinner(String winnerName) {
appendWinner(winnerName, 0);
}
/** Append a winner with an optional money value (money may be 0). */
public synchronized void appendWinner(String winnerName, int money) {
if (winnerName == null) {
return;
}
@@ -41,6 +47,44 @@ public final class HighscoreService {
return;
}
String line;
if (money > 0) {
line =
Instant.now()
+ LINE_SEPARATOR
+ sanitized
+ LINE_SEPARATOR
+ money
+ System.lineSeparator();
} else {
// Keep legacy two-column format for backward compatibility
line = Instant.now() + LINE_SEPARATOR + sanitized + System.lineSeparator();
}
try {
Files.createDirectories(storagePath.getParent());
Files.writeString(
storagePath,
line,
StandardCharsets.UTF_8,
StandardOpenOption.CREATE,
StandardOpenOption.APPEND);
} catch (IOException ignored) {
// Highscore persistence must never break the game state response path.
}
}
/** Append a preformatted highscore entry such as "Lars (1000), Jona (1000)". */
public synchronized void appendFormattedEntry(String entryText) {
if (entryText == null) {
return;
}
String sanitized = sanitizeName(entryText);
if (sanitized.isEmpty()) {
return;
}
String line = Instant.now() + LINE_SEPARATOR + sanitized + System.lineSeparator();
try {
@@ -74,15 +118,25 @@ public final class HighscoreService {
if (raw == null || raw.isBlank()) {
continue;
}
String[] parts = raw.split(LINE_SEPARATOR, 2);
// Support both legacy format (TIMESTAMP \t NAME) and new format (TIMESTAMP \t
// NAME \t MONEY)
String[] parts = raw.split(LINE_SEPARATOR);
if (parts.length < 2) {
continue;
}
try {
Instant ts = Instant.parse(parts[0].trim());
String display = DISPLAY_FORMATTER.format(ts) + " | " + parts[1].trim();
String name = parts[1].trim();
String display = DISPLAY_FORMATTER.format(ts) + " | " + name;
if (parts.length >= MIN_MONEY_COLUMN_COUNT) {
try {
int money = Integer.parseInt(parts[2].trim());
display = display + " | $" + money;
} catch (NumberFormatException nfe) {
// ignore malformed money
}
}
formatted.add(display);
} catch (Exception ignored) {
// Skip malformed lines and keep all valid entries.
@@ -14,7 +14,8 @@ import org.apache.logging.log4j.Logger;
/** Periodically removes expired empty lobbies and notifies connected sessions. */
public class LobbyCleanupJob implements Runnable {
private static final Logger LOGGER = LogManager.getLogger(LobbyCleanupJob.class);
private static final Logger LOGGER =
LogManager.getLogger(LobbyCleanupJob.class.getSimpleName());
private final LobbyManager lobbyManager;
private final SessionManager sessionManager;
@@ -15,7 +15,7 @@ public class UserCleanupJob implements Runnable {
private final Duration reconnectThreshold;
public UserCleanupJob(UserRegistry registry, Duration reconnectThreshold) {
this.logger = LogManager.getLogger(UserCleanupJob.class);
this.logger = LogManager.getLogger(UserCleanupJob.class.getSimpleName());
this.registry = registry;
this.reconnectThreshold = reconnectThreshold;
}
@@ -9,7 +9,7 @@ import org.apache.logging.log4j.Logger;
public class UserFactory {
private final UserRegistry registry;
private final AtomicInteger anonymousCounter = new AtomicInteger(1);
private static final Logger LOGGER = LogManager.getLogger(UserFactory.class);
private static final Logger LOGGER = LogManager.getLogger(UserFactory.class.getSimpleName());
/**
* Creates a new UserFactory backed by the given registry.
@@ -26,7 +26,7 @@ public class NetworkManager implements Runnable {
*/
public NetworkManager(Integer port, SessionManager sessionManager, CommandRouter router) {
this.port = port;
this.logger = LogManager.getLogger(NetworkManager.class);
this.logger = LogManager.getLogger(NetworkManager.class.getSimpleName());
this.thread = new Thread(this, "networkManager");
this.running = true;
this.sessionManager = sessionManager;
@@ -15,7 +15,7 @@ public class SessionDisconnectJob implements Runnable {
public SessionDisconnectJob(
SessionManager sessionManager, EventBus eventBus, Duration timeoutThreshold) {
this.logger = LogManager.getLogger(SessionDisconnectJob.class);
this.logger = LogManager.getLogger(SessionDisconnectJob.class.getSimpleName());
this.sessionManager = sessionManager;
this.eventBus = eventBus;
this.timeoutThreshold = timeoutThreshold;
@@ -24,7 +24,7 @@ public class SessionManager {
public SessionManager(EventBus eventBus, CommandParserDispatcher dispatcher) {
this.sessions = new ConcurrentHashMap<>();
this.eventBus = eventBus;
this.logger = LogManager.getLogger(SessionManager.class);
this.logger = LogManager.getLogger(SessionManager.class.getSimpleName());
this.dispatcher = dispatcher;
}
@@ -41,7 +41,7 @@ public class SessionReader implements Runnable {
this.router = session.getRouter();
this.logger =
LogManager.getLogger(
SessionReader.class.toString() + "-" + session.getId().value());
SessionReader.class.getSimpleName() + "-" + session.getId().value());
}
@Override
@@ -18,7 +18,7 @@ public class SessionWriter implements Runnable {
this.queue = session.getResponseQueue();
this.logger =
LogManager.getLogger(
SessionReader.class.toString() + "-" + session.getId().value());
SessionReader.class.getSimpleName() + "-" + session.getId().value());
}
public void run() {
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 3.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 MiB

@@ -0,0 +1,726 @@
/*main box css*/
.root {
-fx-background-color: #000000;
-fx-font-family: "Monospaced", "Courier New"; /* font */
}
.background {
-fx-background-image: url("/images/parquet-black.png");
-fx-background-size: cover; /* Image fills the entire window */
/* Centers the image and prevents it from repeating in a tile-like pattern. */
-fx-background-position: center center;
-fx-background-repeat: no-repeat;
}
.info-text {
-fx-text-fill: #ffffff;
-fx-font-size: 16px;
}
.casino-table {
-fx-background-color: #000000;
-fx-background-radius: 210;
-fx-border-radius: 200;
-fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10;
-fx-border-width: 12;
/*-fx-min-height: 60vh;*/
/*-fx-max-height: 60vh;*/
/*-fx-min-width: 90vh;*/
/*-fx-max-width: 90vh;*/
-fx-effect: dropshadow(one-pass-box, rgba(255, 255, 255, 0.8), 0, 0, 15, 15);
}
.dealer-box {
-fx-background-color: #ffffff;
-fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10;
-fx-border-width: 3;
-fx-background-radius: 0;
-fx-border-radius: 0;
-fx-min-width: 70;
-fx-min-height: 100;
-fx-effect: dropshadow(one-pass-box, rgba(255, 255, 255, 0.8), 0, 0, 4, 4);
}
.table-title {
-fx-text-fill: #ffffff;
-fx-font-size: 28px;
-fx-font-weight: bold;
/*-fx-effect: dropshadow(one-pass-box, rgba(255, 255, 255), 0, 0, 3, 3);*/
}
.taskbar {
-fx-background-color: #000000;
-fx-background-radius: 23;
-fx-border-radius: 20;
-fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10;
-fx-border-width: 3;
-fx-padding: 10 25;
-fx-cursor: move;
-fx-effect: dropshadow(one-pass-box, rgba(0,0,0,0.8), 0, 0, 5, 5);
}
.taskbar:hover {
-fx-border-color: #ffffff;
}
.taskbar.player-active-turn {
/* Active turn should only change the shadow to avoid flicker/transparency jumps. */
-fx-effect: dropshadow(one-pass-box, rgb(184, 134, 11), 0, 0, 5, 5);
}
.black-input-field {
-fx-background-color: #000000 !important;
-fx-text-fill: #ffffff !important;
-fx-font-family: "Courier New" !important;
-fx-font-weight: bold !important;
-fx-background-radius: 8 !important;
-fx-border-radius: 8 !important;
-fx-border-color: #333333 !important;
-fx-border-width: 2 !important;
-fx-padding: 5 12 !important;
}
.black-input-field:hover {
-fx-translate-y: -3 !important;
-fx-effect: dropshadow(one-pass-box, rgba(255, 255, 255, 0.8), 0, 0, 3, 3) !important;
}
.black-input-field:focused {
-fx-border-color: #ffffff !important;
/*-fx-background-color: #000000;*/
}
.gray-input-field {
-fx-background-color: #000000 !important;
-fx-text-fill: #ffffff !important;
-fx-font-family: "Courier New" !important;
-fx-font-weight: bold !important;
-fx-background-radius: 8 !important;
-fx-border-radius: 8 !important;
-fx-border-color: #333333 !important;
/*-fx-color-color: #000000;*/
-fx-border-width: 2 !important;
-fx-padding: 5 12 !important;
}
.gray-input-field:hover {
-fx-translate-y: -3 !important;
-fx-effect: dropshadow(one-pass-box, rgba(255, 255, 255, 0.8), 0, 0, 3, 3) !important;
}
.gray-input-field:focused {
-fx-border-color: #ffffff !important;
/*-fx-background-color: #333333;*/
}
.yellow-input-field {
-fx-background-color: #b8860b !important;
-fx-text-fill: #ffffff !important;
-fx-prompt-text-fill: #ffffff !important;
-fx-font-family: "Courier New" !important;
-fx-font-weight: bold !important;
-fx-background-radius: 8 !important;
-fx-border-radius: 8 !important;
-fx-border-color: #ffd700 !important;
-fx-border-width: 2 !important;
-fx-padding: 5 12 !important;
}
.yellow-input-field:hover {
-fx-translate-y: -3 !important;
-fx-effect: dropshadow(one-pass-box, rgba(255, 255, 255, 0.8), 0, 0, 3, 3) !important;
}
/*.yellow-input-field:focused {*/
/* -fx-border-color: #ffd700;*/
/* -fx-background-color: #b8860b;*/
/*}*/
.red-input-field {
-fx-background-color: #8b0000 !important;
-fx-text-fill: #ffffff !important;
-fx-prompt-text-fill: #ffffff !important;
-fx-font-family: "Courier New" !important;
-fx-font-weight: bold !important;
-fx-background-radius: 8 !important;
-fx-border-radius: 8 !important;
-fx-border-color: #ff4444 !important;
-fx-border-width: 2 !important;
-fx-padding: 5 12 !important;
}
.red-input-field:hover {
-fx-translate-y: -3 !important;
-fx-effect: dropshadow(one-pass-box, rgba(255, 255, 255, 0.8), 0, 0, 3, 3) !important;
}
/*.red-input-field:focused {*/
/* -fx-border-color: #ff4444;*/
/* -fx-background-color: #8b0000;*/
/*}*/
.yellow-button {
-fx-background-color: #b8860b !important;
-fx-border-color: #ffd700 !important;
-fx-text-fill: #ffffff !important;
}
.red-button {
-fx-background-color: #8b0000 !important;
-fx-border-color: #ff4444 !important;
-fx-text-fill: #ffffff !important;
}
.gray-button {
-fx-background-color: #000000 !important;
-fx-border-color: #333333 !important;
-fx-text-fill: #ffffff !important;
}
.gray-button .label {
-fx-text-fill: #ffffff !important;
}
.green-button {
-fx-background-color: #000000 !important;
-fx-border-color: #333333 !important;
-fx-text-fill: #ffffff !important;
}
.gray-button, .yellow-button, .red-button, .green-button {
-fx-background-radius: 12 !important;
-fx-border-radius: 12 !important;
-fx-font-family: "Courier New" !important;
-fx-font-weight: bold !important;
-fx-border-width: 2 !important;
-fx-padding: 6 15 !important;
-fx-cursor: hand !important;
}
.gray-button:hover, .yellow-button:hover, .red-button:hover, .green-button:hover {
-fx-translate-y: -3 !important;
-fx-effect: dropshadow(one-pass-box, rgba(255, 255, 255, 0.8), 0, 0, 3, 3) !important;
}
.community-cards-box {
-fx-alignment: center !important;
-fx-spacing: 12 !important;
-fx-padding: 15 !important;
-fx-background-color: transparent !important;
}
.community-card {
-fx-effect: dropshadow(one-pass-box, rgba(255, 255, 255, 0.8), 0, 0, 3, 3);
-fx-alignment: center;
-fx-spacing: 12;
-fx-padding: 20;
-fx-background-color: linear-gradient(to bottom, #0b3d2e, #06261d);
-fx-background-radius: 16;
-fx-scale-x: 1;
-fx-scale-y: 1;
-fx-translate-y: 0;
-fx-opacity: 1;
}
/*.community-card:hover {*/
/* -fx-translate-y: -3;*/
/*}*/
.player-card {
-fx-effect: dropshadow(one-pass-box, rgba(255, 255, 255, 0.8), 0, 0, 3, 3);
-fx-alignment: center;
-fx-spacing: 12;
-fx-padding: 20;
-fx-background-color: linear-gradient(to bottom, #0b3d2e, #06261d);
-fx-background-radius: 16;
-fx-scale-x: 1;
-fx-scale-y: 1;
-fx-translate-y: 0;
-fx-opacity: 1;
}
/*.player-card:hover {*/
/* -fx-translate-y: -3;*/
/*}*/
.player-card.player-cards-active-turn {
-fx-effect: dropshadow(one-pass-box, rgb(184, 134, 11), 0, 0, 5, 5);
}
.dealer-box.player-cards-active-turn {
-fx-effect: dropshadow(one-pass-box, rgb(184, 134, 11), 0, 0, 5, 5);
}
/*.dealer-box:hover {*/
/* -fx-translate-y: -3;*/
/*}*/
.chips-icon {
-fx-effect: dropshadow(one-pass-box, rgba(255, 255, 255, 0.8), 0, 0, 3, 3);
}
/*.chips-icon:hover {*/
/* -fx-translate-y: -3;*/
/*}*/
.dealer-icon {
-fx-effect: dropshadow(one-pass-box, rgba(255, 255, 255, 0.8), 0, 0, 3, 3);
}
.player-cards-box {
-fx-padding: 10;
}
/*.player-cards-box.player-cards-active-turn {*/
/* -fx-effect: dropshadow(one-pass-box, rgb(184, 134, 11), 0, 0, 5, 5);*/
/*}*/
.money-value {
-fx-text-fill: #ffffff;
-fx-font-size: 18px;
-fx-font-weight: bold;
/*-fx-effect: dropshadow(one-pass-box, #000000, 0, 0, 2, 2);*/
}
.pot-title {
-fx-text-fill: #ffffff;
-fx-font-size: 14px;
-fx-font-weight: bold;
/*-fx-effect: dropshadow(one-pass-box, #000000, 0, 0, 3, 3);*/
}
.pot-box {
-fx-padding: 5;
}
.player-status-pane {
-fx-background-color: rgba(13, 158, 59, 0);
-fx-background-radius: 15;
-fx-border-radius: 15;
/* -fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10;
-fx-border-width: 2;
-fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 0.5), 10, 0, 0, 4);*/
}
.player-status-pane:hover {
-fx-translate-y: -3;
}
.status-inner-box-top {
-fx-background-color: #000000;
-fx-background-radius: 19;
-fx-border-radius: 15;
-fx-padding: 5 10;
-fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10;
-fx-border-width: 3;
-fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0, 0.8), 0, 0, 3, 3);
}
.status-inner-box-midle {
-fx-background-color: rgba(13, 158, 59, 0);
}
.status-inner-box-bottom {
-fx-background-color: #000000;
-fx-background-radius: 19;
-fx-border-radius: 15;
-fx-padding: 5 10;
-fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10;
-fx-border-width: 3;
-fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0, 0.8), 0, 0, 3, 3);
}
.status-inner-box-top.player-status-active-turn {
-fx-effect: dropshadow(one-pass-box, rgb(184, 134, 11), 0, 0, 5, 5);
}
.status-inner-box-bottom.player-status-active-turn {
-fx-effect: dropshadow(one-pass-box, rgb(184, 134, 11), 0, 0, 5, 5);
}
.status-label-small {
-fx-text-fill: #ffffff;
-fx-font-size: 18px;
-fx-font-weight: bold;
/*-fx-effect: dropshadow(one-pass-box, #000000, 0, 0, 2, 2);*/
}
.status-value-text {
-fx-text-fill: #ffffff;
-fx-font-size: 18px;
-fx-font-weight: bold;
/*-fx-effect: dropshadow(one-pass-box, #000000, 0, 0, 2, 2);*/
}
.status-value-money {
-fx-text-fill: #ffffff;
-fx-font-size: 18px;
-fx-font-weight: bold;
/*-fx-effect: dropshadow(one-pass-box, #000000, 0, 0, 2, 2);*/
}
.status-circle {
-fx-background-color: #000000;
-fx-background-radius: 50;
-fx-border-color: #ffffff;
-fx-border-width: 1;
-fx-border-radius: 50;
-fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0, 0.8), 0, 0, 3, 3);
}
.browser-root {
-fx-background-image: url("/images/background.png");
-fx-background-size: cover;
-fx-background-position: center;
-fx-background-color: #0b2d15;
}
.browser-web-view {
-fx-background-color: rgb(13, 158, 59);
-fx-background-radius: 23;
-fx-border-radius: 20;
-fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10;
-fx-border-width: 3;
-fx-padding: 0;
-fx-cursor: default;
-fx-effect: dropshadow(one-pass-box, rgba(0,0,0,0.8), 0, 0, 5, 5);
}
.web-view {
-fx-background-color: transparent;
}
.taskbar-browser {
-fx-background-color: rgb(13, 158, 59);
-fx-background-radius: 23;
-fx-border-radius: 20;
-fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10;
-fx-border-width: 3;
-fx-padding: 10 25;
-fx-cursor: default;
-fx-effect: dropshadow(one-pass-box, rgba(0,0,0,0.8), 0, 0, 5, 5);
}
/*.taskbar-browser:hover {*/
/* -fx-border-color: #ffffff;*/
/*}*/
.security-label {
-fx-text-fill: #000000;
-fx-font-weight: bold;
-fx-font-size: 16px;
}
.context-menu {
-fx-background-color: #333333 !important;
-fx-text-fill: #ffffff !important;
-fx-font-family: "Courier New" !important;
-fx-font-weight: bold !important;
-fx-background-radius: 8 !important;
-fx-border-radius: 8 !important;
-fx-border-color: #333333 !important;
-fx-color-color: #cccccc !important;
-fx-border-width: 2 !important;
-fx-padding: 5 12 !important;
}
.context-menu .menu-item {
-fx-background-color: #333333 !important;
}
.context-menu .menu-item .label {
-fx-text-fill: #ffffff !important;
-fx-font-size: 13px !important;
}
/*.context-menu .menu-item:focused {*/
/* -fx-background-color: #333333;*/
/*}*/
.context-menu .menu-item:hover .label,
.context-menu .menu-item:focused .label {
/*-fx-text-fill: #b8860b;*/
-fx-font-size: 15px !important;
}
.notebook {
-fx-background-color: #000000;
-fx-background-radius: 23;
-fx-border-radius: 20;
-fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10;
-fx-border-width: 3;
-fx-effect: dropshadow(one-pass-box, rgb(255, 255, 255), 0, 0, 5, 5);
/*-fx-cursor: move;*/
-fx-padding: 0 0 15 0;
}
.notebook:hover {
-fx-border-color: #ffffff;
}
/*.notebook.player-active-turn {*/
/* !* Active turn should only change the shadow to avoid flicker/transparency jumps. *!*/
/* -fx-effect: dropshadow(one-pass-box, rgb(184, 134, 11), 0, 0, 5, 5);*/
/*}*/
.notebook-header {
-fx-background-color: #000000;
-fx-background-radius: 25 25 0 0;
-fx-padding: 8 10;
-fx-cursor: move;
}
.notebook-title {
-fx-text-fill: #ffffff;
-fx-font-size: 18px;
-fx-font-weight: bold;
/*-fx-effect: dropshadow(one-pass-box, rgb(255, 255, 255), 0, 0, 3, 3);*/
}
.notebook-content {
-fx-background-color: transparent;
-fx-padding: 0;
}
.notebook-content > .viewport {
-fx-background-color: #000000;
-fx-background-radius: 0 0 25 25;
}
.scroll-pane {
-fx-background-color: transparent;
-fx-border-color: transparent;
}
.scroll-pane .scroll-bar:vertical,
.scroll-pane .scroll-bar:horizontal {
-fx-background-color: transparent;
}
.scroll-pane .scroll-bar .track {
-fx-background-color: #000000;
-fx-background-radius: 5;
}
.scroll-pane .scroll-bar .thumb {
-fx-background-color: #333333;
-fx-background-radius: 5;
}
.scroll-pane .scroll-bar .thumb:hover {
-fx-background-color: #666666;
}
.notebook-tips-list {
-fx-padding: 10;
-fx-spacing: 10;
-fx-background-color: transparent;
}
.notebook-tips-list .text-flow {
-fx-padding: 12;
-fx-background-color: #000000;
-fx-background-radius: 15;
}
.settings-box {
-fx-background-color: #000000;
-fx-background-radius: 23;
-fx-border-radius: 20;
-fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10;
-fx-border-width: 3;
-fx-effect: dropshadow(one-pass-box, rgb(255, 255, 255), 0, 0, 5, 5);
/*-fx-cursor: move;*/
-fx-padding: 0 0 15 0;
}
.settings-box:hover {
-fx-border-color: #ffffff;
}
/*.settings-box.player-active-turn {*/
/* !* Active turn should only change the shadow to avoid flicker/transparency jumps. *!*/
/* -fx-effect: dropshadow(one-pass-box, rgb(184, 134, 11), 0, 0, 5, 5);*/
/*}*/
.settings-header {
-fx-background-color: transparent;
-fx-background-radius: 20 20 0 0;
-fx-padding: 10 15;
-fx-cursor: move;
-fx-border-width: 0 0 1 0;
}
.settings-label {
-fx-text-fill: #ffffff;
-fx-font-size: 18px;
-fx-font-weight: bold;
/*-fx-effect: dropshadow(one-pass-box, rgb(255, 255, 255), 0, 0, 3, 3);*/
}
.settings-content {
-fx-padding: 15 20 20 20;
-fx-spacing: 12;
}
.settings-content {
-fx-background-color: transparent;
-fx-text-fill: #ffffff;
-fx-font-size: 13px;
}
.radio-button {
-fx-text-fill: #ffffff;
-fx-font-size: 13px;
-fx-padding: 6 8;
-fx-background-radius: 10;
}
.radio-button:hover {
-fx-font-size: 15px;
}
.radio-button:selected {
-fx-text-fill: #b8860b;
}
.radio-button .radio {
-fx-background-color: #b8860b;
/*-fx-border-width: 3;*/
/*-fx-border-color: #b8860b;*/
/*-fx-border-radius: 50%;*/
}
.radio-button:selected .radio {
-fx-background-color: #b8860b;
-fx-effect: dropshadow(gaussian, rgba(184,134,11,0.8), 6, 0.3, 0, 0);
}
.settings-box {
-fx-background-insets: 0;
}
.settings-content {
-fx-background-color: transparent;
}
/*chat box css*/
.chat-box {
-fx-background-color: #000000 !important;
-fx-background-radius: 58;
-fx-border-radius: 50;
-fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10 !important;
-fx-border-width: 12;
-fx-padding: 20;
-fx-effect: dropshadow(one-pass-box, rgba(255, 255, 255), 0, 0, 15, 15) !important;
}
.tab-chat-box {
-fx-background-color: #000000 !important;
-fx-background-radius: 23 !important;
-fx-border-radius: 20 !important;
-fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10 !important;
-fx-border-width: 3 !important;
-fx-padding: 20;
}
.chat-header {
-fx-text-fill: #ffffff !important;
-fx-font-size: 22px;
-fx-font-weight: bold;
-fx-padding: 0 0 10 0;
-fx-effect: dropshadow(one-pass-box, #000000, 0, 0, 0, 0) !important;
}
.chat-separator {
-fx-background-color: #ffffff !important;
-fx-min-height: 4px;
-fx-effect: dropshadow(one-pass-box, #000000, 0, 0, 0, 0) !important;
}
.tab-pane .tab-header-area .tab-header-background {
-fx-opacity: 0;
-fx-background-color: #000000 !important;
-fx-background-radius: 18 !important;
-fx-border-radius: 10 !important;
-fx-border-color: #000000 !important;
-fx-border-width: 5;
-fx-padding: 10;
-fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0), 0, 0, 0, 0) !important;
-fx-min-height: 10;
}
.tab-pane .tab {
-fx-background-color: #000000 !important;
-fx-background-radius: 18 !important;
-fx-border-radius: 10 !important;
-fx-border-color: #000000 !important;
-fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0), 0, 0, 0, 0) !important;
-fx-border-width: 5;
-fx-padding: 6 15;
}
.tab-pane .tab:selected {
-fx-background-color: #000000 !important;
-fx-background-radius: 18 !important;
-fx-border-radius: 10 !important;
-fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10 !important;
-fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0), 0, 0, 0, 0) !important;
-fx-border-width: 3 !important;
-fx-padding: 6 15;
}
.tab-pane .tab .tab-close-button {
-fx-text-fill: #ffffff;
}
.tab-pane .tab-header-area .headers-region {
-fx-background-color: #000000 !important;
}
.tab .tab-label {
-fx-alignment: CENTER;
-fx-text-fill: #ffffff;
-fx-font-size: 12px;
-fx-font-weight: bold;
}
.tab:selected .tab-label {
-fx-alignment: CENTER;
-fx-text-fill: #ffffff;
}
.menu-button {
-fx-background-color: #000000 !important;
-fx-border-color: #333333 !important;
-fx-text-fill: #ffffff !important;
-fx-background-radius: 12 !important;
-fx-border-radius: 12 !important;
-fx-font-family: "Courier New" !important;
-fx-font-weight: bold !important;
-fx-border-width: 2 !important;
-fx-padding: 6 15 !important;
-fx-cursor: hand !important;
}
.menu-button .label {
-fx-text-fill: #ffffff !important;
}
.menu-button:hover {
-fx-translate-y: -3 !important;
-fx-effect: dropshadow(one-pass-box, rgba(255, 255, 255, 0.8), 0, 0, 3, 3) !important;
}
.menu-button .menu-item {
-fx-background-color: #333333 !important;
-fx-border-color: #333333 !important;
-fx-text-fill: #ffffff;
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,846 @@
/*main box css*/
.root {
-fx-background-color: #000000;
-fx-font-family: "Monospaced", "Courier New";
}
.background {
-fx-background-color: #000000;
-fx-background-image: url("/images/glass-walpaper-casono.png");
-fx-background-size: cover;
/* Centers the image and prevents it from repeating in a tile-like pattern. */
-fx-background-position: center center;
-fx-background-repeat: no-repeat;
}
.info-text {
-fx-text-fill: #ffffff;
-fx-font-size: 16px;
}
.casino-table {
-fx-background-color:
linear-gradient(to bottom right,
rgba(255, 255, 255, 0.18),
rgba(255, 255, 255, 0.08));
-fx-background-radius: 210;
-fx-border-radius: 200;
-fx-border-color: rgba(255, 255, 255, 0.25);
-fx-border-width: 12;
/*-fx-min-height: 60vh;*/
/*-fx-max-height: 60vh;*/
/*-fx-min-width: 90vh;*/
/*-fx-max-width: 90vh;*/
-fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0, 0.30), 12, 0, 0, 4);
}
.dealer-box {
-fx-background-color:
linear-gradient(to bottom right,
rgba(255, 255, 255, 0.38),
rgba(255, 255, 255, 0.18));
-fx-border-color: rgba(255, 255, 255, 0.40);
-fx-border-width: 3;
-fx-background-radius: 28;
-fx-border-radius: 0;
-fx-min-width: 70;
-fx-min-height: 100;
-fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0, 0.35), 10, 0, 0, 3);
}
.table-title {
-fx-text-fill: #ffffff;
-fx-font-size: 28px;
-fx-font-weight: bold;
/*-fx-effect: dropshadow(one-pass-box, rgba(255, 255, 255), 0, 0, 3, 3);*/
}
.taskbar {
-fx-background-color:
linear-gradient(to bottom right,
rgba(255, 255, 255, 0.38),
rgba(255, 255, 255, 0.18));
-fx-background-radius: 28;
-fx-border-radius: 28;
-fx-border-color: rgba(255, 255, 255, 0.40);
-fx-border-width: 3;
-fx-padding: 14 30;
-fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0, 0.30), 10, 0, 0, 3);
-fx-cursor: move;
}
.taskbar:hover {
-fx-background-color:
linear-gradient(to bottom right,
rgba(255, 255, 255, 0.44),
rgba(255, 255, 255, 0.22));
-fx-border-color:
rgba(255, 255, 255, 0.55);
}
.taskbar.player-active-turn {
/* Active turn should only change the shadow to avoid flicker/transparency jumps. */
-fx-border-color: rgba(184, 134, 11, 0.40);
-fx-effect: dropshadow(one-pass-box, rgba(184, 134, 11, 0.30), 10, 0, 0, 3);
}
.black-input-field {
-fx-background-color:
linear-gradient(to bottom right,
rgba(0, 0, 0, 0.38),
rgba(0, 0, 0, 0.18)) !important;
-fx-text-fill: #ffffff !important;
-fx-font-family: "Courier New" !important;
-fx-font-weight: bold !important;
-fx-background-radius: 8 !important;
-fx-border-radius: 8 !important;
-fx-border-color: rgba(255, 255, 255, 0.40) !important;
-fx-border-width: 2 !important;
-fx-padding: 5 12 !important;
}
.black-input-field:hover {
-fx-background-color:
linear-gradient(to bottom right,
rgba(0, 0, 0, 0.44),
rgba(0, 0, 0, 0.22)) !important;
}
.black-input-field:focused {
-fx-border-color: rgba(255, 255, 255, 0.55) !important;
/*-fx-background-color: #000000;*/
}
.gray-input-field {
-fx-background-color:
linear-gradient(to bottom right,
rgba(0, 0, 0, 0.38),
rgba(0, 0, 0, 0.18)) !important;
-fx-text-fill: #ffffff !important;
-fx-font-family: "Courier New" !important;
-fx-font-weight: bold !important;
-fx-background-radius: 8 !important;
-fx-border-radius: 8 !important;
-fx-border-color: rgba(255, 255, 255, 0.40) !important;
/*-fx-color-color: #000000;*/
-fx-border-width: 2 !important;
-fx-padding: 5 12 !important;
}
.gray-input-field:hover {
-fx-effect: dropshadow(one-pass-box, rgba(255, 255, 255, 0.25), 10, 0, 0, 3) !important;
}
.gray-input-field:focused {
-fx-border-color: rgba(255, 255, 255, 0.55) !important;
/*-fx-background-color: #333333;*/
}
.yellow-input-field {
-fx-background-color:
linear-gradient(to bottom right,
rgba(184, 134, 11, 0.38),
rgba(184, 134, 11, 0.18)) !important;
-fx-text-fill: #ffffff !important;
-fx-prompt-text-fill: #ffffff !important;
-fx-font-family: "Courier New" !important;
-fx-font-weight: bold !important;
-fx-background-radius: 8 !important;
-fx-border-radius: 8 !important;
-fx-border-color: rgba(255, 215, 0, 0.40) !important;
-fx-border-width: 2 !important;
-fx-padding: 5 12 !important;
}
.yellow-input-field:hover {
-fx-effect: dropshadow(one-pass-box, rgba(255, 255, 255, 0.25), 10, 0, 0, 3) !important;
}
/*.yellow-input-field:focused {*/
/* -fx-border-color: #ffd700;*/
/* -fx-background-color: #b8860b;*/
/*}*/
.red-input-field {
-fx-background-color:
linear-gradient(to bottom right,
rgba(139, 0, 0, 0.38),
rgba(139, 0, 0, 0.18)) !important;
-fx-text-fill: #ffffff !important;
-fx-prompt-text-fill: #ffffff !important;
-fx-font-family: "Courier New" !important;
-fx-font-weight: bold !important;
-fx-background-radius: 8 !important;
-fx-border-radius: 8 !important;
-fx-border-color: rgba(255, 68, 68, 0.40) !important;
-fx-border-width: 2 !important;
-fx-padding: 5 12 !important;
}
.red-input-field:hover {
-fx-effect: dropshadow(one-pass-box, rgba(255, 255, 255, 0.25), 10, 0, 0, 3) !important;
}
/*.red-input-field:focused {*/
/* -fx-border-color: #ff4444;*/
/* -fx-background-color: #8b0000;*/
/*}*/
.yellow-button {
-fx-background-color:
linear-gradient(to bottom right,
rgba(184, 134, 11, 0.38),
rgba(184, 134, 11, 0.18)) !important;
-fx-border-color: rgba(255, 215, 0, 0.40) !important;
-fx-text-fill: #ffffff !important;
}
.red-button {
-fx-background-color:
linear-gradient(to bottom right,
rgba(139, 0, 0, 0.38),
rgba(139, 0, 0, 0.18)) !important;
-fx-border-color: rgba(255, 68, 68, 0.40) !important;
-fx-text-fill: #ffffff !important;
}
.gray-button {
-fx-background-color:
linear-gradient(to bottom right,
rgba(0, 0, 0, 0.38),
rgba(0, 0, 0, 0.18)) !important;
-fx-border-color: rgba(51, 51, 51, 0.40) !important;
-fx-text-fill: #ffffff !important;
}
.gray-button .label {
-fx-text-fill: #ffffff !important;
}
.green-button {
-fx-background-color:
linear-gradient(to bottom right,
rgba(0, 0, 0, 0.38),
rgba(0, 0, 0, 0.18)) !important;
-fx-border-color: rgba(51, 51, 51, 0.40) !important;
-fx-text-fill: #ffffff !important;
}
.gray-button,
.yellow-button,
.red-button,
.green-button {
-fx-background-radius: 12 !important;
-fx-border-radius: 12 !important;
-fx-font-family: "Courier New" !important;
-fx-font-weight: bold !important;
-fx-border-width: 2 !important;
-fx-padding: 6 15 !important;
-fx-cursor: hand !important;
}
.gray-button:hover,
.yellow-button:hover,
.red-button:hover,
.green-button:hover {
-fx-effect: dropshadow(one-pass-box, rgba(255, 255, 255, 0.25), 10, 0, 0, 3) !important;
}
.community-cards-box {
-fx-alignment: center !important;
-fx-spacing: 12 !important;
-fx-padding: 15 !important;
-fx-background-color: transparent !important;
}
.community-card {
-fx-effect: dropshadow(one-pass-box, rgba(255, 255, 255, 0.25), 10, 0, 0, 3) !important;
-fx-alignment: center;
-fx-spacing: 12;
-fx-padding: 20;
-fx-background-color: linear-gradient(to bottom, #0b3d2e, #06261d);
-fx-background-radius: 16;
-fx-scale-x: 1;
-fx-scale-y: 1;
-fx-translate-y: 0;
-fx-opacity: 1;
}
.community-card:hover {}
.player-card {
-fx-effect: dropshadow(one-pass-box, rgba(255, 255, 255, 0.25), 10, 0, 0, 3) !important;
-fx-alignment: center;
-fx-spacing: 12;
-fx-padding: 20;
-fx-background-color: linear-gradient(to bottom, #0b3d2e, #06261d);
-fx-background-radius: 16;
-fx-scale-x: 1;
-fx-scale-y: 1;
-fx-translate-y: 0;
-fx-opacity: 1;
}
.player-card:hover {}
.player-card.player-cards-active-turn {
-fx-effect: dropshadow(one-pass-box, rgba(184, 134, 11, 0.30), 10, 0, 0, 3);
}
.dealer-box.player-cards-active-turn {
-fx-effect: dropshadow(one-pass-box, rgba(184, 134, 11, 0.30), 10, 0, 0, 3);
}
/*.dealer-box:hover {*/
/* -fx-translate-y: -3;*/
/*}*/
.chips-icon {
-fx-effect: dropshadow(one-pass-box, rgba(255, 255, 255, 0.25), 10, 0, 0, 3) !important;
}
/*.chips-icon:hover {*/
/* -fx-translate-y: -3;*/
/*}*/
.dealer-icon {
-fx-effect: dropshadow(one-pass-box, rgba(255, 255, 255, 0.25), 10, 0, 0, 3) !important;
}
.player-cards-box {
-fx-padding: 10;
}
/*.player-cards-box.player-cards-active-turn {*/
/* -fx-effect: dropshadow(one-pass-box, rgb(184, 134, 11), 0, 0, 5, 5);*/
/*}*/
.money-value {
-fx-text-fill: #ffffff;
-fx-font-size: 18px;
-fx-font-weight: bold;
/*-fx-effect: dropshadow(one-pass-box, #000000, 0, 0, 2, 2);*/
}
.pot-title {
-fx-text-fill: #ffffff;
-fx-font-size: 14px;
-fx-font-weight: bold;
/*-fx-effect: dropshadow(one-pass-box, #000000, 0, 0, 3, 3);*/
}
.pot-box {
-fx-padding: 5;
}
.player-status-pane {
-fx-background-color: transparent;
-fx-background-radius: 15;
-fx-border-radius: 15;
/* -fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10;
-fx-border-width: 2;
-fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 0.5), 10, 0, 0, 4);*/
}
.player-status-pane:hover {}
.status-inner-box-top {
-fx-background-color:
linear-gradient(to bottom right,
rgba(0, 0, 0, 0.38),
rgba(0, 0, 0, 0.18)) !important;
-fx-background-radius: 19;
-fx-border-radius: 15;
-fx-padding: 5 10;
-fx-border-color: rgba(255, 255, 255, 0.40);
-fx-border-width: 3;
-fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0, 0.30), 10, 0, 0, 3);
}
.status-inner-box-midle {
-fx-background-color: rgba(13, 158, 59, 0);
}
.status-inner-box-bottom {
-fx-background-color:
linear-gradient(to bottom right,
rgba(0, 0, 0, 0.38),
rgba(0, 0, 0, 0.18)) !important;
-fx-background-radius: 19;
-fx-border-radius: 15;
-fx-padding: 5 10;
-fx-border-color: rgba(255, 255, 255, 0.40);
-fx-border-width: 3;
-fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0, 0.30), 10, 0, 0, 3);
}
.status-inner-box-top.player-status-active-turn {
-fx-effect: dropshadow(one-pass-box, rgba(184, 134, 11, 0.30), 10, 0, 0, 3);
}
.status-inner-box-bottom.player-status-active-turn {
-fx-effect: dropshadow(one-pass-box, rgba(184, 134, 11, 0.30), 10, 0, 0, 3);
}
.status-label-small {
-fx-text-fill: #ffffff;
-fx-font-size: 18px;
-fx-font-weight: bold;
/*-fx-effect: dropshadow(one-pass-box, #000000, 0, 0, 2, 2);*/
}
.status-value-text {
-fx-text-fill: #ffffff;
-fx-font-size: 18px;
-fx-font-weight: bold;
/*-fx-effect: dropshadow(one-pass-box, #000000, 0, 0, 2, 2);*/
}
.status-value-money {
-fx-text-fill: #ffffff;
-fx-font-size: 18px;
-fx-font-weight: bold;
/*-fx-effect: dropshadow(one-pass-box, #000000, 0, 0, 2, 2);*/
}
.status-circle {
-fx-background-color:
linear-gradient(to bottom right,
rgba(0, 0, 0, 0.38),
rgba(0, 0, 0, 0.18)) !important;
-fx-background-radius: 50;
-fx-border-color: rgba(255, 255, 255, 0.40);
-fx-border-width: 1;
-fx-border-radius: 50;
-fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0, 0.30), 10, 0, 0, 3);
}
.browser-root {
-fx-background-image: url("/images/background.png");
-fx-background-size: cover;
-fx-background-position: center;
-fx-background-color: #0b2d15;
}
.browser-web-view {
-fx-background-color: rgb(13, 158, 59);
-fx-background-radius: 23;
-fx-border-radius: 20;
-fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10;
-fx-border-width: 3;
-fx-padding: 0;
-fx-cursor: default;
-fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0, 0.30), 10, 0, 0, 3);
}
.web-view {
-fx-background-color: transparent;
}
.taskbar-browser {
-fx-background-color: rgb(13, 158, 59);
-fx-background-radius: 23;
-fx-border-radius: 20;
-fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10;
-fx-border-width: 3;
-fx-padding: 10 25;
-fx-cursor: default;
-fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0, 0.30), 10, 0, 0, 3);
}
/*.taskbar-browser:hover {*/
/* -fx-border-color: #ffffff;*/
/*}*/
.security-label {
-fx-text-fill: #000000;
-fx-font-weight: bold;
-fx-font-size: 16px;
}
.context-menu {
-fx-background-color:
linear-gradient(to bottom right,
rgba(51, 51, 51, 0.38),
rgba(51, 51, 51, 0.18)) !important;
-fx-text-fill: #ffffff !important;
-fx-font-family: "Courier New" !important;
-fx-font-weight: bold !important;
-fx-background-radius: 8 !important;
-fx-border-radius: 8 !important;
-fx-border-color: rgba(51, 51, 51, 0.40) !important;
-fx-color-color: #cccccc !important;
-fx-border-width: 2 !important;
-fx-padding: 5 12 !important;
}
.context-menu .menu-item {
-fx-background-color:
linear-gradient(to bottom right,
rgba(51, 51, 51, 0.38),
rgba(51, 51, 51, 0.18)) !important;
}
.context-menu .menu-item .label {
-fx-text-fill: #ffffff !important;
-fx-font-size: 13px !important;
}
/*.context-menu .menu-item:focused {*/
/* -fx-background-color: #333333;*/
/*}*/
.context-menu .menu-item:hover .label,
.context-menu .menu-item:focused .label {
/*-fx-text-fill: #b8860b;*/
-fx-font-size: 15px !important;
}
.notebook {
-fx-background-color:
linear-gradient(to bottom right,
rgba(0, 0, 0, 0.38),
rgba(0, 0, 0, 0.18));
-fx-background-radius: 23;
-fx-border-radius: 20;
-fx-border-color: rgba(255, 255, 255, 0.40);
-fx-border-width: 3;
-fx-effect: dropshadow(one-pass-box, rgba(255, 255, 255, 0.25), 10, 0, 0, 3) !important;
/*-fx-cursor: move;*/
-fx-padding: 0 0 15 0;
}
.notebook:hover {
-fx-background-color:
linear-gradient(to bottom right,
rgba(0, 0, 0, 0.44),
rgba(0, 0, 0, 0.22));
-fx-border-color:
rgba(255, 255, 255, 0.55);
}
/*.notebook.player-active-turn {*/
/* !* active turn should only change the shadow to avoid flicker/transparency jumps. *!*/
/* -fx-border-color:*/
/* rgba(184, 134, 11, 0.55);*/
/* -fx-effect:*/
/* dropshadow(*/
/* gaussian,*/
/* rgba(184, 134, 11, 0.45),*/
/* 40,*/
/* 0.2,*/
/* 0,*/
/* 12*/
/* );*/
/*}*/
.notebook-header {
-fx-background-color: transparent !important;
-fx-background-radius: 25 25 0 0;
-fx-padding: 8 10;
-fx-cursor: move;
}
.notebook-title {
-fx-text-fill: #ffffff;
-fx-background-color: transparent !important;
-fx-font-size: 18px;
-fx-font-weight: bold;
/*-fx-effect: dropshadow(one-pass-box, rgb(255, 255, 255), 0, 0, 3, 3);*/
}
.notebook-content {
-fx-background-color: transparent;
-fx-padding: 0;
}
.notebook-content>.viewport {
-fx-background-color: transparent !important;
-fx-background-radius: 0 0 25 25;
}
.scroll-pane {
-fx-background-color: transparent;
-fx-border-color: transparent;
}
.scroll-pane .scroll-bar:vertical,
.scroll-pane .scroll-bar:horizontal {
-fx-background-color: transparent;
}
.scroll-pane .scroll-bar .track {
-fx-background-color: transparent !important;
-fx-background-radius: 5;
}
.scroll-pane .scroll-bar .thumb {
-fx-background-color:
linear-gradient(to bottom right,
rgba(200, 200, 200, 0.38),
rgba(200, 200, 200, 0.18)) !important;
-fx-background-radius: 5;
}
.scroll-pane .scroll-bar .thumb:hover {
-fx-background-color:
linear-gradient(to bottom right,
rgba(255, 255, 255, 0.38),
rgba(255, 255, 255, 0.18)) !important;
}
.notebook-tips-list {
-fx-padding: 10;
-fx-spacing: 10;
-fx-background-color: transparent;
}
.notebook-tips-list .text-flow {
-fx-padding: 12;
-fx-background-color:
linear-gradient(to bottom right,
rgba(0, 0, 0, 0.38),
rgba(0, 0, 0, 0.18));
-fx-background-radius: 15;
}
.settings-box {
-fx-background-color:
linear-gradient(to bottom right,
rgba(0, 0, 0, 0.38),
rgba(0, 0, 0, 0.18));
-fx-background-radius: 23;
-fx-border-radius: 20;
-fx-border-color: rgba(255, 255, 255, 0.40);
-fx-border-width: 3;
-fx-effect: dropshadow(one-pass-box, rgba(255, 255, 255, 0.25), 10, 0, 0, 3) !important;
/*-fx-cursor: move;*/
-fx-padding: 0 0 15 0;
}
.settings-box:hover {
-fx-background-color:
linear-gradient(to bottom right,
rgba(0, 0, 0, 0.44),
rgba(0, 0, 0, 0.22));
-fx-border-color:
rgba(255, 255, 255, 0.55);
}
/*.settings-box.player-active-turn {*/
/* !* Active turn should only change the shadow to avoid flicker/transparency jumps. *!*/
/* -fx-border-color:*/
/* rgba(184, 134, 11, 0.55);*/
/* -fx-effect:*/
/* dropshadow(*/
/* gaussian,*/
/* rgba(184, 134, 11, 0.45),*/
/* 40,*/
/* 0.2,*/
/* 0,*/
/* 12*/
/* );*/
/*}*/
.settings-header {
-fx-background-color: transparent;
-fx-background-radius: 20 20 0 0;
-fx-padding: 10 15;
-fx-cursor: move;
-fx-border-width: 0 0 1 0;
}
.settings-label {
-fx-text-fill: #ffffff;
-fx-font-size: 18px;
-fx-font-weight: bold;
/*-fx-effect: dropshadow(one-pass-box, rgb(255, 255, 255), 0, 0, 3, 3);*/
}
.settings-content {
-fx-padding: 15 20 20 20;
-fx-spacing: 12;
}
.settings-content {
-fx-background-color: transparent;
-fx-text-fill: #ffffff;
-fx-font-size: 13px;
}
.radio-button {
-fx-text-fill: #ffffff;
-fx-font-size: 13px;
-fx-padding: 6 8;
-fx-background-radius: 10;
}
.radio-button:hover {
-fx-font-size: 15px;
}
.radio-button:selected {
-fx-text-fill: #ffffff;
}
.radio-button .radio {
-fx-background-color:
linear-gradient(to bottom right,
rgba(184, 134, 11, 0.38),
rgba(184, 134, 11, 0.18));
/*-fx-border-width: 3;*/
/*-fx-border-color: #b8860b;*/
/*-fx-border-radius: 50%;*/
}
.radio-button:selected .radio {
-fx-background-color:
linear-gradient(to bottom right,
rgba(184, 134, 11, 0.38),
rgba(184, 134, 11, 0.18));
-fx-effect: dropshadow(one-pass-box, rgba(184, 134, 11, 0.30), 10, 0, 0, 3);
}
.settings-box {
-fx-background-insets: 0;
}
.settings-content {
-fx-background-color: transparent;
}
/*chat box css*/
.chat-box {
-fx-background-color:
linear-gradient(to bottom right,
rgba(255, 255, 255, 0.18),
rgba(255, 255, 255, 0.08)) !important;
-fx-background-radius: 58;
-fx-border-radius: 50;
-fx-border-color: rgba(255, 255, 255, 0.25) !important;
-fx-border-width: 12;
-fx-padding: 20;
-fx-effect: dropshadow(one-pass-box, rgba(255, 255, 255, 0.20), 10, 0, 0, 3) !important;
}
.tab-chat-box {
-fx-background-color: transparent !important;
-fx-background-radius: 23 !important;
-fx-border-radius: 20 !important;
-fx-border-color: rgba(255, 255, 255, 0.40) !important;
-fx-border-width: 3 !important;
-fx-padding: 20;
}
.chat-header {
-fx-text-fill: #ffffff !important;
-fx-font-size: 22px;
-fx-font-weight: bold;
-fx-padding: 0 0 10 0;
-fx-effect: dropshadow(one-pass-box, #000000, 0, 0, 0, 0) !important;
}
.chat-separator {
-fx-background-color: transparent !important;
-fx-min-height: 4px;
-fx-effect: dropshadow(one-pass-box, #000000, 0, 0, 0, 0) !important;
}
.tab-header-area .tab-header-background {
-fx-background-color: transparent !important;
-fx-background-radius: 18 !important;
-fx-border-radius: 10 !important;
-fx-border-color: rgba(0, 0, 0, 0) !important;
-fx-border-width: 5;
-fx-padding: 10;
-fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0), 0, 0, 0, 0) !important;
-fx-min-height: 10;
}
.tab-pane .tab {
-fx-background-color: transparent !important;
-fx-background-radius: 18 !important;
-fx-border-radius: 10 !important;
-fx-border-color: rgba(255, 255, 255, 0) !important;
-fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0), 0, 0, 0, 0) !important;
-fx-border-width: 5;
-fx-padding: 6 15;
}
.tab-pane .tab:selected {
-fx-background-color:
linear-gradient(to bottom right,
rgba(0, 0, 0, 0.38),
rgba(0, 0, 0, 0.18)) !important;
-fx-background-radius: 18 !important;
-fx-border-radius: 10 !important;
-fx-border-color: rgba(255, 255, 255, 0.40) !important;
-fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0), 0, 0, 0, 0) !important;
-fx-border-width: 3 !important;
-fx-padding: 6 15;
}
.tab-pane .tab .tab-close-button {
-fx-text-fill: #ffffff !important;
}
.tab-pane .tab-header-area .headers-region {
-fx-background-color: transparent !important;
}
.tab .tab-label {
-fx-alignment: CENTER;
-fx-text-fill: #ffffff;
-fx-font-size: 12px;
-fx-font-weight: bold;
}
.tab:selected .tab-label {
-fx-alignment: CENTER;
-fx-text-fill: #ffffff;
}
.menu-button {
-fx-background-color:
linear-gradient(to bottom right,
rgba(0, 0, 0, 0.38),
rgba(0, 0, 0, 0.18)) !important;
-fx-border-color: rgba(51, 51, 51, 0.40) !important;
-fx-text-fill: #ffffff !important;
-fx-background-radius: 12 !important;
-fx-border-radius: 12 !important;
-fx-font-family: "Courier New" !important;
-fx-font-weight: bold !important;
-fx-border-width: 2 !important;
-fx-padding: 6 15 !important;
-fx-cursor: hand !important;
}
.menu-button .label {
-fx-text-fill: #ffffff !important;
}
.menu-button:hover {
-fx-effect: dropshadow(one-pass-box, rgba(255, 255, 255, 0.25), 10, 0, 0, 3) !important;
}
.menu-button .menu-item {
-fx-background-color:
linear-gradient(to bottom right,
rgba(51, 51, 51, 0.38),
rgba(51, 51, 51, 0.18)) !important;
-fx-border-color: rgba(51, 51, 51, 0.40) !important;
-fx-text-fill: #ffffff;
}
@@ -0,0 +1,613 @@
.root {
-fx-background-color: #000000;
-fx-font-family: "Monospaced", "Courier New"; /* font */
}
.background {
-fx-background-image: url("/images/parquet.png");
-fx-background-size: cover; /* Image fills the entire window */
/* Centers the image and prevents it from repeating in a tile-like pattern. */
-fx-background-position: center center;
-fx-background-repeat: no-repeat;
}
.info-text {
-fx-text-fill: #ffffff;
-fx-font-size: 16px;
}
.casino-table {
-fx-background-color: #0d9e3b;
-fx-background-radius: 210;
-fx-border-radius: 200;
-fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10;
-fx-border-width: 12;
/*-fx-min-height: 60vh;*/
/*-fx-max-height: 60vh;*/
/*-fx-min-width: 90vh;*/
/*-fx-max-width: 90vh;*/
-fx-effect: dropshadow(one-pass-box, rgba(0,0,0,0.8), 0, 0, 15, 15);
}
.dealer-box {
-fx-background-color: #ffffff;
-fx-border-color: #000000;
-fx-border-width: 3;
-fx-background-radius: 0;
-fx-border-radius: 0;
-fx-min-width: 70;
-fx-min-height: 100;
-fx-effect: dropshadow(one-pass-box, rgba(0,0,0,0.8), 0, 0, 4, 4);
}
.table-title {
-fx-text-fill: #ffffff;
-fx-font-size: 28px;
-fx-font-weight: bold;
-fx-effect: dropshadow(one-pass-box, #000000, 0, 0, 3, 3);
}
.taskbar {
-fx-background-color: rgb(13, 158, 59);
-fx-background-radius: 23;
-fx-border-radius: 20;
-fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10;
-fx-border-width: 3;
-fx-padding: 10 25;
-fx-cursor: move;
-fx-effect: dropshadow(one-pass-box, rgba(0,0,0,0.8), 0, 0, 5, 5);
}
.taskbar:hover {
-fx-border-color: #ffffff;
}
.taskbar.player-active-turn {
/* Active turn should only change the shadow to avoid flicker/transparency jumps. */
-fx-effect: dropshadow(one-pass-box, rgb(184, 134, 11), 0, 0, 5, 5);
}
.black-input-field {
-fx-background-color: #1a1a1a;
-fx-text-fill: #ffffff;
-fx-font-family: "Courier New";
-fx-font-weight: bold;
-fx-background-radius: 8;
-fx-border-radius: 8;
-fx-border-color: #444444;
-fx-border-width: 2;
-fx-padding: 5 12;
}
.black-input-field:hover {
-fx-translate-y: -3;
-fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0, 0.8), 0, 0, 3, 3);
}
.black-input-field:focused {
-fx-border-color: #ffffff;
-fx-background-color: #000000;
}
.gray-input-field {
-fx-background-color: #333333;
-fx-text-fill: #ffffff;
-fx-font-family: "Courier New";
-fx-font-weight: bold;
-fx-background-radius: 8;
-fx-border-radius: 8;
-fx-border-color: #666666;
-fx-color-color: #cccccc;
-fx-border-width: 2;
-fx-padding: 5 12;
}
.gray-input-field:hover {
-fx-translate-y: -3;
-fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0, 0.8), 0, 0, 3, 3);
}
.gray-input-field:focused {
-fx-border-color: #ffffff;
-fx-background-color: #333333;
}
.yellow-input-field {
-fx-background-color: #b8860b;
-fx-text-fill: #ffffff;
-fx-prompt-text-fill: #ffffff;
-fx-font-family: "Courier New";
-fx-font-weight: bold;
-fx-background-radius: 8;
-fx-border-radius: 8;
-fx-border-color: #ffd700;
-fx-border-width: 2;
-fx-padding: 5 12;
}
.yellow-input-field:hover {
-fx-translate-y: -3;
-fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0, 0.8), 0, 0, 3, 3);
}
.yellow-input-field:focused {
-fx-border-color: #ffffff;
-fx-background-color: #b8860b;
}
.red-input-field {
-fx-background-color: #8b0000;
-fx-text-fill: #ffffff;
-fx-prompt-text-fill: #ffffff;
-fx-font-family: "Courier New";
-fx-font-weight: bold;
-fx-background-radius: 8;
-fx-border-radius: 8;
-fx-border-color: #ff4444;
-fx-border-width: 2;
-fx-padding: 5 12;
}
.red-input-field:hover {
-fx-translate-y: -3;
-fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0, 0.8), 0, 0, 3, 3);
}
.red-input-field:focused {
-fx-border-color: #ffffff;
-fx-background-color: #8b0000;
}
.yellow-button {
-fx-background-color: #b8860b;
-fx-border-color: #ffd700;
-fx-text-fill: #ffffff;
}
.red-button {
-fx-background-color: #8b0000;
-fx-border-color: #ff4444;
-fx-text-fill: #ffffff;
}
.gray-button {
-fx-background-color: #333333;
-fx-border-color: #666666;
/*-fx-text-fill: #cccccc;*/
-fx-text-fill: #ffffff;
}
.gray-button .label {
-fx-text-fill: #ffffff;
}
.green-button {
-fx-background-color: #144523;
-fx-border-color: #0d9e3b;
-fx-text-fill: #ffffff;
}
.gray-button, .yellow-button, .red-button, .green-button {
-fx-background-radius: 12;
-fx-border-radius: 12;
-fx-font-family: "Courier New";
-fx-font-weight: bold;
-fx-border-width: 2;
-fx-padding: 6 15;
-fx-cursor: hand;
}
.gray-button:hover, .yellow-button:hover, .red-button:hover, .green-button:hover {
-fx-translate-y: -3;
-fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0, 0.8), 0, 0, 3, 3);
}
.community-cards-box {
-fx-alignment: center;
-fx-spacing: 12;
-fx-padding: 15;
-fx-background-color: transparent;
}
.community-card {
-fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0, 0.8), 0, 0, 3, 3);
-fx-alignment: center;
-fx-spacing: 12;
-fx-padding: 20;
-fx-background-color: linear-gradient(to bottom, #0b3d2e, #06261d);
-fx-background-radius: 16;
-fx-scale-x: 1;
-fx-scale-y: 1;
-fx-translate-y: 0;
-fx-opacity: 1;
}
/*.community-card:hover {*/
/* -fx-translate-y: -3;*/
/*}*/
.player-card {
-fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0, 0.8), 0, 0, 3, 3);
-fx-alignment: center;
-fx-spacing: 12;
-fx-padding: 20;
-fx-background-color: linear-gradient(to bottom, #0b3d2e, #06261d);
-fx-background-radius: 16;
-fx-scale-x: 1;
-fx-scale-y: 1;
-fx-translate-y: 0;
-fx-opacity: 1;
}
/*.player-card:hover {*/
/* -fx-translate-y: -3;*/
/*}*/
.player-card.player-cards-active-turn {
-fx-effect: dropshadow(one-pass-box, rgb(184, 134, 11), 0, 0, 5, 5);
}
.dealer-box.player-cards-active-turn {
-fx-effect: dropshadow(one-pass-box, rgb(184, 134, 11), 0, 0, 5, 5);
}
/*.dealer-box:hover {*/
/* -fx-translate-y: -3;*/
/*}*/
.chips-icon {
-fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0, 0.8), 0, 0, 3, 3);
}
/*.chips-icon:hover {*/
/* -fx-translate-y: -3;*/
/*}*/
.dealer-icon {
-fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0, 0.8), 0, 0, 3, 3);
}
.player-cards-box {
-fx-padding: 10;
}
/*.player-cards-box.player-cards-active-turn {*/
/* -fx-effect: dropshadow(one-pass-box, rgb(184, 134, 11), 0, 0, 5, 5);*/
/*}*/
.money-value {
-fx-text-fill: #ffffff;
-fx-font-size: 18px;
-fx-font-weight: bold;
-fx-effect: dropshadow(one-pass-box, #000000, 0, 0, 2, 2);
}
.pot-title {
-fx-text-fill: #ffffff;
-fx-font-size: 14px;
-fx-font-weight: bold;
-fx-effect: dropshadow(one-pass-box, #000000, 0, 0, 3, 3);
}
.pot-box {
-fx-padding: 5;
}
.player-status-pane {
-fx-background-color: rgba(13, 158, 59, 0);
-fx-background-radius: 15;
-fx-border-radius: 15;
/* -fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10;
-fx-border-width: 2;
-fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 0.5), 10, 0, 0, 4);*/
}
.player-status-pane:hover {
-fx-translate-y: -3;
}
.status-inner-box-top {
-fx-background-color: rgba(13, 158, 59);
-fx-background-radius: 19;
-fx-border-radius: 15;
-fx-padding: 5 10;
-fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10;
-fx-border-width: 3;
-fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0, 0.8), 0, 0, 3, 3);
}
.status-inner-box-midle {
-fx-background-color: rgba(13, 158, 59, 0);
}
.status-inner-box-bottom {
-fx-background-color: rgba(13, 158, 59);
-fx-background-radius: 19;
-fx-border-radius: 15;
-fx-padding: 5 10;
-fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10;
-fx-border-width: 3;
-fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0, 0.8), 0, 0, 3, 3);
}
.status-inner-box-top.player-status-active-turn {
-fx-effect: dropshadow(one-pass-box, rgb(184, 134, 11), 0, 0, 5, 5);
}
.status-inner-box-bottom.player-status-active-turn {
-fx-effect: dropshadow(one-pass-box, rgb(184, 134, 11), 0, 0, 5, 5);
}
.status-label-small {
-fx-text-fill: #ffffff;
-fx-font-size: 18px;
-fx-font-weight: bold;
-fx-effect: dropshadow(one-pass-box, #000000, 0, 0, 2, 2);
}
.status-value-text {
-fx-text-fill: #ffffff;
-fx-font-size: 18px;
-fx-font-weight: bold;
-fx-effect: dropshadow(one-pass-box, #000000, 0, 0, 2, 2);
}
.status-value-money {
-fx-text-fill: #ffffff;
-fx-font-size: 18px;
-fx-font-weight: bold;
-fx-effect: dropshadow(one-pass-box, #000000, 0, 0, 2, 2);
}
.status-circle {
-fx-background-color: #000000;
-fx-background-radius: 50;
-fx-border-color: #ffffff;
-fx-border-width: 1;
-fx-border-radius: 50;
-fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0, 0.8), 0, 0, 3, 3);
}
.browser-root {
-fx-background-image: url("/images/background.png");
-fx-background-size: cover;
-fx-background-position: center;
-fx-background-color: #0b2d15;
}
.browser-web-view {
-fx-background-color: rgb(13, 158, 59);
-fx-background-radius: 23;
-fx-border-radius: 20;
-fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10;
-fx-border-width: 3;
-fx-padding: 0;
-fx-cursor: default;
-fx-effect: dropshadow(one-pass-box, rgba(0,0,0,0.8), 0, 0, 5, 5);
}
.web-view {
-fx-background-color: transparent;
}
.taskbar-browser {
-fx-background-color: rgb(13, 158, 59);
-fx-background-radius: 23;
-fx-border-radius: 20;
-fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10;
-fx-border-width: 3;
-fx-padding: 10 25;
-fx-cursor: default;
-fx-effect: dropshadow(one-pass-box, rgba(0,0,0,0.8), 0, 0, 5, 5);
}
/*.taskbar-browser:hover {*/
/* -fx-border-color: #ffffff;*/
/*}*/
.security-label {
-fx-text-fill: #000000;
-fx-font-weight: bold;
-fx-font-size: 16px;
}
.context-menu {
-fx-background-color: #333333;
-fx-text-fill: #ffffff;
-fx-font-family: "Courier New";
-fx-font-weight: bold;
-fx-background-radius: 8;
-fx-border-radius: 8;
-fx-border-color: #333333;
-fx-color-color: #cccccc;
-fx-border-width: 2;
-fx-padding: 5 12;
}
.context-menu .menu-item {
-fx-background-color: #333333;
}
.context-menu .menu-item .label {
-fx-text-fill: #ffffff;
-fx-font-size: 13px;
}
/*.context-menu .menu-item:focused {*/
/* -fx-background-color: #333333;*/
/*}*/
.context-menu .menu-item:hover .label,
.context-menu .menu-item:focused .label {
/*-fx-text-fill: #b8860b;*/
-fx-font-size: 15px;
}
.notebook {
-fx-background-color: rgb(13, 158, 59);
-fx-background-radius: 23;
-fx-border-radius: 20;
-fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10;
-fx-border-width: 3;
-fx-effect: dropshadow(one-pass-box, #000000, 0, 0, 5, 5);
/*-fx-cursor: move;*/
-fx-padding: 0 0 15 0;
}
.notebook:hover {
-fx-border-color: #ffffff;
}
/*.notebook.player-active-turn {*/
/* !* Active turn should only change the shadow to avoid flicker/transparency jumps. *!*/
/* -fx-effect: dropshadow(one-pass-box, rgb(184, 134, 11), 0, 0, 5, 5);*/
/*}*/
.notebook-header {
-fx-background-color: rgb(13, 158, 59);
-fx-background-radius: 25 25 0 0;
-fx-padding: 8 10;
-fx-cursor: move;
}
.notebook-title {
-fx-text-fill: #ffffff;
-fx-font-size: 18px;
-fx-font-weight: bold;
-fx-effect: dropshadow(one-pass-box, #000000, 0, 0, 3, 3);
}
.notebook-content {
-fx-background-color: transparent;
-fx-padding: 0;
}
.notebook-content > .viewport {
-fx-background-color: rgb(13, 158, 59);
-fx-background-radius: 0 0 25 25;
}
.scroll-pane {
-fx-background-color: transparent;
-fx-border-color: transparent;
}
.scroll-pane .scroll-bar:vertical,
.scroll-pane .scroll-bar:horizontal {
-fx-background-color: transparent;
}
.scroll-pane .scroll-bar .track {
/*-fx-background-color: #5c3d10;*/
-fx-background-color: transparent;
-fx-background-radius: 5;
}
.scroll-pane .scroll-bar .thumb {
-fx-background-color: #3d260a;
-fx-background-radius: 5;
}
.scroll-pane .scroll-bar .thumb:hover {
-fx-background-color: #2a1b07;
}
.notebook-tips-list {
-fx-padding: 10;
-fx-spacing: 10;
-fx-background-color: transparent;
}
.notebook-tips-list .text-flow {
-fx-padding: 12;
-fx-background-color: rgb(13, 158, 59);
-fx-background-radius: 15;
}
.settings-box {
-fx-background-color: rgb(13, 158, 59);
-fx-background-radius: 23;
-fx-border-radius: 20;
-fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10;
-fx-border-width: 3;
-fx-effect: dropshadow(one-pass-box, #000000, 0, 0, 5, 5);
/*-fx-cursor: move;*/
-fx-padding: 0 0 15 0;
}
.settings-box:hover {
-fx-border-color: #ffffff;
}
/*.settings-box.player-active-turn {*/
/* !* Active turn should only change the shadow to avoid flicker/transparency jumps. *!*/
/* -fx-effect: dropshadow(one-pass-box, rgb(184, 134, 11), 0, 0, 5, 5);*/
/*}*/
.settings-header {
-fx-background-color: transparent;
-fx-background-radius: 20 20 0 0;
-fx-padding: 10 15;
-fx-cursor: move;
-fx-border-width: 0 0 1 0;
}
.settings-label {
-fx-text-fill: #ffffff;
-fx-font-size: 18px;
-fx-font-weight: bold;
-fx-effect: dropshadow(one-pass-box, #000000, 0, 0, 3, 3);
}
.settings-content {
-fx-padding: 15 20 20 20;
-fx-spacing: 12;
}
.settings-content {
-fx-background-color: transparent;
-fx-text-fill: #e8e8e8;
-fx-font-size: 13px;
}
.radio-button {
-fx-text-fill: #ffffff;
-fx-font-size: 13px;
-fx-padding: 6 8;
-fx-background-radius: 10;
}
.radio-button:hover {
-fx-font-size: 15px;
}
.radio-button:selected {
/*-fx-text-fill: #b8860b;*/
-fx-text-fill: #ffffff;
}
.radio-button .radio {
-fx-background-color: #b8860b;
/*-fx-border-width: 3;*/
/*-fx-border-color: #b8860b;*/
/*-fx-border-radius: 50%;*/
}
.radio-button:selected .radio {
-fx-background-color: #b8860b;
-fx-effect: dropshadow(gaussian, rgba(184,134,11,0.8), 6, 0.3, 0, 0);
}
.settings-box {
-fx-background-insets: 0;
}
.settings-content {
-fx-background-color: transparent;
}
@@ -174,7 +174,12 @@
.gray-button {
-fx-background-color: #333333;
-fx-border-color: #666666;
-fx-text-fill: #cccccc;
/*-fx-text-fill: #cccccc;*/
-fx-text-fill: #ffffff;
}
.gray-button .label {
-fx-text-fill: #ffffff;
}
.green-button {
@@ -219,6 +224,10 @@
-fx-opacity: 1;
}
/*.community-card:hover {*/
/* -fx-translate-y: -3;*/
/*}*/
.player-card {
-fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0, 0.8), 0, 0, 3, 3);
@@ -233,6 +242,10 @@
-fx-opacity: 1;
}
/*.player-card:hover {*/
/* -fx-translate-y: -3;*/
/*}*/
.player-card.player-cards-active-turn {
-fx-effect: dropshadow(one-pass-box, rgb(184, 134, 11), 0, 0, 5, 5);
}
@@ -241,10 +254,17 @@
-fx-effect: dropshadow(one-pass-box, rgb(184, 134, 11), 0, 0, 5, 5);
}
/*.dealer-box:hover {*/
/* -fx-translate-y: -3;*/
/*}*/
.chips-icon {
-fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0, 0.8), 0, 0, 3, 3);
}
/*.chips-icon:hover {*/
/* -fx-translate-y: -3;*/
/*}*/
.dealer-icon {
-fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0, 0.8), 0, 0, 3, 3);
@@ -266,9 +286,10 @@
}
.pot-title {
-fx-text-fill: gold;
-fx-text-fill: #ffffff;
-fx-font-size: 14px;
-fx-font-weight: bold;
-fx-effect: dropshadow(one-pass-box, #000000, 0, 0, 3, 3);
}
.pot-box {
@@ -410,6 +431,183 @@
-fx-background-color: #333333;
}
.context-menu .menu-item:focused {
-fx-background-color: #333333;
.context-menu .menu-item .label {
-fx-text-fill: #ffffff;
-fx-font-size: 13px;
}
/*.context-menu .menu-item:focused {*/
/* -fx-background-color: #333333;*/
/*}*/
.context-menu .menu-item:hover .label,
.context-menu .menu-item:focused .label {
/*-fx-text-fill: #b8860b;*/
-fx-font-size: 15px;
}
.notebook {
-fx-background-color: rgb(13, 158, 59);
-fx-background-radius: 23;
-fx-border-radius: 20;
-fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10;
-fx-border-width: 3;
-fx-effect: dropshadow(one-pass-box, #000000, 0, 0, 5, 5);
/*-fx-cursor: move;*/
-fx-padding: 0 0 15 0;
}
.notebook:hover {
-fx-border-color: #ffffff;
}
/*.notebook.player-active-turn {*/
/* !* Active turn should only change the shadow to avoid flicker/transparency jumps. *!*/
/* -fx-effect: dropshadow(one-pass-box, rgb(184, 134, 11), 0, 0, 5, 5);*/
/*}*/
.notebook-header {
-fx-background-color: rgb(13, 158, 59);
-fx-background-radius: 25 25 0 0;
-fx-padding: 8 10;
-fx-cursor: move;
}
.notebook-title {
-fx-text-fill: #ffffff;
-fx-font-size: 18px;
-fx-font-weight: bold;
-fx-effect: dropshadow(one-pass-box, #000000, 0, 0, 3, 3);
}
.notebook-content {
-fx-background-color: transparent;
-fx-padding: 0;
}
.notebook-content > .viewport {
-fx-background-color: rgb(13, 158, 59);
-fx-background-radius: 0 0 25 25;
}
.scroll-pane {
-fx-background-color: transparent;
-fx-border-color: transparent;
}
.scroll-pane .scroll-bar:vertical,
.scroll-pane .scroll-bar:horizontal {
-fx-background-color: transparent;
}
.scroll-pane .scroll-bar .track {
/*-fx-background-color: #5c3d10;*/
-fx-background-color: transparent;
-fx-background-radius: 5;
}
.scroll-pane .scroll-bar .thumb {
-fx-background-color: #3d260a;
-fx-background-radius: 5;
}
.scroll-pane .scroll-bar .thumb:hover {
-fx-background-color: #2a1b07;
}
.notebook-tips-list {
-fx-padding: 10;
-fx-spacing: 10;
-fx-background-color: transparent;
}
.notebook-tips-list .text-flow {
-fx-padding: 12;
-fx-background-color: rgb(13, 158, 59);
-fx-background-radius: 15;
}
.settings-box {
-fx-background-color: rgb(13, 158, 59);
-fx-background-radius: 23;
-fx-border-radius: 20;
-fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10;
-fx-border-width: 3;
-fx-effect: dropshadow(one-pass-box, #000000, 0, 0, 5, 5);
/*-fx-cursor: move;*/
-fx-padding: 0 0 15 0;
}
.settings-box:hover {
-fx-border-color: #ffffff;
}
/*.settings-box.player-active-turn {*/
/* !* Active turn should only change the shadow to avoid flicker/transparency jumps. *!*/
/* -fx-effect: dropshadow(one-pass-box, rgb(184, 134, 11), 0, 0, 5, 5);*/
/*}*/
.settings-header {
-fx-background-color: transparent;
-fx-background-radius: 20 20 0 0;
-fx-padding: 10 15;
-fx-cursor: move;
-fx-border-width: 0 0 1 0;
}
.settings-label {
-fx-text-fill: #ffffff;
-fx-font-size: 18px;
-fx-font-weight: bold;
-fx-effect: dropshadow(one-pass-box, #000000, 0, 0, 3, 3);
}
.settings-content {
-fx-padding: 15 20 20 20;
-fx-spacing: 12;
}
.settings-content {
-fx-background-color: transparent;
-fx-text-fill: #e8e8e8;
-fx-font-size: 13px;
}
.radio-button {
-fx-text-fill: #ffffff;
-fx-font-size: 13px;
-fx-padding: 6 8;
-fx-background-radius: 10;
}
.radio-button:hover {
-fx-font-size: 15px;
}
.radio-button:selected {
/*-fx-text-fill: #b8860b;*/
-fx-text-fill: #ffffff;
}
.radio-button .radio {
-fx-background-color: #b8860b;
/*-fx-border-width: 3;*/
/*-fx-border-color: #b8860b;*/
/*-fx-border-radius: 50%;*/
}
.radio-button:selected .radio {
-fx-background-color: #b8860b;
-fx-effect: dropshadow(gaussian, rgba(184,134,11,0.8), 6, 0.3, 0, 0);
}
.settings-box {
-fx-background-insets: 0;
}
.settings-content {
-fx-background-color: transparent;
}
@@ -7,11 +7,11 @@
<!-- Main container: Links the UI to the CasinoGameController and loads Casinogameui.css -->
<AnchorPane xmlns="http://javafx.com/javafx/21"
<AnchorPane fx:id="rootPane"
xmlns="http://javafx.com/javafx/21"
xmlns:fx="http://javafx.com/fxml/1"
fx:controller="ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.CasinoGameController"
styleClass="background"
stylesheets="@Casinogameui.css">
styleClass="background">
<GridPane prefWidth="1200" prefHeight="800" AnchorPane.topAnchor="0" AnchorPane.bottomAnchor="0" AnchorPane.leftAnchor="0" AnchorPane.rightAnchor="0">
<columnConstraints>
@@ -41,6 +41,8 @@
alignment="CENTER"
styleClass="casino-table"
spacing="20"
mouseTransparent="false"
pickOnBounds="false"
maxWidth="Infinity"
maxHeight="Infinity">
@@ -67,7 +69,7 @@
</Label>
<Label fx:id="tableText"
text="Welcome! The round is about to begin."
text="Welcome! The game starts when 4 players are present..."
styleClass="table-title" />
</VBox>
@@ -161,8 +163,18 @@
</GridPane>
<!-- movable taskbar -->
<AnchorPane>
<AnchorPane pickOnBounds="false">
<fx:include fx:id="taskbarInclude" source="gameuicomponents/Taskbar.fxml" GridPane.columnIndex="1"/>
</AnchorPane>
<!-- movable notebook (Tips Box) -->
<AnchorPane pickOnBounds="false">
<fx:include fx:id="notebookInclude" source="gameuicomponents/Notebook.fxml" GridPane.columnIndex="1"/>
</AnchorPane>
<!-- movable settings -->
<AnchorPane pickOnBounds="false">
<fx:include fx:id="settingsInclude" source="gameuicomponents/Settings.fxml" GridPane.columnIndex="1"/>
</AnchorPane>
</AnchorPane>
@@ -4,7 +4,7 @@
<?import javafx.scene.layout.*?>
<?import javafx.geometry.Insets?>
<AnchorPane xmlns="http://javafx.com"
<AnchorPane fx:id="rootPane" xmlns="http://javafx.com"
xmlns:fx="http://javafx.com"
fx:controller="ch.unibas.dmi.dbis.cs108.casono.client.ui.lobbyui.CasinomainuiController"
styleClass="anchor-pane-bg"
@@ -0,0 +1,52 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import javafx.geometry.Insets?>
<VBox xmlns="http://javafx.com/javafx/21"
xmlns:fx="http://javafx.com/fxml/1"
fx:controller="ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.gameuicomponents.NotebookController"
fx:id="notebook"
styleClass="notebook"
spacing="0"
layoutX="900.0" layoutY="50.0"
prefWidth="350" prefHeight="500">
<HBox styleClass="notebook-header"
onMousePressed="#onNotebookPressed"
onMouseDragged="#onNotebookDragged"
onMouseReleased="#onNotebookReleased"
spacing="10"
alignment="CENTER_LEFT">
<Label text="📓 TIPS"
styleClass="notebook-title"
HBox.hgrow="ALWAYS"/>
<Button fx:id="toggleButton"
text=""
onAction="#onToggleNotebook"
styleClass="red-button"
prefWidth="40"
prefHeight="30"/>
</HBox>
<ScrollPane fx:id="tipsContent"
styleClass="notebook-content"
fitToWidth="true"
vbarPolicy="ALWAYS"
hbarPolicy="NEVER"
VBox.vgrow="ALWAYS">
<VBox fx:id="tipsList"
styleClass="notebook-tips-list"
spacing="8"
prefWidth="330">
</VBox>
</ScrollPane>
</VBox>
@@ -4,6 +4,7 @@
<?import javafx.scene.layout.*?>
<?import javafx.scene.image.ImageView?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.shape.Circle?>
<!--
This layout currently serves as a visual structure. As soon as the game engine and
@@ -14,7 +15,7 @@ and status displays will be dynamically populated with real-time data from the s
<VBox fx:id="playerStatusBox"
xmlns="http://javafx.com/javafx/21"
xmlns:fx="http://javafx.com/fxml/1"
fx:controller="ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.gameuicomponents.PlayerStatusController"
fx:controller="ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.gameuicomponents.PlayerStatusController"
styleClass="player-status-pane"
prefWidth="200"
spacing="5">
@@ -25,7 +26,7 @@ and status displays will be dynamically populated with real-time data from the s
<HBox fx:id="statusInnerBoxTop"
styleClass="status-inner-box-top"
alignment="CENTER_LEFT"
prefHeight="40">
prefHeight="45">
<padding>
<Insets left="10"/>
@@ -33,6 +34,7 @@ and status displays will be dynamically populated with real-time data from the s
<Label styleClass="status-label-small"/>
<Label fx:id="playerName" text="-" styleClass="status-value-text"/>
</HBox>
<!-- spacer -->
@@ -50,6 +52,7 @@ and status displays will be dynamically populated with real-time data from the s
<Label styleClass="status-label-small"/>
<Label fx:id="playerMoney" text="0 $" styleClass="status-value-money"/>
</HBox>
<!-- ICON AREA -->
@@ -61,12 +64,28 @@ and status displays will be dynamically populated with real-time data from the s
prefWidth="40"
prefHeight="40"/>
<ImageView fx:id="playerProfileImage"
fitWidth="40"
fitHeight="40"
preserveRatio="true"
smooth="true"
layoutX="-25"
layoutY="-125"
styleClass="status-profile-image">
<clip>
<Circle centerX="20" centerY="20" radius="20" />
</clip>
</ImageView>
<ImageView fx:id="dealerIcon"
fitWidth="43"
fitHeight="43"
layoutY="-120"
visible="false"
styleClass="dealer-icon"/>
</Pane>
</children>
@@ -0,0 +1,66 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import javafx.geometry.Insets?>
<VBox xmlns="http://javafx.com/javafx/21"
xmlns:fx="http://javafx.com/fxml/1"
fx:controller="ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.gameuicomponents.SettingsController"
fx:id="settingsBox"
styleClass="settings-box"
spacing="0"
layoutX="100.0" layoutY="100.0"
prefWidth="300" prefHeight="250">
<HBox styleClass="settings-header"
onMousePressed="#onSettingsPressed"
onMouseDragged="#onSettingsDragged"
onMouseReleased="#onSettingsReleased"
spacing="10"
alignment="CENTER_LEFT">
<Label text="⚙ SETTINGS"
styleClass="settings-label"
HBox.hgrow="ALWAYS"/>
<Button fx:id="toggleButton"
text=""
onAction="#onToggleSettings"
styleClass="red-button"
prefWidth="40"
prefHeight="30"/>
</HBox>
<VBox styleClass="settings-content"
spacing="15"
VBox.vgrow="ALWAYS">
<Label text="Theme:"
styleClass="settings-label"/>
<RadioButton fx:id="themeStandard"
text="STANDARD (DEFAULT)"
styleClass="radio-button"/>
<RadioButton fx:id="themeStandardParquet"
text="STANDARD WOODEN"
styleClass="radio-button"/>
<RadioButton fx:id="themeBlackWhite"
text="BLACK AND WHITE"
styleClass="radio-button"/>
<RadioButton fx:id="themeGlass"
text="GLASS"
styleClass="radio-button"/>
<RadioButton fx:id="themeLightGlass"
text="LIGHT GLASS"
styleClass="radio-button"/>
</VBox>
</VBox>
@@ -6,7 +6,7 @@
<!-- movable taskbar -->
<HBox xmlns="http://javafx.com/javafx/21"
xmlns:fx="http://javafx.com/fxml/1"
fx:controller="ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.gameuicomponents.TaskbarController"
fx:controller="ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.gameuicomponents.TaskbarController"
fx:id="taskbar"
styleClass="taskbar"
spacing="15"
@@ -17,10 +17,18 @@
layoutX="50.0" layoutY="710.0">
<!-- Placeholder buttons -->
<Button text="SETTINGS" styleClass="gray-button" />
<Button text="HIGHSCORE" onAction="#onHighscoreButtonClick" styleClass="gray-button" />
<!-- Buten for the Casono Browser -->
<Button text="HELP" onAction="#onBrowserButtonClick" styleClass="gray-button" />
<Button text="SETTINGS" onAction="#onSettingsButtonClick" styleClass="gray-button" />
<Button text="HIGHSCORE" onAction="#onHighscoreButtonClick" styleClass="gray-button" />
<!-- Help Button with Dropdown Menu -->
<MenuButton text="HELP" styleClass="gray-button">
<items>
<MenuItem text="SHOW TIPS" onAction="#onShowTipsButtonClick" />
<MenuItem text="CASONO BROWSER: Manual" onAction="#onBrowserButtonClick" />
<MenuItem text="CASONO BROWSER: Wikipedia" onAction="#onBrowserButtonClickWiki" />
<MenuItem text="CASONO BROWSER: Brave Search" onAction="#onBrowserButtonClickBrave" />
</items>
</MenuButton>
<!-- Vertical dividing line: Creates a visual separation between two button areas -->
<Separator orientation="VERTICAL" prefHeight="20" opacity="0.3" />
@@ -17,6 +17,7 @@ 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.rules.RuleEngine;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.rules.showdown.CardsSpeakRule;
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;
@@ -281,13 +282,9 @@ public class GameControllerTest {
Set<String> seenCards = new HashSet<>();
for (List<Card> cards : playerCards.values()) {
for (Card c : cards) {
String key = c.getRank() + "-" + c.getSuit();
assertFalse(seenCards.contains(key), "Duplicate card found: " + key);
seenCards.add(key);
}
}
@@ -617,4 +614,80 @@ public class GameControllerTest {
LOGGER.info("Test 5 was successfully completed");
}
/**
* This test simulates a scenario where two players have identical Two Pair hands at showdown.
*/
@Test
public void testSplitPotWithTrueIdenticalTwoPair() {
GameState state = new GameState();
state.addPlayer(PlayerId.of("PlayerA"), 5000);
state.addPlayer(PlayerId.of("PlayerB"), 5000);
state.getHoleCards(PlayerId.of("PlayerA")).add(new Card(Suit.CLUBS, Rank.TWO));
state.getHoleCards(PlayerId.of("PlayerA")).add(new Card(Suit.DIAMONDS, Rank.THREE));
state.getHoleCards(PlayerId.of("PlayerB")).add(new Card(Suit.HEARTS, Rank.FOUR));
state.getHoleCards(PlayerId.of("PlayerB")).add(new Card(Suit.SPADES, Rank.FIVE));
state.addCommunityCard(new Card(Suit.CLUBS, Rank.QUEEN));
state.addCommunityCard(new Card(Suit.DIAMONDS, Rank.QUEEN));
state.addCommunityCard(new Card(Suit.SPADES, Rank.EIGHT));
state.addCommunityCard(new Card(Suit.HEARTS, Rank.EIGHT));
state.addCommunityCard(new Card(Suit.CLUBS, Rank.ACE));
List<Card> board = state.getCommunityCards();
List<Card> handA = new ArrayList<>(board);
handA.addAll(state.getHoleCards(PlayerId.of("PlayerA")));
List<Card> handB = new ArrayList<>(board);
handB.addAll(state.getHoleCards(PlayerId.of("PlayerB")));
HandRank rankA = HandEvaluator.evaluate(handA);
HandRank rankB = HandEvaluator.evaluate(handB);
assertEquals(0, rankA.compareTo(rankB), "Both hands must be exactly the same");
CardsSpeakRule showdown = new CardsSpeakRule();
List<Player> winners = showdown.determineWinners(state);
assertEquals(2, winners.size(), "There must be two winners");
assertEquals("PlayerA", winners.get(0).getName());
assertEquals("PlayerB", winners.get(1).getName());
int chipsBeforeA =
state.getPlayers().stream()
.filter(p -> p.getId().equals(PlayerId.of("PlayerA")))
.findFirst()
.orElseThrow()
.getChips();
int chipsBeforeB =
state.getPlayers().stream()
.filter(p -> p.getId().equals(PlayerId.of("PlayerB")))
.findFirst()
.orElseThrow()
.getChips();
state.getPot().add(1000);
showdown.awardPot(state);
Player playerA =
state.getPlayers().stream()
.filter(p -> p.getId().equals(PlayerId.of("PlayerA")))
.findFirst()
.orElseThrow();
Player playerB =
state.getPlayers().stream()
.filter(p -> p.getId().equals(PlayerId.of("PlayerB")))
.findFirst()
.orElseThrow();
assertEquals(chipsBeforeA + 500, playerA.getChips(), "Player A must receive half the pot");
assertEquals(chipsBeforeB + 500, playerB.getChips(), "Player B must receive half the pot");
assertEquals(0, state.getPot().getAmount(), "The pot must be 0 after the payout");
LOGGER.info("Passed the Split-Pot-Test");
}
}