Fix: Resolve blocking issues to enable full game loop

This commit is contained in:
Julian Kropff
2026-04-12 21:39:24 +02:00
parent fb593d48a6
commit 6216268658
11 changed files with 398 additions and 431 deletions
@@ -6,6 +6,8 @@ import ch.unibas.dmi.dbis.cs108.casono.server.network.transport.TcpTransport;
import java.io.IOException;
import java.net.Socket;
import java.util.ArrayList;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ArrayBlockingQueue;
@@ -110,8 +112,7 @@ public class ClientService {
List<String> lines = new ArrayList<>();
int depth = 0;
boolean inPlayer = false;
Deque<String> blockStack = new ArrayDeque<>();
for (String rawLine : responseText.split("\\n")) {
String line = rawLine;
@@ -136,27 +137,15 @@ public class ClientService {
line = line.replaceFirst("^\\s+", "");
trimmed = line.trim();
if ("PLAYER".equals(trimmed)) {
inPlayer = true;
lines.add("PLAYER");
continue;
}
if (isContainerStart(trimmed)) {
depth++;
blockStack.push(trimmed);
lines.add(trimmed);
continue;
}
if ("END".equals(trimmed)) {
if (inPlayer) {
inPlayer = false;
lines.add("END");
continue;
}
if (depth > 0) {
depth--;
if (!blockStack.isEmpty()) {
blockStack.pop();
lines.add("END");
continue;
}
@@ -180,8 +169,8 @@ public class ClientService {
}
}
logger.debug("Parsed response lines (rid={}, success={}, depthEnd={}, inPlayerEnd={}): {}",
rid, success, depth, inPlayer, lines);
logger.debug("Parsed response lines (rid={}, success={}, openBlocks={}): {}",
rid, success, blockStack.size(), lines);
if (rid == 0) {
for (Consumer<List<String>> l : eventListeners) {
@@ -205,8 +194,9 @@ public class ClientService {
return token.equals("LOBBIES")
|| token.equals("LOBBY")
|| token.equals("PLAYERS")
|| token.equals("CARDS");
// NOTE: PLAYER deliberately excluded
|| token.equals("PLAYER")
|| token.equals("CARDS")
|| token.equals("CARD");
}
/**
@@ -107,6 +107,7 @@ public class GameClient {
Player currentPlayer = null;
Card currentCard = null;
boolean insidePlayer = false;
for (String raw : input.split("\n")) {
String line = raw.trim();
@@ -143,6 +144,7 @@ public class GameClient {
currentPlayer = new Player();
s.players.add(currentPlayer);
currentCard = null;
insidePlayer = true;
continue;
}
@@ -153,10 +155,10 @@ public class GameClient {
if (line.equals("CARD")) {
currentCard = new Card("", "");
if (currentPlayer != null) {
currentPlayer.addCard(currentCard); // hole card
if (insidePlayer && currentPlayer != null) {
currentPlayer.addCard(currentCard); // hole card
} else {
s.communityCards.add(currentCard); // community card
s.communityCards.add(currentCard); // community card
}
continue;
}
@@ -171,17 +173,16 @@ public class GameClient {
}
if (line.equals("END")) {
if (currentCard != null) {
if (currentCard != null) { // schließt CARD
currentCard = null;
continue;
}
if (currentPlayer != null) {
if (insidePlayer) { // schließt PLAYER
insidePlayer = false;
currentPlayer = null;
continue;
}
continue;
continue; // root END
}
if (currentPlayer != null) {
@@ -47,6 +47,7 @@ public class CasinoGameController {
private GameService gameService;
private PlayerId myPlayerId;
@FXML private TaskbarController taskbarIncludeController;
private TaskbarController taskbarController;
private Image dealerImage;
private javafx.animation.Timeline timeline;
@@ -154,6 +155,10 @@ public class CasinoGameController {
*/
public void setGameService(GameService gameService) {
this.gameService = gameService;
TaskbarController controller = resolveTaskbarController();
if (controller != null && myPlayerId != null) {
controller.setGameService(gameService, myPlayerId);
}
}
/** Set the PlayerId of the current player. */
@@ -161,6 +166,11 @@ public class CasinoGameController {
public void initialize() {
LOGGER.info("INIT UI");
taskbarController = resolveTaskbarController();
if (taskbarController != null && gameService != null && myPlayerId != null) {
taskbarController.setGameService(gameService, myPlayerId);
}
if (communityCardsBox == null) {
LOGGER.warning("communityCardsBox is NULL");
return;
@@ -471,8 +481,12 @@ public class CasinoGameController {
* @param s The current game state.
*/
private void updateTaskbar(GameState s) {
if (taskbarController != null) {
taskbarController.update(s, myPlayerId);
TaskbarController controller = resolveTaskbarController();
if (controller != null) {
if (gameService != null && myPlayerId != null) {
controller.setGameService(gameService, myPlayerId);
}
controller.update(s, myPlayerId);
}
}
@@ -868,23 +882,29 @@ public class CasinoGameController {
return BACKSIDE;
}
String rawSuit = card.getSuit();
String rawValue = card.getValue();
if (rawSuit == null || rawValue == null) {
return BACKSIDE;
}
String suit =
switch (card.getSuit().toLowerCase()) {
case "hearts" -> "heart";
case "diamonds" -> "diamond";
case "clubs", "cross" -> "cross";
case "spades", "pik" -> "pik";
default -> card.getSuit().toLowerCase();
switch (rawSuit.trim().toLowerCase()) {
case "h", "hearts", "heart" -> "heart";
case "d", "diamonds", "diamond" -> "diamond";
case "c", "clubs", "club", "cross" -> "cross";
case "s", "spades", "spade", "pik" -> "pik";
default -> rawSuit.trim().toLowerCase();
};
String value =
switch (card.getValue().toLowerCase()) {
switch (rawValue.trim().toLowerCase()) {
case "a", "ace" -> "ace";
case "k", "king" -> "king";
case "q", "queen" -> "queen";
case "j", "jack" -> "jack";
case "10", "t" -> "10";
default -> card.getValue().toLowerCase();
default -> rawValue.trim().toLowerCase();
};
String path = "/images/card-" + suit + "-" + value + "-3.png";
@@ -1026,5 +1046,20 @@ public class CasinoGameController {
public void setMyPlayerId(PlayerId id) {
this.myPlayerId = id;
TaskbarController controller = resolveTaskbarController();
if (controller != null && gameService != null && myPlayerId != null) {
controller.setGameService(gameService, myPlayerId);
}
}
private TaskbarController resolveTaskbarController() {
if (taskbarController != null) {
return taskbarController;
}
if (taskbarIncludeController != null) {
taskbarController = taskbarIncludeController;
return taskbarController;
}
return null;
}
}
@@ -54,7 +54,7 @@ public class ServerApp {
private static final int USER_CLEANUP_JOB_RECONNECT_THRESHOLD = 10;
private static final int SESSION_DISCONNECT_JOB_DELAY = 0;
private static final int SESSION_DISCONNECT_JOB_PERIOD = 2;
private static final int SESSION_DISCONNECT_JOB_TIMEOUT = 30000;
private static final int SESSION_DISCONNECT_JOB_TIMEOUT = 30;
private static final int LOBBY_EXPIRY_SECONDS = 30;
private static final int LOBBY_CLEANUP_INITIAL_DELAY_SECONDS = 5;
private static final int LOBBY_CLEANUP_PERIOD_SECONDS = 5;
@@ -28,9 +28,9 @@ public class GetGameStateHandler extends CommandHandler<GetGameStateRequest> {
@Override
public void execute(GetGameStateRequest request) {
Integer gameId = request.getGameId();
String username = request.getUsername();
String username = resolveUsername(request);
Lobby lobby = null;
Lobby lobby;
if (gameId != null) {
lobby = lobbyManager.getLobby(LobbyId.of(gameId));
if (lobby == null) {
@@ -71,4 +71,17 @@ public class GetGameStateHandler extends CommandHandler<GetGameStateRequest> {
responseDispatcher.dispatch(new GetGameStateResponse(request.getContext(), game, username));
}
private String resolveUsername(GetGameStateRequest request) {
String username = request.getUsername();
if (username != null && !username.isBlank()) {
return username.trim();
}
return userRegistry.getBySessionId(request.getSessionId())
.map(User::getName)
.map(String::trim)
.filter(name -> !name.isEmpty())
.orElse(null);
}
}
@@ -12,25 +12,58 @@ import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.SuccessR
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.builder.ResponseBody;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.builder.ResponseBodyBuilder;
import java.util.List;
import java.util.Objects;
/** Response carrying a snapshot of the current game state using the project's wire format. */
/**
* Response carrying a snapshot of the current game state using the project's wire format.
*
* Wire format (relevant parts):
*
* +OK
* PHASE=...
* POT=...
* CURRENT_BET=...
* DEALER=...
* ACTIVE_PLAYER=...
* CARD
* VALUE=...
* SUIT=...
* END
* PLAYER
* NAME=...
* CHIPS=...
* BET=...
* STATE=...
* CARD (only for requesting user)
* VALUE=...
* SUIT=...
* END
* END
* END
*
* Notes:
* - Community cards are always emitted at top-level (0..5). If none exist (e.g. PREFLOP), none are emitted.
* - Hole cards are only emitted for the requesting player (privacy).
*/
public class GetGameStateResponse extends SuccessResponse {
public GetGameStateResponse(
RequestContext context, GameController game, String requestingUsername) {
super(context, buildBody(game.getState(), requestingUsername));
public GetGameStateResponse(RequestContext context, GameController game, String requestingUsername) {
super(context, buildBody(Objects.requireNonNull(game, "game must not be null").getState(), requestingUsername));
}
private static ResponseBody buildBody(GameState state, String requestingUsername) {
int globalCurrentBet = computeGlobalCurrentBet(state);
Objects.requireNonNull(state, "state must not be null");
ResponseBodyBuilder builder = ResponseBody.builder();
builder.param("PHASE", state.getPhase() == null ? "UNKNOWN" : state.getPhase().name());
builder.param("POT", state.getPot().getAmount());
builder.param("CURRENT_BET", globalCurrentBet);
builder.param("POT", state.getPot() == null ? 0 : state.getPot().getAmount());
builder.param("CURRENT_BET", computeGlobalCurrentBet(state));
builder.param("DEALER", state.getDealerIndex());
builder.param("ACTIVE_PLAYER", state.getCurrentPlayerIndex());
appendCommunityCards(builder, state);
appendPlayers(builder, state, requestingUsername);
return builder.build();
@@ -39,53 +72,63 @@ public class GetGameStateResponse extends SuccessResponse {
private static int computeGlobalCurrentBet(GameState state) {
int globalCurrentBet = 0;
for (Player p : state.getPlayers()) {
if (p == null) continue;
int b = state.getCurrentBet(p.getId());
if (b > globalCurrentBet) {
globalCurrentBet = b;
}
if (b > globalCurrentBet) globalCurrentBet = b;
}
return globalCurrentBet;
}
private static void appendCommunityCards(ResponseBodyBuilder builder, GameState state) {
for (Card c : state.getCommunityCards()) {
builder.block(
"CARD",
card -> {
card.param("VALUE", rankToWire(c.getRank()));
card.param("SUIT", suitToWire(c.getSuit()));
});
List<Card> community = state.getCommunityCards();
if (community == null || community.isEmpty()) {
return;
}
for (Card c : community) {
if (c == null) continue;
builder.block("CARD", card -> {
card.param("VALUE", rankToWire(c.getRank()));
card.param("SUIT", suitToWire(c.getSuit()));
});
}
}
private static void appendPlayers(
ResponseBodyBuilder builder, GameState state, String requestingUsername) {
for (Player p : state.getPlayers()) {
PlayerId pid = p.getId();
builder.block(
"PLAYER",
pblock -> {
pblock.param("NAME", pid.value());
pblock.param("CHIPS", p.getChips());
pblock.param("BET", state.getCurrentBet(pid));
pblock.param("STATE", p.isFolded() ? "FOLDED" : "ACTIVE");
private static void appendPlayers(ResponseBodyBuilder builder, GameState state, String requestingUsername) {
String req = (requestingUsername == null) ? null : requestingUsername.trim();
if (requestingUsername != null && requestingUsername.equals(pid.value())) {
List<Card> hole = state.getHoleCards(pid);
for (Card hc : hole) {
pblock.block(
"CARD",
cblock -> {
cblock.param("VALUE", rankToWire(hc.getRank()));
cblock.param("SUIT", suitToWire(hc.getSuit()));
});
}
for (Player p : state.getPlayers()) {
if (p == null) continue;
PlayerId pid = p.getId();
String name = (pid == null || pid.value() == null) ? "" : pid.value().trim();
builder.block("PLAYER", pblock -> {
pblock.param("NAME", name);
pblock.param("CHIPS", p.getChips());
pblock.param("BET", pid == null ? 0 : state.getCurrentBet(pid));
pblock.param("STATE", p.isFolded() ? "FOLDED" : "ACTIVE");
if (req != null && pid != null && req.equals(name)) {
List<Card> hole = state.getHoleCards(pid);
if (hole != null) {
for (Card hc : hole) {
if (hc == null) continue;
pblock.block("CARD", cblock -> {
cblock.param("VALUE", rankToWire(hc.getRank()));
cblock.param("SUIT", suitToWire(hc.getSuit()));
});
}
});
}
}
});
}
}
private static String rankToWire(Rank r) {
if (r == null) return "";
return switch (r) {
case TWO -> "2";
case THREE -> "3";
@@ -104,6 +147,7 @@ public class GetGameStateResponse extends SuccessResponse {
}
private static String suitToWire(Suit s) {
if (s == null) return "";
return switch (s) {
case HEARTS -> "H";
case DIAMONDS -> "D";
@@ -29,7 +29,6 @@ public class GameController {
private int dealerIndex = 0;
private static final int NEXT_PLAYER_OFFSET = 3;
private static final int DEALER_OFFSET = 1;
private static final int SMALL_BLIND_OFFSET = 1;
private static final int BIG_BLIND_OFFSET = 2;
@@ -81,13 +80,13 @@ public class GameController {
dealHoleCards();
postBlinds();
int nextPlayerIndex = (dealerIndex + NEXT_PLAYER_OFFSET) % players.size();
engine.getState().setCurrentPlayerIndex(nextPlayerIndex);
engine.getState().setCurrentPlayerToPreflopFirstToAct();
}
/** Rotates the dealer position to the next player in the list. */
private void rotateDealer() {
dealerIndex = (dealerIndex + DEALER_OFFSET) % players.size();
engine.getState().setDealerIndex(dealerIndex);
}
/**
@@ -1,133 +1,189 @@
package ch.unibas.dmi.dbis.cs108.casono.server.domain.game.engine;
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.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.PlayerStatus;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.state.GamePhase;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.state.GameState;
import java.util.ArrayList;
import java.util.List;
/**
* RoundManager is responsible for managing the flow of a poker game round. It handles the
* progression of the game through its various phases (pre-flop, flop, turn, river) and manages
* player actions such as posting blinds and dealing cards. The RoundManager ensures that the game
* state is updated correctly based on player actions and the current phase of the game.
* RoundManager manages hand lifecycle and phase progression (PREFLOP -> FLOP -> TURN -> RIVER -> SHOWDOWN).
*
* <p>This implementation:
* <ul>
* <li>starts a new hand (reset + hole cards + blinds)</li>
* <li>progresses phases once betting round is finished</li>
* <li>deals community cards (3/1/1)</li>
* </ul>
*/
public class RoundManager {
public static final int SMALL_BLIND = 100;
public static final int BIG_BLIND = 200;
/**
* Starts a new hand by initializing the game state, dealing cards to players, and posting
* blinds. This method sets the hand as active and resets any necessary state variables to
* prepare for a new round of play.
*
* @param state The game state to be used for starting the new hand.
*/
public void startNewHand(GameState state) {
state.setHandActive(true);
state.resetBets();
// Use GameState's canonical reset
state.startNewHand();
dealCards(state);
// Ensure deck exists
ensureDeck(state);
// Deal hole cards
dealHoleCards(state);
// Post blinds
postBlinds(state);
// Preflop always starts left of BB (or dealer in heads-up).
state.setCurrentPlayerToPreflopFirstToAct();
}
/**
* Checks if the betting round is finished and advances the game phase if necessary. This method
* evaluates the current bets of all active players and determines if the betting round can be
* concluded. If all players have met the current bet or are all-in, the game phase is advanced
* to the next stage.
*
* @param state The current game state to be evaluated for betting round progression.
*/
public void progressIfNeeded(GameState state) {
if (state.getPhase() == null) {
state.setPhase(GamePhase.PREFLOP);
}
if (isBettingRoundFinished(state)) {
advancePhase(state);
}
}
/**
* Determines if the betting round is finished by checking if all active players have met the
* current bet or are all-in. This method iterates through all players in the game state and
* evaluates their bets against the current bet on the table.
*
* @param state The current game state to be evaluated for betting round completion.
* @return true if the betting round is finished, false otherwise.
*/
private boolean isBettingRoundFinished(GameState state) {
int target = state.getTableState().getCurrentBet();
for (Player p : state.getPlayers()) {
if (p == null) continue;
if (p.getStatus() == PlayerStatus.FOLDED) {
continue;
}
// be tolerant if codebase mixes status + boolean flags
if (p.getStatus() == PlayerStatus.FOLDED || p.isFolded()) continue;
int bet = state.getCurrentBet(p.getId());
// The player must have placed at least one bet
if (bet < target && !p.isAllIn()) {
return false;
}
}
return true;
}
/**
* Advances the game phase to the next stage (flop, turn, river, or showdown) based on the
* current phase of the game. This method is called when the betting round is finished and
* updates the game state accordingly to reflect the new phase of play.
*
* @param state The current game state to be updated with the new phase.
*/
private void advancePhase(GameState state) {
switch (state.getPhase()) {
case PREFLOP -> dealFlop(state);
case FLOP -> dealTurn(state);
case TURN -> dealRiver(state);
case RIVER -> showdown(state);
case SHOWDOWN -> { /* nothing */ }
}
}
private void ensureDeck(GameState state) {
Deck deck = state.getDeck();
if (deck == null) {
deck = new Deck();
deck.shuffle();
state.setDeck(deck);
}
}
private void dealHoleCards(GameState state) {
Deck deck = state.getDeck();
for (Player p : state.getPlayers()) {
if (p == null) continue;
PlayerId pid = p.getId();
Card c1 = deck.draw();
Card c2 = deck.draw();
state.giveHoleCards(pid, c1, c2);
}
}
/**
* Handles the posting of blinds at the start of a new hand. This method identifies the players
* responsible for posting the small and big blinds, updates their chip counts, adds the blind
* amounts to the pot, and updates the current bets for those players in the game state.
*
* @param state The current game state to be updated with the posted blinds.
* NOTE: Blind assignment here is intentionally minimal because GameState does not expose seating order.
* If you want correct dealer-relative blinds, add helpers in GameState (e.g. getPlayerIdAt(int)).
*/
private void postBlinds(GameState state) {
List<Player> playerList = new ArrayList<>(state.getPlayers());
if (playerList.size() < 2) return;
Player sb = playerList.get(0);
Player bb = playerList.get(1);
// pick first two non-folded players as SB/BB
Player sb = null;
Player bb = null;
int smallBlind = SMALL_BLIND;
int bigBlind = BIG_BLIND;
for (Player p : playerList) {
if (p == null) continue;
if (p.isFolded()) continue;
sb.removeChips(smallBlind);
bb.removeChips(bigBlind);
if (sb == null) {
sb = p;
} else {
bb = p;
break;
}
}
state.addToPot(smallBlind + bigBlind);
if (sb == null || bb == null) return;
state.setCurrentBet(sb.getId(), smallBlind);
state.setCurrentBet(bb.getId(), bigBlind);
sb.removeChips(SMALL_BLIND);
bb.removeChips(BIG_BLIND);
state.getTableState().setCurrentBet(bigBlind);
state.addToPot(SMALL_BLIND + BIG_BLIND);
state.setCurrentBet(sb.getId(), SMALL_BLIND);
state.setCurrentBet(bb.getId(), BIG_BLIND);
state.getTableState().setCurrentBet(BIG_BLIND);
}
private void dealCards(GameState state) {}
private void dealFlop(GameState state) {
ensureDeck(state);
Deck deck = state.getDeck();
private void dealFlop(GameState state) {}
state.addCommunityCard(deck.draw());
state.addCommunityCard(deck.draw());
state.addCommunityCard(deck.draw());
private void dealTurn(GameState state) {}
state.setPhase(GamePhase.FLOP);
private void dealRiver(GameState state) {}
// new betting round
state.resetBets();
state.setCurrentPlayerToPostflopFirstToAct();
}
private void showdown(GameState state) {}
}
private void dealTurn(GameState state) {
ensureDeck(state);
Deck deck = state.getDeck();
state.addCommunityCard(deck.draw());
state.setPhase(GamePhase.TURN);
// new betting round
state.resetBets();
state.setCurrentPlayerToPostflopFirstToAct();
}
private void dealRiver(GameState state) {
ensureDeck(state);
Deck deck = state.getDeck();
state.addCommunityCard(deck.draw());
state.setPhase(GamePhase.RIVER);
// new betting round
state.resetBets();
state.setCurrentPlayerToPostflopFirstToAct();
}
private void showdown(GameState state) {
state.setPhase(GamePhase.SHOWDOWN);
// winner evaluation is elsewhere (rules/controller)
// do NOT reset cards here; clients still need board visible during showdown
}
}
@@ -18,9 +18,6 @@ public class TurnManager {
* @param state The current game state that will be updated to reflect the next player's turn.
*/
public void nextPlayer(GameState state) {
int next = (state.getCurrentPlayerIndex() + 1) % state.getPlayers().size();
state.setCurrentPlayerIndex(next);
state.nextPlayer();
}
}
@@ -6,8 +6,6 @@ import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.player.PlayerId;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.rules.Rule;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.rules.RuleViolationException;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.state.GameState;
import java.util.ArrayList;
import java.util.List;
/**
* The ActionOrderRule class implements the Rule interface and defines the validation logic for
@@ -31,8 +29,7 @@ public class ActionOrderRule implements Rule {
PlayerId actingPlayerId = action.getPlayerId();
List<Player> playerList = new ArrayList<>(state.getPlayers());
Player current = playerList.get(state.getCurrentPlayerIndex());
Player current = state.getCurrentPlayer();
if (!current.getId().equals(actingPlayerId)) {
throw new RuleViolationException("Not your turn");
@@ -11,225 +11,154 @@ import java.util.List;
import java.util.Map;
/**
* The GameState class encapsulates the entire state of a poker game at any given moment. It
* maintains information about the players, the pot, the current phase of the game, the dealer
* position, and the cards in play. This class is central to managing the flow of the game and
* ensuring that all actions and decisions are based on an accurate representation of the current
* game state.
* The GameState class encapsulates the entire state of a poker game at any given moment.
*
* <p>Important invariants this implementation maintains:
* <ul>
* <li>{@code playerOrder} defines the stable seating/turn order.</li>
* <li>{@code currentBets} always contains an entry for every player in {@code playerOrder}.</li>
* <li>{@code holeCards} maps every player to a mutable list; if not present, it is created on demand.</li>
* </ul>
*/
public class GameState {
private Map<PlayerId, Player> players = new HashMap<>();
private List<PlayerId> playerOrder = new ArrayList<>();
private final Map<PlayerId, Player> players = new HashMap<>();
private final List<PlayerId> playerOrder = new ArrayList<>();
private Pot pot = new Pot();
private TableState tableState = new TableState();
private final TableState tableState = new TableState();
private int currentPlayerIndex;
private boolean handActive;
private boolean allowOutOfTurn;
private GamePhase phase;
private int dealerIndex;
private Map<PlayerId, Integer> currentBets = new HashMap<>();
private final Map<PlayerId, Integer> currentBets = new HashMap<>();
private final Map<PlayerId, Integer> playerBetCommitments = new HashMap<>();
private Map<PlayerId, Integer> playerBetCommitments = new HashMap<>();
private Map<PlayerId, List<Card>> holeCards = new HashMap<>();
private List<Card> communityCards = new ArrayList<>();
// private final Set<PlayerId> foldedPlayers = new HashSet<>();
private final Map<PlayerId, List<Card>> holeCards = new HashMap<>();
private final List<Card> communityCards = new ArrayList<>();
private Deck deck;
// Getter
/**
* Returns the list of players currently in the game.
*
* @return A list of Player objects representing the players in the game.
*/
// Getters
public Collection<Player> getPlayers() {
return players.values();
List<Player> ordered = new ArrayList<>(playerOrder.size());
for (PlayerId id : playerOrder) {
Player p = players.get(id);
if (p != null) {
ordered.add(p);
}
}
return ordered;
}
/**
* Returns the current player whose turn it is to act.
*
* @return The Player object representing the current player.
*/
public Player getCurrentPlayer() {
PlayerId id = playerOrder.get(currentPlayerIndex);
return players.get(id);
}
/**
* Returns the index of the current player in the players list.
*
* @return An integer representing the index of the current player.
*/
public int getCurrentPlayerIndex() {
return currentPlayerIndex;
}
/**
* Indicates whether a hand is currently active in the game.
*
* @return true if a hand is active, false otherwise.
*/
public boolean isHandActive() {
return handActive;
}
/**
* Returns the current phase of the game (e.g., PREFLOP, FLOP, TURN, RIVER).
*
* @return The GamePhase enum value representing the current phase of the game.
*/
public GamePhase getPhase() {
return phase;
}
/**
* Returns the current state of the table, including player statuses and positions.
*
* @return A TableState object representing the current state of the table.
*/
public TableState getTableState() {
return tableState;
}
/**
* Returns the current pot, which contains the total amount of chips bet by players in the
* current hand.
*
* @return A Pot object representing the current pot.
*/
public Pot getPot() {
return pot;
}
/**
* Returns the current deck of cards being used in the game.
*
* @return A Deck object representing the current deck of cards.
*/
public Deck getDeck() {
return deck;
}
/**
* Returns the list of community cards currently on the table.
*
* @return A list of Card objects representing the community cards.
*/
public List<Card> getCommunityCards() {
return communityCards;
}
/**
* Returns the hole cards for a specific player based on their ID.
*
* @param playerId The ID of the player whose hole cards are being requested.
* @return A list of Card objects representing the player's hole cards, or an empty list if the
* player has no hole cards.
*/
/** Always returns a mutable list (never null). */
public List<Card> getHoleCards(PlayerId playerId) {
return holeCards.getOrDefault(playerId, new ArrayList<>());
return holeCards.computeIfAbsent(playerId, k -> new ArrayList<>());
}
/**
* Returns the index of the dealer in the players list.
*
* @return An integer representing the index of the dealer.
*/
public int getDealerIndex() {
return dealerIndex;
}
/**
* Returns a map of player IDs to their current hole cards.
*
* @return A map where the key is the player ID and the value is a list of Card objects
* representing the player's hole cards.
*/
public Map<PlayerId, List<Card>> getPlayerCards() {
return holeCards;
}
/**
* Returns the number of players currently in the game.
*
* @return An integer representing the number of players in the game.
*/
public int getPlayerCount() {
return players.size();
}
// SETTER
/**
* Sets the index of the current player in the players list.
*
* @param index An integer representing the index of the current player.
*/
// Setters / Mutators
public void setCurrentPlayerIndex(int index) {
if (playerOrder.isEmpty()) {
this.currentPlayerIndex = 0;
return;
}
this.currentPlayerIndex = index;
}
/**
* Sets whether a hand is currently active in the game.
*
* @param handActive A boolean value indicating whether a hand is active.
*/
public void setCurrentPlayerToPreflopFirstToAct() {
int size = playerOrder.size();
if (size == 0) {
currentPlayerIndex = 0;
return;
}
// Heads-up: dealer (small blind) acts first preflop.
int first = (size == 2) ? dealerIndex : (dealerIndex + 3) % size;
currentPlayerIndex = first;
if (!canPlayerAct(currentPlayerIndex)) {
nextPlayer();
}
}
public void setCurrentPlayerToPostflopFirstToAct() {
int size = playerOrder.size();
if (size == 0) {
currentPlayerIndex = 0;
return;
}
int first = (dealerIndex + 1) % size;
currentPlayerIndex = first;
if (!canPlayerAct(currentPlayerIndex)) {
nextPlayer();
}
}
public void setHandActive(boolean handActive) {
this.handActive = handActive;
}
/**
* Sets the current phase of the game.
*
* @param phase The GamePhase enum value representing the new phase of the game.
*/
public void setPhase(GamePhase phase) {
this.phase = phase;
}
/**
* Sets the index of the dealer in the players list.
*
* @param dealerIndex An integer representing the index of the dealer.
*/
public void setDealerIndex(int dealerIndex) {
this.dealerIndex = dealerIndex;
}
/**
* Sets the current deck of cards being used in the game.
*
* @param deck A Deck object representing the new deck of cards to be used in the game.
*/
public void setDeck(Deck deck) {
this.deck = deck;
}
/**
* Adds a player to the game with the specified ID and initial chip count. This method creates a
* new Player object, adds it to the list of players, and initializes the player's current bet
* and bet commitment in the game state.
*
* @param id The ID of the player to add.
* @param chips The initial number of chips the player has.
*/
public void addPlayer(PlayerId id, int chips) {
Player player = new Player(id, chips);
players.put(id, player);
@@ -237,213 +166,130 @@ public class GameState {
currentBets.put(id, 0);
playerBetCommitments.put(id, 0);
holeCards.computeIfAbsent(id, k -> new ArrayList<>());
}
// BETTING LOGIC
/**
* Retrieves the current bet amount for a specific player based on their ID.
*
* @param playerId The ID of the player whose current bet is being requested.
* @return An integer representing the current bet amount for the specified player, or 0 if the
* player has not placed any bets.
*/
// Betting
public int getCurrentBet(PlayerId playerId) {
return currentBets.getOrDefault(playerId, 0);
}
/**
* Sets the current bet amount for a specific player based on their ID. This method updates the
* currentBets map with the new 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 be set for the specified player.
*/
public void setCurrentBet(PlayerId playerId, int amount) {
currentBets.put(playerId, amount);
}
/**
* Resets the current bets for all players by clearing the currentBets map. This method is
* typically called at the start of a new hand to ensure that all players' bets are reset to
* zero.
* Reset bets to 0 for ALL players (do not clear the map).
* Also resets table current bet to 0.
*/
public void resetBets() {
currentBets.clear();
for (PlayerId id : playerOrder) {
currentBets.put(id, 0);
}
tableState.setCurrentBet(0);
}
/**
* Adds a specified amount to the pot. This method updates the total amount in the pot by adding
* the given amount to it.
*
* @param amount The amount of chips to be added to the pot.
*/
public void addToPot(int amount) {
pot.add(amount);
}
/**
* Returns whether out-of-turn actions are allowed in the game. Out-of-turn actions refer to
* players being able to act when it is not their turn, which can be a feature in some poker
* variants or game modes.
*
* @return true if out-of-turn actions are allowed, false otherwise.
*/
public boolean isAllowOutOfTurn() {
return allowOutOfTurn;
}
/**
* Sets whether out-of-turn actions are allowed in the game. This method updates the
* allowOutOfTurn flag, which determines if players can act when it is not their turn.
*
* @param allowOutOfTurn A boolean value indicating whether out-of-turn actions should be
* allowed in the game.
*/
public void setAllowOutOfTurn(boolean allowOutOfTurn) {
this.allowOutOfTurn = allowOutOfTurn;
}
// PLAYER HELPERS
/**
* Retrieves a player from the game based on their ID. This method searches the list of players
* for a player with the specified ID and returns it. If no player with the given ID is found, a
* RuntimeException is thrown.
*
* @param id The ID of the player to retrieve.
* @return The Player object corresponding to the specified ID.
* @throws RuntimeException if no player with the given ID is found in the game.
*/
// Player helpers
public Player getPlayer(PlayerId id) {
Player player = players.get(id);
if (player == null) {
throw new RuntimeException("Player not found: " + id);
}
if (player == null) throw new RuntimeException("Player not found: " + id);
return player;
}
// BET COMMITMENTS
/**
* Retrieves the current bet commitment for a specific player based on their ID. A bet
* commitment represents the total amount a player has committed to the pot in the current hand,
* including all bets, raises, and calls they have made.
*
* @param playerId The ID of the player whose current bet commitment is being requested.
* @return An integer representing the current bet commitment for the specified player, or 0 if
* the player has not made any bet commitments.
*/
// Bet commitments
public int getCurrentBetCommitment(PlayerId playerId) {
return playerBetCommitments.getOrDefault(playerId, 0);
}
/**
* Sets the current bet commitment for a specific player based on their ID. This method updates
* the playerBetCommitments map with the new bet commitment amount for the specified player.
*
* @param playerId The ID of the player whose current bet commitment is being set.
* @param amount The new bet commitment amount to be set for the specified player.
*/
public void setCurrentBetCommitment(PlayerId playerId, int amount) {
playerBetCommitments.put(playerId, amount);
}
// CARDS
// Cards
/**
* Gives hole cards to a specific player based on their ID. This method takes two Card objects
* representing the player's hole cards and adds them to the holeCards map under the player's
* ID.
*
* @param playerId The ID of the player to whom the hole cards are being given.
* @param c1 The first Card object representing one of the player's hole cards.
* @param c2 The second Card object representing the other hole card for the player.
* Gives (overwrites) the two hole cards for the given player.
* Ensures stable list identity (important if other code holds references).
*/
public void giveHoleCards(PlayerId playerId, Card c1, Card c2) {
List<Card> cards = new ArrayList<>();
List<Card> cards = holeCards.computeIfAbsent(playerId, k -> new ArrayList<>());
cards.clear();
cards.add(c1);
cards.add(c2);
holeCards.put(playerId, cards);
}
/**
* Adds a community card to the game state. This method takes a Card object representing a
* community card and adds it to the list of community cards on the table.
*
* @param card The Card object representing the community card to be added to the game state.
*/
public void addCommunityCard(Card card) {
communityCards.add(card);
}
/**
* Resets the community cards by clearing the list of community cards. This method is typically
* called at the start of a new hand to ensure that all community cards from the previous hand
* are removed from the game state.
*/
public void resetCommunityCards() {
communityCards.clear();
}
// TURN MANAGEMENT
/**
* Advances the turn to the next player in the players list. This method updates the
* currentPlayerIndex by incrementing it and wrapping around to the start of the list if
* necessary, ensuring that the turn order is maintained correctly throughout the game.
*/
// Turn / dealer management
public void nextPlayer() {
if (playerOrder.isEmpty()) {
currentPlayerIndex = 0;
return;
}
int start = currentPlayerIndex;
do {
currentPlayerIndex = (currentPlayerIndex + 1) % playerOrder.size();
Player p = getCurrentPlayer();
if (!p.isFolded() && !p.isAllIn()) {
if (canPlayerAct(currentPlayerIndex)) {
return;
}
} while (currentPlayerIndex != start);
}
// Dealer Rotation
private boolean canPlayerAct(int index) {
if (index < 0 || index >= playerOrder.size()) {
return false;
}
PlayerId id = playerOrder.get(index);
Player p = players.get(id);
return p != null && !p.isFolded() && !p.isAllIn();
}
/**
* Rotates the dealer position to the next player in the players list. This method updates the
* dealerIndex by incrementing it and wrapping around to the start of the list if necessary,
* ensuring that the dealer position rotates correctly after each hand.
*/
public void rotateDealer() {
dealerIndex = (dealerIndex + 1) % playerOrder.size();
}
/**
* Retrieves the current dealer based on the dealerIndex. This method returns the Player object
* corresponding to the current dealer position in the players list.
*
* @return The Player object representing the current dealer.
*/
public Player getDealer() {
PlayerId id = playerOrder.get(dealerIndex);
return players.get(id);
}
// HAND RESET
// Hand reset / lifecycle
/**
* Starts a new hand by resetting the game state for the next round of poker. This method sets
* the handActive flag to true, resets the game phase to PREFLOP, clears the pot, resets all
* player bets and bet commitments, clears the community cards, and initializes a new shuffled
* deck of cards for the new hand.
* Starts a new hand by resetting the game state for the next round of poker.
*
* <p>This:
* <ul>
* <li>sets {@code phase=PREFLOP}</li>
* <li>resets pot/bets/commitments</li>
* <li>clears community + hole cards</li>
* <li>creates & shuffles a new deck</li>
* </ul>
*/
public void startNewHand() {
handActive = true;
phase = GamePhase.PREFLOP;
@@ -455,6 +301,10 @@ public class GameState {
resetCommunityCards();
holeCards.clear();
for (PlayerId id : playerOrder) {
holeCards.put(id, new ArrayList<>());
}
for (Player player : players.values()) {
player.setFolded(false);
}
@@ -462,28 +312,13 @@ public class GameState {
deck = new Deck();
deck.shuffle();
currentPlayerIndex = (dealerIndex + 1) % playerOrder.size();
setCurrentPlayerToPreflopFirstToAct();
}
/**
* Folds a player in the current hand. This method adds the specified player's ID to the set of
* folded players, indicating that the player has folded and is no longer active in the current
* hand.
*
* @param playerId The ID of the player who is folding.
*/
public void foldPlayer(PlayerId playerId) {
getPlayer(playerId).setFolded(true);
}
/**
* Checks if a specific player has folded in the current hand. This method checks if the
* specified player's ID is present in the set of folded players, indicating that the player has
* folded.
*
* @param playerId The ID of the player to check for folding status.
* @return true if the player has folded, false otherwise.
*/
public boolean isFolded(PlayerId playerId) {
return getPlayer(playerId).isFolded();
}