Feat: New Game UI Features:

- Call, Raise, Fold, Bet and Winner action are displayed in the Lobby Chat.
- More security features in the Game UI
- The Game UI is now even more Texas Hold'em friendly
This commit is contained in:
Julian Kropff
2026-04-26 16:58:23 +02:00
parent 8ca41c5ab4
commit fb68ecc777
2 changed files with 414 additions and 51 deletions
@@ -1,6 +1,8 @@
package ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui; package ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui;
import ch.unibas.dmi.dbis.cs108.casono.client.chat.ChatController; import ch.unibas.dmi.dbis.cs108.casono.client.chat.ChatController;
import ch.unibas.dmi.dbis.cs108.casono.client.chat.ChatType;
import ch.unibas.dmi.dbis.cs108.casono.client.chat.Message;
import ch.unibas.dmi.dbis.cs108.casono.client.game.Card; import ch.unibas.dmi.dbis.cs108.casono.client.game.Card;
import ch.unibas.dmi.dbis.cs108.casono.client.game.GameService; import ch.unibas.dmi.dbis.cs108.casono.client.game.GameService;
import ch.unibas.dmi.dbis.cs108.casono.client.game.GameState; import ch.unibas.dmi.dbis.cs108.casono.client.game.GameState;
@@ -142,14 +144,18 @@ public class CasinoGameController {
private static final String ACTIVE_CARD_BOX_STYLE_CLASS = "player-cards-active-turn"; private static final String ACTIVE_CARD_BOX_STYLE_CLASS = "player-cards-active-turn";
private static final int DEALER_OFFSET = 3; private static final int DEALER_OFFSET = 3;
private static final String FIRST_PREFLOP_INFO_TEMPLATE = private static final String FIRST_PREFLOP_INFO_TEMPLATE =
"Set the big blind amount: %d$ or %d$"; "Bet the big blind amount: %d$ or +%d$";
private String chatUsername; private String chatUsername;
private ClientService chatClientService; private ClientService chatClientService;
private int chatLobbyId = -1; private int chatLobbyId = -1;
private boolean chatInitialized; private boolean chatInitialized;
private boolean gameFinished; private boolean gameFinished;
private boolean localWinAnnouncementSent;
private String lastWinnerName; private String lastWinnerName;
private static final int SMALL_BLIND = 100;
private static final int BIG_BLIND = 200;
private static final int NO_BET = 0;
/** Standard constructor. Used by FXML. */ /** Standard constructor. Used by FXML. */
public CasinoGameController() { public CasinoGameController() {
@@ -201,11 +207,18 @@ public class CasinoGameController {
if (controller != null && myPlayerId != null) { if (controller != null && myPlayerId != null) {
controller.setGameService(gameService, myPlayerId); controller.setGameService(gameService, myPlayerId);
} }
configureTaskbarLobbyActionAnnouncements();
} }
/**
* Set the PlayerId of the current player.
*
* @param chatController The ChatController instance used for managing the chat functionality.
*/
public void startChat(ChatController chatController) { public void startChat(ChatController chatController) {
this.chatController = chatController; this.chatController = chatController;
initializeChatIfPossible(); initializeChatIfPossible();
configureTaskbarLobbyActionAnnouncements();
} }
/** Set the PlayerId of the current player. */ /** Set the PlayerId of the current player. */
@@ -217,6 +230,7 @@ public class CasinoGameController {
if (taskbarController != null && gameService != null && myPlayerId != null) { if (taskbarController != null && gameService != null && myPlayerId != null) {
taskbarController.setGameService(gameService, myPlayerId); taskbarController.setGameService(gameService, myPlayerId);
} }
configureTaskbarLobbyActionAnnouncements();
if (communityCardsBox == null) { if (communityCardsBox == null) {
LOGGER.warning("communityCardsBox is NULL"); LOGGER.warning("communityCardsBox is NULL");
@@ -270,6 +284,7 @@ public class CasinoGameController {
this.chatUsername = username; this.chatUsername = username;
this.chatClientService = clientService; this.chatClientService = clientService;
this.chatLobbyId = lobbyId; this.chatLobbyId = lobbyId;
configureTaskbarLobbyActionAnnouncements();
} }
/** /**
@@ -512,6 +527,7 @@ public class CasinoGameController {
highlightDealer(s); highlightDealer(s);
updateTaskbar(s); updateTaskbar(s);
clearActiveTurnHighlights(); clearActiveTurnHighlights();
announceLocalWinIfNeeded(s);
finishGameUiLoop(); finishGameUiLoop();
return; return;
} }
@@ -683,11 +699,11 @@ public class CasinoGameController {
* Returns a context-specific info message for the first player to act preflop. * Returns a context-specific info message for the first player to act preflop.
* *
* @param s The current game state. * @param s The current game state.
* @return An English hint with the big blind amount and its double, or null if the special * @return An English hint with the big blind amount and its double, or null if the special case
* case does not apply. * does not apply.
*/ */
private String resolveFirstPreflopInfo(GameState s) { private String resolveFirstPreflopInfo(GameState s) {
if (s == null || myPlayerId == null || s.players == null || s.players.size() < 2) { if (s == null || s.players == null || s.players.size() < 2) {
return null; return null;
} }
@@ -695,22 +711,9 @@ public class CasinoGameController {
return null; return null;
} }
int myIndex = -1;
for (int i = 0; i < s.players.size(); i++) {
Player player = s.players.get(i);
if (player != null && myPlayerId.equals(player.getId())) {
myIndex = i;
break;
}
}
if (myIndex < 0) {
return null;
}
int firstIndex = int firstIndex =
(s.players.size() == 2) ? s.dealer : (s.dealer + DEALER_OFFSET) % s.players.size(); (s.players.size() == 2) ? s.dealer : (s.dealer + DEALER_OFFSET) % s.players.size();
if (s.activePlayer != firstIndex || myIndex != firstIndex) { if (s.activePlayer != firstIndex) {
return null; return null;
} }
@@ -724,7 +727,8 @@ public class CasinoGameController {
} }
long raiseTarget = (long) callTarget * 2L; long raiseTarget = (long) callTarget * 2L;
int safeRaiseTarget = raiseTarget > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) raiseTarget; int safeRaiseTarget =
raiseTarget > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) raiseTarget;
return String.format(FIRST_PREFLOP_INFO_TEMPLATE, callTarget, safeRaiseTarget); return String.format(FIRST_PREFLOP_INFO_TEMPLATE, callTarget, safeRaiseTarget);
} }
@@ -746,11 +750,11 @@ public class CasinoGameController {
} }
int bet = Math.max(0, player.getBet()); int bet = Math.max(0, player.getBet());
if (bet == 100) { if (bet == SMALL_BLIND) {
sbCount++; sbCount++;
} else if (bet == 200) { } else if (bet == BIG_BLIND) {
bbCount++; bbCount++;
} else if (bet == 0) { } else if (bet == NO_BET) {
zeroCount++; zeroCount++;
} }
} }
@@ -1089,6 +1093,39 @@ public class CasinoGameController {
return null; return null;
} }
/**
* Sends a short lobby chat message when the local player wins, but only once per hand.
*
* @param s The current game state.
*/
private void announceLocalWinIfNeeded(GameState s) {
if (localWinAnnouncementSent || s == null || !isTerminalPhase(s.phase)) {
return;
}
Player winner = resolveWinner(s);
boolean isLocalWinner =
winner != null
&& winner.getId() != null
&& myPlayerId != null
&& myPlayerId.equals(winner.getId());
if (!isLocalWinner && lastWinnerName != null && !lastWinnerName.isBlank()) {
String localName =
(chatUsername != null && !chatUsername.isBlank())
? chatUsername
: (winner != null ? winner.getName() : null);
isLocalWinner = localName != null && localName.equalsIgnoreCase(lastWinnerName);
}
if (!isLocalWinner) {
return;
}
localWinAnnouncementSent = true;
sendLobbyActionMessage("I won");
}
/** /**
* Determine if the game is in a finished state based on the game state and the presence of a * Determine if the game is in a finished state based on the game state and the presence of a
* winner name. * winner name.
@@ -1736,6 +1773,47 @@ public class CasinoGameController {
if (controller != null && gameService != null && myPlayerId != null) { if (controller != null && gameService != null && myPlayerId != null) {
controller.setGameService(gameService, myPlayerId); controller.setGameService(gameService, myPlayerId);
} }
configureTaskbarLobbyActionAnnouncements();
}
/**
* Configure the TaskbarController to announce lobby actions by sending chat messages when
* certain actions occur in the game, such as winning a hand.
*/
private void configureTaskbarLobbyActionAnnouncements() {
TaskbarController controller = resolveTaskbarController();
if (controller == null) {
return;
}
controller.setLobbyActionAnnouncer(this::sendLobbyActionMessage);
}
/**
* Send a lobby action message to the chat controller with the specified action label, which can
* be used to announce game events in the lobby chat.
*
* @param actionLabel The label describing the action to announce in the lobby chat, such as "I
* won" when the local player wins a hand.
*/
private void sendLobbyActionMessage(String actionLabel) {
if (chatController == null
|| chatLobbyId < 0
|| actionLabel == null
|| actionLabel.isBlank()) {
return;
}
String sender =
(chatUsername != null && !chatUsername.isBlank())
? chatUsername
: chatController.getCurrentUsername();
if (sender == null || sender.isBlank()) {
return;
}
Message message =
new Message(ChatType.LOBBY, chatLobbyId, sender, null, actionLabel.trim());
chatController.onSendToNetwork(message);
} }
/** /**
@@ -11,6 +11,8 @@ import ch.unibas.dmi.dbis.cs108.casono.client.ui.lobbyui.Casinomainui;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.util.Optional; import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.UnaryOperator;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.scene.control.Alert; import javafx.scene.control.Alert;
import javafx.scene.control.Button; import javafx.scene.control.Button;
@@ -18,6 +20,8 @@ import javafx.scene.control.ButtonBar;
import javafx.scene.control.ButtonType; import javafx.scene.control.ButtonType;
import javafx.scene.control.Label; import javafx.scene.control.Label;
import javafx.scene.control.TextField; import javafx.scene.control.TextField;
import javafx.scene.control.TextFormatter;
import javafx.scene.control.Tooltip;
import javafx.scene.image.Image; import javafx.scene.image.Image;
import javafx.scene.image.ImageView; import javafx.scene.image.ImageView;
import javafx.scene.input.KeyCode; import javafx.scene.input.KeyCode;
@@ -71,6 +75,11 @@ public class TaskbarController {
private static final double ALERT_LOGO_HEIGHT = 40.0; private static final double ALERT_LOGO_HEIGHT = 40.0;
private boolean inputActionAllowed; private boolean inputActionAllowed;
private int lastReferenceBet = BIG_BLIND; private int lastReferenceBet = BIG_BLIND;
private Consumer<String> lobbyActionAnnouncer;
private String lastPhase;
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;
/** Standard constructor. Used by FXML. */ /** Standard constructor. Used by FXML. */
public TaskbarController() { public TaskbarController() {
@@ -223,6 +232,11 @@ public class TaskbarController {
this.myPlayerId = myPlayerId; this.myPlayerId = myPlayerId;
} }
/** Sets an optional callback that publishes simple action labels to lobby chat. */
public void setLobbyActionAnnouncer(Consumer<String> lobbyActionAnnouncer) {
this.lobbyActionAnnouncer = lobbyActionAnnouncer;
}
/** /**
* Initializes the taskbar controller. This method is called automatically after the FXML * Initializes the taskbar controller. This method is called automatically after the FXML
* components have been loaded. * components have been loaded.
@@ -230,15 +244,102 @@ public class TaskbarController {
@FXML @FXML
public void initialize() { public void initialize() {
updateBasicButtons(false, false); updateBasicButtons(false, false);
if (taskbarInput != null) { if (taskbarInput != null) {
UnaryOperator<TextFormatter.Change> filter =
change -> {
String newText = change.getControlNewText();
if (newText.length() > MAX_INPUT_LENGTH) {
return null;
}
if (!newText.matches("\\d*")) {
return null;
}
return change;
};
taskbarInput.setTextFormatter(new TextFormatter<>(filter));
taskbarInput taskbarInput
.textProperty() .textProperty()
.addListener((obs, oldValue, newValue) -> refreshBetInputUi()); .addListener((obs, oldValue, newValue) -> refreshBetInputUi());
} }
Tooltip callTooltip = new Tooltip();
callButton.setTooltip(callTooltip);
callButton.setOnMouseEntered(e -> callTooltip.setText(previewCall()));
Tooltip raiseTooltip = new Tooltip();
raiseButton.setTooltip(raiseTooltip);
raiseButton.setOnMouseEntered(e -> raiseTooltip.setText(previewRaise()));
setBetButtonVisible(false); setBetButtonVisible(false);
} }
/**
* Generates a preview string for the Call action, showing the amount that would be called based
* on the current game state and the player's bet status.
*
* @return A string representing the Call action preview, including the amount that would be
* called, or an empty string if the game state is not available.
*/
private String previewCall() {
if (lastState == null) {
return "";
}
int callTarget = effectiveCallTarget(lastState);
Player me = findCurrentPlayer(lastState);
int alreadyInvested = (me != null) ? Math.max(0, me.getBet()) : 0;
int amount = Math.max(0, callTarget - alreadyInvested);
return "CALL " + amount;
}
/**
* Generates a preview string for the Raise action, showing the amount that would be raised.
*
* @return A string representing the Raise action preview, including the amount that would be
* raised.
*/
private String previewRaise() {
if (lastState == null) {
return "";
}
// int callTarget = effectiveCallTarget(lastState);
// Player me = findCurrentPlayer(lastState);
// int alreadyInvested = (me != null) ? Math.max(0, me.getBet()) : 0;
// Integer targetBet = resolveTargetBet(ActionType.RAISE_BUTTON, lastState);
// if (targetBet == null) {
// return "";
// }
// int totalContribution = Math.max(0, targetBet);
GameState state = ensureLatestStateForAction(ActionType.CALL_BUTTON.name().toLowerCase());
// int raiseBy = Math.max(0, targetBet);
int raiseBy = effectiveCallTarget(state);
int callTarget = effectiveCallTarget(lastState);
Player me = findCurrentPlayer(lastState);
int alreadyInvested = (me != null) ? Math.max(0, me.getBet()) : 0;
int amount = Math.max(0, callTarget - alreadyInvested);
int raiseamout = amount + raiseBy;
return "RAISE +" + raiseBy + " TO " + raiseamout;
}
/** /**
* Updates the taskbar based on the current game state and the player's status. * Updates the taskbar based on the current game state and the player's status.
* *
@@ -258,6 +359,7 @@ public class TaskbarController {
} }
this.lastState = state; this.lastState = state;
if (state.currentBet > 0) { if (state.currentBet > 0) {
lastReferenceBet = state.currentBet; lastReferenceBet = state.currentBet;
} }
@@ -279,8 +381,14 @@ public class TaskbarController {
boolean isOut = me.getState() == PlayerState.FOLDED; boolean isOut = me.getState() == PlayerState.FOLDED;
boolean isGameFinished = isHandFinished(state); boolean isGameFinished = isHandFinished(state);
boolean phaseChanged = lastPhase == null || !lastPhase.equalsIgnoreCase(state.phase);
lastPhase = state.phase;
updateBasicButtons(isMyTurn, isOut || isGameFinished); updateBasicButtons(isMyTurn, isOut || isGameFinished);
applyActionAvailability(state, me, isMyTurn, isOut || isGameFinished);
applyActionAvailability(state, me, isMyTurn, isOut || isGameFinished, phaseChanged);
setMoney(me.getChips()); setMoney(me.getChips());
refreshBetInputUi(); refreshBetInputUi();
} }
@@ -297,7 +405,14 @@ public class TaskbarController {
* if the game is finished, which disables actions. * if the game is finished, which disables actions.
*/ */
private void applyActionAvailability( private void applyActionAvailability(
GameState state, Player me, boolean isMyTurn, boolean isOutOrFinished) { GameState state,
Player me,
boolean isMyTurn,
boolean isOutOrFinished,
boolean phaseChanged) {
// boolean firstPlayerNewRound =
// phaseChanged && isFirstPreflopPlayerInputOnly(state, me);
inputActionAllowed = false; inputActionAllowed = false;
if (!isMyTurn || isOutOrFinished || state == null || me == null) { if (!isMyTurn || isOutOrFinished || state == null || me == null) {
setBetButtonVisible(false); setBetButtonVisible(false);
@@ -318,7 +433,17 @@ public class TaskbarController {
return; return;
} }
if (isFirstPreflopPlayerInputOnly(state, me)) { // if (isFirstPreflopPlayerInputOnly(state, me)) {
// setActionEnabled(betButton, true);
// setActionEnabled(callButton, false);
// setActionEnabled(foldButton, false);
// setActionEnabled(raiseButton, false);
// activateInputField(taskbarInput);
// inputActionAllowed = true;
// return;
// }
if (isFirstPlayerOfPhase(state, me)) {
setActionEnabled(betButton, true); setActionEnabled(betButton, true);
setActionEnabled(callButton, false); setActionEnabled(callButton, false);
setActionEnabled(foldButton, false); setActionEnabled(foldButton, false);
@@ -363,7 +488,16 @@ public class TaskbarController {
if (value > Integer.MAX_VALUE / 2) { if (value > Integer.MAX_VALUE / 2) {
return Integer.MAX_VALUE; return Integer.MAX_VALUE;
} }
return value * 2;
int callTarget = effectiveCallTarget(lastState);
Player me = findCurrentPlayer(lastState);
int alreadyInvested = (me != null) ? Math.max(0, me.getBet()) : 0;
int amount = Math.max(0, callTarget - alreadyInvested);
int raisevalue = amount + value;
return raisevalue;
} }
/** /**
@@ -441,10 +575,22 @@ public class TaskbarController {
* @return An integer representing the effective call target for the current game state. * @return An integer representing the effective call target for the current game state.
*/ */
private int effectiveCallTarget(GameState state) { private int effectiveCallTarget(GameState state) {
if (state != null && state.currentBet > 0) { int currentBet = (state != null) ? Math.max(0, state.currentBet) : 0;
return state.currentBet; int rememberedBet = Math.max(0, lastReferenceBet);
if (state != null && isPreflop(state.phase) && isInitialPreflopBlindLayout(state)) {
return currentBet;
} }
return Math.max(0, lastReferenceBet);
if (currentBet <= 0) {
return rememberedBet;
}
if (rememberedBet <= 0) {
return currentBet;
}
return Math.max(currentBet, rememberedBet);
} }
/** /**
@@ -483,6 +629,46 @@ public class TaskbarController {
return isInitialPreflopBlindLayout(state); return isInitialPreflopBlindLayout(state);
} }
/**
* 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.
*
* @param state The current GameState object representing the state of the game, which includes
* information about
* @param me The Player object representing the current player, used to determine their
* position.
* @return A boolean value indicating whether the current player is the first to act in the
* current phase of the game.
*/
private boolean isFirstPlayerOfPhase(GameState state, Player me) {
if (state == null || me == null || state.players == null) {
return false;
}
int size = state.players.size();
if (size < 2) {
return false;
}
int myIndex = state.players.indexOf(me);
if (myIndex < 0) {
return false;
}
int dealer = state.dealer;
int firstIndex;
if (isPreflop(state.phase)) {
firstIndex = (size == 2) ? dealer : (dealer + DEALER_OFFSET) % size;
} else {
firstIndex = (dealer + 1) % size;
}
return state.activePlayer == firstIndex && myIndex == firstIndex;
}
/** /**
* Checks if the initial blind layout is still in place during the pre-flop phase. * Checks if the initial blind layout is still in place during the pre-flop phase.
* *
@@ -492,6 +678,10 @@ public class TaskbarController {
* the pre-flop phase. * the pre-flop phase.
*/ */
private boolean isInitialPreflopBlindLayout(GameState state) { private boolean isInitialPreflopBlindLayout(GameState state) {
if (state == null || state.players == null || state.players.isEmpty()) {
return false;
}
int sbCount = 0; int sbCount = 0;
int bbCount = 0; int bbCount = 0;
int zeroCount = 0; int zeroCount = 0;
@@ -652,7 +842,7 @@ public class TaskbarController {
return; return;
} }
executeAction(state, targetBet); executeAction(state, targetBet, actionType);
if (targetBet > 0) { if (targetBet > 0) {
lastReferenceBet = targetBet; lastReferenceBet = targetBet;
@@ -682,7 +872,8 @@ public class TaskbarController {
} }
if (actionType == ActionType.RAISE_BUTTON) { if (actionType == ActionType.RAISE_BUTTON) {
return safeDouble(callTarget); Integer input = parseInputTarget(taskbarInput.getText());
return input != null ? input : callTarget + BIG_BLIND;
} }
return parseInputTarget(taskbarInput.getText()); return parseInputTarget(taskbarInput.getText());
@@ -728,19 +919,78 @@ public class TaskbarController {
* @param targetBet The integer value representing the target bet amount for the action being * @param targetBet The integer value representing the target bet amount for the action being
* executed, which is used to determine * executed, which is used to determine
*/ */
private void executeAction(GameState state, int targetBet) { private void executeAction(GameState state, int targetBet, ActionType actionType) {
int callTarget = effectiveCallTarget(state); int callTarget = effectiveCallTarget(state);
String phase = (state != null ? state.phase : "UNKNOWN");
int alreadyInvested =
(findCurrentPlayer(state) != null)
? Math.max(0, findCurrentPlayer(state).getBet())
: 0;
if (actionType == ActionType.CALL_BUTTON) {
int amount = Math.max(0, callTarget - alreadyInvested);
gameService.call();
LOGGER.info("[{}] CALL +{} (total to {})", phase, amount, callTarget);
announceLobbyAction("CALL " + amount);
return;
}
if (actionType == ActionType.RAISE_BUTTON) {
int raiseBy = Math.max(0, targetBet - callTarget);
gameService.raise(raiseBy);
LOGGER.info("[{}] RAISE +{} (target={})", phase, raiseBy, targetBet);
announceLobbyAction("RAISE +" + raiseBy + " TO " + targetBet);
return;
}
if (state.currentBet <= 0) { if (state.currentBet <= 0) {
gameService.bet(targetBet); gameService.bet(targetBet);
LOGGER.info("BET {}", targetBet);
} else if (targetBet == callTarget) { LOGGER.info("[{}] BET {}", phase, targetBet);
announceLobbyAction("BET " + targetBet);
return;
}
if (targetBet == callTarget) {
int amount = Math.max(0, callTarget - alreadyInvested);
gameService.call(); gameService.call();
LOGGER.info("CALL {}", targetBet);
LOGGER.info("[{}] CALL +{} (total to {})", phase, amount, callTarget);
announceLobbyAction("CALL " + amount);
} else { } else {
int raiseBy = Math.max(0, targetBet - state.currentBet);
int raiseBy = Math.max(0, targetBet - callTarget);
gameService.raise(raiseBy); gameService.raise(raiseBy);
LOGGER.info("RAISE {} (target={})", raiseBy, targetBet);
LOGGER.info("[{}] RAISE +{} (target={})", phase, raiseBy, targetBet);
announceLobbyAction("RAISE +" + raiseBy + " TO " + targetBet);
}
}
/** Publishes a simple action label to the lobby chat, if a publisher is configured. */
private void announceLobbyAction(String actionLabel) {
if (lobbyActionAnnouncer == null || actionLabel == null || actionLabel.isBlank()) {
return;
}
try {
lobbyActionAnnouncer.accept(actionLabel.trim());
} catch (RuntimeException ex) {
LOGGER.warn("Could not publish lobby action '{}': {}", actionLabel, ex.getMessage());
} }
} }
@@ -757,8 +1007,15 @@ public class TaskbarController {
if (text == null || text.trim().isEmpty()) { if (text == null || text.trim().isEmpty()) {
return null; return null;
} }
try { try {
return Integer.parseInt(text.trim()); int value = Integer.parseInt(text.trim());
if (value % SMALL_BLIND != 0) {
return null;
}
return value;
} catch (NumberFormatException e) { } catch (NumberFormatException e) {
return null; return null;
} }
@@ -803,21 +1060,44 @@ public class TaskbarController {
* triggers a warning, or if it is blocked due to being invalid or too risky. * triggers a warning, or if it is blocked due to being invalid or too risky.
*/ */
private ValidationResult validateTarget(GameState state, Player me, int targetBet) { private ValidationResult validateTarget(GameState state, Player me, int targetBet) {
if (state == null || me == null) { if (state == null || me == null) {
return ValidationResult.blocked("Invalid game state"); return ValidationResult.blocked("Invalid game state");
} }
int callTarget = effectiveCallTarget(state); final int callTarget = effectiveCallTarget(state);
int raiseTarget = safeDouble(callTarget);
if (targetBet != callTarget && targetBet != raiseTarget) { final int alreadyInvested = Math.max(0, me.getBet());
return ValidationResult.blocked("Only calls or exactly two raises allowed"); final int chips = Math.max(0, me.getChips());
final int required = Math.max(0, targetBet - alreadyInvested);
final int minRequiredToCall = Math.max(0, callTarget - alreadyInvested);
if (required < minRequiredToCall) {
return ValidationResult.blocked("Bet must match at least the call amount");
} }
int alreadyInvested = Math.max(0, me.getBet()); if (isFirstPlayerOfPhase(state, me)) {
int required = Math.max(0, targetBet - alreadyInvested); if (required > FIRST_PLAYER_MAX_BET) {
int chips = Math.max(0, me.getChips()); return ValidationResult.blocked(
"First player cannot bet more than " + FIRST_PLAYER_MAX_BET);
}
}
if (targetBet == callTarget && required == 0) { if (isPreflop(state.phase)) {
int maxAllowed = (int) Math.floor(me.getChips() * MAX_CHIP_PERCENT);
if (required > maxAllowed) {
return ValidationResult.blocked(
"Preflop: You can only bet up to 30% of your stack (" + maxAllowed + ")");
}
}
if (required == minRequiredToCall) {
if (required > chips) {
return ValidationResult.blocked("Not enough chips to call");
}
return ValidationResult.ok(); return ValidationResult.ok();
} }
@@ -826,16 +1106,19 @@ public class TaskbarController {
} }
if (required > chips) { if (required > chips) {
return ValidationResult.blocked("Not enough chips for the next bet"); return ValidationResult.blocked("Not enough chips for this bet");
} }
BetRisk risk = evaluateBetRisk(state, required, chips); BetRisk risk = evaluateBetRisk(state, required, chips);
if (risk == BetRisk.BLOCKED) { if (risk == BetRisk.BLOCKED) {
return ValidationResult.blocked("Assignment for this phase is blocked"); return ValidationResult.blocked("Bet too risky for this phase");
} }
if (risk == BetRisk.WARNING) { if (risk == BetRisk.WARNING) {
return ValidationResult.warning(warningTextForPhase(state)); return ValidationResult.warning(warningTextForPhase(state));
} }
return ValidationResult.ok(); return ValidationResult.ok();
} }
@@ -980,8 +1263,6 @@ public class TaskbarController {
* Called while dragging the taskbar with the mouse. Updates the position and slightly scales * Called while dragging the taskbar with the mouse. Updates the position and slightly scales
* the taskbar for visual feedback. * the taskbar for visual feedback.
* *
* <p>TODO: It still needs to be fixed that the taskbar cannot disappear out of the window.
*
* @param event Das Mausereignis * @param event Das Mausereignis
*/ */
@FXML @FXML
@@ -1099,7 +1380,7 @@ public class TaskbarController {
/** Called when the Call button is clicked. */ /** Called when the Call button is clicked. */
@FXML @FXML
private void onInputPlayerCall() { private void onInputPlayerCall() {
submitPresetInputAndProcess("call"); submitAction(ActionType.CALL_BUTTON);
} }
/** Called when the Fold button is clicked. */ /** Called when the Fold button is clicked. */
@@ -1120,6 +1401,7 @@ public class TaskbarController {
gameService.fold(); gameService.fold();
LOGGER.info("Player FOLD"); LOGGER.info("Player FOLD");
announceLobbyAction("FOLD");
refreshGame(); refreshGame();
} }
@@ -1233,6 +1515,9 @@ public class TaskbarController {
GameState refreshed = gameService.refresh(); GameState refreshed = gameService.refresh();
if (refreshed != null) { if (refreshed != null) {
lastState = refreshed; lastState = refreshed;
if (refreshed.currentBet > 0 && refreshed.currentBet >= lastReferenceBet) {
lastReferenceBet = refreshed.currentBet;
}
return refreshed; return refreshed;
} }
} catch (Exception e) { } catch (Exception e) {