Merge branch 'feat/game-ui-optimization' into 'main'
Game UI Optimisation See merge request cs108-fs26/Gruppe-13!164
This commit was merged in pull request #320.
This commit is contained in:
@@ -18,4 +18,6 @@ public class GameState {
|
||||
public int dealer;
|
||||
public int activePlayer;
|
||||
public int winnerIndex = -1;
|
||||
public List<String> winnerNames = new ArrayList<>();
|
||||
public int potPerWinner;
|
||||
}
|
||||
|
||||
@@ -111,6 +111,7 @@ public class GameClient {
|
||||
GameState s = new GameState();
|
||||
s.players = new ArrayList<>();
|
||||
s.communityCards = new ArrayList<>();
|
||||
s.winnerNames = new ArrayList<>();
|
||||
|
||||
ParseState state = new ParseState();
|
||||
for (String raw : input.split("\n")) {
|
||||
@@ -155,6 +156,14 @@ public class GameClient {
|
||||
s.winnerIndex = intVal(l);
|
||||
yield true;
|
||||
}
|
||||
case String l when l.startsWith("WINNER_NAME=") -> {
|
||||
s.winnerNames.add(value(l));
|
||||
yield true;
|
||||
}
|
||||
case String l when l.startsWith("POT_PER_WINNER=") -> {
|
||||
s.potPerWinner = intVal(l);
|
||||
yield true;
|
||||
}
|
||||
case String l when l.startsWith("HIGHSCORE=") -> {
|
||||
s.highscoreEntries.add(value(l));
|
||||
yield true;
|
||||
@@ -224,6 +233,13 @@ public class GameClient {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (line.startsWith("ID=")) {
|
||||
if (state.currentPlayer.getId() == null) {
|
||||
state.currentPlayer.setId(PlayerId.of(value(line)));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (line.startsWith("CHIPS=")) {
|
||||
state.currentPlayer.setChips(intVal(line));
|
||||
return true;
|
||||
|
||||
@@ -244,6 +244,33 @@ public class LobbyClient {
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the current lobby members from GET_LOBBY_STATUS and returns their usernames in order.
|
||||
*
|
||||
* @param lobbyId id of the lobby to inspect
|
||||
* @return list of usernames currently known in the lobby
|
||||
*/
|
||||
public List<String> fetchLobbyPlayerNames(int lobbyId) {
|
||||
List<String> lines = client.processCommand("GET_LOBBY_STATUS ID=" + lobbyId);
|
||||
List<RequestParameter> params = ClientService.convertToRequestParameters(lines);
|
||||
|
||||
List<String> names = new ArrayList<>();
|
||||
for (RequestParameter parameter : params) {
|
||||
if (!"USERNAME".equalsIgnoreCase(parameter.key())) {
|
||||
continue;
|
||||
}
|
||||
String name = parameter.value();
|
||||
if (name != null) {
|
||||
String trimmed = name.trim();
|
||||
if (!trimmed.isEmpty() && !names.contains(trimmed)) {
|
||||
names.add(trimmed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return names;
|
||||
}
|
||||
|
||||
/** Simple data holder for lobby metadata returned by the server. */
|
||||
public static final class LobbyInfo {
|
||||
public final int id;
|
||||
|
||||
+463
-62
@@ -10,7 +10,10 @@ import ch.unibas.dmi.dbis.cs108.casono.client.game.Player;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.game.PlayerId;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.game.PlayerState;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.network.ClientService;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.network.LobbyClient;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.gameuicomponents.NotebookController;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.gameuicomponents.PlayerStatusController;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.gameuicomponents.SettingsController;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.gameuicomponents.TaskbarController;
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
@@ -21,6 +24,7 @@ import java.util.concurrent.CompletableFuture;
|
||||
import java.util.logging.Logger;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.fxml.FXMLLoader;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.image.Image;
|
||||
@@ -60,8 +64,13 @@ public class CasinoGameController {
|
||||
|
||||
private GameService gameService;
|
||||
private PlayerId myPlayerId;
|
||||
private String myPlayerName;
|
||||
@FXML private TaskbarController taskbarIncludeController;
|
||||
private TaskbarController taskbarController;
|
||||
@FXML private NotebookController notebookIncludeController;
|
||||
private NotebookController notebookController;
|
||||
@FXML private SettingsController settingsIncludeController;
|
||||
// private SettingsController settingsController;
|
||||
private Image dealerImage;
|
||||
private javafx.animation.Timeline timeline;
|
||||
private boolean gameStarted = false;
|
||||
@@ -73,6 +82,7 @@ public class CasinoGameController {
|
||||
public static final double CHAT_CONTAINER_CONSTANT = 6.0;
|
||||
private static final int TOTAL_SLOTS = 5;
|
||||
private static final int PLAYER_SLOTS = 2;
|
||||
private static final int MAX_OPPONENTS_TO_RENDER = 3;
|
||||
private int pot = 0;
|
||||
private static final String BACKSIDE = "/images/card-background-3.png";
|
||||
private static final String DEALER_IMAGE_PATH = "/images/chip-dealer-blue-3.png";
|
||||
@@ -115,6 +125,8 @@ public class CasinoGameController {
|
||||
private static final double COMMUNITY_CARD_WIDTH_RATIO = 0.11;
|
||||
private static final double PLAYER_CARD_HEIGHT_RATIO = 0.30;
|
||||
private static final double PLAYER_CARD_WIDTH_RATIO = 0.15;
|
||||
private static final double DEALER_HEIGHT_RATIO = 0.13;
|
||||
private static final double DEALER_WIDTH_RATIO = 0.13;
|
||||
private static final double PLAYER_CARDS_Y_OFFSET_FACTOR = 0.10;
|
||||
private static final int[] CHIP_VALUES = {
|
||||
100000, 50000, 20000, 10000,
|
||||
@@ -139,6 +151,12 @@ public class CasinoGameController {
|
||||
private static final long CHIP_SCALE_DURATION_MS = 220;
|
||||
private static final long CHIP_DROP_DURATION_MS = 250;
|
||||
private static final long CHIP_STAGGER_DELAY_MULTIPLIER = 35L;
|
||||
private static final int MAX_POT_ROWS = 2;
|
||||
private static final double POT_CHIP_H_GAP = 6.0;
|
||||
private static final double POT_CHIP_V_GAP = 6.0;
|
||||
private static final double POT_WIDTH_FACTOR = 0.45;
|
||||
private static final double FALLBACK_POT_WIDTH = 500.0;
|
||||
private static final double FALLBACK_CHIP_WIDTH = 36.0;
|
||||
private static final double CHAT_WIDTH = 400;
|
||||
private static final double CHAT_HEIGHT = 600;
|
||||
private static final String ACTIVE_CARD_BOX_STYLE_CLASS = "player-cards-active-turn";
|
||||
@@ -156,6 +174,8 @@ public class CasinoGameController {
|
||||
private static final int SMALL_BLIND = 100;
|
||||
private static final int BIG_BLIND = 200;
|
||||
private static final int NO_BET = 0;
|
||||
private boolean myDealerIconSizeBound;
|
||||
private java.util.List<String> lobbyPlayerNames = java.util.List.of();
|
||||
|
||||
/** Standard constructor. Used by FXML. */
|
||||
public CasinoGameController() {
|
||||
@@ -230,6 +250,16 @@ public class CasinoGameController {
|
||||
if (taskbarController != null && gameService != null && myPlayerId != null) {
|
||||
taskbarController.setGameService(gameService, myPlayerId);
|
||||
}
|
||||
|
||||
notebookController = resolveNotebookController();
|
||||
if (taskbarController != null && notebookController != null) {
|
||||
taskbarController.setNotebookController(notebookController);
|
||||
}
|
||||
|
||||
if (taskbarController != null && settingsIncludeController != null) {
|
||||
taskbarController.setSettingsController(settingsIncludeController);
|
||||
settingsIncludeController.setThemeChangeListener(this::applyTheme);
|
||||
}
|
||||
configureTaskbarLobbyActionAnnouncements();
|
||||
|
||||
if (communityCardsBox == null) {
|
||||
@@ -251,6 +281,7 @@ public class CasinoGameController {
|
||||
}
|
||||
|
||||
myDealerIcon.setVisible(false);
|
||||
styleMyDealerIcon();
|
||||
tableText.setWrapText(true);
|
||||
|
||||
javafx.stage.Screen screen = javafx.stage.Screen.getPrimary();
|
||||
@@ -267,6 +298,8 @@ public class CasinoGameController {
|
||||
renderPlayerCards(List.of());
|
||||
|
||||
chatContainer.toFront();
|
||||
|
||||
javafx.application.Platform.runLater(() -> applyTheme("standard"));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -475,6 +508,7 @@ public class CasinoGameController {
|
||||
.thenAccept(
|
||||
state -> {
|
||||
if (state == null) {
|
||||
refreshLobbyOpponentsBeforeGameStart();
|
||||
LOGGER.info("No state received yet.");
|
||||
return;
|
||||
}
|
||||
@@ -492,6 +526,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.
|
||||
@@ -621,12 +685,12 @@ public class CasinoGameController {
|
||||
*/
|
||||
private List<Card> getMyCards(List<Player> players) {
|
||||
|
||||
if (myPlayerId == null || players == null) {
|
||||
if (players == null) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
for (Player p : players) {
|
||||
if (p.getId() != null && p.getId().equals(myPlayerId)) {
|
||||
if (isCurrentPlayer(p)) {
|
||||
return p.getCards() != null ? p.getCards() : List.of();
|
||||
}
|
||||
}
|
||||
@@ -641,13 +705,13 @@ public class CasinoGameController {
|
||||
*/
|
||||
private void updatePlayerCards(GameState s) {
|
||||
|
||||
if (s.players == null || myPlayerId == null) {
|
||||
if (s.players == null) {
|
||||
renderPlayerCards(List.of());
|
||||
return;
|
||||
}
|
||||
|
||||
for (Player p : s.players) {
|
||||
if (p.getId().equals(myPlayerId)) {
|
||||
if (isCurrentPlayer(p)) {
|
||||
renderPlayerCards(p.getCards());
|
||||
return;
|
||||
}
|
||||
@@ -672,7 +736,18 @@ public class CasinoGameController {
|
||||
|
||||
String winnerName = resolveWinnerName(s);
|
||||
if (winnerName != null && !winnerName.isBlank()) {
|
||||
tableText.setText("Winner: " + winnerName);
|
||||
StringBuilder winnerText = new StringBuilder("Winner");
|
||||
|
||||
if (s.winnerNames != null && s.winnerNames.size() > 1) {
|
||||
winnerText.append("s: ").append(String.join(", ", s.winnerNames));
|
||||
if (s.potPerWinner > 0) {
|
||||
winnerText.append(" (").append(s.potPerWinner).append(" each)");
|
||||
}
|
||||
} else {
|
||||
winnerText.append(": ").append(winnerName);
|
||||
}
|
||||
|
||||
tableText.setText(winnerText.toString());
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -770,13 +845,13 @@ public class CasinoGameController {
|
||||
*/
|
||||
private void updateTaskbar(GameState s) {
|
||||
TaskbarController controller = resolveTaskbarController();
|
||||
if (controller == null || myPlayerId == null) {
|
||||
if (controller == null) {
|
||||
return;
|
||||
}
|
||||
if (gameService != null) {
|
||||
controller.setGameService(gameService, myPlayerId);
|
||||
}
|
||||
controller.update(s, myPlayerId);
|
||||
controller.update(s, myPlayerName);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -832,30 +907,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;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -970,12 +1066,12 @@ public class CasinoGameController {
|
||||
clearActiveTurnHighlights();
|
||||
|
||||
Player activePlayer = getPlayerAtIndex(players, activePlayerIndex);
|
||||
if (activePlayer == null || activePlayer.getId() == null) {
|
||||
if (activePlayer == null) {
|
||||
setTaskbarTurnHighlight(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (myPlayerId != null && myPlayerId.equals(activePlayer.getId())) {
|
||||
if (isCurrentPlayer(activePlayer)) {
|
||||
setPlayerCardsTurnHighlighted(true);
|
||||
setTaskbarTurnHighlight(true);
|
||||
return;
|
||||
@@ -1104,11 +1200,7 @@ public class CasinoGameController {
|
||||
}
|
||||
|
||||
Player winner = resolveWinner(s);
|
||||
boolean isLocalWinner =
|
||||
winner != null
|
||||
&& winner.getId() != null
|
||||
&& myPlayerId != null
|
||||
&& myPlayerId.equals(winner.getId());
|
||||
boolean isLocalWinner = winner != null && isCurrentPlayer(winner);
|
||||
|
||||
if (!isLocalWinner && lastWinnerName != null && !lastWinnerName.isBlank()) {
|
||||
String localName =
|
||||
@@ -1255,11 +1347,11 @@ public class CasinoGameController {
|
||||
}
|
||||
|
||||
Player dealerPlayer = s.players.get(s.dealer);
|
||||
if (dealerPlayer == null || dealerPlayer.getId() == null) {
|
||||
if (dealerPlayer == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (myPlayerId != null && myPlayerId.equals(dealerPlayer.getId())) {
|
||||
if (isCurrentPlayer(dealerPlayer)) {
|
||||
myDealerIcon.setVisible(true);
|
||||
return;
|
||||
}
|
||||
@@ -1292,10 +1384,17 @@ public class CasinoGameController {
|
||||
* otherwise.
|
||||
*/
|
||||
private boolean samePlayer(Player a, Player b) {
|
||||
if (a == null || b == null || a.getId() == null || b.getId() == null) {
|
||||
if (a == null || b == null) {
|
||||
return false;
|
||||
}
|
||||
return a.getId().equals(b.getId());
|
||||
|
||||
String aName = normalizeIdentifier(a.getName());
|
||||
String bName = normalizeIdentifier(b.getName());
|
||||
if (aName != null && bName != null) {
|
||||
return aName.equals(bName);
|
||||
}
|
||||
|
||||
return a.getId() != null && b.getId() != null && a.getId().equals(b.getId());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1326,8 +1425,8 @@ public class CasinoGameController {
|
||||
|
||||
view.setImage(image);
|
||||
|
||||
view.setOnMouseEntered(e -> animateCardHover(view, true));
|
||||
view.setOnMouseExited(e -> animateCardHover(view, false));
|
||||
// view.setOnMouseEntered(e -> animateCardHover(view, true));
|
||||
// view.setOnMouseExited(e -> animateCardHover(view, false));
|
||||
|
||||
view.setOpacity(CARD_START_OPACITY);
|
||||
|
||||
@@ -1370,8 +1469,8 @@ public class CasinoGameController {
|
||||
|
||||
view.setImage(image);
|
||||
|
||||
view.setOnMouseEntered(e -> animateCardHover(view, true));
|
||||
view.setOnMouseExited(e -> animateCardHover(view, false));
|
||||
// view.setOnMouseEntered(e -> animateCardHover(view, true));
|
||||
// view.setOnMouseExited(e -> animateCardHover(view, false));
|
||||
|
||||
view.setOpacity(CARD_START_OPACITY);
|
||||
|
||||
@@ -1384,6 +1483,47 @@ public class CasinoGameController {
|
||||
}
|
||||
}
|
||||
|
||||
/** Apply styling to the myDealerIcon ImageView. */
|
||||
private void styleMyDealerIcon() {
|
||||
if (myDealerIcon == null || playerCardsBox == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
myDealerIcon.setPreserveRatio(true);
|
||||
myDealerIcon.setSmooth(true);
|
||||
|
||||
javafx.scene.Scene scene = playerCardsBox.getScene();
|
||||
if (scene != null) {
|
||||
bindMyDealerIconSize(scene);
|
||||
} else {
|
||||
playerCardsBox
|
||||
.sceneProperty()
|
||||
.addListener(
|
||||
(obs, oldScene, newScene) -> {
|
||||
if (newScene != null) {
|
||||
bindMyDealerIconSize(newScene);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind the size of the myDealerIcon ImageView to the dimensions of the scene using predefined
|
||||
* ratios.
|
||||
*
|
||||
* @param scene The JavaFX Scene to which the myDealerIcon belongs, used for binding the size
|
||||
* properties.
|
||||
*/
|
||||
private void bindMyDealerIconSize(javafx.scene.Scene scene) {
|
||||
if (myDealerIconSizeBound || scene == null || myDealerIcon == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
myDealerIcon.fitHeightProperty().bind(scene.heightProperty().multiply(DEALER_HEIGHT_RATIO));
|
||||
myDealerIcon.fitWidthProperty().bind(scene.widthProperty().multiply(DEALER_WIDTH_RATIO));
|
||||
myDealerIconSizeBound = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load an image from the given path safely, falling back to a default backside image if the
|
||||
* specified image cannot be found.
|
||||
@@ -1551,28 +1691,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
|
||||
@@ -1640,13 +1783,26 @@ public class CasinoGameController {
|
||||
|
||||
potBox.getChildren().clear();
|
||||
|
||||
int chipsPerRow = resolveChipsPerPotRow();
|
||||
int maxVisibleChips = chipsPerRow * MAX_POT_ROWS;
|
||||
HBox topRow = createPotRow();
|
||||
HBox bottomRow = createPotRow();
|
||||
VBox rows = new VBox(POT_CHIP_V_GAP, topRow, bottomRow);
|
||||
rows.setAlignment(Pos.CENTER);
|
||||
potBox.getChildren().add(rows);
|
||||
|
||||
int remaining = pot;
|
||||
|
||||
int index = 0;
|
||||
boolean limitReached = false;
|
||||
|
||||
for (int chipValue : CHIP_VALUES) {
|
||||
|
||||
while (remaining >= chipValue) {
|
||||
if (index >= maxVisibleChips) {
|
||||
limitReached = true;
|
||||
break;
|
||||
}
|
||||
|
||||
ImageView chip =
|
||||
new ImageView(loadImageSafe("/images/chip-" + chipValue + "-5.png"));
|
||||
@@ -1658,18 +1814,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.
|
||||
*
|
||||
@@ -1769,13 +1974,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.
|
||||
@@ -1833,4 +2156,82 @@ public class CasinoGameController {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the NotebookController for displaying tips and game information.
|
||||
*
|
||||
* @return The NotebookController instance, which may be the directly injected
|
||||
* notebookController or the one obtained from the notebookIncludeController.
|
||||
*/
|
||||
private NotebookController resolveNotebookController() {
|
||||
if (notebookController != null) {
|
||||
return notebookController;
|
||||
}
|
||||
if (notebookIncludeController != null) {
|
||||
notebookController = notebookIncludeController;
|
||||
return notebookController;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the SettingsController for theme switching.
|
||||
*
|
||||
* @return The SettingsController instance, which may be the directly injected
|
||||
* settingsController or the one obtained from the settingsIncludeController.
|
||||
*/
|
||||
public SettingsController getSettingsController() {
|
||||
return settingsIncludeController;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies a UI theme by switching the application's stylesheet.
|
||||
*
|
||||
* @param theme The theme name: "standard", "blackwhite", or "glass"
|
||||
*/
|
||||
private void applyTheme(String theme) {
|
||||
if (theme == null || theme.isBlank()) {
|
||||
LOGGER.warning("Theme is null or empty -> ignoring theme change");
|
||||
return;
|
||||
}
|
||||
|
||||
javafx.scene.Scene scene = casinoTable != null ? casinoTable.getScene() : null;
|
||||
|
||||
if (scene == null) {
|
||||
LOGGER.warning("Scene not ready yet -> cannot apply theme: " + theme);
|
||||
return;
|
||||
}
|
||||
|
||||
String cssPath =
|
||||
switch (theme.toLowerCase()) {
|
||||
case "blackwhite" -> "/ui-structure/Casinogameui-blackwhite.css";
|
||||
case "glass" -> "/ui-structure/Casinogameui-glass.css";
|
||||
default -> "/ui-structure/Casinogameui.css";
|
||||
};
|
||||
|
||||
java.net.URL cssUrl = getClass().getResource(cssPath);
|
||||
|
||||
if (cssUrl == null) {
|
||||
LOGGER.severe("CSS resource not found: " + cssPath);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
String cssExternalForm = cssUrl.toExternalForm();
|
||||
|
||||
scene.getStylesheets()
|
||||
.removeIf(
|
||||
s ->
|
||||
s.contains("Casinogameui.css")
|
||||
|| s.contains("Casinogameui-blackwhite.css")
|
||||
|| s.contains("Casinogameui-glass.css"));
|
||||
|
||||
scene.getStylesheets().add(cssExternalForm);
|
||||
|
||||
LOGGER.info("Theme switched successfully to: " + theme);
|
||||
|
||||
} catch (Exception e) {
|
||||
LOGGER.severe("Failed to apply theme '" + theme + "': " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import ch.unibas.dmi.dbis.cs108.casono.client.game.GameService;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.game.PlayerId;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.network.ClientService;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.network.GameClient;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.network.LobbyClient;
|
||||
import java.io.IOException;
|
||||
import java.util.UUID;
|
||||
import java.util.logging.Logger;
|
||||
@@ -130,6 +131,13 @@ public class CasinoGameUI extends Application {
|
||||
controller.setMyPlayerId(PlayerId.of(effectiveUsername));
|
||||
controller.setChatContext(effectiveUsername, clientService, lobbyId);
|
||||
|
||||
try {
|
||||
LobbyClient lobbyClient = new LobbyClient(clientService);
|
||||
controller.setLobbyPlayerNames(lobbyClient.fetchLobbyPlayerNames(lobbyId));
|
||||
} catch (Exception e) {
|
||||
LOG.fine("Could not preload lobby player names: " + e.getMessage());
|
||||
}
|
||||
|
||||
controller.startChat(chatController);
|
||||
|
||||
Scene scene = new Scene(root, DEFAULT_WIDTH, DEFAULT_HEIGHT);
|
||||
|
||||
+268
@@ -0,0 +1,268 @@
|
||||
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);
|
||||
|
||||
@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));
|
||||
}
|
||||
}
|
||||
+18
-5
@@ -115,14 +115,27 @@ public class PlayerStatusController {
|
||||
* @return true if both represent the same player.
|
||||
*/
|
||||
public boolean hasPlayer(Player candidate) {
|
||||
if (player == null
|
||||
|| candidate == null
|
||||
|| player.getId() == null
|
||||
|| candidate.getId() == null) {
|
||||
if (player == null || candidate == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return player.getId().equals(candidate.getId());
|
||||
String currentName = normalizeIdentifier(player.getName());
|
||||
String candidateName = normalizeIdentifier(candidate.getName());
|
||||
if (currentName != null && candidateName != null) {
|
||||
return currentName.equals(candidateName);
|
||||
}
|
||||
|
||||
return player.getId() != null
|
||||
&& candidate.getId() != null
|
||||
&& player.getId().equals(candidate.getId());
|
||||
}
|
||||
|
||||
private String normalizeIdentifier(String value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
String trimmed = value.trim();
|
||||
return trimmed.isEmpty() ? null : trimmed.toLowerCase();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+196
@@ -0,0 +1,196 @@
|
||||
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);
|
||||
|
||||
@FXML private VBox settingsBox;
|
||||
@FXML private RadioButton themeStandard;
|
||||
@FXML private RadioButton themeBlackWhite;
|
||||
@FXML private RadioButton themeGlass;
|
||||
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 || themeBlackWhite == null || themeGlass == null) {
|
||||
LOGGER.warn("Theme buttons not injected from FXML");
|
||||
return;
|
||||
}
|
||||
|
||||
ToggleGroup group = new ToggleGroup();
|
||||
|
||||
themeStandard.setToggleGroup(group);
|
||||
themeBlackWhite.setToggleGroup(group);
|
||||
themeGlass.setToggleGroup(group);
|
||||
|
||||
themeStandard.setSelected(true);
|
||||
|
||||
group.selectedToggleProperty()
|
||||
.addListener(
|
||||
(obs, oldVal, newVal) -> {
|
||||
if (newVal == themeStandard) {
|
||||
setTheme("standard");
|
||||
} else if (newVal == themeBlackWhite) {
|
||||
setTheme("blackwhite");
|
||||
} else if (newVal == themeGlass) {
|
||||
setTheme("glass");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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));
|
||||
}
|
||||
}
|
||||
+260
-16
@@ -51,15 +51,19 @@ public class TaskbarController {
|
||||
|
||||
private GameService gameService;
|
||||
private PlayerId myPlayerId;
|
||||
private String myPlayerName;
|
||||
private GameState lastState;
|
||||
private double xOffset = 0;
|
||||
private double yOffset = 0;
|
||||
private NotebookController notebookController;
|
||||
private SettingsController settingsController;
|
||||
private static final double TASKBAR_SCALE = 0.95;
|
||||
private static final double PREFLOP_BLOCK_RATIO = 0.50;
|
||||
private static final double FLOP_WARN_RATIO = 0.50;
|
||||
private static final double FLOP_BLOCK_RATIO = 0.80;
|
||||
private static final double TURN_RIVER_WARN_RATIO = 0.80;
|
||||
private static final double TURN_RIVER_BLOCK_RATIO = 1.00;
|
||||
private static final int SMALLEST_VALUE = 5;
|
||||
private static final int SMALL_BLIND = 100;
|
||||
private static final int BIG_BLIND = 200;
|
||||
private static final String STYLE_YELLOW_BUTTON = "yellow-button";
|
||||
@@ -80,6 +84,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() {
|
||||
@@ -230,6 +237,17 @@ public class TaskbarController {
|
||||
public void setGameService(GameService gameService, PlayerId myPlayerId) {
|
||||
this.gameService = gameService;
|
||||
this.myPlayerId = myPlayerId;
|
||||
this.myPlayerName = myPlayerId != null ? normalizeIdentifier(myPlayerId.value()) : null;
|
||||
}
|
||||
|
||||
/** Sets the NotebookController reference for showing/hiding tips. */
|
||||
public void setNotebookController(NotebookController notebookController) {
|
||||
this.notebookController = notebookController;
|
||||
}
|
||||
|
||||
/** Sets the SettingsController reference for theme switching. */
|
||||
public void setSettingsController(SettingsController settingsController) {
|
||||
this.settingsController = settingsController;
|
||||
}
|
||||
|
||||
/** Sets an optional callback that publishes simple action labels to lobby chat. */
|
||||
@@ -340,6 +358,37 @@ public class TaskbarController {
|
||||
return "RAISE +" + raiseBy + " TO " + raiseamout;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the typed input as the additional amount the player wants to contribute.
|
||||
*
|
||||
* <p>If the player has already invested chips in the current betting round, the typed value is
|
||||
* interpreted as an additional amount on top of that investment. This lets the user enter the
|
||||
* amount they still need to put in directly (for example: already invested 200, type 400 ->
|
||||
* total target 600).
|
||||
*
|
||||
* @param state current game state
|
||||
* @param rawInput text from the input field
|
||||
* @return total target bet, or null if the input is invalid
|
||||
*/
|
||||
private Integer resolveTypedContributionTarget(GameState state, String rawInput) {
|
||||
Integer input = parseInputTarget(rawInput);
|
||||
if (input == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (state == null || state.players == null) {
|
||||
return input;
|
||||
}
|
||||
|
||||
Player me = findCurrentPlayer(state);
|
||||
if (me == null) {
|
||||
return input;
|
||||
}
|
||||
|
||||
int alreadyInvested = Math.max(0, me.getBet());
|
||||
return alreadyInvested > 0 ? alreadyInvested + input : input;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the taskbar based on the current game state and the player's status.
|
||||
*
|
||||
@@ -352,24 +401,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;
|
||||
@@ -393,6 +462,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.
|
||||
*
|
||||
@@ -443,7 +558,7 @@ public class TaskbarController {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isFirstPlayerOfPhase(state, me)) {
|
||||
if (isFirstFlopPlayer(state, me)) {
|
||||
setActionEnabled(betButton, true);
|
||||
setActionEnabled(callButton, false);
|
||||
// setActionEnabled(foldButton, false);
|
||||
@@ -629,6 +744,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.
|
||||
@@ -876,7 +1038,8 @@ public class TaskbarController {
|
||||
return input != null ? input : callTarget + BIG_BLIND;
|
||||
}
|
||||
|
||||
return parseInputTarget(taskbarInput.getText());
|
||||
return resolveTypedContributionTarget(
|
||||
state, taskbarInput != null ? taskbarInput.getText() : null);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1011,7 +1174,7 @@ public class TaskbarController {
|
||||
try {
|
||||
int value = Integer.parseInt(text.trim());
|
||||
|
||||
if (value % SMALL_BLIND != 0) {
|
||||
if (value % SMALLEST_VALUE != 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1033,7 +1196,7 @@ public class TaskbarController {
|
||||
* current game state.
|
||||
*/
|
||||
private ValidationResult validateTypedAmount(GameState state, String text) {
|
||||
Integer target = parseInputTarget(text);
|
||||
Integer target = resolveTypedContributionTarget(state, text);
|
||||
if (target == null || state == null) {
|
||||
return ValidationResult.blocked("Invalid input");
|
||||
}
|
||||
@@ -1491,15 +1654,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();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1550,6 +1754,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() {
|
||||
|
||||
+55
-8
@@ -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,19 @@ 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);
|
||||
int potPerWinner = computePotPerWinner(state, winnerNames.size());
|
||||
|
||||
for (String name : winnerNames) {
|
||||
builder.param("WINNER_NAME", name);
|
||||
}
|
||||
builder.param("POT_PER_WINNER", potPerWinner);
|
||||
|
||||
appendWinnerToHighscoresIfFinished(state, winnerNames);
|
||||
appendHighscoreEntries(builder);
|
||||
|
||||
appendCommunityCards(builder, state);
|
||||
@@ -64,8 +77,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 +92,13 @@ 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, avoid duplicate writes for the same winner in one hand.
|
||||
Set<String> uniqueWinners = new LinkedHashSet<>(winnerNames);
|
||||
for (String name : uniqueWinners) {
|
||||
if (name != null && !name.isBlank()) {
|
||||
HighscoreService.getInstance().appendWinner(name);
|
||||
}
|
||||
}
|
||||
|
||||
HighscoreService.getInstance().appendWinner(winner.getId().value());
|
||||
}
|
||||
|
||||
private static Player findPlayerByIndex(GameState state, int winnerIndex) {
|
||||
@@ -120,6 +140,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()) {
|
||||
|
||||
+66
-16
@@ -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.
|
||||
*
|
||||
|
||||
+32
-9
@@ -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();
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 2.2 MiB |
@@ -0,0 +1,726 @@
|
||||
/*main box css*/
|
||||
|
||||
.root {
|
||||
-fx-background-color: #000000;
|
||||
-fx-font-family: "Monospaced", "Courier New"; /* font */
|
||||
}
|
||||
|
||||
.background {
|
||||
-fx-background-color: #000000;
|
||||
-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
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -14,7 +14,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 +25,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"/>
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
<?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="themeBlackWhite"
|
||||
text="BLACK AND WHITE"
|
||||
styleClass="radio-button"/>
|
||||
|
||||
<RadioButton fx:id="themeGlass"
|
||||
text="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" />
|
||||
|
||||
+77
-4
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user