// Copyright (c) 2022 Dominic Masters // // This software is released under the MIT License. // https://opensource.org/licenses/MIT #include "PokerPlayer.hpp" #include "PokerGame.hpp" #include "util/Math.hpp" #include "util/Random.hpp" #include "util/Easing.hpp" using namespace Dawn; PokerPlayer::PokerPlayer(std::weak_ptr pokerGame) { this->pokerGame = pokerGame; this->chips = POKER_PLAYER_CHIPS_DEFAULT; } void PokerPlayer::addChips(const int32_t chips) { assertTrue(chips > 0, "Must add a positive amount of chips."); this->chips += chips; if(this->chips > 0) this->isOut = false; this->eventChipsChanged.emit(); } void PokerPlayer::setChips(const int32_t chips) { this->chips = 0; this->addChips(chips); } bool_t PokerPlayer::needsToBetThisRound() { if(this->isFolded) return false; if(this->chips <= 0) return false; if(!this->hasBetThisRound) return true; auto pg = this->pokerGame.lock(); assertNotNull(pg, "PokerGame has become invalid."); if(this->currentBet < pg->getCurrentCallValue()) return true; return false; } void PokerPlayer::bet( struct PokerPot &pot, const int32_t chips ) { assertTrue(chips >= 0, "Chips must be a positive value."); assertTrue(!this->isFolded, "Cannot bet if player is folded."); assertTrue(!this->isOut, "Cannot bet if player is out."); this->setChips(this->chips - chips); this->currentBet += chips; this->hasBetThisRound = true; if(chips > 0) { this->timesRaised++; } else { this->timesRaised = 0; } pot.chips += chips; pot.call = Math::max(pot.call, this ->currentBet); auto existing = std::find(pot.players.begin(), pot.players.end(), this); if(existing == pot.players.end()) pot.players.push_back(this); } void PokerPlayer::bet(const int32_t chips) { auto pg = this->pokerGame.lock(); assertNotNull(pg, "PokerGame has become invalid."); assertTrue(pg->pots.size() > 0, "PokerGame has no pots?"); assertUnreachable("Bugged"); this->bet(pg->pots.back(), chips); } void PokerPlayer::fold() { this->isFolded = true; this->hasBetThisRound = true; this->timesRaised = 0; } bool_t PokerPlayer::canCheck() { if(this->isFolded) return false; if(this->isOut) return false; auto pg = this->pokerGame.lock(); assertNotNull(pg, "PokerGame has become invalid."); return pg->getCurrentCallValue() == this->currentBet; } struct PokerTurn PokerPlayer::getAITurn() { struct PokerTurn turn; float_t confidence; int32_t callBet; float_t potOdds; auto pg = this->pokerGame.lock(); assertNotNull(pg, "PokerGame has become invalid."); // Can the player do anything? if(this->isFolded || this->isOut) { turn.type = POKER_TURN_TYPE_OUT; return turn; } // The following logic is heavily inspired by; // https://github.com/gorel/C-Poker-AI/blob/master/src/common/pokerai.c // But with some changes and smarts added by me. The original source code will // essentially just run a crap tun of simulated games and get the times that // they are expected to win from those games, but I'm just going to use the // odds of the winning hand. // Is this preflop? if(pg->community.size() == 0) { assertTrue( this->hand.size() == POKER_PLAYER_HAND_SIZE_MAX, "Invalid hand size." ); // Get the hand weight auto cardNumber0 = this->hand[0].getValue(); auto suitNumber0 = this->hand[0].getSuit(); auto cardNumber1 = this->hand[1].getValue(); auto suitNumber1 = this->hand[1].getSuit(); // Get delta between cards auto i = (uint8_t)Math::abs( (int8_t)cardNumber0 - (int8_t)cardNumber1 ); // Get card weight confidence = (float_t)cardNumber0 + (float_t)cardNumber1; if(cardNumber0 == cardNumber1) {// Pairs confidence += 6; } else if(suitNumber0 == suitNumber1) {// Same suit confidence += 4; } // Get difference from cards for guessing flush if(i > 4) { confidence -= 4; } else if(i > 2) { confidence -= i; } // Get the confidence delta 0-1 confidence = confidence / 30.0f; // This may change in future, but I was finding the AI did not want to bet // during the preflop enough, this curves the AI to want to preflop call // often. confidence = Easing::easeOutCubic(confidence); } else { // Simulate my hand being the winning hand, use that as the confidence auto winning = this->getWinning(); confidence = PokerWinning::getWinningTypeConfidence(winning.type); } // Now we know how confident the AI is, let's put a chip value to that weight // How many chips to call? callBet = this->getCallBet(); // Do they need chips to call, or is it possible to check? if(callBet > 0) { potOdds = (float_t)callBet / ( (float_t)callBet + (float_t)this->getSumOfChips() ); } else { potOdds = 1.0f / (float_t)pg->getRemainingBettersCount(); } // Now determine the expected ROI auto expectedGain = confidence / potOdds; // Now get a random 0-100 auto random = Random::random() % 100; // Determine the max bet that the AI is willing to make auto maxBet = (int32_t)((float_t)this->chips / 1.75f) - (random / 2); maxBet -= callBet; // Determine what's a good bluff bet. auto bluffBet = random * maxBet / 100 / 2; // Now prep the output auto isBluff = false; auto amount = 0; // Now the actual AI can happen. This is basically a weight to confidence // ratio. The higher the gains and the confidence then the more likely the AI // is to betting. There are also bluff chances within here. if(expectedGain < 0.8f && confidence < 0.8f) { if(random < 85) { amount = 0; } else { amount = bluffBet; isBluff = true; } } else if((expectedGain < 1.0f && confidence < 0.85f) || confidence < 0.1f) { if(random < 80) { amount = 0; } else if(random < 5) { amount = callBet; isBluff = true; } else { amount = bluffBet; isBluff = true; } } else if((expectedGain < 1.3f && confidence < 0.9f) || confidence < 0.5f) { if(random < 60 || confidence < 0.5f) { amount = callBet; } else { amount = maxBet; } } else if(confidence < 0.95f || pg->community.size() < 4) { if(random < 20) { amount = callBet; } else { amount = maxBet; } } else { amount = (this->chips - callBet) * 9 / 10; } // TODO: We can nicely round the amounts here to get us to a more "human" // number. // If this is the first round... make it a lot less likely I'll bet if(pg->community.size() == 0 && amount > callBet) { if(random > 5) amount = callBet; } // Did we actually bet? if(amount > 0) { std::cout << "AI is betting " << amount << " chips, bluff:" << isBluff << std::endl; // Let's not get caught in a raising loop with AI. if(this->timesRaised >= POKER_PLAYER_MAX_RAISES) { amount = callBet; } amount = Math::max(amount, callBet); turn = PokerTurn::bet(this, amount); turn.confidence = confidence; } else if(this->canCheck()) { turn = PokerTurn::bet(this, 0); turn.confidence = 1; } else { turn = PokerTurn::fold(this); turn.confidence = 1 - confidence; } return turn; } int32_t PokerPlayer::getCallBet() { auto pg = this->pokerGame.lock(); assertNotNull(pg, "PokerGame has become invalid."); return pg->getCurrentCallValue() - this->currentBet; } int32_t PokerPlayer::getSumOfChips() { auto pg = this->pokerGame.lock(); assertNotNull(pg, "PokerGame has become invalid."); int32_t count = 0; auto it = pg->pots.begin(); while(it != pg->pots.end()) { if(std::find(it->players.begin(), it->players.end(), this) != it->players.end()) { count += it->chips; } ++it; } return count; } struct PokerWinning PokerPlayer::getWinning() { struct PokerWinning winning; struct Card card(0x00); uint8_t i, j; int32_t index; enum CardValue number, look; enum CardSuit suit; std::vector pairs; auto pg = this->pokerGame.lock(); assertNotNull(pg, "PokerGame has become invalid."); winning.player = shared_from_this(); // Get the full poker hand (should be a 7 card hand, but MAY not be) for(i = 0; i < pg->community.size(); i++) { winning.full.push_back(pg->community[i]); } for(i = 0; i < this->hand.size(); i++) { winning.full.push_back(this->hand[i]); } Card::sort(winning.full); //////////////////////// Now look for the winning set //////////////////////// // Royal / Straight Flush for(i = 0; i < winning.full.size(); i++) { card = winning.full[i]; number = card.getValue(); if(number < CardValue::Five) continue; suit = card.getSuit(); winning.set.clear(); winning.set.push_back(card); // Now look for the matching cards (Reverse order to order from A to 10) for(j = 1; j <= 4; j++) { // Ace low. look = ( number == CardValue::Five && j == 4 ? (enum CardValue)CardValue::Ace : (enum CardValue)((uint8_t)number - j) ); index = Card::contains(winning.full, Card(suit, look)); if(index == -1) break; winning.set.push_back(winning.full[index]); } // Check if has all necessary cards. if(winning.set.size() < POKER_WINNING_SET_SIZE) continue; // Add self to array winning.type = ( number == CardValue::Ace ? PokerWinningType::RoyalFlush : PokerWinningType::StraightFlush ); winning.fillRemaining(); return winning; } // Four of a kind. for(i = 0; i < winning.full.size(); i++) { card = winning.full[i]; number = card.getValue(); pairs = Card::countPairs(winning.full, number); if(pairs.size() < CARD_SUIT_COUNT) continue; winning.set = pairs; winning.type = PokerWinningType::FourOfAKind; winning.fillRemaining(); return winning; } // Full House winning.set.clear(); for(i = 0; i < winning.full.size(); i++) { // Check we haven't already added this card. card = winning.full[i]; if(Card::contains(winning.set, card) != -1) { continue; } number = card.getValue(); pairs = Card::countPairs(winning.full, number); // Did we find either two pair or three pair? if(pairs.size() != 2 && pairs.size() != 3) continue; if(winning.set.size() == 3) {//Clamp to 5 max. pairs.pop_back(); } // Copy found pairs. for(j = 0; j < pairs.size(); j++) { winning.set.push_back(pairs[j]); } // Winned? if(winning.set.size() != POKER_WINNING_SET_SIZE) continue; winning.type = PokerWinningType::FullHouse; winning.fillRemaining(); return winning; } // Flush (5 same suit) for(i = 0; i < winning.full.size(); i++) { card = winning.full[i]; suit = card.getSuit(); winning.set.clear(); winning.set.push_back(card); for(j = i+1; j < winning.full.size(); j++) { if(winning.full[j].getSuit() != suit) continue; winning.set.push_back(winning.full[j]); if(winning.set.size() == POKER_WINNING_SET_SIZE) break; } if(winning.set.size() < POKER_WINNING_SET_SIZE) continue; winning.type = PokerWinningType::Flush; winning.fillRemaining(); return winning; } // Straight (sequence any suit) for(i = 0; i < winning.full.size(); i++) { card = winning.full[i]; number = card.getValue(); if(number < CardValue::Five) continue; winning.set.clear(); winning.set.push_back(card); for(j = 1; j <= 4; j++) { // Ace low. look = ( number == CardValue::Five && j == 4 ? (enum CardValue)CardValue::Ace : (enum CardValue)((uint8_t)number - j) ); index = Card::containsValue(winning.full, look); if(index == -1) break; winning.set.push_back(winning.full[index]); } // Check if has all necessary cards. if(winning.set.size() < POKER_WINNING_SET_SIZE) continue; winning.type = PokerWinningType::Straight; winning.fillRemaining(); return winning; } // Three of a kind for(i = 0; i < winning.full.size(); i++) { card = winning.full[i]; number = card.getValue(); pairs = Card::countPairs(winning.full, number); if(pairs.size() != 3) continue; winning.set = pairs; winning.type = PokerWinningType::ThreeOfAKind; winning.fillRemaining(); return winning; } // Two Pair winning.set.clear(); for(i = 0; i < winning.full.size(); i++) { card = winning.full[i];// Check we haven't already added this card. if( winning.set.size() > 0 && Card::contains(winning.set, card) != -1 ) { continue; } number = card.getValue(); pairs = Card::countPairs(winning.full, number); if(pairs.size() != 2) continue; for(j = 0; j < pairs.size(); j++) { winning.set.push_back(pairs[j]); } if(winning.set.size() != 4) continue; winning.type = PokerWinningType::TwoPair; winning.fillRemaining(); return winning; } // Pair if(winning.set.size() == 2) { winning.type = PokerWinningType::Pair; winning.fillRemaining(); return winning; } // High card winning.set.clear(); winning.fillRemaining(); winning.type = PokerWinningType::HighCard; return winning; }