Merge branch 'fix/game-turn-and-action-bugs' into 'main'
Fix: improve round management, betting validation and game engine test coverage Closes #118 See merge request cs108-fs26/Gruppe-13!134
This commit was merged in pull request #290.
This commit is contained in:
+4
-3
@@ -633,13 +633,14 @@ public class CasinoGameController {
|
|||||||
*/
|
*/
|
||||||
private void updateTaskbar(GameState s) {
|
private void updateTaskbar(GameState s) {
|
||||||
TaskbarController controller = resolveTaskbarController();
|
TaskbarController controller = resolveTaskbarController();
|
||||||
if (controller != null) {
|
if (controller == null || myPlayerId == null) {
|
||||||
if (gameService != null && myPlayerId != null) {
|
return;
|
||||||
|
}
|
||||||
|
if (gameService != null) {
|
||||||
controller.setGameService(gameService, myPlayerId);
|
controller.setGameService(gameService, myPlayerId);
|
||||||
}
|
}
|
||||||
controller.update(s, myPlayerId);
|
controller.update(s, myPlayerId);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the opponent player slots based on the list of players in the game state. This method
|
* Update the opponent player slots based on the list of players in the game state. This method
|
||||||
|
|||||||
+687
-82
@@ -7,6 +7,7 @@ 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.game.PlayerState;
|
||||||
import ch.unibas.dmi.dbis.cs108.casono.client.ui.lobbyui.Casinomainui;
|
import ch.unibas.dmi.dbis.cs108.casono.client.ui.lobbyui.Casinomainui;
|
||||||
import javafx.fxml.FXML;
|
import javafx.fxml.FXML;
|
||||||
|
import javafx.scene.control.Alert;
|
||||||
import javafx.scene.control.Button;
|
import javafx.scene.control.Button;
|
||||||
import javafx.scene.control.Label;
|
import javafx.scene.control.Label;
|
||||||
import javafx.scene.control.TextField;
|
import javafx.scene.control.TextField;
|
||||||
@@ -25,7 +26,7 @@ import org.apache.logging.log4j.Logger;
|
|||||||
*/
|
*/
|
||||||
public class TaskbarController {
|
public class TaskbarController {
|
||||||
|
|
||||||
private static final Logger LOGGER = LogManager.getLogger(CasinoBrowserController.class);
|
private static final Logger LOGGER = LogManager.getLogger(TaskbarController.class);
|
||||||
|
|
||||||
@FXML private HBox taskbar;
|
@FXML private HBox taskbar;
|
||||||
@FXML private TextField taskbarInput;
|
@FXML private TextField taskbarInput;
|
||||||
@@ -41,9 +42,13 @@ public class TaskbarController {
|
|||||||
private double xOffset = 0;
|
private double xOffset = 0;
|
||||||
private double yOffset = 0;
|
private double yOffset = 0;
|
||||||
private static final double TASKBAR_SCALE = 0.95;
|
private static final double TASKBAR_SCALE = 0.95;
|
||||||
private static final int MIN_CREDITS = 5;
|
private static final double PREFLOP_BLOCK_RATIO = 0.50;
|
||||||
private static final int MAX_CREDITS = 100000;
|
private static final double FLOP_WARN_RATIO = 0.50;
|
||||||
private static final int CREDIT_STEP = 5;
|
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 SMALL_BLIND = 100;
|
||||||
|
private static final int BIG_BLIND = 200;
|
||||||
private static final String STYLE_YELLOW_BUTTON = "yellow-button";
|
private static final String STYLE_YELLOW_BUTTON = "yellow-button";
|
||||||
private static final String STYLE_GRAY_BUTTON = "gray-button";
|
private static final String STYLE_GRAY_BUTTON = "gray-button";
|
||||||
private static final String STYLE_RED_BUTTON = "red-button";
|
private static final String STYLE_RED_BUTTON = "red-button";
|
||||||
@@ -51,6 +56,9 @@ public class TaskbarController {
|
|||||||
private static final String STYLE_GRAY_INPUT = "gray-input-field";
|
private static final String STYLE_GRAY_INPUT = "gray-input-field";
|
||||||
private static final String STYLE_RED_INPUT = "red-input-field";
|
private static final String STYLE_RED_INPUT = "red-input-field";
|
||||||
private static final double SCALE_NORMAL = 1.0;
|
private static final double SCALE_NORMAL = 1.0;
|
||||||
|
private static final int DEALER_OFFSET = 3;
|
||||||
|
private boolean inputActionAllowed;
|
||||||
|
private int lastReferenceBet = BIG_BLIND;
|
||||||
|
|
||||||
/** Standard constructor. Used by FXML. */
|
/** Standard constructor. Used by FXML. */
|
||||||
public TaskbarController() {
|
public TaskbarController() {
|
||||||
@@ -58,7 +66,7 @@ public class TaskbarController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the state of the basic action buttons (Bet, Call, Fold, Raise) and the input field
|
* Updates the state of the basic action buttons (Bet, Call, Fold, Raise) and the input field.
|
||||||
*
|
*
|
||||||
* @param isMyTurn Indicates whether it is currently the player's turn.
|
* @param isMyTurn Indicates whether it is currently the player's turn.
|
||||||
* @param isOut Indicates whether the player is currently out of the game (folded or all-in).
|
* @param isOut Indicates whether the player is currently out of the game (folded or all-in).
|
||||||
@@ -90,9 +98,10 @@ public class TaskbarController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the given button to a disabled state with red styling, indicating that the player is out
|
* Sets the given button to a disabled state with red styling, indicating that the player is
|
||||||
|
* out.
|
||||||
*
|
*
|
||||||
* @param b The button to be styled as red and disabled
|
* @param b The button to be styled as red and disabled.
|
||||||
*/
|
*/
|
||||||
private void setRedButton(Button b) {
|
private void setRedButton(Button b) {
|
||||||
b.setDisable(true);
|
b.setDisable(true);
|
||||||
@@ -107,9 +116,9 @@ public class TaskbarController {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the given input field to a disabled state with red styling, indicating that the player
|
* Sets the given input field to a disabled state with red styling, indicating that the player
|
||||||
* is out
|
* is out.
|
||||||
*
|
*
|
||||||
* @param t The text field to be styled as red and disabled
|
* @param t The text field to be styled as red and disabled.
|
||||||
*/
|
*/
|
||||||
private void setRedInputField(TextField t) {
|
private void setRedInputField(TextField t) {
|
||||||
t.setDisable(true);
|
t.setDisable(true);
|
||||||
@@ -124,9 +133,9 @@ public class TaskbarController {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Activates the given button by enabling it and applying yellow styling, indicating that it is
|
* Activates the given button by enabling it and applying yellow styling, indicating that it is
|
||||||
* the player's turn
|
* the player's turn.
|
||||||
*
|
*
|
||||||
* @param b The button to be activated and styled for the player's turn
|
* @param b The button to be activated and styled for the player's turn.
|
||||||
*/
|
*/
|
||||||
private void activateButton(Button b) {
|
private void activateButton(Button b) {
|
||||||
b.setDisable(false);
|
b.setDisable(false);
|
||||||
@@ -140,9 +149,9 @@ public class TaskbarController {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Activates the given input field by enabling it and applying yellow styling, indicating that
|
* Activates the given input field by enabling it and applying yellow styling, indicating that
|
||||||
* it is the player's turn
|
* it is the player's turn.
|
||||||
*
|
*
|
||||||
* @param t The text field to be activated and styled for the player's turn
|
* @param t The text field to be activated and styled for the player's turn.
|
||||||
*/
|
*/
|
||||||
private void activateInputField(TextField t) {
|
private void activateInputField(TextField t) {
|
||||||
t.setDisable(false);
|
t.setDisable(false);
|
||||||
@@ -156,9 +165,9 @@ public class TaskbarController {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Deactivates the given button by disabling it and applying gray styling, indicating that it is
|
* Deactivates the given button by disabling it and applying gray styling, indicating that it is
|
||||||
* not the player's turn
|
* not the player's turn.
|
||||||
*
|
*
|
||||||
* @param b The button to be deactivated and styled for non-active state
|
* @param b The button to be deactivated and styled for non-active state.
|
||||||
*/
|
*/
|
||||||
private void deactivateButton(Button b) {
|
private void deactivateButton(Button b) {
|
||||||
b.setDisable(true);
|
b.setDisable(true);
|
||||||
@@ -173,9 +182,9 @@ public class TaskbarController {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Deactivates the given input field by disabling it and applying gray styling, indicating that
|
* Deactivates the given input field by disabling it and applying gray styling, indicating that
|
||||||
* it is not the player's turn
|
* it is not the player's turn.
|
||||||
*
|
*
|
||||||
* @param t The text field to be deactivated and styled for non-active state
|
* @param t The text field to be deactivated and styled for non-active state.
|
||||||
*/
|
*/
|
||||||
private void deactivateInputField(TextField t) {
|
private void deactivateInputField(TextField t) {
|
||||||
t.setDisable(true);
|
t.setDisable(true);
|
||||||
@@ -209,15 +218,25 @@ public class TaskbarController {
|
|||||||
@FXML
|
@FXML
|
||||||
public void initialize() {
|
public void initialize() {
|
||||||
updateBasicButtons(false, false);
|
updateBasicButtons(false, false);
|
||||||
|
if (taskbarInput != null) {
|
||||||
|
taskbarInput
|
||||||
|
.textProperty()
|
||||||
|
.addListener((obs, oldValue, newValue) -> refreshBetInputUi());
|
||||||
|
}
|
||||||
|
|
||||||
|
setBetButtonVisible(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the taskbar based on the current game state and the player's status. It checks if
|
* Updates the taskbar based on the current game state and the player's status.
|
||||||
* it's the player's turn and whether they are out of the game (folded or all-in) to adjust the
|
|
||||||
* button states and displayed money accordingly.
|
|
||||||
*
|
*
|
||||||
* @param state
|
* @param state The current GameState object representing the state of the game, which includes
|
||||||
* @param myPlayerId
|
* information about the players, their bets, the current phase and other relevant details
|
||||||
|
* needed to update the taskbar UI accurately based on the player's status and the game
|
||||||
|
* context.
|
||||||
|
* @param myPlayerId The PlayerId object representing the current player, used to identify the
|
||||||
|
* player's status and update the taskbar UI accordingly based on whether it is their turn
|
||||||
|
* and whether they are out of the game.
|
||||||
*/
|
*/
|
||||||
public void update(GameState state, PlayerId myPlayerId) {
|
public void update(GameState state, PlayerId myPlayerId) {
|
||||||
|
|
||||||
@@ -227,6 +246,9 @@ public class TaskbarController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.lastState = state;
|
this.lastState = state;
|
||||||
|
if (state.currentBet > 0) {
|
||||||
|
lastReferenceBet = state.currentBet;
|
||||||
|
}
|
||||||
|
|
||||||
Player me =
|
Player me =
|
||||||
state.players.stream()
|
state.players.stream()
|
||||||
@@ -243,9 +265,594 @@ public class TaskbarController {
|
|||||||
|
|
||||||
boolean isMyTurn = state.activePlayer == myIndex;
|
boolean isMyTurn = state.activePlayer == myIndex;
|
||||||
boolean isOut = me.getState() == PlayerState.FOLDED;
|
boolean isOut = me.getState() == PlayerState.FOLDED;
|
||||||
|
boolean isGameFinished = isHandFinished(state);
|
||||||
|
|
||||||
updateBasicButtons(isMyTurn, isOut);
|
updateBasicButtons(isMyTurn, isOut || isGameFinished);
|
||||||
|
applyActionAvailability(state, me, isMyTurn, isOut || isGameFinished);
|
||||||
setMoney(me.getChips());
|
setMoney(me.getChips());
|
||||||
|
refreshBetInputUi();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies the availability of actions (Bet, Call, Raise, Fold) based on the current game state.
|
||||||
|
*
|
||||||
|
* @param state The current game state to evaluate action availability.
|
||||||
|
* @param me The player object representing the current player, used to determine their chips
|
||||||
|
* and bet status.
|
||||||
|
* @param isMyTurn Indicates whether it is currently the player's turn, which affects whether
|
||||||
|
* actions can be taken.
|
||||||
|
* @param isOutOrFinished Indicates whether the player is out of the game (folded or all-in) or
|
||||||
|
* if the game is finished, which disables actions.
|
||||||
|
*/
|
||||||
|
private void applyActionAvailability(
|
||||||
|
GameState state, Player me, boolean isMyTurn, boolean isOutOrFinished) {
|
||||||
|
inputActionAllowed = false;
|
||||||
|
if (!isMyTurn || isOutOrFinished || state == null || me == null) {
|
||||||
|
setBetButtonVisible(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int chips = Math.max(0, me.getChips());
|
||||||
|
int toCall = getToCall(state, me);
|
||||||
|
|
||||||
|
boolean forcedFoldOnly = chips <= 0 || toCall > chips;
|
||||||
|
if (forcedFoldOnly) {
|
||||||
|
setActionEnabled(betButton, false);
|
||||||
|
setActionEnabled(callButton, false);
|
||||||
|
setActionEnabled(raiseButton, false);
|
||||||
|
setActionEnabled(foldButton, true);
|
||||||
|
deactivateInputField(taskbarInput);
|
||||||
|
setBetButtonVisible(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isFirstPreflopPlayerInputOnly(state, me)) {
|
||||||
|
setActionEnabled(betButton, true);
|
||||||
|
setActionEnabled(callButton, false);
|
||||||
|
setActionEnabled(foldButton, false);
|
||||||
|
setActionEnabled(raiseButton, false);
|
||||||
|
activateInputField(taskbarInput);
|
||||||
|
inputActionAllowed = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int callTarget = effectiveCallTarget(state);
|
||||||
|
int raiseTarget = safeDouble(callTarget);
|
||||||
|
ValidationResult callValidation = validateTarget(state, me, callTarget);
|
||||||
|
ValidationResult raiseValidation = validateTarget(state, me, raiseTarget);
|
||||||
|
boolean canCall = callValidation.valid;
|
||||||
|
boolean canRaise = raiseValidation.valid;
|
||||||
|
boolean canInput = canCall || canRaise;
|
||||||
|
|
||||||
|
setActionEnabled(betButton, canInput);
|
||||||
|
setActionEnabled(callButton, canCall);
|
||||||
|
setActionEnabled(raiseButton, canRaise);
|
||||||
|
setActionEnabled(foldButton, true);
|
||||||
|
|
||||||
|
if (canInput) {
|
||||||
|
activateInputField(taskbarInput);
|
||||||
|
inputActionAllowed = true;
|
||||||
|
} else {
|
||||||
|
deactivateInputField(taskbarInput);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Safely doubles the given value while preventing integer overflow.
|
||||||
|
*
|
||||||
|
* @param value The integer value to be safely doubled.
|
||||||
|
* @return The safely doubled value, or 0 if the input is non-positive, or Integer.MAX_VALUE if
|
||||||
|
* doubling would overflow.
|
||||||
|
*/
|
||||||
|
private int safeDouble(int value) {
|
||||||
|
if (value <= 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (value > Integer.MAX_VALUE / 2) {
|
||||||
|
return Integer.MAX_VALUE;
|
||||||
|
}
|
||||||
|
return value * 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the visibility of the Bet button in the taskbar.
|
||||||
|
*
|
||||||
|
* @param visible A boolean indicating whether the Bet button should be visible (true) or hidden
|
||||||
|
* (false).
|
||||||
|
*/
|
||||||
|
private void setBetButtonVisible(boolean visible) {
|
||||||
|
if (betButton == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
betButton.setVisible(visible);
|
||||||
|
betButton.setManaged(visible);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refreshes the user interface of the bet input field and the Bet button based on the current
|
||||||
|
* game state and the validity of the input.
|
||||||
|
*/
|
||||||
|
private void refreshBetInputUi() {
|
||||||
|
if (!inputActionAllowed || taskbarInput == null || taskbarInput.isDisabled()) {
|
||||||
|
setBetButtonVisible(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ValidationResult validation = validateTypedAmount(lastState, taskbarInput.getText());
|
||||||
|
boolean valid = validation.valid;
|
||||||
|
setBetButtonVisible(valid);
|
||||||
|
|
||||||
|
if (valid) {
|
||||||
|
activateButton(betButton);
|
||||||
|
} else {
|
||||||
|
deactivateButton(betButton);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enables or disables the given button based on the provided boolean value.
|
||||||
|
*
|
||||||
|
* @param button The Button object to be enabled or disabled based on the provided boolean
|
||||||
|
* value.
|
||||||
|
* @param enabled A boolean value indicating whether the button should be enabled (true) or
|
||||||
|
* disabled (false).
|
||||||
|
*/
|
||||||
|
private void setActionEnabled(Button button, boolean enabled) {
|
||||||
|
if (enabled) {
|
||||||
|
activateButton(button);
|
||||||
|
} else {
|
||||||
|
deactivateButton(button);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the amount needed to call the current bet in the game.
|
||||||
|
*
|
||||||
|
* @param state The current GameState object representing the state of the game, which includes
|
||||||
|
* information about the current bet and the player's bet status.
|
||||||
|
* @param me The Player object representing the current player, used to determine how much they
|
||||||
|
* have already invested in the current bet.
|
||||||
|
* @return An integer representing the amount needed for the player to call the current bet.
|
||||||
|
*/
|
||||||
|
private int getToCall(GameState state, Player me) {
|
||||||
|
int current = effectiveCallTarget(state);
|
||||||
|
int alreadyInvested = Math.max(0, me.getBet());
|
||||||
|
return Math.max(0, current - alreadyInvested);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines the effective call target for the current game state. If there is an active
|
||||||
|
* current bet, it returns that as the call target.
|
||||||
|
*
|
||||||
|
* @param state The current GameState object representing the state of the game, which includes
|
||||||
|
* information about the current bet and the last reference bet.
|
||||||
|
* @return An integer representing the effective call target for the current game state.
|
||||||
|
*/
|
||||||
|
private int effectiveCallTarget(GameState state) {
|
||||||
|
if (state != null && state.currentBet > 0) {
|
||||||
|
return state.currentBet;
|
||||||
|
}
|
||||||
|
return Math.max(0, lastReferenceBet);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the current player is the first to act in the pre-flop phase and if the initial
|
||||||
|
* blind layout is still in place.
|
||||||
|
*
|
||||||
|
* @param state The current GameState object representing the state of the game, which includes
|
||||||
|
* information about the phase of the game, the players, the dealer position, and the active
|
||||||
|
* player.
|
||||||
|
* @param me The Player object representing the current player, used to determine their position
|
||||||
|
* in the player list and whether they are the active player.
|
||||||
|
* @return A boolean value indicating whether the current player is the first to act in the
|
||||||
|
* pre-flop phase with only the initial blind layout in place.
|
||||||
|
*/
|
||||||
|
private boolean isFirstPreflopPlayerInputOnly(GameState state, Player me) {
|
||||||
|
if (state == null || me == null || !isPreflop(state.phase) || 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 = (size == 2) ? dealer : (dealer + DEALER_OFFSET) % size;
|
||||||
|
if (state.activePlayer != firstIndex || myIndex != firstIndex) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return isInitialPreflopBlindLayout(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the initial blind layout is still in place during the pre-flop phase.
|
||||||
|
*
|
||||||
|
* @param state The current GameState object representing the state of the game, which includes
|
||||||
|
* information about the players and their bets.
|
||||||
|
* @return A boolean value indicating whether the initial blind layout is still in place during
|
||||||
|
* the pre-flop phase.
|
||||||
|
*/
|
||||||
|
private boolean isInitialPreflopBlindLayout(GameState state) {
|
||||||
|
int sbCount = 0;
|
||||||
|
int bbCount = 0;
|
||||||
|
int zeroCount = 0;
|
||||||
|
|
||||||
|
for (Player player : state.players) {
|
||||||
|
if (player == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
int bet = Math.max(0, player.getBet());
|
||||||
|
if (bet == SMALL_BLIND) {
|
||||||
|
sbCount++;
|
||||||
|
} else if (bet == BIG_BLIND) {
|
||||||
|
bbCount++;
|
||||||
|
} else if (bet == 0) {
|
||||||
|
zeroCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int playerCount = state.players.size();
|
||||||
|
return playerCount >= 2 && sbCount == 1 && bbCount == 1 && zeroCount == playerCount - 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the given phase string corresponds to the pre-flop phase of the game.
|
||||||
|
*
|
||||||
|
* @param phase The string representing the current phase of the game, which is expected to be
|
||||||
|
* compared against "PREFLOP" to determine if it is the pre-flop phase.
|
||||||
|
* @return A boolean value indicating whether the given phase string corresponds to the pre-flop
|
||||||
|
* phase.
|
||||||
|
*/
|
||||||
|
private boolean isPreflop(String phase) {
|
||||||
|
return "PREFLOP".equalsIgnoreCase(phase);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the given phase string corresponds to the flop phase of the game.
|
||||||
|
*
|
||||||
|
* @param phase The string representing the current phase of the game, which is expected to be
|
||||||
|
* compared against "FLOP" to determine if it is the flop phase.
|
||||||
|
* @return A boolean value indicating whether the given phase string corresponds to the flop
|
||||||
|
* phase.
|
||||||
|
*/
|
||||||
|
private boolean isFlop(String phase) {
|
||||||
|
return "FLOP".equalsIgnoreCase(phase);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the given phase string corresponds to either the turn or river phase of the game.
|
||||||
|
*
|
||||||
|
* @param phase The string representing the current phase of the game, which is expected to be
|
||||||
|
* compared against "TURN" and "RIVER" to determine if it is either the turn or river phase.
|
||||||
|
* @return A boolean value indicating whether the given phase string corresponds to either the
|
||||||
|
* turn or river phase.
|
||||||
|
*/
|
||||||
|
private boolean isTurnOrRiver(String phase) {
|
||||||
|
return "TURN".equalsIgnoreCase(phase) || "RIVER".equalsIgnoreCase(phase);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the hand is finished based on the current game state.
|
||||||
|
*
|
||||||
|
* @param state The current GameState object representing the state of the game, which includes
|
||||||
|
* information
|
||||||
|
* @return A boolean value indicating whether the hand is finished, which can be determined by
|
||||||
|
* checking if the phase is "FINISHED" or "SHOWDOWN",
|
||||||
|
*/
|
||||||
|
private boolean isHandFinished(GameState state) {
|
||||||
|
if (state == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("FINISHED".equalsIgnoreCase(state.phase) || "SHOWDOWN".equalsIgnoreCase(state.phase)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return state.players != null
|
||||||
|
&& state.winnerIndex >= 0
|
||||||
|
&& state.winnerIndex < state.players.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Evaluates the risk level of a proposed bet based on the current game state, the amount of the
|
||||||
|
* bet and the player's available chips.
|
||||||
|
*/
|
||||||
|
private enum BetRisk {
|
||||||
|
ALLOWED,
|
||||||
|
WARNING,
|
||||||
|
BLOCKED
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Enum representing the types of actions that can be submitted from the taskbar. */
|
||||||
|
private enum ActionType {
|
||||||
|
INPUT,
|
||||||
|
CALL_BUTTON,
|
||||||
|
RAISE_BUTTON
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Class representing the result of validating a proposed bet or action. */
|
||||||
|
private static final class ValidationResult {
|
||||||
|
private final boolean valid;
|
||||||
|
private final boolean warning;
|
||||||
|
private final String message;
|
||||||
|
|
||||||
|
private ValidationResult(boolean valid, boolean warning, String message) {
|
||||||
|
this.valid = valid;
|
||||||
|
this.warning = warning;
|
||||||
|
this.message = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ValidationResult ok() {
|
||||||
|
return new ValidationResult(true, false, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ValidationResult warning(String message) {
|
||||||
|
return new ValidationResult(true, true, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ValidationResult blocked(String message) {
|
||||||
|
return new ValidationResult(false, false, message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Submits a player action based on the specified ActionType.
|
||||||
|
*
|
||||||
|
* @param actionType The type of action being submitted, which can be an input-based action
|
||||||
|
* (where the player types an amount) or a specific button action for calling or raising.
|
||||||
|
*/
|
||||||
|
private void submitAction(ActionType actionType) {
|
||||||
|
if (gameService == null) {
|
||||||
|
LOGGER.error("GameService not initialized");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
GameState state = ensureLatestStateForAction(actionType.name().toLowerCase());
|
||||||
|
if (state == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isHandFinished(state)) {
|
||||||
|
LOGGER.info("Action {} ignored: hand is already finished", actionType);
|
||||||
|
update(state, myPlayerId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Player me = findCurrentPlayer(state);
|
||||||
|
if (me == null) {
|
||||||
|
LOGGER.error("Action {} blocked: player missing in state", actionType);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int callTarget = effectiveCallTarget(state);
|
||||||
|
int targetBet;
|
||||||
|
if (actionType == ActionType.CALL_BUTTON) {
|
||||||
|
targetBet = callTarget;
|
||||||
|
} else if (actionType == ActionType.RAISE_BUTTON) {
|
||||||
|
targetBet = safeDouble(callTarget);
|
||||||
|
} else {
|
||||||
|
Integer fromInput = parseInputTarget(taskbarInput.getText());
|
||||||
|
if (fromInput == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
targetBet = fromInput;
|
||||||
|
}
|
||||||
|
|
||||||
|
ValidationResult validation = validateTarget(state, me, targetBet);
|
||||||
|
if (!validation.valid) {
|
||||||
|
refreshBetInputUi();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (validation.warning) {
|
||||||
|
showWarningDialog("WARNING", validation.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.currentBet <= 0) {
|
||||||
|
gameService.bet(targetBet);
|
||||||
|
LOGGER.info("Player BET (target={})", targetBet);
|
||||||
|
} else if (targetBet == callTarget) {
|
||||||
|
gameService.call();
|
||||||
|
LOGGER.info("Player CALL (target={})", targetBet);
|
||||||
|
} else {
|
||||||
|
int raiseBy = Math.max(0, targetBet - state.currentBet);
|
||||||
|
gameService.raise(raiseBy);
|
||||||
|
LOGGER.info("Player RAISE by {} (target={})", raiseBy, targetBet);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetBet > 0) {
|
||||||
|
lastReferenceBet = targetBet;
|
||||||
|
}
|
||||||
|
|
||||||
|
taskbarInput.clear();
|
||||||
|
refreshGame();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensures that the latest game state is available for processing a player action. If the last
|
||||||
|
* known state is null, it attempts to fetch the current state from the GameService.
|
||||||
|
*
|
||||||
|
* @param text The text associated with the action being processed, used for logging purposes to
|
||||||
|
* indicate which action is being attempted when the state is not available.
|
||||||
|
* @return The latest GameState object if available, or null if the state cannot be retrieved,
|
||||||
|
* indicating that the action cannot be processed.
|
||||||
|
*/
|
||||||
|
private Integer parseInputTarget(String text) {
|
||||||
|
if (text == null || text.trim().isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return Integer.parseInt(text.trim());
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds the current player in the given game state based on the player's ID.
|
||||||
|
*
|
||||||
|
* @param state The current GameState object representing the state of the game, which includes
|
||||||
|
* a list of players.
|
||||||
|
* @param text The text associated with the action being processed, used for logging purposes to
|
||||||
|
* indicate which action is being attempted when the player cannot be found in the state.
|
||||||
|
* @return The Player object representing the current player if found in the game state, or null
|
||||||
|
* if no matching player is found, indicating that the player cannot be identified in the
|
||||||
|
* current game state.
|
||||||
|
*/
|
||||||
|
private ValidationResult validateTypedAmount(GameState state, String text) {
|
||||||
|
Integer target = parseInputTarget(text);
|
||||||
|
if (target == null || state == null) {
|
||||||
|
return ValidationResult.blocked("Invalid input");
|
||||||
|
}
|
||||||
|
Player me = findCurrentPlayer(state);
|
||||||
|
if (me == null) {
|
||||||
|
return ValidationResult.blocked("Player not found");
|
||||||
|
}
|
||||||
|
return validateTarget(state, me, target);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates a proposed target bet against the current game state and the player's status. It
|
||||||
|
* checks if the target bet is either a valid call or a valid raise (specifically, exactly
|
||||||
|
* double the call target).
|
||||||
|
*
|
||||||
|
* @param state The current GameState object representing the state of the game, which includes
|
||||||
|
* information about the current bet, the phase of the game, and the players.
|
||||||
|
* @param me The Player object representing the current player, used to determine their current
|
||||||
|
* bet, available chips, and to evaluate whether the proposed target bet is valid for this
|
||||||
|
* player based on their status in the game.
|
||||||
|
* @param targetBet The integer value representing the proposed target bet that the player
|
||||||
|
* intends to make.
|
||||||
|
* @return A ValidationResult object indicating whether the proposed target bet is valid, if it
|
||||||
|
* triggers a warning, or if it is blocked due to being invalid or too risky.
|
||||||
|
*/
|
||||||
|
private ValidationResult validateTarget(GameState state, Player me, int targetBet) {
|
||||||
|
if (state == null || me == null) {
|
||||||
|
return ValidationResult.blocked("Invalid game state");
|
||||||
|
}
|
||||||
|
|
||||||
|
int callTarget = effectiveCallTarget(state);
|
||||||
|
int raiseTarget = safeDouble(callTarget);
|
||||||
|
if (targetBet != callTarget && targetBet != raiseTarget) {
|
||||||
|
return ValidationResult.blocked("Only calls or exactly two raises allowed");
|
||||||
|
}
|
||||||
|
|
||||||
|
int alreadyInvested = Math.max(0, me.getBet());
|
||||||
|
int required = Math.max(0, targetBet - alreadyInvested);
|
||||||
|
int chips = Math.max(0, me.getChips());
|
||||||
|
|
||||||
|
if (targetBet == callTarget && required == 0) {
|
||||||
|
return ValidationResult.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (required <= 0) {
|
||||||
|
return ValidationResult.blocked("Invalid bet");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (required > chips) {
|
||||||
|
return ValidationResult.blocked("Not enough chips for the next bet");
|
||||||
|
}
|
||||||
|
|
||||||
|
BetRisk risk = evaluateBetRisk(state, required, chips);
|
||||||
|
if (risk == BetRisk.BLOCKED) {
|
||||||
|
return ValidationResult.blocked("Assignment for this phase is blocked");
|
||||||
|
}
|
||||||
|
if (risk == BetRisk.WARNING) {
|
||||||
|
return ValidationResult.warning(warningTextForPhase(state));
|
||||||
|
}
|
||||||
|
return ValidationResult.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a warning message based on the current phase of the game.
|
||||||
|
*
|
||||||
|
* @param state The current GameState object representing the state of the game, which includes
|
||||||
|
* information about the phase of the game.
|
||||||
|
* @return A string containing the warning message appropriate for the current phase of the game
|
||||||
|
* when a player's bet triggers a warning due to being at least 50% of their stack.
|
||||||
|
*/
|
||||||
|
private String warningTextForPhase(GameState state) {
|
||||||
|
String phase = state != null ? state.phase : null;
|
||||||
|
if (isFlop(phase)) {
|
||||||
|
return "WARNING: On the flop, you're betting at least 50% of your stack.";
|
||||||
|
}
|
||||||
|
if (isTurnOrRiver(phase)) {
|
||||||
|
return "WARNING: On the flop, you're betting at least 50% of your stack.";
|
||||||
|
}
|
||||||
|
return "WARNING: High stakes.";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Evaluates the risk level of a proposed bet based on the current game state, the amount of the
|
||||||
|
* bet, and the player's available chips.
|
||||||
|
*
|
||||||
|
* @param state The current GameState object representing the state of the game, which includes
|
||||||
|
* information about the phase of the game.
|
||||||
|
* @param amount The integer value representing the amount of the proposed bet that the player
|
||||||
|
* intends to make.
|
||||||
|
* @param chips The integer value representing the player's available chips.
|
||||||
|
* @return A BetRisk enum value indicating the risk level of the proposed bet.
|
||||||
|
*/
|
||||||
|
private BetRisk evaluateBetRisk(GameState state, int amount, int chips) {
|
||||||
|
if (chips <= 0 || amount <= 0) {
|
||||||
|
return BetRisk.BLOCKED;
|
||||||
|
}
|
||||||
|
|
||||||
|
double ratio = (double) amount / chips;
|
||||||
|
String phase = state != null ? state.phase : null;
|
||||||
|
|
||||||
|
if (isPreflop(phase)) {
|
||||||
|
if (ratio > PREFLOP_BLOCK_RATIO) {
|
||||||
|
return BetRisk.BLOCKED;
|
||||||
|
}
|
||||||
|
return BetRisk.ALLOWED;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isFlop(phase)) {
|
||||||
|
if (ratio > FLOP_BLOCK_RATIO) {
|
||||||
|
return BetRisk.BLOCKED;
|
||||||
|
}
|
||||||
|
if (ratio >= FLOP_WARN_RATIO) {
|
||||||
|
return BetRisk.WARNING;
|
||||||
|
}
|
||||||
|
return BetRisk.ALLOWED;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isTurnOrRiver(phase)) {
|
||||||
|
if (ratio >= TURN_RIVER_BLOCK_RATIO) {
|
||||||
|
return BetRisk.BLOCKED;
|
||||||
|
}
|
||||||
|
if (ratio > TURN_RIVER_WARN_RATIO) {
|
||||||
|
return BetRisk.WARNING;
|
||||||
|
}
|
||||||
|
return BetRisk.ALLOWED;
|
||||||
|
}
|
||||||
|
|
||||||
|
return BetRisk.ALLOWED;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays a warning dialog with the specified title and content.
|
||||||
|
*
|
||||||
|
* @param title The string representing the title of the warning dialog, which is displayed in
|
||||||
|
* the title bar of the dialog window.
|
||||||
|
* @param content The string representing the content of the warning message, which is displayed
|
||||||
|
* in the body of the dialog.
|
||||||
|
*/
|
||||||
|
private void showWarningDialog(String title, String content) {
|
||||||
|
Alert alert = new Alert(Alert.AlertType.WARNING);
|
||||||
|
alert.setTitle(title);
|
||||||
|
alert.setHeaderText(null);
|
||||||
|
alert.setContentText(content);
|
||||||
|
alert.showAndWait();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -297,6 +904,12 @@ public class TaskbarController {
|
|||||||
@FXML
|
@FXML
|
||||||
private void onInputSubmitted(KeyEvent event) {
|
private void onInputSubmitted(KeyEvent event) {
|
||||||
if (event.getCode() == KeyCode.ENTER) {
|
if (event.getCode() == KeyCode.ENTER) {
|
||||||
|
ValidationResult validation = validateTypedAmount(lastState, taskbarInput.getText());
|
||||||
|
if (!validation.valid) {
|
||||||
|
event.consume();
|
||||||
|
setBetButtonVisible(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
processBet();
|
processBet();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -313,16 +926,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");
|
||||||
if (gameService == null) {
|
|
||||||
LOGGER.error("GameService not initialized");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
gameService.call();
|
|
||||||
LOGGER.info("Player CALL");
|
|
||||||
|
|
||||||
refreshGame();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Called when the Fold button is clicked. */
|
/** Called when the Fold button is clicked. */
|
||||||
@@ -334,6 +938,13 @@ public class TaskbarController {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
GameState state = ensureLatestStateForAction("fold");
|
||||||
|
if (isHandFinished(state)) {
|
||||||
|
LOGGER.info("Fold ignored: hand is already finished");
|
||||||
|
update(state, myPlayerId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
gameService.fold();
|
gameService.fold();
|
||||||
LOGGER.info("Player FOLD");
|
LOGGER.info("Player FOLD");
|
||||||
|
|
||||||
@@ -343,36 +954,25 @@ public class TaskbarController {
|
|||||||
/** Called when the Raise button is clicked. */
|
/** Called when the Raise button is clicked. */
|
||||||
@FXML
|
@FXML
|
||||||
private void onInputPlayerRaise() {
|
private void onInputPlayerRaise() {
|
||||||
|
submitPresetInputAndProcess("raise");
|
||||||
|
}
|
||||||
|
|
||||||
if (gameService == null) {
|
/**
|
||||||
LOGGER.error("GameService not initialized");
|
* Submits a preset input for either calling or raising based on the specified mode.
|
||||||
|
*
|
||||||
|
* @param mode A string indicating the mode of the action, which can be "call" for calling the
|
||||||
|
* current bet or "raise" for raising to double the current bet.
|
||||||
|
*/
|
||||||
|
private void submitPresetInputAndProcess(String mode) {
|
||||||
|
GameState state = ensureLatestStateForAction(mode);
|
||||||
|
if (state == null || taskbarInput == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
GameState state = lastState;
|
int callTarget = effectiveCallTarget(state);
|
||||||
if (state == null) {
|
int target = "raise".equals(mode) ? safeDouble(callTarget) : callTarget;
|
||||||
try {
|
taskbarInput.setText(String.valueOf(target));
|
||||||
state = gameService.refresh();
|
processBet();
|
||||||
lastState = state;
|
|
||||||
} catch (Exception e) {
|
|
||||||
LOGGER.error("Failed to refresh game state for raise: {}", e.getMessage());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state == null || state.currentBet <= 0) {
|
|
||||||
LOGGER.warn(
|
|
||||||
"Raise not possible: current bet is {}",
|
|
||||||
state != null ? state.currentBet : null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
int raiseAmount = state.currentBet;
|
|
||||||
gameService.raise(raiseAmount);
|
|
||||||
LOGGER.info("Player RAISE by {} (target: double table bet)", raiseAmount);
|
|
||||||
|
|
||||||
taskbarInput.clear();
|
|
||||||
refreshGame();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -433,35 +1033,40 @@ public class TaskbarController {
|
|||||||
* displayed on the console.
|
* displayed on the console.
|
||||||
*/
|
*/
|
||||||
private void processBet() {
|
private void processBet() {
|
||||||
|
submitAction(ActionType.INPUT);
|
||||||
if (gameService == null) {
|
|
||||||
LOGGER.error("Error: GameService not initialized");
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
String input = taskbarInput.getText();
|
private Player findCurrentPlayer(GameState state) {
|
||||||
|
if (state == null || state.players == null || myPlayerId == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return state.players.stream()
|
||||||
|
.filter(player -> player != null && myPlayerId.equals(player.getId()))
|
||||||
|
.findFirst()
|
||||||
|
.orElse(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensures that the latest game state is available for processing a player action.
|
||||||
|
*
|
||||||
|
* @param actionName The name of the action being processed, used for logging purposes to
|
||||||
|
* indicate which action is being attempted when the state is not available.
|
||||||
|
* @return The latest GameState object if available, or null if the state cannot be retrieved,
|
||||||
|
* indicating that the action cannot be processed due to the lack of a valid game state.
|
||||||
|
*/
|
||||||
|
private GameState ensureLatestStateForAction(String actionName) {
|
||||||
try {
|
try {
|
||||||
|
GameState refreshed = gameService.refresh();
|
||||||
int credits = Integer.parseInt(input.trim());
|
if (refreshed != null) {
|
||||||
|
lastState = refreshed;
|
||||||
if (credits >= MIN_CREDITS && credits <= MAX_CREDITS && credits % CREDIT_STEP == 0) {
|
return refreshed;
|
||||||
|
}
|
||||||
gameService.bet(credits);
|
} catch (Exception e) {
|
||||||
|
LOGGER.error("Failed to refresh game state for {}: {}", actionName, e.getMessage());
|
||||||
LOGGER.info("Bet placed: {}", credits);
|
|
||||||
|
|
||||||
taskbarInput.clear();
|
|
||||||
|
|
||||||
refreshGame();
|
|
||||||
|
|
||||||
} else {
|
|
||||||
LOGGER.error("Error: Bet must be between 5 and 100000 and multiple of 5");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (NumberFormatException e) {
|
return lastState;
|
||||||
LOGGER.error("Error: Invalid number entered");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+16
-2
@@ -10,6 +10,7 @@ import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.deck.Deck;
|
|||||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.engine.GameEngine;
|
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.engine.GameEngine;
|
||||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.evaluator.HandEvaluator;
|
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.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.player.PlayerId;
|
||||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.state.GamePhase;
|
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.game.state.GameState;
|
||||||
@@ -183,9 +184,22 @@ public class GameController {
|
|||||||
*
|
*
|
||||||
* @param playerId The ID of the player who is folding.
|
* @param playerId The ID of the player who is folding.
|
||||||
*/
|
*/
|
||||||
public void playerFold(PlayerId playerId) {
|
public PlayerId playerFold(PlayerId playerId) {
|
||||||
// engine.processAction(new FoldAction(PlayerId.of(playerId)));
|
|
||||||
engine.processAction(new FoldAction(playerId));
|
engine.processAction(new FoldAction(playerId));
|
||||||
|
|
||||||
|
GameState state = engine.getState();
|
||||||
|
|
||||||
|
if (state.countNonFoldedPlayers() == 1) {
|
||||||
|
|
||||||
|
for (Player p : state.getPlayers()) {
|
||||||
|
if (!p.isFolded()) {
|
||||||
|
return p.getId();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+29
@@ -1,6 +1,9 @@
|
|||||||
package ch.unibas.dmi.dbis.cs108.casono.server.domain.game.engine;
|
package ch.unibas.dmi.dbis.cs108.casono.server.domain.game.engine;
|
||||||
|
|
||||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.action.Action;
|
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.action.Action;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.action.ActionType;
|
||||||
|
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.RuleEngine;
|
||||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.state.GameState;
|
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.state.GameState;
|
||||||
|
|
||||||
@@ -80,12 +83,38 @@ public class GameEngine {
|
|||||||
*/
|
*/
|
||||||
public void handleAction(GameState state, Action action) {
|
public void handleAction(GameState state, Action action) {
|
||||||
|
|
||||||
|
if (action.getType() != ActionType.BLIND) {
|
||||||
|
|
||||||
|
Player currentPlayer = state.getCurrentPlayer();
|
||||||
|
PlayerId actingPlayerId = action.getPlayerId();
|
||||||
|
|
||||||
|
if (currentPlayer == null || actingPlayerId == null) {
|
||||||
|
throw new RuntimeException("Invalid turn state");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!state.isAllowOutOfTurn() && !currentPlayer.getId().equals(actingPlayerId)) {
|
||||||
|
throw new RuntimeException("Not your turn");
|
||||||
|
}
|
||||||
|
|
||||||
|
Player actingPlayer = state.getPlayer(actingPlayerId);
|
||||||
|
if (actingPlayer.isFolded()) {
|
||||||
|
throw new RuntimeException("Player already folded");
|
||||||
|
}
|
||||||
|
if (actingPlayer.isAllIn()) {
|
||||||
|
throw new RuntimeException("Player is all-in");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 1.Check the rules
|
// 1.Check the rules
|
||||||
ruleEngine.validate(state, action);
|
ruleEngine.validate(state, action);
|
||||||
|
|
||||||
// 2. Perform action
|
// 2. Perform action
|
||||||
action.execute(state);
|
action.execute(state);
|
||||||
|
|
||||||
|
if (action.getType() != ActionType.BLIND) {
|
||||||
|
state.markActedThisRound(action.getPlayerId());
|
||||||
|
}
|
||||||
|
|
||||||
// 3. Next turn
|
// 3. Next turn
|
||||||
turnManager.nextPlayer(state);
|
turnManager.nextPlayer(state);
|
||||||
|
|
||||||
|
|||||||
+90
-10
@@ -4,7 +4,6 @@ import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.deck.Card;
|
|||||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.deck.Deck;
|
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.deck.Deck;
|
||||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.player.Player;
|
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.player.PlayerId;
|
||||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.player.PlayerStatus;
|
|
||||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.state.GamePhase;
|
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.game.state.GameState;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -27,6 +26,13 @@ public class RoundManager {
|
|||||||
public static final int SMALL_BLIND = 100;
|
public static final int SMALL_BLIND = 100;
|
||||||
public static final int BIG_BLIND = 200;
|
public static final int BIG_BLIND = 200;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts a new hand by resetting the game state, ensuring a deck is available, dealing hole
|
||||||
|
* cards to players, posting blinds, and setting the first player to act for the preflop phase.
|
||||||
|
*
|
||||||
|
* @param state The GameState object representing the current state of the game, which will be
|
||||||
|
* modified to
|
||||||
|
*/
|
||||||
public void startNewHand(GameState state) {
|
public void startNewHand(GameState state) {
|
||||||
// Use GameState's canonical reset
|
// Use GameState's canonical reset
|
||||||
state.startNewHand();
|
state.startNewHand();
|
||||||
@@ -44,12 +50,19 @@ public class RoundManager {
|
|||||||
state.setCurrentPlayerToPreflopFirstToAct();
|
state.setCurrentPlayerToPreflopFirstToAct();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the current betting round is finished and advances the game phase if necessary.
|
||||||
|
*
|
||||||
|
* @param state The GameState object representing the current state of the game, which may be
|
||||||
|
* modified to advance the phase or end the hand.
|
||||||
|
*/
|
||||||
public void progressIfNeeded(GameState state) {
|
public void progressIfNeeded(GameState state) {
|
||||||
if (state.getPhase() == null) {
|
if (state.getPhase() == null) {
|
||||||
state.setPhase(GamePhase.PREFLOP);
|
state.setPhase(GamePhase.PREFLOP);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.countNonFoldedPlayers() == 1) {
|
if (state.countNonFoldedPlayers() == 1) {
|
||||||
|
state.setHandActive(false);
|
||||||
state.setPhase(GamePhase.FINISHED);
|
state.setPhase(GamePhase.FINISHED);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -59,27 +72,51 @@ public class RoundManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if the current betting round is finished by checking if all active players have
|
||||||
|
* met the current bet or are all-in and by handling special cases for preflop betting rounds.
|
||||||
|
*
|
||||||
|
* @param state The GameState object representing the current state of the game, which is used
|
||||||
|
* to evaluate the betting round status.
|
||||||
|
* @return true if the betting round is finished and the game can progress to the next phase,
|
||||||
|
* false otherwise.
|
||||||
|
*/
|
||||||
private boolean isBettingRoundFinished(GameState state) {
|
private boolean isBettingRoundFinished(GameState state) {
|
||||||
int target = state.getTableState().getCurrentBet();
|
// A betting round only ends after every active player had at least one chance to act.
|
||||||
|
return allActivePlayersActedThisRound(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method to check if all active players have acted in the current betting round. This is
|
||||||
|
* used to handle the special case of preflop rounds where no bets have been made yet, but
|
||||||
|
* players still need to have the opportunity to act.
|
||||||
|
*
|
||||||
|
* @param state The GameState object representing the current state of the game, which is used
|
||||||
|
* to check if all active players have acted in the current round.
|
||||||
|
* @return true if all active players have acted in the current round, false if there are still
|
||||||
|
* active players who have not acted yet.
|
||||||
|
*/
|
||||||
|
private boolean allActivePlayersActedThisRound(GameState state) {
|
||||||
for (Player p : state.getPlayers()) {
|
for (Player p : state.getPlayers()) {
|
||||||
if (p == null) {
|
if (p == null || p.isFolded() || p.isAllIn()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// be tolerant if codebase mixes status + boolean flags
|
if (!state.hasActedThisRound(p.getId())) {
|
||||||
if (p.getStatus() == PlayerStatus.FOLDED || p.isFolded()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
int bet = state.getCurrentBet(p.getId());
|
|
||||||
if (bet < target && !p.isAllIn()) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Advances the game phase to the next stage (FLOP, TURN, RIVER, SHOWDOWN) based on the current
|
||||||
|
* phase.
|
||||||
|
*
|
||||||
|
* @param state The GameState object representing the current state of the game, which will be
|
||||||
|
* modified to advance the phase and deal community cards as needed when progressing to the
|
||||||
|
* next stage of the hand.
|
||||||
|
*/
|
||||||
private void advancePhase(GameState state) {
|
private void advancePhase(GameState state) {
|
||||||
switch (state.getPhase()) {
|
switch (state.getPhase()) {
|
||||||
case PREFLOP -> dealFlop(state);
|
case PREFLOP -> dealFlop(state);
|
||||||
@@ -93,6 +130,12 @@ public class RoundManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensures that a deck of cards is available in the game state.
|
||||||
|
*
|
||||||
|
* @param state The GameState object representing the current state of the game, which will be
|
||||||
|
* modified to include a new shuffled deck if one does not already exist.
|
||||||
|
*/
|
||||||
private void ensureDeck(GameState state) {
|
private void ensureDeck(GameState state) {
|
||||||
Deck deck = state.getDeck();
|
Deck deck = state.getDeck();
|
||||||
if (deck == null) {
|
if (deck == null) {
|
||||||
@@ -102,6 +145,13 @@ public class RoundManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deals hole cards to each player in the game state by drawing two cards from the deck for each
|
||||||
|
* player and assigning them as their hole cards.
|
||||||
|
*
|
||||||
|
* @param state The GameState object representing the current state of the game, which will be
|
||||||
|
* modified to assign hole cards to each active player.
|
||||||
|
*/
|
||||||
private void dealHoleCards(GameState state) {
|
private void dealHoleCards(GameState state) {
|
||||||
Deck deck = state.getDeck();
|
Deck deck = state.getDeck();
|
||||||
|
|
||||||
@@ -165,6 +215,14 @@ public class RoundManager {
|
|||||||
state.getTableState().setCurrentBet(BIG_BLIND);
|
state.getTableState().setCurrentBet(BIG_BLIND);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deals the flop by drawing three community cards from the deck and adding them to the game
|
||||||
|
* state, then setting the game phase to FLOP and resetting bets for the new betting round.
|
||||||
|
*
|
||||||
|
* @param state The GameState object representing the current state of the game, which will be
|
||||||
|
* modified to add three community cards for the flop, set the phase to FLOP, and reset bets
|
||||||
|
* for the new betting round.
|
||||||
|
*/
|
||||||
private void dealFlop(GameState state) {
|
private void dealFlop(GameState state) {
|
||||||
ensureDeck(state);
|
ensureDeck(state);
|
||||||
Deck deck = state.getDeck();
|
Deck deck = state.getDeck();
|
||||||
@@ -180,6 +238,14 @@ public class RoundManager {
|
|||||||
state.setCurrentPlayerToPostflopFirstToAct();
|
state.setCurrentPlayerToPostflopFirstToAct();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deals the turn by drawing one community card from the deck and adding it to the game state,
|
||||||
|
* then setting the game phase to TURN and resetting bets for the new betting round.
|
||||||
|
*
|
||||||
|
* @param state The GameState object representing the current state of the game, which will be
|
||||||
|
* modified to add one community card for the turn, set the phase to TURN, and reset bets
|
||||||
|
* for the new betting round.
|
||||||
|
*/
|
||||||
private void dealTurn(GameState state) {
|
private void dealTurn(GameState state) {
|
||||||
ensureDeck(state);
|
ensureDeck(state);
|
||||||
Deck deck = state.getDeck();
|
Deck deck = state.getDeck();
|
||||||
@@ -193,6 +259,14 @@ public class RoundManager {
|
|||||||
state.setCurrentPlayerToPostflopFirstToAct();
|
state.setCurrentPlayerToPostflopFirstToAct();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deals the river by drawing one community card from the deck and adding it to the game state,
|
||||||
|
* then setting the game phase to RIVER and resetting bets for the new betting round.
|
||||||
|
*
|
||||||
|
* @param state The GameState object representing the current state of the game, which will be
|
||||||
|
* modified to add one community card for the river, set the phase to RIVER, and reset bets
|
||||||
|
* for the new betting round.
|
||||||
|
*/
|
||||||
private void dealRiver(GameState state) {
|
private void dealRiver(GameState state) {
|
||||||
ensureDeck(state);
|
ensureDeck(state);
|
||||||
Deck deck = state.getDeck();
|
Deck deck = state.getDeck();
|
||||||
@@ -206,6 +280,12 @@ public class RoundManager {
|
|||||||
state.setCurrentPlayerToPostflopFirstToAct();
|
state.setCurrentPlayerToPostflopFirstToAct();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the showdown phase by setting the game phase to SHOWDOWN.
|
||||||
|
*
|
||||||
|
* @param state The GameState object representing the current state of the game, which will be
|
||||||
|
* modified to set the phase to SHOWDOWN.
|
||||||
|
*/
|
||||||
private void showdown(GameState state) {
|
private void showdown(GameState state) {
|
||||||
state.setPhase(GamePhase.SHOWDOWN);
|
state.setPhase(GamePhase.SHOWDOWN);
|
||||||
|
|
||||||
|
|||||||
+227
-12
@@ -7,8 +7,10 @@ import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.player.PlayerId;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The GameState class encapsulates the entire state of a poker game at any given moment.
|
* The GameState class encapsulates the entire state of a poker game at any given moment.
|
||||||
@@ -38,6 +40,7 @@ public class GameState {
|
|||||||
|
|
||||||
private final Map<PlayerId, Integer> currentBets = new HashMap<>();
|
private final Map<PlayerId, Integer> currentBets = new HashMap<>();
|
||||||
private final Map<PlayerId, Integer> playerBetCommitments = new HashMap<>();
|
private final Map<PlayerId, Integer> playerBetCommitments = new HashMap<>();
|
||||||
|
private final Set<PlayerId> actedThisRound = new HashSet<>();
|
||||||
|
|
||||||
private final Map<PlayerId, List<Card>> holeCards = new HashMap<>();
|
private final Map<PlayerId, List<Card>> holeCards = new HashMap<>();
|
||||||
private final List<Card> communityCards = new ArrayList<>();
|
private final List<Card> communityCards = new ArrayList<>();
|
||||||
@@ -46,7 +49,11 @@ public class GameState {
|
|||||||
|
|
||||||
private static final int DEALER_OFFSET = 3;
|
private static final int DEALER_OFFSET = 3;
|
||||||
|
|
||||||
// Getters
|
/**
|
||||||
|
* Returns the players in seating/turn order.
|
||||||
|
*
|
||||||
|
* @return a collection of players in the order they are seated/act.
|
||||||
|
*/
|
||||||
public Collection<Player> getPlayers() {
|
public Collection<Player> getPlayers() {
|
||||||
List<Player> ordered = new ArrayList<>(playerOrder.size());
|
List<Player> ordered = new ArrayList<>(playerOrder.size());
|
||||||
for (PlayerId id : playerOrder) {
|
for (PlayerId id : playerOrder) {
|
||||||
@@ -58,57 +65,123 @@ public class GameState {
|
|||||||
return ordered;
|
return ordered;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the current player whose turn it is to act.
|
||||||
|
*
|
||||||
|
* @return the current player, or null if there are no players or the index is out of bounds.
|
||||||
|
*/
|
||||||
public Player getCurrentPlayer() {
|
public Player getCurrentPlayer() {
|
||||||
PlayerId id = playerOrder.get(currentPlayerIndex);
|
PlayerId id = playerOrder.get(currentPlayerIndex);
|
||||||
return players.get(id);
|
return players.get(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the index of the current player in the player order list.
|
||||||
|
*
|
||||||
|
* @return the index of the current player, or 0 if there are no players.
|
||||||
|
*/
|
||||||
public int getCurrentPlayerIndex() {
|
public int getCurrentPlayerIndex() {
|
||||||
return currentPlayerIndex;
|
return currentPlayerIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether a hand is currently active (i.e., in progress).
|
||||||
|
*
|
||||||
|
* @return true if a hand is active, false otherwise.
|
||||||
|
*/
|
||||||
public boolean isHandActive() {
|
public boolean isHandActive() {
|
||||||
return handActive;
|
return handActive;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the current phase of the game (e.g., PREFLOP, FLOP, TURN, RIVER).
|
||||||
|
*
|
||||||
|
* @return the current game phase.
|
||||||
|
*/
|
||||||
public GamePhase getPhase() {
|
public GamePhase getPhase() {
|
||||||
return phase;
|
return phase;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the current state of the table, including information such as the current bet and pot
|
||||||
|
* size.
|
||||||
|
*
|
||||||
|
* @return the current table state.
|
||||||
|
*/
|
||||||
public TableState getTableState() {
|
public TableState getTableState() {
|
||||||
return tableState;
|
return tableState;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the current pot, which includes the total amount of chips in the pot and the
|
||||||
|
* contributions from each player.
|
||||||
|
*
|
||||||
|
* @return the current pot.
|
||||||
|
*/
|
||||||
public Pot getPot() {
|
public Pot getPot() {
|
||||||
return pot;
|
return pot;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the current deck of cards being used in the game.
|
||||||
|
*
|
||||||
|
* @return the current deck of cards.
|
||||||
|
*/
|
||||||
public Deck getDeck() {
|
public Deck getDeck() {
|
||||||
return deck;
|
return deck;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the list of community cards currently on the table.
|
||||||
|
*
|
||||||
|
* @return a list of community cards, which may be empty if no cards have been dealt yet.
|
||||||
|
*/
|
||||||
public List<Card> getCommunityCards() {
|
public List<Card> getCommunityCards() {
|
||||||
return communityCards;
|
return communityCards;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Always returns a mutable list (never null). */
|
/**
|
||||||
|
* Returns the hole cards for the specified player. If the player does not have hole cards yet,
|
||||||
|
* an empty list is returned and stored in the map for future reference.
|
||||||
|
*
|
||||||
|
* @param playerId the ID of the player whose hole cards are being requested.
|
||||||
|
* @return a list of hole cards for the specified player, which may be empty if the player has
|
||||||
|
* not been dealt cards yet.
|
||||||
|
*/
|
||||||
public List<Card> getHoleCards(PlayerId playerId) {
|
public List<Card> getHoleCards(PlayerId playerId) {
|
||||||
return holeCards.computeIfAbsent(playerId, k -> new ArrayList<>());
|
return holeCards.computeIfAbsent(playerId, k -> new ArrayList<>());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the index of the dealer in the player order list.
|
||||||
|
*
|
||||||
|
* @return the index of the dealer, or 0 if there are no players.
|
||||||
|
*/
|
||||||
public int getDealerIndex() {
|
public int getDealerIndex() {
|
||||||
return dealerIndex;
|
return dealerIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a map of player IDs to their respective hole cards. Each player's hole cards are
|
||||||
|
* represented as a list of Card objects. If a player does not have hole cards yet, they will be
|
||||||
|
* associated with an empty list.
|
||||||
|
*
|
||||||
|
* @return a map where the key is the player's ID and the value is a list of their hole cards.
|
||||||
|
*/
|
||||||
public Map<PlayerId, List<Card>> getPlayerCards() {
|
public Map<PlayerId, List<Card>> getPlayerCards() {
|
||||||
return holeCards;
|
return holeCards;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the number of players currently in the game, based on the size of the player order
|
||||||
|
* list.
|
||||||
|
*
|
||||||
|
* @return the number of players in the game.
|
||||||
|
*/
|
||||||
public int getPlayerCount() {
|
public int getPlayerCount() {
|
||||||
return players.size();
|
return players.size();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Setters / Mutators
|
|
||||||
public void setCurrentPlayerIndex(int index) {
|
public void setCurrentPlayerIndex(int index) {
|
||||||
if (playerOrder.isEmpty()) {
|
if (playerOrder.isEmpty()) {
|
||||||
this.currentPlayerIndex = 0;
|
this.currentPlayerIndex = 0;
|
||||||
@@ -117,6 +190,10 @@ public class GameState {
|
|||||||
this.currentPlayerIndex = index;
|
this.currentPlayerIndex = index;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the current player index to the first player who should act preflop, which is determined
|
||||||
|
* based on the dealer index and the number of players.
|
||||||
|
*/
|
||||||
public void setCurrentPlayerToPreflopFirstToAct() {
|
public void setCurrentPlayerToPreflopFirstToAct() {
|
||||||
int size = playerOrder.size();
|
int size = playerOrder.size();
|
||||||
if (size == 0) {
|
if (size == 0) {
|
||||||
@@ -127,11 +204,16 @@ public class GameState {
|
|||||||
// Heads-up: dealer (small blind) acts first preflop.
|
// Heads-up: dealer (small blind) acts first preflop.
|
||||||
int first = (size == 2) ? dealerIndex : (dealerIndex + DEALER_OFFSET) % size;
|
int first = (size == 2) ? dealerIndex : (dealerIndex + DEALER_OFFSET) % size;
|
||||||
currentPlayerIndex = first;
|
currentPlayerIndex = first;
|
||||||
|
resetRoundActions();
|
||||||
if (!canPlayerAct(currentPlayerIndex)) {
|
if (!canPlayerAct(currentPlayerIndex)) {
|
||||||
nextPlayer();
|
nextPlayer();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the current player index to the first player who should act postflop, which is the
|
||||||
|
* player immediately to the left of the dealer (i.e., dealerIndex + 1).
|
||||||
|
*/
|
||||||
public void setCurrentPlayerToPostflopFirstToAct() {
|
public void setCurrentPlayerToPostflopFirstToAct() {
|
||||||
int size = playerOrder.size();
|
int size = playerOrder.size();
|
||||||
if (size == 0) {
|
if (size == 0) {
|
||||||
@@ -141,27 +223,86 @@ public class GameState {
|
|||||||
|
|
||||||
int first = (dealerIndex + 1) % size;
|
int first = (dealerIndex + 1) % size;
|
||||||
currentPlayerIndex = first;
|
currentPlayerIndex = first;
|
||||||
|
resetRoundActions();
|
||||||
if (!canPlayerAct(currentPlayerIndex)) {
|
if (!canPlayerAct(currentPlayerIndex)) {
|
||||||
nextPlayer();
|
nextPlayer();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marks the specified player as having acted in the current round.
|
||||||
|
*
|
||||||
|
* @param playerId the ID of the player to mark as having acted this round.
|
||||||
|
*/
|
||||||
|
public void markActedThisRound(PlayerId playerId) {
|
||||||
|
if (playerId != null) {
|
||||||
|
actedThisRound.add(playerId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the specified player has already acted in the current round by checking if their ID
|
||||||
|
* is present in the set of players who have acted this round.
|
||||||
|
*
|
||||||
|
* @param playerId the ID of the player to check for having acted this round.
|
||||||
|
* @return true if the player has acted this round, false otherwise.
|
||||||
|
*/
|
||||||
|
public boolean hasActedThisRound(PlayerId playerId) {
|
||||||
|
return playerId != null && actedThisRound.contains(playerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resets the tracking of which players have acted this round by clearing the set of player IDs.
|
||||||
|
*/
|
||||||
|
public void resetRoundActions() {
|
||||||
|
actedThisRound.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets whether a hand is currently active (i.e., in progress).
|
||||||
|
*
|
||||||
|
* @param handActive a boolean value indicating whether a hand is active (true) or not (false).
|
||||||
|
*/
|
||||||
public void setHandActive(boolean handActive) {
|
public void setHandActive(boolean handActive) {
|
||||||
this.handActive = handActive;
|
this.handActive = handActive;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the current phase of the game (e.g., PREFLOP, FLOP, TURN, RIVER).
|
||||||
|
*
|
||||||
|
* @param phase the GamePhase to set as the current phase of the game.
|
||||||
|
*/
|
||||||
public void setPhase(GamePhase phase) {
|
public void setPhase(GamePhase phase) {
|
||||||
this.phase = phase;
|
this.phase = phase;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the index of the dealer in the player order list.
|
||||||
|
*
|
||||||
|
* @param dealerIndex the index to set as the dealer index, which should be within the bounds of
|
||||||
|
* the player order list if it is not empty.
|
||||||
|
*/
|
||||||
public void setDealerIndex(int dealerIndex) {
|
public void setDealerIndex(int dealerIndex) {
|
||||||
this.dealerIndex = dealerIndex;
|
this.dealerIndex = dealerIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the current deck of cards being used in the game.
|
||||||
|
*
|
||||||
|
* @param deck the Deck object to set as the current deck of cards for the game. This should be
|
||||||
|
* a valid Deck instance that can be used for dealing cards during the game.
|
||||||
|
*/
|
||||||
public void setDeck(Deck deck) {
|
public void setDeck(Deck deck) {
|
||||||
this.deck = deck;
|
this.deck = deck;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a new player to the game with the specified ID and initial chip count.
|
||||||
|
*
|
||||||
|
* @param id the PlayerId of the new player to add to the game.
|
||||||
|
* @param chips the initial number of chips the player has, which can be used for betting during
|
||||||
|
* the game.
|
||||||
|
*/
|
||||||
public void addPlayer(PlayerId id, int chips) {
|
public void addPlayer(PlayerId id, int chips) {
|
||||||
Player player = new Player(id, chips);
|
Player player = new Player(id, chips);
|
||||||
|
|
||||||
@@ -213,24 +354,48 @@ public class GameState {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method to move an entry in a map from an old key to a new key, with a fallback value
|
||||||
|
* if the old key is not present.
|
||||||
|
*
|
||||||
|
* @param map the map in which to move the entry, where the key is a PlayerId and the value is
|
||||||
|
* of type T.
|
||||||
|
* @param oldId the existing PlayerId key that is being renamed.
|
||||||
|
* @param newId the new PlayerId key to which the entry should be moved.
|
||||||
|
* @param fallback the value to use if the oldId is not present in the map.
|
||||||
|
* @param <T> the type of the values in the map, which can be any type that is used for
|
||||||
|
* player-related data in the game state.
|
||||||
|
*/
|
||||||
private <T> void moveMapEntry(
|
private <T> void moveMapEntry(
|
||||||
Map<PlayerId, T> map, PlayerId oldId, PlayerId newId, T fallback) {
|
Map<PlayerId, T> map, PlayerId oldId, PlayerId newId, T fallback) {
|
||||||
T value = map.remove(oldId);
|
T value = map.remove(oldId);
|
||||||
map.put(newId, value != null ? value : fallback);
|
map.put(newId, value != null ? value : fallback);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Betting
|
// BETTING
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the current bet amount for the specified player.
|
||||||
|
*
|
||||||
|
* @param playerId the ID of the player whose current bet is being requested.
|
||||||
|
* @return the current bet amount for the specified player, or 0 if the player does not have a
|
||||||
|
* current bet.
|
||||||
|
*/
|
||||||
public int getCurrentBet(PlayerId playerId) {
|
public int getCurrentBet(PlayerId playerId) {
|
||||||
return currentBets.getOrDefault(playerId, 0);
|
return currentBets.getOrDefault(playerId, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the current bet amount for the specified player.
|
||||||
|
*
|
||||||
|
* @param playerId the ID of the player whose current bet is being set.
|
||||||
|
* @param amount the new bet amount to set for the specified player.
|
||||||
|
*/
|
||||||
public void setCurrentBet(PlayerId playerId, int amount) {
|
public void setCurrentBet(PlayerId playerId, int amount) {
|
||||||
currentBets.put(playerId, amount);
|
currentBets.put(playerId, amount);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Reset bets to 0 for ALL players (do not clear the map). */
|
||||||
* Reset bets to 0 for ALL players (do not clear the map). Also resets table current bet to 0.
|
|
||||||
*/
|
|
||||||
public void resetBets() {
|
public void resetBets() {
|
||||||
for (PlayerId id : playerOrder) {
|
for (PlayerId id : playerOrder) {
|
||||||
currentBets.put(id, 0);
|
currentBets.put(id, 0);
|
||||||
@@ -238,10 +403,20 @@ public class GameState {
|
|||||||
tableState.setCurrentBet(0);
|
tableState.setCurrentBet(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds the specified amount to the pot.
|
||||||
|
*
|
||||||
|
* @param amount the amount to add to the pot.
|
||||||
|
*/
|
||||||
public void addToPot(int amount) {
|
public void addToPot(int amount) {
|
||||||
pot.add(amount);
|
pot.add(amount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether out-of-turn actions are allowed in the current game state.
|
||||||
|
*
|
||||||
|
* @return true if out-of-turn actions are allowed, false otherwise.
|
||||||
|
*/
|
||||||
public boolean isAllowOutOfTurn() {
|
public boolean isAllowOutOfTurn() {
|
||||||
return allowOutOfTurn;
|
return allowOutOfTurn;
|
||||||
}
|
}
|
||||||
@@ -250,7 +425,13 @@ public class GameState {
|
|||||||
this.allowOutOfTurn = allowOutOfTurn;
|
this.allowOutOfTurn = allowOutOfTurn;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Player helpers
|
/**
|
||||||
|
* Retrieves the Player object associated with the given PlayerId.
|
||||||
|
*
|
||||||
|
* @param id the PlayerId of the player to retrieve.
|
||||||
|
* @return the Player object associated with the specified PlayerId, or a RuntimeException if
|
||||||
|
* the player is not found.
|
||||||
|
*/
|
||||||
public Player getPlayer(PlayerId id) {
|
public Player getPlayer(PlayerId id) {
|
||||||
Player player = players.get(id);
|
Player player = players.get(id);
|
||||||
if (player == null) {
|
if (player == null) {
|
||||||
@@ -260,7 +441,13 @@ public class GameState {
|
|||||||
return player;
|
return player;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bet commitments
|
/**
|
||||||
|
* Retrieves the current bet commitment for the specified player.
|
||||||
|
*
|
||||||
|
* @param playerId the ID of the player whose current bet commitment is being requested.
|
||||||
|
* @return the current bet commitment for the specified player, or 0 if the player does not have
|
||||||
|
* a bet commitment.
|
||||||
|
*/
|
||||||
public int getCurrentBetCommitment(PlayerId playerId) {
|
public int getCurrentBetCommitment(PlayerId playerId) {
|
||||||
return playerBetCommitments.getOrDefault(playerId, 0);
|
return playerBetCommitments.getOrDefault(playerId, 0);
|
||||||
}
|
}
|
||||||
@@ -269,7 +456,7 @@ public class GameState {
|
|||||||
playerBetCommitments.put(playerId, amount);
|
playerBetCommitments.put(playerId, amount);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cards
|
// CARDS
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gives (overwrites) the two hole cards for the given player. Ensures stable list identity
|
* Gives (overwrites) the two hole cards for the given player. Ensures stable list identity
|
||||||
@@ -282,15 +469,21 @@ public class GameState {
|
|||||||
cards.add(c2);
|
cards.add(c2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a community card to the list of community cards on the table.
|
||||||
|
*
|
||||||
|
* @param card the Card object representing the community card to be added to the table.
|
||||||
|
*/
|
||||||
public void addCommunityCard(Card card) {
|
public void addCommunityCard(Card card) {
|
||||||
communityCards.add(card);
|
communityCards.add(card);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Resets the community cards by clearing the list of community cards on the table. */
|
||||||
public void resetCommunityCards() {
|
public void resetCommunityCards() {
|
||||||
communityCards.clear();
|
communityCards.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Turn / dealer management
|
/** Advances the current player index to the next player who can act. */
|
||||||
public void nextPlayer() {
|
public void nextPlayer() {
|
||||||
if (playerOrder.isEmpty()) {
|
if (playerOrder.isEmpty()) {
|
||||||
currentPlayerIndex = 0;
|
currentPlayerIndex = 0;
|
||||||
@@ -308,6 +501,14 @@ public class GameState {
|
|||||||
} while (currentPlayerIndex != start);
|
} while (currentPlayerIndex != start);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the player at the specified index in the player order list is able to take an
|
||||||
|
* action.
|
||||||
|
*
|
||||||
|
* @param index the index of the player in the player order list to check for actionability.
|
||||||
|
* @return true if the player at the specified index can act (i.e., is not folded and not
|
||||||
|
* all-in), false otherwise.
|
||||||
|
*/
|
||||||
private boolean canPlayerAct(int index) {
|
private boolean canPlayerAct(int index) {
|
||||||
if (index < 0 || index >= playerOrder.size()) {
|
if (index < 0 || index >= playerOrder.size()) {
|
||||||
return false;
|
return false;
|
||||||
@@ -322,12 +523,19 @@ public class GameState {
|
|||||||
dealerIndex = (dealerIndex + 1) % playerOrder.size();
|
dealerIndex = (dealerIndex + 1) % playerOrder.size();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the Player object representing the current dealer based on the dealer index in the
|
||||||
|
* player order list.
|
||||||
|
*
|
||||||
|
* @return the Player object representing the current dealer, or null if there are no players or
|
||||||
|
* the dealer index is out of bounds.
|
||||||
|
*/
|
||||||
public Player getDealer() {
|
public Player getDealer() {
|
||||||
PlayerId id = playerOrder.get(dealerIndex);
|
PlayerId id = playerOrder.get(dealerIndex);
|
||||||
return players.get(id);
|
return players.get(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hand reset / lifecycle
|
// HAND RESET / LIFECYCLE
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Starts a new hand by resetting the game state for the next round of poker.
|
* Starts a new hand by resetting the game state for the next round of poker.
|
||||||
@@ -371,6 +579,13 @@ public class GameState {
|
|||||||
getPlayer(playerId).setFolded(true);
|
getPlayer(playerId).setFolded(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the specified player has folded by retrieving their Player object and checking
|
||||||
|
* their folded status.
|
||||||
|
*
|
||||||
|
* @param playerId the ID of the player to check for folded status.
|
||||||
|
* @return true if the player has folded, false otherwise.
|
||||||
|
*/
|
||||||
public boolean isFolded(PlayerId playerId) {
|
public boolean isFolded(PlayerId playerId) {
|
||||||
return getPlayer(playerId).isFolded();
|
return getPlayer(playerId).isFolded();
|
||||||
}
|
}
|
||||||
|
|||||||
+152
-21
@@ -37,6 +37,127 @@ public class GameControllerTest {
|
|||||||
|
|
||||||
private static final Logger LOGGER = Logger.getLogger(GameControllerTest.class.getName());
|
private static final Logger LOGGER = Logger.getLogger(GameControllerTest.class.getName());
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method to retrieve the current player's ID from the game state.
|
||||||
|
*
|
||||||
|
* @param game The GameController instance from which to retrieve the current player's ID.
|
||||||
|
* @return The PlayerId of the current player in the game.
|
||||||
|
*/
|
||||||
|
private static PlayerId currentPlayerId(GameController game) {
|
||||||
|
return game.getState().getCurrentPlayer().getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method to simulate a call action for the current player in the game.
|
||||||
|
*
|
||||||
|
* @param game The GameController instance on which to perform the call action for the current
|
||||||
|
* player.
|
||||||
|
*/
|
||||||
|
private static void callCurrent(GameController game) {
|
||||||
|
game.playerCall(currentPlayerId(game));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method to simulate a fold action for the current player in the game.
|
||||||
|
*
|
||||||
|
* @param game The GameController instance on which to perform the fold action for the current
|
||||||
|
* player.
|
||||||
|
*/
|
||||||
|
private static void foldCurrent(GameController game) {
|
||||||
|
game.playerFold(currentPlayerId(game));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies that the preflop phase ends once all active (non-folded) players have completed
|
||||||
|
* their required actions for the current betting round.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void testPreflopEndsAfterOneActionPerActivePlayer() {
|
||||||
|
|
||||||
|
GameState state = new GameState();
|
||||||
|
GameEngine engine =
|
||||||
|
new GameEngine(
|
||||||
|
state,
|
||||||
|
new RuleEngine(new ArrayList<>()),
|
||||||
|
new RoundManager(),
|
||||||
|
new TurnManager());
|
||||||
|
|
||||||
|
GameController game = new GameController(engine);
|
||||||
|
game.addPlayer(PlayerId.of("P1"), 10000);
|
||||||
|
game.addPlayer(PlayerId.of("P2"), 10000);
|
||||||
|
game.addPlayer(PlayerId.of("P3"), 10000);
|
||||||
|
game.addPlayer(PlayerId.of("P4"), 10000);
|
||||||
|
|
||||||
|
game.startGame();
|
||||||
|
|
||||||
|
assertEquals(GamePhase.PREFLOP, game.getState().getPhase());
|
||||||
|
assertEquals(0, game.getCommunityCards().size());
|
||||||
|
|
||||||
|
callCurrent(game);
|
||||||
|
callCurrent(game);
|
||||||
|
callCurrent(game);
|
||||||
|
|
||||||
|
assertEquals(GamePhase.PREFLOP, game.getState().getPhase());
|
||||||
|
assertEquals(0, game.getCommunityCards().size());
|
||||||
|
|
||||||
|
callCurrent(game);
|
||||||
|
|
||||||
|
assertEquals(GamePhase.FLOP, game.getState().getPhase());
|
||||||
|
assertEquals(3, game.getCommunityCards().size());
|
||||||
|
|
||||||
|
callCurrent(game);
|
||||||
|
callCurrent(game);
|
||||||
|
callCurrent(game);
|
||||||
|
callCurrent(game);
|
||||||
|
|
||||||
|
assertEquals(GamePhase.TURN, game.getState().getPhase());
|
||||||
|
assertEquals(4, game.getCommunityCards().size());
|
||||||
|
|
||||||
|
callCurrent(game);
|
||||||
|
callCurrent(game);
|
||||||
|
callCurrent(game);
|
||||||
|
callCurrent(game);
|
||||||
|
|
||||||
|
assertEquals(GamePhase.RIVER, game.getState().getPhase());
|
||||||
|
assertEquals(5, game.getCommunityCards().size());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensures that during the preflop phase, folded players are excluded from progression checks
|
||||||
|
* and do not block phase advancement.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void testPreflopCountsOnlyNonFoldedPlayersForProgress() {
|
||||||
|
|
||||||
|
GameState state = new GameState();
|
||||||
|
GameEngine engine =
|
||||||
|
new GameEngine(
|
||||||
|
state,
|
||||||
|
new RuleEngine(new ArrayList<>()),
|
||||||
|
new RoundManager(),
|
||||||
|
new TurnManager());
|
||||||
|
|
||||||
|
GameController game = new GameController(engine);
|
||||||
|
game.addPlayer(PlayerId.of("A"), 10000);
|
||||||
|
game.addPlayer(PlayerId.of("B"), 10000);
|
||||||
|
game.addPlayer(PlayerId.of("C"), 10000);
|
||||||
|
game.addPlayer(PlayerId.of("D"), 10000);
|
||||||
|
|
||||||
|
game.startGame();
|
||||||
|
assertEquals(GamePhase.PREFLOP, game.getState().getPhase());
|
||||||
|
|
||||||
|
callCurrent(game);
|
||||||
|
foldCurrent(game);
|
||||||
|
callCurrent(game);
|
||||||
|
|
||||||
|
assertEquals(GamePhase.PREFLOP, game.getState().getPhase());
|
||||||
|
|
||||||
|
callCurrent(game);
|
||||||
|
|
||||||
|
assertEquals(GamePhase.FLOP, game.getState().getPhase());
|
||||||
|
assertEquals(3, game.getCommunityCards().size());
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This test simulates a specific game scenario where Julian is expected to win with a Two Pair
|
* This test simulates a specific game scenario where Julian is expected to win with a Two Pair
|
||||||
* hand. It sets up a fixed deck to ensure that the desired cards are dealt to the players and
|
* hand. It sets up a fixed deck to ensure that the desired cards are dealt to the players and
|
||||||
@@ -83,11 +204,14 @@ public class GameControllerTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
game.playerCall(PlayerId.of("Julian"));
|
callCurrent(game);
|
||||||
game.playerFold(PlayerId.of("Mathis"));
|
game.playerFold(PlayerId.of("Mathis"));
|
||||||
game.playerCall(PlayerId.of("Jona"));
|
callCurrent(game);
|
||||||
game.playerRaise(PlayerId.of("Lars"), 1200);
|
game.playerRaise(PlayerId.of("Lars"), 1200);
|
||||||
|
|
||||||
|
// New street model: once all non-folded players acted, preflop is complete.
|
||||||
|
assertEquals(GamePhase.FLOP, game.getState().getPhase());
|
||||||
|
|
||||||
List<Card> board = game.getCommunityCards();
|
List<Card> board = game.getCommunityCards();
|
||||||
assertTrue(board.size() >= 3 && board.size() <= 5);
|
assertTrue(board.size() >= 3 && board.size() <= 5);
|
||||||
|
|
||||||
@@ -170,10 +294,10 @@ public class GameControllerTest {
|
|||||||
|
|
||||||
int potBefore = game.getState().getPot().getAmount();
|
int potBefore = game.getState().getPot().getAmount();
|
||||||
|
|
||||||
game.playerRaise(PlayerId.of("Lars"), 1200);
|
game.playerRaise(currentPlayerId(game), 1200);
|
||||||
game.playerCall(PlayerId.of("Julian"));
|
callCurrent(game);
|
||||||
game.playerFold(PlayerId.of("Mathis"));
|
foldCurrent(game);
|
||||||
game.playerCall(PlayerId.of("Jona"));
|
callCurrent(game);
|
||||||
|
|
||||||
System.out.println("Pot after actions: " + game.getState().getPot().getAmount());
|
System.out.println("Pot after actions: " + game.getState().getPot().getAmount());
|
||||||
|
|
||||||
@@ -183,11 +307,15 @@ public class GameControllerTest {
|
|||||||
|
|
||||||
assertEquals(3, game.getCommunityCards().size(), "The flop must have 3 cards");
|
assertEquals(3, game.getCommunityCards().size(), "The flop must have 3 cards");
|
||||||
|
|
||||||
game.playerCall(PlayerId.of("Julian"));
|
for (int i = 0; i < 10 && game.getCommunityCards().size() < 4; i++) {
|
||||||
|
callCurrent(game);
|
||||||
|
}
|
||||||
|
|
||||||
assertEquals(4, game.getCommunityCards().size(), "A turn must result in 4 cards");
|
assertEquals(4, game.getCommunityCards().size(), "A turn must result in 4 cards");
|
||||||
|
|
||||||
game.playerCall(PlayerId.of("Jona"));
|
for (int i = 0; i < 10 && game.getCommunityCards().size() < 5; i++) {
|
||||||
|
callCurrent(game);
|
||||||
|
}
|
||||||
|
|
||||||
assertEquals(5, game.getCommunityCards().size(), "The river must consist of 5 cards");
|
assertEquals(5, game.getCommunityCards().size(), "The river must consist of 5 cards");
|
||||||
|
|
||||||
@@ -278,13 +406,12 @@ public class GameControllerTest {
|
|||||||
game.playerCall(PlayerId.of("Jona"));
|
game.playerCall(PlayerId.of("Jona"));
|
||||||
game.playerRaise(PlayerId.of("Lars"), 1200);
|
game.playerRaise(PlayerId.of("Lars"), 1200);
|
||||||
|
|
||||||
game.playerCall(PlayerId.of("Julian"));
|
assertEquals(GamePhase.FLOP, game.getState().getPhase());
|
||||||
game.playerCall(PlayerId.of("Jona"));
|
|
||||||
|
|
||||||
assertTrue(game.getCommunityCards().size() >= 3);
|
assertTrue(game.getCommunityCards().size() >= 3);
|
||||||
|
|
||||||
for (int i = 0; i < 10 && game.getCommunityCards().size() < 5; i++) {
|
for (int i = 0; i < 10 && game.getCommunityCards().size() < 5; i++) {
|
||||||
game.playerCall(PlayerId.of("Julian"));
|
callCurrent(game);
|
||||||
}
|
}
|
||||||
|
|
||||||
assertEquals(5, game.getCommunityCards().size());
|
assertEquals(5, game.getCommunityCards().size());
|
||||||
@@ -367,10 +494,14 @@ public class GameControllerTest {
|
|||||||
|
|
||||||
assertEquals(3, game.getCommunityCards().size());
|
assertEquals(3, game.getCommunityCards().size());
|
||||||
|
|
||||||
game.playerCall(PlayerId.of("Julian"));
|
for (int i = 0; i < 10 && game.getCommunityCards().size() < 4; i++) {
|
||||||
|
callCurrent(game);
|
||||||
|
}
|
||||||
assertEquals(4, game.getCommunityCards().size());
|
assertEquals(4, game.getCommunityCards().size());
|
||||||
|
|
||||||
game.playerCall(PlayerId.of("Mathis"));
|
for (int i = 0; i < 10 && game.getCommunityCards().size() < 5; i++) {
|
||||||
|
callCurrent(game);
|
||||||
|
}
|
||||||
assertEquals(5, game.getCommunityCards().size());
|
assertEquals(5, game.getCommunityCards().size());
|
||||||
|
|
||||||
Set<String> boardSeen = new HashSet<>();
|
Set<String> boardSeen = new HashSet<>();
|
||||||
@@ -424,10 +555,10 @@ public class GameControllerTest {
|
|||||||
|
|
||||||
game.startGame();
|
game.startGame();
|
||||||
|
|
||||||
game.playerRaise(PlayerId.of("BigStack"), 1000);
|
game.playerRaise(currentPlayerId(game), 1000);
|
||||||
game.playerCall(PlayerId.of("Caller"));
|
callCurrent(game);
|
||||||
game.playerCall(PlayerId.of("MidStack"));
|
callCurrent(game);
|
||||||
game.playerCall(PlayerId.of("ShortStack"));
|
callCurrent(game);
|
||||||
|
|
||||||
for (Player p : game.getState().getPlayers()) {
|
for (Player p : game.getState().getPlayers()) {
|
||||||
assertTrue(p.getChips() >= 0, "Negative chips detected for " + p.getId());
|
assertTrue(p.getChips() >= 0, "Negative chips detected for " + p.getId());
|
||||||
@@ -435,13 +566,13 @@ public class GameControllerTest {
|
|||||||
|
|
||||||
assertTrue(game.getState().getPot().getAmount() > 0);
|
assertTrue(game.getState().getPot().getAmount() > 0);
|
||||||
|
|
||||||
game.playerRaise(PlayerId.of("BigStack"), 100);
|
game.playerRaise(currentPlayerId(game), 100);
|
||||||
|
|
||||||
int pot = game.getState().getPot().getAmount();
|
int pot = game.getState().getPot().getAmount();
|
||||||
assertTrue(pot > 0, "Pot must remain valid after re-raises");
|
assertTrue(pot > 0, "Pot must remain valid after re-raises");
|
||||||
|
|
||||||
game.playerFold(PlayerId.of("Caller"));
|
foldCurrent(game);
|
||||||
game.playerFold(PlayerId.of("MidStack"));
|
foldCurrent(game);
|
||||||
|
|
||||||
long activePlayers =
|
long activePlayers =
|
||||||
game.getState().getPlayers().stream().filter(p -> p.getChips() > 0).count();
|
game.getState().getPlayers().stream().filter(p -> p.getChips() > 0).count();
|
||||||
@@ -451,7 +582,7 @@ public class GameControllerTest {
|
|||||||
assertTrue(game.getCommunityCards().size() >= 3);
|
assertTrue(game.getCommunityCards().size() >= 3);
|
||||||
|
|
||||||
for (int i = 0; i < 10 && game.getCommunityCards().size() < 5; i++) {
|
for (int i = 0; i < 10 && game.getCommunityCards().size() < 5; i++) {
|
||||||
game.playerCall(PlayerId.of("BigStack"));
|
callCurrent(game);
|
||||||
}
|
}
|
||||||
|
|
||||||
assertEquals(5, game.getCommunityCards().size());
|
assertEquals(5, game.getCommunityCards().size());
|
||||||
|
|||||||
Reference in New Issue
Block a user