Skip to content

Commit

Permalink
Merge pull request #55 from Duzzuti/54-add-showdown-simulation-in-ord…
Browse files Browse the repository at this point in the history
…er-to-create-the-handstrengths-lookup-table

Handstrengths lookup table tool
  • Loading branch information
Duzzuti authored May 10, 2024
2 parents 738127c + 52b39b8 commit 8686fe0
Show file tree
Hide file tree
Showing 11 changed files with 309 additions and 3 deletions.
4 changes: 3 additions & 1 deletion .github/workflows/format_check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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'
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ data
model
issues.txt
log.*txt
log_tool.*txt
hand_strengths*.csv

# Prerequisites
*.d
Expand Down
3 changes: 3 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ set(CMAKE_CXX_FLAGS_RELEASE "-Ofast")
set(INCLUDE_DIR ${PROJECT_SOURCE_DIR}/include)
set(SRC_DIR ${PROJECT_SOURCE_DIR}/src)
set(TEST_DIR ${PROJECT_SOURCE_DIR}/tests)
set(TOOLS_DIR ${PROJECT_SOURCE_DIR}/tools)
set(THAND_STRENGTHS_DIR ${TOOLS_DIR}/hand_strengths)
set(PLAYER_DIR ${SRC_DIR}/players)
set(CHECK_PLAYER ${PLAYER_DIR}/check_player/check_player.cpp)
set(RAND_PLAYER ${PLAYER_DIR}/rand_player/rand_player.cpp)
Expand All @@ -31,6 +33,7 @@ include_directories(${plog_SOURCE_DIR}/include)
get_cmake_property(_variableNames VARIABLES)

add_subdirectory(src)
add_subdirectory(tools)

if(CMAKE_BUILD_TYPE STREQUAL "Debug")
add_subdirectory(tests)
Expand Down
2 changes: 1 addition & 1 deletion format.sh
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
2 changes: 1 addition & 1 deletion include/deck.h
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ class Deck {
/// @brief Generates a new deck of 52 cards
/// @exception Guarantee No-throw
constexpr Deck() noexcept {
// generate a 53 cards poker deck
// generate a 52 cards poker deck
u_int8_t i = 0;
for (u_int8_t suit = 0; suit < 4; suit++) {
for (u_int8_t rank = 2; rank < 15; rank++) {
Expand Down
5 changes: 5 additions & 0 deletions tests/unittests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ add_executable(poker_test_gametest main_test.cpp gametest_unittest.cpp ${SRC_DIR
target_link_libraries(poker_test_gametest gtest_main)
target_include_directories(poker_test_gametest PUBLIC ${INCLUDE_DIR} ${PLAYER_DIR} ${TEST_DIR})

add_executable(poker_test_thandstrengths main_test.cpp thandutils_unittest.cpp ${THAND_STRENGTHS_DIR}/hand_utils.cpp)
target_link_libraries(poker_test_thandstrengths gtest_main)
target_include_directories(poker_test_thandstrengths PUBLIC ${INCLUDE_DIR} ${THAND_STRENGTHS_DIR})

add_executable(test main_test.cpp test_test.cpp)
target_link_libraries(test gtest_main)
target_include_directories(test PUBLIC ${INCLUDE_DIR})
Expand All @@ -43,4 +47,5 @@ add_test(POT_TEST poker_test_pot)
add_test(UTILS_TEST poker_test_utils)
add_test(CONST_TEST poker_test_const)
add_test(GAME_TEST poker_test_gametest)
add_test(THANDSTRENGTHS_TEST poker_test_thandstrengths)
add_test(TEST_TEST test)
32 changes: 32 additions & 0 deletions tests/unittests/thandutils_unittest.cpp
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);
}
7 changes: 7 additions & 0 deletions tools/CMakeLists.txt
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)
82 changes: 82 additions & 0 deletions tools/hand_strengths/hand_utils.cpp
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;
}
}
}
107 changes: 107 additions & 0 deletions tools/hand_strengths/hand_utils.h
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];
};
66 changes: 66 additions & 0 deletions tools/hand_strengths/main.cpp
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;
}

0 comments on commit 8686fe0

Please sign in to comment.