From fb71c6de7b4f2f0555fce22a37d09cfb1bd170a9 Mon Sep 17 00:00:00 2001 From: Julian Kropff Date: Sat, 4 Apr 2026 23:35:43 +0200 Subject: [PATCH] Add: game engine and flow control --- .../server/domain/game/GameController.java | 257 ++++++++++++++++++ .../server/domain/game/engine/GameEngine.java | 108 ++++++++ .../domain/game/engine/RoundManager.java | 144 ++++++++++ .../domain/game/engine/TurnManager.java | 31 +++ 4 files changed, 540 insertions(+) create mode 100644 src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/game/GameController.java create mode 100644 src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/game/engine/GameEngine.java create mode 100644 src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/game/engine/RoundManager.java create mode 100644 src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/game/engine/TurnManager.java 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 new file mode 100644 index 0000000..9c42a47 --- /dev/null +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/game/GameController.java @@ -0,0 +1,257 @@ +package ch.unibas.dmi.dbis.cs108.casono.server.domain.game; + +import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.action.BlindAction; +import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.action.CallAction; +import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.action.FoldAction; +import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.action.RaiseAction; +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.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.HandRank; +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.GameState; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * GameController is responsible for managing the flow of the poker game. It + * interacts with the GameEngine to process player actions and update the game + * state accordingly. + */ +public class GameController { + + private final GameEngine engine; + + private final List players = new ArrayList<>(); + + 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; + private static final int SMALL_BLIND = 100; + private static final int BIG_BLIND = 200; + + /** + * Initializes the GameController with a reference to the GameEngine. + * + * @param engine The GameEngine instance that manages the game state and logic. + */ + public GameController(GameEngine engine) { + this.engine = engine; + } + + /** + * Adds a player to the game with the specified name and initial chip count. + * + * @param name The name of the player to add. + * @param chips The initial number of chips the player has. + */ + public void addPlayer(PlayerId name, int chips) { + + // PlayerId id = PlayerId.of(name); + // + // players.add(id); + // engine.getState().addPlayer(id, chips); + + players.add(name); + engine.getState().addPlayer(name, chips); + } + + /** + * Initializes a new hand by preparing the deck, setting the phase to PREFLOP, + * rotating the dealer, dealing hole cards, posting blinds, + * and setting the first active player. + */ + public void startGame() { + + if (engine.getState().getDeck() == null) { + Deck deck = new Deck(); + deck.shuffle(); + engine.getState().setDeck(deck); + } + + engine.getState().setHandActive(true); + engine.getState().setPhase(GamePhase.PREFLOP); + + rotateDealer(); + dealHoleCards(); + postBlinds(); + + int nextPlayerIndex = (dealerIndex + NEXT_PLAYER_OFFSET) % players.size(); + engine.getState().setCurrentPlayerIndex(nextPlayerIndex); + } + + /** + * Rotates the dealer position to the next player in the list. + */ + private void rotateDealer() { + dealerIndex = (dealerIndex + DEALER_OFFSET) % players.size(); + } + + /** + * Returns the player currently acting as dealer. + * + * @return The name of the current dealer. + */ + public PlayerId getDealer() { + return players.get(dealerIndex); + } + + /** + * Determines the small and big blind players relative to the dealer + * and submits the corresponding blind actions to the engine. + */ + public void postBlinds() { + + PlayerId smallBlind = players.get((dealerIndex + SMALL_BLIND_OFFSET) % players.size()); + PlayerId bigBlind = players.get((dealerIndex + BIG_BLIND_OFFSET) % players.size()); + + engine.processAction(new BlindAction(smallBlind, SMALL_BLIND)); + engine.processAction(new BlindAction(bigBlind, BIG_BLIND)); + } + + /** + * Deals hole cards to each player from the deck. + */ + public void dealHoleCards() { + + Deck deck = engine.getState().getDeck(); + + for (PlayerId player : players) { + + Card c1 = deck.draw(); + Card c2 = deck.draw(); + + engine.getState().giveHoleCards(player, c1, c2); + } + } + + /** + * Draws three cards from the deck and adds them as community cards (flop). + */ + public void dealFlop() { + + Deck deck = engine.getState().getDeck(); + + engine.getState().addCommunityCard(deck.draw()); + engine.getState().addCommunityCard(deck.draw()); + engine.getState().addCommunityCard(deck.draw()); + } + + /** + * Deals the turn by drawing one community card from the deck and adding it to + * the game state. + */ + public void dealTurn() { + engine.getState().addCommunityCard(engine.getState().getDeck().draw()); + } + + /** + * Deals the river by drawing one community card from the deck and adding it to + * the game state. + */ + public void dealRiver() { + engine.getState().addCommunityCard(engine.getState().getDeck().draw()); + } + + /** + * Processes a player's fold action by sending a FoldAction to the GameEngine. + * + * @param playerId The ID of the player who is folding. + */ + public void playerFold(PlayerId playerId) { + // engine.processAction(new FoldAction(PlayerId.of(playerId))); + engine.processAction(new FoldAction(playerId)); + } + + /** + * Processes a player's call action by sending a CallAction to the GameEngine. + * + * @param playerId The ID of the player who is calling. + */ + public void playerCall(PlayerId playerId) { + // engine.processAction(new CallAction(PlayerId.of(playerId))); + engine.processAction(new CallAction(playerId)); + } + + /** + * Processes a player's raise action by sending a RaiseAction to the GameEngine. + * + * @param playerId The ID of the player who is raising. + * @param amount The amount the player is raising. + */ + public void playerRaise(PlayerId playerId, int amount) { + // engine.processAction(new RaiseAction(PlayerId.of(playerId), amount)); + engine.processAction(new RaiseAction(playerId, amount)); + } + + /** + * Retrieves the current game state from the GameEngine. + * + * @return The current GameState object representing the state of the game. + */ + public GameState getState() { + return engine.getState(); + } + + /** + * Retrieves the list of community cards currently on the table. + * + * @return A list of Card objects representing the community cards. + */ + public List getCommunityCards() { + return engine.getState().getCommunityCards(); + } + + /** + * Retrieves the hole cards for each player in the game. + * + * @return A map where the key is the player's name and the value is a list of + * Card objects representing the player's hole cards. + */ + public Map> getPlayerCards() { + return engine.getState().getPlayerCards(); + } + + /** + * Determines the winner of the current hand by evaluating the best possible + * poker hand for each active player. + * + * The evaluation is based on the player's two hole cards combined with the + * five community cards on the board. + * + * @return The ID of the winning player. + */ + public PlayerId determineWinner() { + + List board = engine.getState().getCommunityCards(); + GameState state = engine.getState(); + + PlayerId bestPlayer = null; + HandRank bestRank = null; + + for (PlayerId player : players) { + + if (state.isFolded(player)) { + continue; + } + + List cards = new ArrayList<>(board); + cards.addAll(state.getHoleCards(player)); + + HandRank rank = HandEvaluator.evaluate(cards); + + if (bestRank == null || rank.compareTo(bestRank) > 0) { + bestRank = rank; + bestPlayer = player; + } + } + + return bestPlayer; + } +} diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/game/engine/GameEngine.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/game/engine/GameEngine.java new file mode 100644 index 0000000..8fa6431 --- /dev/null +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/game/engine/GameEngine.java @@ -0,0 +1,108 @@ +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.rules.RuleEngine; +import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.state.GameState; + +/** + * The GameEngine class is responsible for managing the core logic of the poker + * game. It integrates the RuleEngine, RoundManager, and TurnManager to + * orchestrate the flow of the game. The GameEngine processes player actions, + * updates the game state accordingly, and ensures that the game progresses + * through its various stages (pre-flop, flop, turn, river) while adhering to + * the rules of poker. + */ +public class GameEngine { + + private final RuleEngine ruleEngine; + private final RoundManager roundManager; + private final TurnManager turnManager; + + private final GameState state; + + /** + * Constructs a GameEngine with the specified game state, rule engine, round + * manager, and turn manager. + * + * @param state The initial game state to be managed by the engine. + * @param ruleEngine The RuleEngine instance responsible for validating player + * actions. + * @param roundManager The RoundManager instance responsible for managing the + * progression of rounds. + * @param turnManager The TurnManager instance responsible for managing player + * turns. + */ + public GameEngine(GameState state, + RuleEngine ruleEngine, + RoundManager roundManager, + TurnManager turnManager) { + this.state = state; + this.ruleEngine = ruleEngine; + this.roundManager = roundManager; + this.turnManager = turnManager; + } + + /** + * Starts a new hand with the provided game state. This method initializes the + * round manager to begin a new hand and sets up the game state accordingly. + * + * @param state The game state to be used for starting the new hand. + */ + public void startNewHand(GameState state) { + roundManager.startNewHand(state); + } + + /** + * Starts a new hand using the current game state. This method is a convenience + * method that calls the startNewHand method with the current state of the game. + */ + public void startNewHand() { + roundManager.startNewHand(state); + } + + /** + * Processes a player action by validating it against the game rules, executing + * the action, progressing to the next player's turn, and checking if the + * round should progress to the next stage. + * + * @param action The player action to be processed. + */ + public void processAction(Action action) { + handleAction(state, action); + } + + /** + * Handles a player action by performing the following steps: + * 1. Validates the action against the game rules using the RuleEngine. + * 2. Executes the action, which updates the game state accordingly. + * 3. Advances to the next player's turn using the TurnManager. + * 4. Checks if the round should progress to the next stage (e.g., from pre-flop + * to flop) using the RoundManager. + * + * @param state The current game state on which to execute the action. + * @param action The player action to be processed. + */ + public void handleAction(GameState state, Action action) { + + // 1.Check the rules + ruleEngine.validate(state, action); + + // 2. Perform action + action.execute(state); + + // 3. Next turn + turnManager.nextPlayer(state); + + // 4. Check loop logic + roundManager.progressIfNeeded(state); + } + + /** + * Retrieves the current game state managed by the GameEngine. + * + * @return The current GameState instance. + */ + public GameState getState() { + return state; + } +} 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 new file mode 100644 index 0000000..80d29a3 --- /dev/null +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/game/engine/RoundManager.java @@ -0,0 +1,144 @@ +package ch.unibas.dmi.dbis.cs108.casono.server.domain.game.engine; + +import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.player.Player; +import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.player.PlayerStatus; +import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.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. + */ +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(); + + dealCards(state); + + postBlinds(state); + } + + /** + * 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 (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.getStatus() == PlayerStatus.FOLDED) { + 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); + } + } + + /** + * 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. + */ + private void postBlinds(GameState state) { + List playerList = new ArrayList<>(state.getPlayers()); + + Player sb = playerList.get(0); + Player bb = playerList.get(1); + + int smallBlind = SMALL_BLIND; + int bigBlind = BIG_BLIND; + + sb.removeChips(smallBlind); + bb.removeChips(bigBlind); + + state.addToPot(smallBlind + bigBlind); + + state.setCurrentBet(sb.getId(), smallBlind); + state.setCurrentBet(bb.getId(), bigBlind); + + state.getTableState().setCurrentBet(bigBlind); + } + + private void dealCards(GameState state) { + } + + private void dealFlop(GameState state) { + } + + private void dealTurn(GameState state) { + } + + private void dealRiver(GameState state) { + } + + private void showdown(GameState state) { + } +} 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 new file mode 100644 index 0000000..a233b44 --- /dev/null +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/game/engine/TurnManager.java @@ -0,0 +1,31 @@ +package ch.unibas.dmi.dbis.cs108.casono.server.domain.game.engine; + +import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.state.GameState; + +/** + * The TurnManager class is responsible for managing the flow of turns in a + * poker + * game. It provides functionality to determine the next player in the sequence + * and update the game state accordingly. The TurnManager ensures that players + * take their turns in the correct order, allowing for a smooth and organized + * gameplay experience. + */ +public class TurnManager { + + /** + * Advances the game state to the next player's turn. This method calculates + * the index of the next player based on the current player index and the total + * number of players in the game. It then updates the game state to reflect + * the new current player. + * + * @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); + } +}