Skip to content

Commit

Permalink
Add big blind raise option + refactor game
Browse files Browse the repository at this point in the history
  • Loading branch information
Duzzuti committed Dec 29, 2023
1 parent 49b08a0 commit 2bd7565
Show file tree
Hide file tree
Showing 10 changed files with 112 additions and 24 deletions.
3 changes: 3 additions & 0 deletions docs/data.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
3 changes: 2 additions & 1 deletion docs/player.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
10 changes: 10 additions & 0 deletions include/data_structs.h
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,13 @@ enum HandKinds {
ROYAL_FLUSH,
};

enum BetRoundState {
PREFLOP = 0,
FLOP,
TURN,
RIVER,
};

struct Action {
Actions action;
u_int64_t bet = 0;
Expand All @@ -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)
Expand Down
12 changes: 12 additions & 0 deletions include/game.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion include/player.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class Player {
virtual void setHand(const Card card1, const Card card2) noexcept final;
virtual const std::pair<Card, Card> 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;

Expand Down
67 changes: 61 additions & 6 deletions src/game.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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++) {
Expand All @@ -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
Expand All @@ -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
Expand Down
6 changes: 5 additions & 1 deletion src/players/check_player/check_player.cpp
Original file line number Diff line number Diff line change
@@ -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;
}
2 changes: 1 addition & 1 deletion src/players/check_player/check_player.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
29 changes: 16 additions & 13 deletions src/players/rand_player/rand_player.cpp
Original file line number Diff line number Diff line change
@@ -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));
Expand Down
2 changes: 1 addition & 1 deletion src/players/rand_player/rand_player.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};

0 comments on commit 2bd7565

Please sign in to comment.