From 621626865891758752240c5fc6fa534639a24358 Mon Sep 17 00:00:00 2001 From: Julian Kropff Date: Sun, 12 Apr 2026 21:39:24 +0200 Subject: [PATCH] Fix: Resolve blocking issues to enable full game loop --- .../casono/client/network/ClientService.java | 32 +- .../casono/client/network/GameClient.java | 17 +- .../ui/gameui/CasinoGameController.java | 55 ++- .../dbis/cs108/casono/server/ServerApp.java | 2 +- .../get_game_state/GetGameStateHandler.java | 17 +- .../get_game_state/GetGameStateResponse.java | 122 ++++-- .../server/domain/game/GameController.java | 5 +- .../domain/game/engine/RoundManager.java | 188 ++++++--- .../domain/game/engine/TurnManager.java | 5 +- .../game/rules/betting/ActionOrderRule.java | 5 +- .../server/domain/game/state/GameState.java | 381 +++++------------- 11 files changed, 398 insertions(+), 431 deletions(-) diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/ClientService.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/ClientService.java index 79438c0..0998fb3 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/ClientService.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/ClientService.java @@ -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 lines = new ArrayList<>(); - int depth = 0; - boolean inPlayer = false; + Deque 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> 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"); } /** diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/GameClient.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/GameClient.java index de49850..456bd2f 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/GameClient.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/GameClient.java @@ -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) { diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/gameui/CasinoGameController.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/gameui/CasinoGameController.java index 47d0e10..e674082 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/gameui/CasinoGameController.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/gameui/CasinoGameController.java @@ -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; } } diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/ServerApp.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/ServerApp.java index 80d2f42..ff3d672 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/ServerApp.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/ServerApp.java @@ -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; diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/game/get_game_state/GetGameStateHandler.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/game/get_game_state/GetGameStateHandler.java index 17e6a96..4ea677a 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/game/get_game_state/GetGameStateHandler.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/game/get_game_state/GetGameStateHandler.java @@ -28,9 +28,9 @@ public class GetGameStateHandler extends CommandHandler { @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 { 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); + } } diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/game/get_game_state/GetGameStateResponse.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/game/get_game_state/GetGameStateResponse.java index 455abe8..8fee488 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/game/get_game_state/GetGameStateResponse.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/app/commands/game/get_game_state/GetGameStateResponse.java @@ -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 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 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 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"; diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/game/GameController.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/game/GameController.java index 02254ba..1199630 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/game/GameController.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/game/GameController.java @@ -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); } /** diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/game/engine/RoundManager.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/game/engine/RoundManager.java index 0fdec45..5faa01f 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/game/engine/RoundManager.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/game/engine/RoundManager.java @@ -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). + * + *

This implementation: + *

    + *
  • starts a new hand (reset + hole cards + blinds)
  • + *
  • progresses phases once betting round is finished
  • + *
  • deals community cards (3/1/1)
  • + *
*/ 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 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 + } +} \ No newline at end of file diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/game/engine/TurnManager.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/game/engine/TurnManager.java index 397303d..98c5346 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/game/engine/TurnManager.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/game/engine/TurnManager.java @@ -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(); } } diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/game/rules/betting/ActionOrderRule.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/game/rules/betting/ActionOrderRule.java index 110a8d1..8db3a61 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/game/rules/betting/ActionOrderRule.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/game/rules/betting/ActionOrderRule.java @@ -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 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"); diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/game/state/GameState.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/game/state/GameState.java index 040bcec..9b614e5 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/game/state/GameState.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/game/state/GameState.java @@ -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. + * + *

Important invariants this implementation maintains: + *

    + *
  • {@code playerOrder} defines the stable seating/turn order.
  • + *
  • {@code currentBets} always contains an entry for every player in {@code playerOrder}.
  • + *
  • {@code holeCards} maps every player to a mutable list; if not present, it is created on demand.
  • + *
*/ public class GameState { - private Map players = new HashMap<>(); - - private List playerOrder = new ArrayList<>(); + private final Map players = new HashMap<>(); + private final List 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 currentBets = new HashMap<>(); + private final Map currentBets = new HashMap<>(); + private final Map playerBetCommitments = new HashMap<>(); - private Map playerBetCommitments = new HashMap<>(); - - private Map> holeCards = new HashMap<>(); - - private List communityCards = new ArrayList<>(); - - // private final Set foldedPlayers = new HashSet<>(); + private final Map> holeCards = new HashMap<>(); + private final List 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 getPlayers() { - return players.values(); + List 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 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 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> 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 cards = new ArrayList<>(); + List 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. + * + *

This: + *

    + *
  • sets {@code phase=PREFLOP}
  • + *
  • resets pot/bets/commitments
  • + *
  • clears community + hole cards
  • + *
  • creates & shuffles a new deck
  • + *
*/ 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(); }