// Copyright (c) 2022 Dominic Masters // // This software is released under the MIT License. // https://opensource.org/licenses/MIT #include "PokerPlayer.hpp" #include "PokerGame.hpp" using namespace Dawn; PokerPlayer::PokerPlayer(SceneItem *item) : SceneItemComponent(item) { } void PokerPlayer::onStart() { SceneItemComponent::onStart(); this->pokerGame = this->getScene()->findComponent(); } void PokerPlayer::addChips(int32_t chips) { assertTrue(chips > 0); this->chips += chips; if(this->chips > 0) this->isOut = false; eventChipsChanged.invoke(); } void PokerPlayer::setChips(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; if(this->currentBet < this->pokerGame->getCurrentCallValue()) return true; return false; } void PokerPlayer::bet(struct PokerPot *pot, int32_t chips) { assertNotNull(pot); assertTrue(chips >= 0); assertTrue(!this->isFolded); assertTrue(!this->isOut); 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 = mathMax(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(int32_t chips) { assertTrue(this->pokerGame->pots.size() > 0); this->bet(&this->pokerGame->pots.back(), chips); } void PokerPlayer::fold() { this->isFolded = true; this->hasBetThisRound = true; this->timesRaised = 0; } bool_t PokerPlayer::canCheck() { return this->pokerGame->getCurrentCallValue() <= this->currentBet; } struct PokerTurn PokerPlayer::getAITurn() { struct PokerTurn turn; float_t confidence; int32_t callBet; float_t potOdds; // 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(this->pokerGame->community.size() == 0) { assertTrue(this->hand.size() == POKER_PLAYER_HAND_SIZE_MAX); // 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)mathAbs( (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 = 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)this->pokerGame->getRemainingBettersCount(); } // Now determine the expected ROI auto expectedGain = confidence / potOdds; // Now get a random 0-100 auto random = randomGenerate() % 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 || this->pokerGame->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(this->pokerGame->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 = mathMax(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() { return this->pokerGame->getCurrentCallValue() - this->currentBet; } int32_t PokerPlayer::getSumOfChips() { int32_t count = 0; auto it = this->pokerGame->pots.begin(); while(it != this->pokerGame->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; winning.player = this; // Get the full poker hand (should be a 7 card hand, but MAY not be) for(i = 0; i < this->pokerGame->community.size(); i++) { winning.full.push_back(this->pokerGame->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 < CARD_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 == CARD_FIVE && j == 4 ? (enum CardValue)CARD_ACE : (enum CardValue)(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 == CARD_ACE ? POKER_WINNING_TYPE_ROYAL_FLUSH : POKER_WINNING_TYPE_STRAIGHT_FLUSH ); 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 = POKER_WINNING_TYPE_FOUR_OF_A_KIND; 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 = POKER_WINNING_TYPE_FULL_HOUSE; 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 = POKER_WINNING_TYPE_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 < CARD_FIVE) continue; winning.set.clear(); winning.set.push_back(card); for(j = 1; j <= 4; j++) { // Ace low. look = ( number == CARD_FIVE && j == 4 ? (enum CardValue)CARD_ACE : (enum CardValue)(number - j) ); index = Card::containsNumber(&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 = POKER_WINNING_TYPE_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 = POKER_WINNING_TYPE_THREE_OF_A_KIND; 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 = POKER_WINNING_TYPE_TWO_PAIR; winning.fillRemaining(); return winning; } // Pair if(winning.set.size() == 2) { winning.type = POKER_WINNING_TYPE_PAIR; winning.fillRemaining(); return winning; } // High card winning.set.clear(); winning.fillRemaining(); winning.type = POKER_WINNING_TYPE_HIGH_CARD; return winning; }