From 2bd7565f97cf179da13a9635c34787c1e9e0270f Mon Sep 17 00:00:00 2001 From: Duzzuti Date: Fri, 29 Dec 2023 14:25:14 +0000 Subject: [PATCH] Add big blind raise option + refactor game --- docs/data.md | 3 + docs/player.md | 3 +- include/data_structs.h | 10 ++++ include/game.h | 12 ++++ include/player.h | 2 +- src/game.cpp | 67 +++++++++++++++++++++-- src/players/check_player/check_player.cpp | 6 +- src/players/check_player/check_player.h | 2 +- src/players/rand_player/rand_player.cpp | 29 +++++----- src/players/rand_player/rand_player.h | 2 +- 10 files changed, 112 insertions(+), 24 deletions(-) diff --git a/docs/data.md b/docs/data.md index 91c3188..e552850 100644 --- a/docs/data.md +++ b/docs/data.md @@ -25,10 +25,13 @@ The round data contains information about one round (until the pot is won). It i - big blind (int) - add blind (int, which is added to the small blind after the button has moved one time around the table) - dealer position (int) +- small blind position (int) +- big blind position (int) - pot (int) - bool array of players who folded (bool[]) - community cards (Card[]) - OutEnum which represents the state of the round (OutEnum) +- BetRoundState which represents which bet round is currently active (BetRoundState) ### Bet Round Data The bet round data contains information about one bet round (until all players have bet the same amount). It is stored in the `BetRoundData` struct and has the following form: diff --git a/docs/player.md b/docs/player.md index c1f496c..8ef9f08 100644 --- a/docs/player.md +++ b/docs/player.md @@ -3,6 +3,7 @@ for all available player implementations see [players](players.md) A player has only access to its hand cards and is initialized with a number which represents the player's position on the table. -Action `turn`(const Data& data) is called when it is the player's turn. The player has to return a valid action. The action is represented by a struct of the form (`action_type`, `bet`). The `action_type` is an enum which represents the type of the action. If the `action_type` requires a bet, the `bet` is the second element of the struct. If the `action_type` does not require a bet, the bet is ignored. +Action `turn`(const Data& data, const bool onlyRaise) is called when it is the player's turn. The player has to return a valid action. The action is represented by a struct of the form (`action_type`, `bet`). The `action_type` is an enum which represents the type of the action. If the `action_type` requires a bet, the `bet` is the second element of the struct. If the `action_type` does not require a bet, the bet is ignored. +If `onlyRaise` is true, the player is only allowed to raise or call. This is the case when the player is the big blind and no one has raised yet. The player can raise or just call the big blind (basically not adding any chips to the pot). The player has access to all information from the [data](data.md) struct. \ No newline at end of file diff --git a/include/data_structs.h b/include/data_structs.h index d12d7dd..be9aac1 100644 --- a/include/data_structs.h +++ b/include/data_structs.h @@ -33,6 +33,13 @@ enum HandKinds { ROYAL_FLUSH, }; +enum BetRoundState { + PREFLOP = 0, + FLOP, + TURN, + RIVER, +}; + struct Action { Actions action; u_int64_t bet = 0; @@ -57,10 +64,13 @@ struct RoundData { u_int64_t bigBlind; // big blind u_int64_t addBlind; // add blind amount every time the dealer is again at position 0 u_int8_t dealerPos; // position of the dealer + u_int8_t smallBlindPos; // position of the small blind + u_int8_t bigBlindPos; // position of the big blind u_int64_t pot; // current pot bool playerFolded[MAX_PLAYERS]; // true if player folded Card communityCards[5]; // community cards OutEnum result; // whats the state of the round (continue, round won, game won) + BetRoundState betRoundState; // current bet round state }; // contains the data for a single game (until only one player is left) diff --git a/include/game.h b/include/game.h index ec6e9a1..ed9182a 100644 --- a/include/game.h +++ b/include/game.h @@ -31,9 +31,21 @@ class Game { // runs a single bet round (preflop, flop, turn, river) OutEnum betRound(); + // returns true if the bet round should continue + bool betRoundContinue(short firstChecker) const noexcept; + + // true if the current player is active (not out or folded) + bool currentPlayerActive() const noexcept; + + // true if special condition is met that the current player can only raise or call + bool currentPlayerCanOnlyRaiseOrCall() const noexcept; + // runs a single non out player turn OutEnum playerTurn(short& firstChecker); + // runs a single non out player turn where the player can only raise or call + OutEnum playerTurnOnlyRaise(); + // the current player bets amount and the next player is selected // note that amount is the total amount that the player bets (e.g. if the player has to call 200 but he already bet 100 => amount is still 200) bool bet(const u_int64_t amount) noexcept; diff --git a/include/player.h b/include/player.h index 86ea85e..f23b687 100644 --- a/include/player.h +++ b/include/player.h @@ -14,7 +14,7 @@ class Player { virtual void setHand(const Card card1, const Card card2) noexcept final; virtual const std::pair getHand() const noexcept final; - virtual Action turn(const Data& data) const noexcept = 0; + virtual Action turn(const Data& data, const bool onlyRaise = false) const noexcept = 0; virtual ~Player() noexcept = default; diff --git a/src/game.cpp b/src/game.cpp index bd6ce88..b526b99 100644 --- a/src/game.cpp +++ b/src/game.cpp @@ -103,16 +103,20 @@ OutEnum Game::setBlinds() noexcept { // blinds OutEnum res = OutEnum::ROUND_CONTINUE; PLOG_DEBUG << this->getPlayerInfo() << " bets small blind " << this->data.roundData.smallBlind; + this->data.roundData.smallBlindPos = this->data.betRoundData.playerPos; while (!this->bet(this->data.roundData.smallBlind)) { res = this->playerOut(); if (res != OutEnum::ROUND_CONTINUE) return res; PLOG_DEBUG << this->getPlayerInfo() << " bets small blind " << this->data.roundData.smallBlind; + this->data.roundData.smallBlindPos = this->data.betRoundData.playerPos; } PLOG_DEBUG << this->getPlayerInfo() << " bets big blind " << this->data.roundData.bigBlind; + this->data.roundData.bigBlindPos = this->data.betRoundData.playerPos; while (!this->bet(this->data.roundData.bigBlind)) { res = this->playerOut(); if (res != OutEnum::ROUND_CONTINUE) return res; PLOG_DEBUG << this->getPlayerInfo() << " bets big blind " << this->data.roundData.bigBlind; + this->data.roundData.bigBlindPos = this->data.betRoundData.playerPos; } return OutEnum::ROUND_CONTINUE; } @@ -154,16 +158,19 @@ OutEnum Game::betRound() { // this loop will run until all players have either folded, checked or called // we can only exit if it is a players turn and he is in the game, has the same bet as the current bet and all players have checked if the bet is 0 // we need to consider the case where every player folds except one, then the last player wins the pot - while (this->data.roundData.playerFolded[this->data.betRoundData.playerPos] || this->data.gameData.playerOut[this->data.betRoundData.playerPos] || - this->data.betRoundData.currentBet != this->data.betRoundData.playerBets[this->data.betRoundData.playerPos] || - (this->data.betRoundData.currentBet == 0 && firstChecker != this->data.betRoundData.playerPos)) { - if (this->data.roundData.playerFolded[this->data.betRoundData.playerPos] || this->data.gameData.playerOut[this->data.betRoundData.playerPos]) { + while (this->betRoundContinue(firstChecker)) { + if (!this->currentPlayerActive()) { // player is out of the game or folded, skip turn this->data.nextPlayer(); continue; + } else if (this->currentPlayerCanOnlyRaiseOrCall()) { + // player can only raise or call + OutEnum turnRes = this->playerTurnOnlyRaise(); + if (turnRes != OutEnum::ROUND_CONTINUE) return turnRes; + } else { + OutEnum turnRes = this->playerTurn(firstChecker); + if (turnRes != OutEnum::ROUND_CONTINUE) return turnRes; } - OutEnum turnRes = this->playerTurn(firstChecker); - if (turnRes != OutEnum::ROUND_CONTINUE) return turnRes; } PLOG_DEBUG << "Bet round finished with bet " << this->data.betRoundData.currentBet << " and pot " << this->data.roundData.pot; @@ -172,6 +179,21 @@ OutEnum Game::betRound() { return OutEnum::ROUND_CONTINUE; } +bool Game::betRoundContinue(short firstChecker) const noexcept { + return !this->currentPlayerActive() || // player is out of the game or folded, should be skipped + this->data.betRoundData.currentBet != this->data.betRoundData.playerBets[this->data.betRoundData.playerPos] || // current bet is not called by the player + (this->data.betRoundData.currentBet == 0 && firstChecker != this->data.betRoundData.playerPos) || // current bet is 0 and the current player is not the first checker + this->currentPlayerCanOnlyRaiseOrCall(); +} + +bool Game::currentPlayerActive() const noexcept { return !this->data.gameData.playerOut[this->data.betRoundData.playerPos] && !this->data.roundData.playerFolded[this->data.betRoundData.playerPos]; } + +bool Game::currentPlayerCanOnlyRaiseOrCall() const noexcept { + // current bet is the big blind and the current player is the big blind and it is the preflop round (in the preflop round the big blind can raise) + return this->data.roundData.betRoundState == BetRoundState::PREFLOP && this->data.betRoundData.currentBet == this->data.roundData.bigBlind && + this->data.betRoundData.playerPos == this->data.roundData.bigBlindPos; +} + OutEnum Game::playerTurn(short& firstChecker) { Action action = this->players[this->data.betRoundData.playerPos]->turn(this->data); OutEnum res = OutEnum::ROUND_CONTINUE; @@ -228,6 +250,35 @@ OutEnum Game::playerTurn(short& firstChecker) { return OutEnum::ROUND_CONTINUE; } +OutEnum Game::playerTurnOnlyRaise() { + Action action = this->players[this->data.betRoundData.playerPos]->turn(this->data, true); + OutEnum res = OutEnum::ROUND_CONTINUE; + switch (action.action) { + case Actions::CALL: + PLOG_DEBUG << this->getPlayerInfo() << " called"; + if (!this->bet(this->data.betRoundData.currentBet)) { + // this move is not adding chips to the pot, so it can not be illegal + PLOG_FATAL << "Player " << this->data.betRoundData.playerPos << " called but could not bet"; + } + break; + + case Actions::RAISE: + PLOG_DEBUG << this->getPlayerInfo() << " raised to " << action.bet; + if (!this->bet(action.bet)) { + // illegal move leads to loss of the game + res = playerOut(); + if (res != OutEnum::ROUND_CONTINUE) return res; + } + break; + + default: + // illegal move leads to loss of the game + res = playerOut(); + if (res != OutEnum::ROUND_CONTINUE) return res; + } + return OutEnum::ROUND_CONTINUE; +} + bool Game::bet(const u_int64_t amount) noexcept { // amount is the whole bet, not the amount that is added to the pot if ((amount < this->data.betRoundData.currentBet) || // call condition @@ -282,12 +333,14 @@ OutEnum Game::getOutEnum() const noexcept { void Game::preflop() { if (this->data.roundData.result != OutEnum::ROUND_CONTINUE) return; + this->data.roundData.betRoundState = BetRoundState::PREFLOP; PLOG_DEBUG << "Starting PREFLOP bet round"; this->data.roundData.result = this->betRound(); } void Game::flop() { if (this->data.roundData.result != OutEnum::ROUND_CONTINUE) return; + this->data.roundData.betRoundState = BetRoundState::FLOP; this->setupBetRound(); PLOG_DEBUG << "Starting FLOP bet round"; for (u_int8_t i = 0; i < 3; i++) { @@ -298,6 +351,7 @@ void Game::flop() { void Game::turn() { if (this->data.roundData.result != OutEnum::ROUND_CONTINUE) return; + this->data.roundData.betRoundState = BetRoundState::TURN; this->setupBetRound(); PLOG_DEBUG << "Starting TURN bet round"; this->data.roundData.communityCards[3] = this->deck.draw(); // draw turn card @@ -306,6 +360,7 @@ void Game::turn() { void Game::river() { if (this->data.roundData.result != OutEnum::ROUND_CONTINUE) return; + this->data.roundData.betRoundState = BetRoundState::RIVER; this->setupBetRound(); PLOG_DEBUG << "Starting RIVER bet round"; this->data.roundData.communityCards[4] = this->deck.draw(); // draw river card diff --git a/src/players/check_player/check_player.cpp b/src/players/check_player/check_player.cpp index 3afdd3c..22823b3 100644 --- a/src/players/check_player/check_player.cpp +++ b/src/players/check_player/check_player.cpp @@ -1,7 +1,11 @@ #include "check_player.h" -Action CheckPlayer::turn(const Data& data) const noexcept { +Action CheckPlayer::turn(const Data& data, const bool onlyRaise) const noexcept { Action action; + if (onlyRaise) { + action.action = Actions::CALL; + return action; + } action.action = data.betRoundData.currentBet == 0 ? Actions::CHECK : data.getCallAdd() <= data.getChips() ? Actions::CALL : Actions::FOLD; return action; } \ No newline at end of file diff --git a/src/players/check_player/check_player.h b/src/players/check_player/check_player.h index 3b4c461..84771b8 100644 --- a/src/players/check_player/check_player.h +++ b/src/players/check_player/check_player.h @@ -6,5 +6,5 @@ class CheckPlayer : public Player { CheckPlayer(const std::string& name) noexcept : Player(name){}; CheckPlayer(u_int8_t playerNum = 0) noexcept : Player(!playerNum ? "CheckPlayer" : "CheckPlayer" + std::to_string(playerNum)){}; - Action turn(const Data& data) const noexcept override; + Action turn(const Data& data, const bool onlyRaise = false) const noexcept override; }; \ No newline at end of file diff --git a/src/players/rand_player/rand_player.cpp b/src/players/rand_player/rand_player.cpp index 63de41b..c35389d 100644 --- a/src/players/rand_player/rand_player.cpp +++ b/src/players/rand_player/rand_player.cpp @@ -1,30 +1,33 @@ #include "rand_player.h" -Action RandPlayer::turn(const Data& data) const noexcept { +Action RandPlayer::turn(const Data& data, const bool onlyRaise) const noexcept { Action action; + u_int8_t randMod = 10; + if (onlyRaise) randMod = 4; bool done = false; while (!done) { - switch (std::rand() % 10) { - case 0 ... 1: // Fold - action.action = Actions::FOLD; - done = true; - break; - case 2 ... 3: // Check - action.action = Actions::CHECK; - if (data.betRoundData.currentBet == 0) done = true; - break; - - case 4 ... 6: // Call + switch (std::rand() % randMod) { + case 0 ... 2: // Call action.action = Actions::CALL; if (data.getChips() >= data.getCallAdd() && data.betRoundData.currentBet != 0) done = true; break; - case 7: // Raise + case 3: // Raise action.action = Actions::RAISE; action.bet = (u_int64_t)(data.betRoundData.currentBet * (2 + (std::rand() % 20) / 10.0f)); if (data.getChips() >= data.getRaiseAdd(action.bet) && data.betRoundData.currentBet != 0) done = true; break; + case 4 ... 5: // Fold + action.action = Actions::FOLD; + done = true; + break; + + case 6 ... 7: // Check + action.action = Actions::CHECK; + if (data.betRoundData.currentBet == 0) done = true; + break; + case 8 ... 9: // Bet action.action = Actions::BET; action.bet = (u_int64_t)(data.roundData.smallBlind * (1 + (std::rand() % 30) / 10.0f)); diff --git a/src/players/rand_player/rand_player.h b/src/players/rand_player/rand_player.h index 7a171aa..849e667 100644 --- a/src/players/rand_player/rand_player.h +++ b/src/players/rand_player/rand_player.h @@ -6,5 +6,5 @@ class RandPlayer : public Player { RandPlayer(const std::string& name) noexcept : Player(name){}; RandPlayer(u_int8_t playerNum = 0) noexcept : Player(!playerNum ? "RandPlayer" : "RandPlayer" + std::to_string(playerNum)){}; - Action turn(const Data& data) const noexcept override; + Action turn(const Data& data, const bool onlyRaise = false) const noexcept override; }; \ No newline at end of file