-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #55 from Duzzuti/54-add-showdown-simulation-in-ord…
…er-to-create-the-handstrengths-lookup-table Handstrengths lookup table tool
- Loading branch information
Showing
11 changed files
with
309 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -17,11 +17,13 @@ jobs: | |
- 'src/players/*/' | ||
- 'include' | ||
- 'tests' | ||
- 'tests/*/' | ||
- 'tools/*/' | ||
steps: | ||
- uses: actions/checkout@v3 | ||
- name: Run clang-format style check for C/C++/Protobuf programs. | ||
uses: jidicula/[email protected] | ||
with: | ||
clang-format-version: '13' | ||
check-path: ${{ matrix.path }} | ||
fallback-style: 'Google' | ||
fallback-style: 'Google' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -13,6 +13,8 @@ data | |
model | ||
issues.txt | ||
log.*txt | ||
log_tool.*txt | ||
hand_strengths*.csv | ||
|
||
# Prerequisites | ||
*.d | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1 @@ | ||
clang-format -i src/*.cpp include/*.h tests/*.cpp tests/*.h tests/*/*.cpp tests/*/*.h src/players/*/*.cpp src/players/*/*.h --style file:.clang-format | ||
clang-format -i src/*.cpp include/*.h tests/*.cpp tests/*.h tests/*/*.cpp tests/*/*.h src/players/*/*.cpp src/players/*/*.h tools/*/*.h tools/*/*.cpp --style file:.clang-format |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
#include <gtest/gtest.h> | ||
|
||
#include "hand_utils.h" | ||
|
||
TEST(THandUtils, handIndex) { | ||
// iterates through all card combinations where r1 >= r2 and through all suit combinations | ||
// checks if the index correct for all combinations and if the back-converted name is also correct | ||
u_int8_t expectedInd = 0; | ||
const std::string rankNames = "23456789TJQKA"; | ||
for (u_int8_t r1 = 2; r1 < 15; r1++) { | ||
for (u_int8_t r2 = 2; r2 <= r1; r2++) { | ||
if (!(r1 == 2 && r2 == 2)) expectedInd++; | ||
for (u_int8_t s1 = 0; s1 < 4; s1++) { | ||
for (u_int8_t s2 = 0; s2 < 4; s2++) { | ||
if (r1 == r2 && s1 == s2) continue; // skip same cards (impossible hand) | ||
std::pair<Card, Card> cards = {Card{r1, s1}, Card{r2, s2}}; | ||
std::pair<Card, Card> cards2 = {Card{r2, s2}, Card{r1, s1}}; | ||
std::pair<Card, Card> cards3 = {Card{r1, s2}, Card{r2, s1}}; | ||
std::pair<Card, Card> cards4 = {Card{r2, s1}, Card{r1, s2}}; | ||
u_int8_t index = HandUtils::getHandIndex(cards); | ||
EXPECT_EQ(index, HandUtils::getHandIndex(cards2)); | ||
EXPECT_EQ(index, HandUtils::getHandIndex(cards3)); | ||
EXPECT_EQ(index, HandUtils::getHandIndex(cards4)); | ||
EXPECT_EQ(index, expectedInd); | ||
std::string name = HandUtils::getHandName(index); | ||
EXPECT_EQ(name, std::string() + rankNames[r1 - 2] + rankNames[r2 - 2]); | ||
} | ||
} | ||
} | ||
} | ||
EXPECT_EQ(expectedInd, 90); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
# Add the executable target | ||
add_executable(hand_strengths ${SRC_DIR}/deck.cpp ${THAND_STRENGTHS_DIR}/main.cpp ${THAND_STRENGTHS_DIR}/hand_utils.cpp) | ||
# Include headers | ||
target_include_directories(hand_strengths PUBLIC ${INCLUDE_DIR}) | ||
|
||
# Link with plog library | ||
target_link_libraries(hand_strengths plog) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
#include "hand_utils.h" | ||
|
||
#include <fstream> | ||
|
||
constexpr u_int8_t HandUtils::getHandIndex(const std::pair<Card, Card> playerCards) noexcept { | ||
const int16_t r1 = std::max(playerCards.first.rank, playerCards.second.rank); | ||
const int16_t r2 = std::min(playerCards.first.rank, playerCards.second.rank); | ||
return r1 + r2 - 4 + (r1 - 2) * (r1 - 3) / 2; | ||
} | ||
|
||
const std::string HandUtils::getHandName(int8_t handIndex) noexcept { | ||
// remove the possible ranks until the index is negative or 0. r1 is now the last removed rank + 1 | ||
// r2 is r1 if the index is 0, otherwise the index + rank | ||
u_int8_t r1 = 0; | ||
u_int8_t r2 = 0; | ||
for (u_int8_t i = 1; i < 14; i++) { | ||
if (i != 1) handIndex -= i; | ||
if (handIndex <= 0) { | ||
r1 = i + 1; | ||
r2 = handIndex == 0 ? i + 1 : handIndex + i + 1; | ||
break; | ||
} | ||
} | ||
std::string name = ""; | ||
name += HandUtils::ranks[r1 - 2]; | ||
name += HandUtils::ranks[r2 - 2]; | ||
return name; | ||
} | ||
|
||
void HandUtils::evaluateHands(const Card communityCards[], const std::pair<Card, Card> playerCards[], const u_int8_t players) noexcept { | ||
// set player 0 as the default winner and their hand as the strongest | ||
u_int8_t winners[players] = {0}; | ||
u_int8_t numWinners = 1; | ||
HandStrengths strongestHand = HandStrengths::getHandStrength(playerCards[0], communityCards); | ||
HandStrengths tmpHand; | ||
// compare the hands of all players to find the strongest | ||
for (u_int8_t p = 1; p < players; p++) { | ||
tmpHand = HandStrengths::getHandStrength(playerCards[p], communityCards); | ||
if (tmpHand > strongestHand) { | ||
// new strongest hand, new winner | ||
strongestHand = tmpHand; | ||
numWinners = 1; | ||
winners[0] = p; | ||
} else if (tmpHand == strongestHand) | ||
// same strength, add to winners | ||
winners[numWinners++] = p; | ||
} | ||
// update the internal arrays with the winners and the total count for each hand | ||
this->addWinners(playerCards, winners, numWinners, players); | ||
} | ||
|
||
void HandUtils::writeResults(const std::string& filename, const u_int8_t players, const bool newFile) const noexcept { | ||
// write hand + total + hand/total in csv file | ||
std::ofstream file(filename, newFile ? std::ios::trunc : std::ios::app); | ||
if (newFile) file << "Players, Hand, Suited, Name, Wins, Total, Wins/Total, Wins/Total*Players\n"; | ||
for (u_int8_t i = 0; i < 91; i++) { | ||
file << +players << ", " << +i << ", true, " << this->getHandName(i) << "s, " << this->handsSuited[i] << ", " << this->handsSuitedTotal[i] << ", " | ||
<< (double)this->handsSuited[i] / this->handsSuitedTotal[i] << ", " << (double)this->handsSuited[i] / this->handsSuitedTotal[i] * players << ",\n"; | ||
file << +players << ", " << +i << ", false, " << this->getHandName(i) << "o, " << this->handsUnsuited[i] << ", " << this->handsUnsuitedTotal[i] << ", " | ||
<< (double)this->handsUnsuited[i] / this->handsUnsuitedTotal[i] << ", " << (double)this->handsUnsuited[i] / this->handsUnsuitedTotal[i] * players << ",\n"; | ||
} | ||
file.close(); | ||
} | ||
|
||
void HandUtils::addWinners(const std::pair<Card, Card> playerCards[], const u_int8_t winners[], const u_int8_t numWinners, const u_int8_t players) noexcept { | ||
// the amount that is added to the win stat if the hand wins | ||
const u_int8_t add = (numWinners == 1 ? this->winnerAdd : this->splitAdd); | ||
// iterate over all player hands | ||
for (u_int8_t i = 0; i < players; i++) { | ||
const u_int8_t ind = this->getHandIndex(playerCards[i]); | ||
// add total to the total count and only if the hand wins, add to the win count | ||
if (playerCards[i].first.suit == playerCards[i].second.suit) { | ||
// suited | ||
this->handsSuitedTotal[ind] += this->totalAdd; | ||
if (std::find(winners, winners + numWinners, i) != winners + numWinners) this->handsSuited[ind] += add; | ||
} else { | ||
// offsuited | ||
this->handsUnsuitedTotal[ind] += this->totalAdd; | ||
if (std::find(winners, winners + numWinners, i) != winners + numWinners) this->handsUnsuited[ind] += add; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
#pragma once | ||
|
||
#include "hand_strengths.h" | ||
|
||
class HandUtils { | ||
public: | ||
/// @brief Constructor clears all arrays | ||
/// @param winnerAdd The amount to add to the win stat if the hand is the only winner | ||
/// @param splitAdd The amount to add to the win stat if the hand is a split winner | ||
/// @param totalAdd The amount to add to the total stat for each occurrence | ||
/// @exception Guarantee No-throw | ||
/// @note The totalAdd should be greater or equal than the maximum of winnerAdd and splitAdd to keep a meaningful total count | ||
/// @note winnerAdd and splitAdd are used to balance the effects of winning and splitting | ||
/// @note if splitAdd is 0, a split is treated as a loss, if winnerAdd is twice the value of splitAdd, a split is treated as a half win, etc. | ||
HandUtils(const u_int8_t winnerAdd, const u_int8_t splitAdd, const u_int8_t totalAdd) noexcept : winnerAdd(winnerAdd), splitAdd(splitAdd), totalAdd(totalAdd) { | ||
std::memset(this->handsSuited, 0, sizeof(this->handsSuited)); | ||
std::memset(this->handsUnsuited, 0, sizeof(this->handsUnsuited)); | ||
std::memset(this->handsSuitedTotal, 0, sizeof(this->handsSuitedTotal)); | ||
std::memset(this->handsUnsuitedTotal, 0, sizeof(this->handsUnsuitedTotal)); | ||
}; | ||
|
||
/// @brief Get the internal array index for a hand | ||
/// @param playerCards The player's hand cards | ||
/// @return The index of the hand in the internal arrays | ||
/// @exception Guarantee No-throw | ||
/// @note The index is calculated as r1+r2-4+(r1-2)*(r1-3)/2 with r1 >= r2 | ||
/// @note The index is needed to map the player hands to the internal arrays (91 different options) (save memory and time) | ||
/// @note There are hands that are "equal" like 2,3 and 3,2 | ||
/// @note The suit is ignored for the index, the caller has to check if the hand is suited or not | ||
/// @note With this in mind, there are 91 different hands (13+12+11+...+1) | ||
/// @see getHandName() to get the name of a hand by its index | ||
static constexpr u_int8_t getHandIndex(const std::pair<Card, Card> playerCards) noexcept; | ||
|
||
/// @brief Get the name of a hand by its index | ||
/// @param handIndex The (array) index of the hand | ||
/// @return The name of the hand as a string in the format "XY" where X and Y are the ranks of the cards (X >= Y) | ||
/// @exception Guarantee No-throw | ||
/// @note The name is calculated by inverting r1+r2-4+(r1-2)*(r1-3)/2 with r1 >= r2 and r1, r2 in [2, 14] | ||
/// @note The return array does not include the suit. You can add a "s" or "o" to indicate suited or offsuited | ||
/// @note The caller has to know if the hand is suited or not (and add the suit to the name if needed) | ||
/// @see getHandIndex() to get the index of a hand by its cards | ||
static const std::string getHandName(int8_t handIndex) noexcept; | ||
|
||
/// @brief Evaluate the hands of the players and update the internal arrays | ||
/// @param communityCards The community cards on the table (5 cards) | ||
/// @param playerCards The players hand cards (number of players = array length with 2 cards each) | ||
/// @param players The number of players | ||
/// @exception Guarantee No-throw | ||
/// @note The function calculates the hand strength of each player and compares them to find the winners and splits | ||
/// @note The function then updates the internal arrays with the count for wins and split and the total count for each hand | ||
/// @note The function does not check for impossible hands like multiple same cards | ||
/// @note There are also a few impossible suited hands in the arrays like AAs but to remove them would be more complex (there are just 0 occurrences of them) | ||
/// @see The hands are mapped to the internal arrays with the getHandIndex() function | ||
/// @see The function addWinners() is used to update the internal arrays | ||
void evaluateHands(const Card communityCards[], const std::pair<Card, Card> playerCards[], const u_int8_t players) noexcept; | ||
|
||
/// @brief Write the results to a file in csv format | ||
/// @param filename The name (path) of the file to write to | ||
/// @param players The number of players | ||
/// @param newFile If true, the file is created or overwritten, otherwise the results are appended | ||
/// @exception Guarantee No-throw | ||
/// @note The function writes the results for each hand in the internal arrays to the file | ||
/// @note It writes Player number, Hand index, Suited (true, false), Hand Name, Wins and Split count, Total, Wins/Total, Wins/Total*Players | ||
/// @note The last two are used to rank the hands by their strength | ||
/// @note Wins/Total is the win rate of the hand and Wins/Total*Players is the normalized win rate (1 is average win rate, <1 is under average, >1 is above average) | ||
void writeResults(const std::string& filename, const u_int8_t players, const bool newFile = true) const noexcept; | ||
|
||
/// @brief The array for the ranks of the cards (lookup array) | ||
static constexpr char ranks[14] = "23456789TJQKA"; // + null terminator | ||
|
||
private: | ||
/// @brief Add the data to the internal arrays | ||
/// @param playerCards The players hand cards | ||
/// @param winners The indices of the winners | ||
/// @param numWinners The number of winners | ||
/// @param players The number of players | ||
/// @exception Guarantee No-throw | ||
/// @note The function adds the data to the internal arrays based on the winners and the players hands | ||
/// @note The function uses the winnerAdd and splitAdd values to balance the effects of winning and splitting | ||
/// @note The function uses the totalAdd value to add to the total count for each occurring hand | ||
void addWinners(const std::pair<Card, Card> playerCards[], const u_int8_t winners[], const u_int8_t numWinners, const u_int8_t players) noexcept; | ||
|
||
/// @brief The amount to add to the win stat if the hand is the only winner | ||
const u_int8_t winnerAdd; | ||
|
||
/// @brief The amount to add to the win stat if the hand is a split winner | ||
const u_int8_t splitAdd; | ||
|
||
/// @brief The amount to add to the total stat for each occurrence | ||
const u_int8_t totalAdd; | ||
|
||
/// @brief The array for win stats of each suited hand | ||
/// @note The index is calculated with getHandIndex() | ||
u_int32_t handsSuited[91]; | ||
|
||
/// @brief The array for win stats of each unsuited hand | ||
/// @note The index is calculated with getHandIndex() | ||
u_int32_t handsUnsuited[91]; | ||
|
||
/// @brief The array for total stats of each suited hand | ||
/// @note The index is calculated with getHandIndex() | ||
u_int32_t handsSuitedTotal[91]; | ||
|
||
/// @brief The array for total stats of each unsuited hand | ||
/// @note The index is calculated with getHandIndex() | ||
u_int32_t handsUnsuitedTotal[91]; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
#include "hand_utils.h" | ||
|
||
int main(int argc, char** argv) { | ||
srand(time(NULL)); // init random seed | ||
// init logger | ||
static plog::ColorConsoleAppender<plog::TxtFormatter> consoleAppender; | ||
// add file logger | ||
static plog::RollingFileAppender<plog::TxtFormatter> fileAppender("log_tool.txt", 1024 * 1024 * 10, 5); | ||
// options | ||
u_int8_t winnerAdd = 1; | ||
u_int8_t splitAdd = 1; | ||
u_int8_t totalAdd = 1; | ||
u_int64_t iters = 10000000; | ||
std::string filename = ""; | ||
for (int i = 0; i < argc; i++) { | ||
if (strcmp(argv[i], "-v") == 0) { | ||
plog::init(plog::verbose, &consoleAppender).addAppender(&fileAppender); | ||
} else if (strcmp(argv[i], "-i") == 0) { | ||
plog::init(plog::info, &consoleAppender).addAppender(&fileAppender); | ||
} else if (strcmp(argv[i], "--iters") == 0) { | ||
iters = std::stoull(argv[i + 1]); | ||
} else if (strcmp(argv[i], "-o") == 0) { | ||
std::cout << "Output file: " << argv[i + 1] << std::endl; | ||
filename = argv[i + 1]; | ||
} else if (strcmp(argv[i], "--options") == 0) { | ||
winnerAdd = std::stoi(argv[i + 1]); | ||
splitAdd = std::stoi(argv[i + 2]); | ||
totalAdd = std::stoi(argv[i + 3]); | ||
} | ||
} | ||
if (filename == "") { | ||
filename = "hand_strengths" + std::to_string(+winnerAdd) + std::to_string(+splitAdd) + std::to_string(+totalAdd) + ".csv"; | ||
} | ||
|
||
PLOG_INFO << "Starting Handstrengths Tool"; | ||
|
||
Deck deck; | ||
Card communityCards[5]; | ||
std::pair<Card, Card> playerCards[MAX_PLAYERS]; | ||
// iterate over any meaningful number of players | ||
for (u_int8_t players = 2; players <= MAX_PLAYERS; players++) { | ||
// set up HandUtils | ||
HandUtils handUtils(winnerAdd, splitAdd, totalAdd); | ||
// simulate for an amount of iterations | ||
for (u_int64_t i = 0; i < iters; i++) { | ||
// shuffle deck and draw cards | ||
deck.shuffle(); | ||
for (u_int8_t j = 0; j < 5; j++) communityCards[j] = deck.draw(); | ||
for (u_int8_t j = 0; j < players; j++) { | ||
playerCards[j].first = deck.draw(); | ||
playerCards[j].second = deck.draw(); | ||
} | ||
// simulate a showdown and remember any winners and splits as well as the total for each occurring hand | ||
handUtils.evaluateHands(communityCards, playerCards, players); | ||
// reset deck | ||
deck.reset(); | ||
} | ||
// write the results for each player count to a file | ||
handUtils.writeResults(filename, players, players == 2); | ||
std::cout << "Wrote results for " << +players << " players\n"; | ||
} | ||
|
||
PLOG_INFO << "Finished Handstrengths Tool"; | ||
|
||
return 0; | ||
} |