Advertisement

Poker hand probabilities for n-card hand, where n=1 through n=5

Started by December 23, 2024 04:34 PM
9 comments, last by taby 1 week, 1 day ago

I asked this on Math Stack Exchange, but my question was closed, so I'm cross-posting here.

I have a poker code that properly classifies 5-card hands. I also have a code to best classify 1-card hands, 2-card hands, 3-card hands, and 4-card hands.

For instance, where n=5 we get these frequencies that closely match those on https://en.wikipedia.org/wiki/Poker_probability:

High Card
1301457

One Pair
1099504

Two Pair
123713

Three of a kind
54592

Straight
10151

Flush
5038

Full House
3838

Four of a kind
619

Straight Flush
44

Royal Flush
4

And for instance, for a 1-card hand (plus the remaining unflipped cards), the frequencies become:

Straight Flush
1599447

Royal Flush
999513

It turns out that the frequency ratio is like 1.6=8/5. This makes sense. Using one card plus the remaining unflipped cards leads to Royal Flush when the one card is Ace through 10 (which is 5 possibilities, versus Straight Flush 2 through 9 (which is 8 possibilities).

For a 3-card hand:

Three of a kind
1566610

Straight
450344

Flush
104363

Four of a kind
447411

Straight Flush
25582

Royal Flush
4650

In terms of the binomial coefficient, what are the actual odds for the 3-card hand?

The whole C++ code is too long to paste here. The code is at: https://github.com/sjhalayka/poker

I don't know the answer, but pondering about it, I concluded you have to address the “double counting” problem.

Suppose you have a die of 6, and you want to know

- A: how often you throw a number less than 5 (ie 66%), or

- B: how often you have an number more than 2 and less than 6 (50%).

If you treat each of the criteria as completely independent and just sum the odds, you'd get 116% chance to get either one, which is of course a trivially incorrect result. The reason that this happens is because there is overlap in matches. Eg value 3 is counted both in A and in B.

As far as I can see the way out is to distinguish the subsets in more detail. Eg

- Split the die values in (A and not B), (B and not A), and (A and B), and sum those.

- Compute the total number of possible values, and subtract the (not A and not B) set.

- Add (A) and (B and not A) together, ie narrow some criteria such that double counting does not happen.

No idea what is most useful for poker hands. It likely depends on the rules of what a “hand” is, and how the rules interact with each other.

Advertisement

Thank you for your comment. I will keep this in mind.

Here is an example code. I believe that this code calculates the probabilities p where the sum of the probabilities equals 1.0.

int main(void)
{
	srand(static_cast<unsigned int>(time(0)));

	map<short unsigned int, size_t> hand_class_counts;

	const size_t count = 2598960;

	const size_t num_init_cards = 4;	//MAX_NUM_CARDS_PER_HAND;

	for(size_t i = 0; i < count; i++)
	{
//		if (i % 10000 == 0)
//			cout << i / static_cast<long double>(count) << endl;

		vector<card> deck;
		init_deck(deck);
		shuffle_cards(deck);

		vector<card> hand;
		deal_hand(deck, hand, num_init_cards);

		short unsigned int classification = 0;
		
		if (num_init_cards < MAX_NUM_CARDS_PER_HAND)
			classification = get_best_classification(hand, deck);
		else
			classification = classify_hand(hand);

		hand_class_counts[classification]++;

//		if (classification == ROYAL_FLUSH)
//		{
//			print_hand_classification(classification);
//			print_cards(hand);
//			cout << endl;
//			exit(0);
//		}
	}

	string filename = "d" + to_string(num_init_cards) + ".txt";

	ofstream out_file(filename.c_str());

	// See frequency in: https://en.wikipedia.org/wiki/Poker_probability
	for (map<short unsigned int, size_t>::const_iterator ci = hand_class_counts.begin(); 
		ci != hand_class_counts.end(); 
		ci++)
	{
		print_hand_classification(ci->first);
		cout << ci->second << endl << endl;

		out_file << ci->first << " " << ci->second << endl;
	}
	
	return 0;
}

Ooh, I am sure you can compute it. Basically:

for all hand:
  if (hand contains straight || hand contains flush || ... ) goodcount++ else badcount++;

A little surprised you can do that with a deck of cards. 52! is a lot but probably there is much room for optimizing. Parallelism looks quite trivial too.

In my code just above, duplicate counts are eliminated in the ||.

Your code is doing that in “get_best_classification”, where apparently all but the “best” classification is discarded.

$$ \binom{52}{5} $$ is only 2598960. It is what I use in my code:

https://github.com/sjhalayka/poker/blob/9c575d96e44842d2e171d5dd0ddffc989b0582e7/main.cpp#L9

I suppose that I should add comments to explain that line. Sorry about that.

I am using the best hand classification code because I'll be using that to make choices about which cards to accept in the hand, and which cards to reject.

If you have a read-only virtual machine with Windows on it, there is a compiled version of the game, from 2011, at: https://github.com/sjhalayka/blind_poker

It is now my task to recreate this game. The ‘AI' - the hard part - is pretty much done now, and the rest is just the OpenGL code.

Thanks again for your help and insight.

Advertisement

taby said:
(55 over 2​) is only 2598960.

Ah right, my bad. 2,5M is very doable.

I guessed as much that you were writing a computer player, as knowing about the odds, and finding the best hand then makes sense.

I will have to hard-code the probabilities, which are based on the number of cards in the hand. That way I don't need to calculate the probabilities at runtime. I mean, parsing 2.5M random deals takes quite a few seconds on my computer. That's not really acceptable, even though it's amortized.

Write it as a const expression, and let the computer compute it at compile time :D

But yeah, inserting the probabilities is the right solution here. The card deck is not likely to change anyway.

I would probably also insert the code to compute it, even if it is just comment. In that way it's less likely the code gets lost.

I did once stumble upon a big int library.

#include <iostream>
#include <sstream>
#include <iomanip>
#include <vector>
#include <ctime>
using namespace std;


// https://mattmccutchen.net/bigint/
#include "BigInteger.hh"
#include "BigIntegerUtils.hh"
#include "BigIntegerAlgorithms.hh"


BigUnsigned factorial(const BigUnsigned &n)
{
    BigUnsigned factorial = 1;

    for (BigUnsigned i = 0; i < n; i++)
        factorial *= (i + 1);

    return factorial;
}

BigUnsigned n_choose_k(const BigUnsigned& n, const BigUnsigned& k)
{
    return factorial(n) / (factorial(k)*factorial(n - k));
}

int main() 
{
//    srand(static_cast<unsigned int>(time(0)));

    cout << n_choose_k(52, 5) << endl;

    //	const BigUnsigned p = stringToBigUnsigned("54661163828798316406139641599131347203445399912295442826728168170210404446004717881354193865401223990331513412680314853190460368937597393179445867548835085746203514200061810259071519181681661892618329");
    //	const BigUnsigned q = stringToBigUnsigned("58021664585639791181184025950440248398226136069516938232493687505822471836536824298822733710342250697739996825938232641940670857624514103125986134050997697160127301547995788468137887651823707102007839");

	return 0;
}
Advertisement