Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

More Texas Holdem Analysis in C#: Part 2

4.93/5 (32 votes)
20 May 2008LGPL327 min read 1   2.9K  
Using C# to do sophisticated analysis of Texas Holdem

Benchmark

Introduction

In Part 1 of this article, you will find the following topics covered:

  • Pocket Query Language - This offers a straightforward set of methods that allow you experiment with different pocket hand combinations without rewriting your code.
  • Outs and draw methods - Determining outs and draws is key to analyzing a situation. This section discusses techniques for doing just that.

In this article, we will cover the following topics.

  • Monte Carlo analysis - Sometimes quick estimates are good enough. Monte Carlo analysis makes it possible to make high quality estimates in a very short time.
  • Multi-player hand analysis - More often than not, you are playing against multiple players. Here's how to calculate win odds against multiple players in a reasonable amount of time.
  • Multiple core support - The speed-ups are nearly linear when adding cores. Notice that the benchmark image above shows over 204 million hands/second calculated on a dual Quad Core system.

Monte Carlo analysis

Monte Carlo analysis can be used in analyzing poker hands. There are many reasons people choose to use Monte Carlo Analysis. In poker, the most common reason is to get a quick answer for something that otherwise would take a prohibitively long time to calculate. I resisted using Monte Carlo analysis for quite some time. I clung to my wish that a sufficiently fast Poker Hand Evaluator Library would eliminate the need for woo-woo techniques like this. To convince myself of the value of Monte Carlo analysis, I had to work through the analysis of win odds for one or more players. The following two sections go through my logic for accepting Monte Carlo analysis. Hopefully you will see the value of it and how easy it is to do.

Calculating win odds

Consider the following code. It calculates the win odds for a player being dealt AKs against a random opponent.

This program takes about 299 seconds (nearly 5 minutes) on my laptop to calculate with no board cards. That means the code is evaluating 14,084,025 hands/sec and a total of 2,097,572,400 (yes, billions) of hand match-ups. With three cards on the board, it takes 0.263 seconds and a total of 1,070,190 hand match-ups.

When there are cards on the board, this function is very usable. However, if you are calculating hole card win probabilities with this, then the algorithm is far too slow to be usable.

C#
using System;
using System.Collections.Generic;
using HoldemHand;

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            // This code calculates the probablity of As Ks winning against
            // another random hand.            
            ulong pocketmask = Hand.ParseHand("As Ks");  // Hole hand  
            ulong board = Hand.ParseHand("");            // No board cards yet
            long wins = 0, ties = 0, loses = 0, count = 0;  
                // Iterate through all possible opponent hole cards 
            foreach (ulong oppmask in Hand.Hands(0UL, board | pocketmask, 2))
            {
                // Iterate through all board cards      
                foreach (ulong boardmask in Hand.Hands(
                    board, pocketmask | oppmask, 5))
                {
                    // Evaluate the player and opponent hands and 
                    // tally the results    
                    uint pocketHandVal = 
                        Hand.Evaluate(pocketmask | boardmask, 7);
                    uint oppHandVal = Hand.Evaluate(oppmask | boardmask, 7);
                    if (pocketHandVal > oppHandVal)
                    {
                        wins++;
                    }
                    else if (pocketHandVal == oppHandVal)
                    {
                        ties++;
                    }
                    else
                    {
                        loses++;
                    }
                    count++;
                }
            }
            // Prints: Win 67.0446323092352%   
            Console.WriteLine("Win {0}%",
                (((double)wins) + ((double)ties) / 2.0) / (
                    (double)count) * 100.0);
        }
    }
}

I've gotten around problems like this in the past by pre-calculating the exact values and then using lookup tables. However, there many situations in Texas Holdem where the computation time required is very long and lookup tables aren't feasible, such as calculating odds win odds against multiple players. Because of this, other techniques need to be considered for some problems. That's why I first looked into Monte Carlo analysis.

Getting "good enough" results fast

I'm not a perfectionist and apparently neither are many of the successful professional Texas Holdem Players.

In Phil Gordon's Little Green Book -- a very good book, I might add -- he describes a very simple heuristic that can be calculated in your head and that gives the approximate odds of hitting your "outs." He calls this "the rule of 4 and 2." The way this heuristic works is that you count your outs. If you've just seen the flop and not the turn card, then multiply the outs by 4 and you will have the approximate percentage of hitting your outs. If you've seen the turn but not the river, you multiply the outs by 2 to get the approximate odds of hitting your outs.

These types of simple estimates are used by many of the professional players to help evaluate their situation. If an estimate is good enough for them, then it's probably good enough for the rest of us if used correctly. Monte Carlo Analysis is just a method for a computer to quickly estimate the odds in a specific situation.

Consider our previous example of calculating the win odds for AKs. It would take 299 seconds to iterate through all the possible situations and give us an exact answer. The following table was calculated using Monte Carlo analysis. The number on the left is the number of trials and the second number is the estimated win odds. The exact odds were 67.0446323092352%. The third column is the difference between the exact answer and the estimate answer. The fourth column is the time taken in seconds.

TrialsWinsDifferenceDuration
1080.00%12.95540.00005
5068.00%0.95540.00007
10061.00%6.04460.00009
50068.90%1.85540.00038
100067.10%0.05540.00071
500068.32%1.27540.00345
1000067.15%0.10040.00759
1500066.52%0.52460.01065
2000067.96%0.91040.01451
2500066.84%0.20060.01772
3000067.00%0.04800.02197
4000067.05%0.00290.02837
5000067.16%0.11840.03610
10000067.19%0.14490.07154
15000066.98%0.06630.10710
20000066.96%0.08560.15701
50000066.94%0.10180.36579
100000067.04%0.00240.79401
200000067.04%0.00251.43816
500000067.05%0.00233.84238
1000000067.03%0.01107.76830
2000000067.04%0.009215.02408

Notice that at 0.00071 seconds (0.7 milliseconds) we have 2 good digits. At 0.02197 (22 milliseconds) we have about 3 good digits. At 0.79401 (794 milliseconds) we have 4 good digits. It's easy to see that we can get very good estimates in well less than a second. The following code was used to generate the previous table.

C#
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using HoldemHand;

namespace WinOddsMonteCarlo
{    
    /// 
    /// This program shows the increase in accuracy of the Monte Carlo method
    /// when increasing the number of samples.
    /// 
    class Program    
    {        
        static void Main(string[] args)        
        {            
            // This code calculates the probablity of As Ks winning against
            // another random hand.            
            ulong pocketmask = Hand.ParseHand("As Ks"); // Hole hand      
            ulong board = Hand.ParseHand("");           // No board cards yet
            // Trial numbers 
            int[] trialsTable = {                
                10, 50, 100, 500, 1000, 5000, 10000, 15000, 20000, 
                25000, 30000, 40000, 50000, 100000, 150000, 200000,  
                500000, 1000000, 2000000, 5000000, 10000000, 20000000  
            };       
            // timer values  
            double start = 0.0;    
     
            Console.WriteLine("Trials,Wins,Difference,Duration");  
   
            foreach (int trials in trialsTable) 
            {    
                long wins = 0, ties = 0, count = 0;
                // Get start time  
                start = Hand.CurrentTime;   
                // Iterate through a series board cards  
                foreach (ulong boardmask in Hand.RandomHands(board, 
                    pocketmask, 5, trials)) 
                {
                    // Get a random opponent hand 
                    ulong oppmask = 
                        Hand.RandomHand(boardmask | pocketmask, 2);  
                    // Evaluate the player and opponent hands
                    uint pocketHandVal = 
                        Hand.Evaluate(pocketmask | boardmask, 7); 
                    uint oppHandVal = Hand.Evaluate(oppmask | boardmask, 7;
                    // Calculate Statistics 
                    if (pocketHandVal > oppHandVal) 
                    {
                        wins++;   
                    }  
                    else if (pocketHandVal == oppHandVal)
                    {  
                        ties++; 
                    }
                    count++;   
                }

                double duration = Hand.CurrentTime - start;
                // Correct answer is 67.0446323092352%   
                Console.WriteLine("{0},{1:0.00}%,{2:0.0000},{3:0.00000}",
                    trials, 
                    (((double)wins) + (
                    (double)ties) / 2.0) / ((double)count) *100.0, 
                    Math.Abs(67.0446323092352 - ((((double)wins) +
                    ((double)ties) / 2.0) / ((double)count) * 100)), 
                    duration);            
            }        
        }    
    }
}

Monte Carlo analysis helper methods

The following method is intentionally similar to the IEnumerable Hands(ulong shared, ulong dead, int ncards) method. One additional argument is added, which is the number of random hands that are to be iterated through by this enumerator.

C#
public static IEnumerable RandomHands(
    ulong shared, ulong dead, int ncards, int trials)

This IEnumerable method allows the foreach command to iterate through random hands. The hands returned must meet the criterion specified. The number of hands specified in the last argument are returned.

C#
public static IEnumerable RandomHands(
    ulong shared, ulong dead, int ncards, double duration)

This method is similar to the previous method. However, rather than specifying the number of trials, a time duration is specified. This allows a time budget to be specified for getting an answer. I have a mild preference to using this version of these methods.

Calculating win odds for multiple players

It's interesting to calculate odds against one player. However, more often than not, you are playing against multiple players. The following example shows how to calculate the win odds for the same situation with 1 through 9 players.

C#
using System;
using HoldemHand;

// This example calculates the win odds for a player having "As Ks" against 
// 1-9 random players
namespace WinOddsMultipleOpponents
{
    class Program
    {
        // Expected output (approximate values)
        // 1: win 67.08%
        // 2: win 50.82%
        // 3: win 41.53%
        // 4: win 35.46%
        // 5: win 31.13%
        // 6: win 27.82%
        // 7: win 24.99%
        // 8: win 22.77%
        // 9: win 20.77%
        static void Main(string[] args)
        {
            const double time = 5.0;
            ulong pocket = Hand.ParseHand("As Ks");
            ulong board = Hand.ParseHand("");
            ulong dead = Hand.ParseHand("");

            for (int opponents = 1; opponents <= 9; opponents++)
            {
                Console.WriteLine("{0}: win {1:0.00}%", 
                    opponents, WinOddsMonteCarlo(
                    pocket, board, dead, opponents, time) * 100.0);
            }
        }

        // An example of how to calculate win odds for multiple players
        static double WinOddsMonteCarlo(
            ulong pocket, ulong board, ulong dead, int nopponents, 
            double duration)
        {
            System.Diagnostics.Debug.Assert(
                nopponents > 0 && nopponents <= 9);
            System.Diagnostics.Debug.Assert(
                duration > 0.0);
            System.Diagnostics.Debug.Assert(
                Hand.BitCount(pocket) == 2);
            System.Diagnostics.Debug.Assert(
                Hand.BitCount(board) >= 0 && Hand.BitCount(board) <= 5);

            // Keep track of stats
            double win = 0.0, count = 0.0;

            // Keep track of time
            double start = Hand.CurrentTime;

            // Loop for specified time duration
            while ((Hand.CurrentTime-start) < duration)
            {
                // Player and board info
                ulong boardmask = Hand.RandomHand(board, dead | pocket, 5);
                uint playerHandVal = Hand.Evaluate(pocket | boardmask);

                // Ensure that dead, board, and pocket cards are not
                // available to opponent hands.
                ulong deadmask = dead | boardmask | pocket;

                // Comparison Results
                bool greaterthan = true;
                bool greaterthanequal = true;

                // Get random opponent hand values
                for (int i = 0; i < nopponents; i++)
                {
                    // Get Opponent hand info
                    ulong oppmask = Hand.RandomHand(deadmask, 2);
                    uint oppHandVal = Hand.Evaluate(oppmask | boardmask);

                    // Remove these opponent cards from future opponents
                    deadmask |= oppmask;

                    // Determine compare status
                    if (playerHandVal < oppHandVal)
                    {
                        greaterthan = greaterthanequal = false;
                        break;
                    }
                    else if (playerHandVal <= oppHandVal)
                    {
                        greaterthan = false;
                    }
                }

                // Calculate stats
                if (greaterthan)
                    win += 1.0;
                else if (greaterthanequal)
                    win += 0.5;

                count += 1.0;
            }

            // Return stats
            return (count == 0.0 ? 0.0 : win/count);
        }
    }
}

The method WinOddsMonteCarlo does all of the hard work here. You will notice that the results for a single opponent appear to be accurate to 3 digits when compared against our previous calculation. I can't vouch for the digits of accuracy for opponent counts greater than 1 -- frankly, it takes too long to get answers -- but the values are very stable when plotted on the MultiOddsApp application. You'll find MultiOddsApp in the demo programs included in the downloadable project. Because of this, I believe that the results are reasonably accurate.

Calculating win odds for all pocket hands

A simple variation of the above program can be used to calculate the approximate win odds for each of the 169 possible starting pocket hands for 1-9 opponents. Note that I've include the resulting table that gives approximate odds for a representative example of every possible starting hand for 1 though 9 opponents. I haven't run across the equivalent information online and thought that might be interesting for some players.

C#
using System;
using HoldemHand;

// This example calculates the win odds for a player having "As Ks" against 
// 1-9 random players. The results is comma separated so that 
// it can be imported
// into Excel using the .csv file type.
namespace WinOddsMultipleOpponentsTable
{
    class Program
    {
        static void Main(string[] args)
        {
            const double time = 5.0;
            ulong board = Hand.ParseHand("");
            ulong dead = Hand.ParseHand("");

            // Table Header
            Console.Write(",");
            for (int i = 1; i <= 9; i++)
            {
                Console.Write("{0},", i);
            }
            Console.WriteLine();

            // Iterates through one representative hand of each of the 
            // 169 possible
            // pocket hand types
            foreach (ulong pocket in PocketHands.Hands169())
            {
                // Show Pocker Hand
                Console.Write("\"{0}\",", Hand.MaskToString(pocket));

                // Calculate and Display the Approximate odds for 1-9 
                // opponents
                for (int opponents = 1; opponents <= 9; opponents++)
                {
                    Console.Write("{0}%,", WinOddsMonteCarlo(
                        pocket, board, dead, opponents, time) * 100.0);
                }
                Console.WriteLine();
            } 
        }

        // An example of how to calculate win odds for multiple players
        static double WinOddsMonteCarlo(ulong pocket, ulong board, ulong dead, 
            int nopponents, double duration)
        {
            System.Diagnostics.Debug.Assert(
                nopponents > 0 && nopponents <= 9);
            System.Diagnostics.Debug.Assert(duration > 0.0);
            System.Diagnostics.Debug.Assert(Hand.BitCount(pocket) == 2);
            System.Diagnostics.Debug.Assert(
                Hand.BitCount(board) >= 0 && Hand.BitCount(board) <= 5);

            // Keep track of stats
            double win = 0.0, count = 0.0;

            // Keep track of time
            double start = Hand.CurrentTime;

            // Loop for specified time duration
            while ((Hand.CurrentTime-start) < duration)
            {
                // Player and board info
                ulong boardmask = Hand.RandomHand(board, dead | pocket, 5);
                uint playerHandVal = Hand.Evaluate(pocket | boardmask);

                // Ensure that dead, board, and pocket cards are not
                // available to opponent hands.
                ulong deadmask = dead | boardmask | pocket;

                // Comparison Results
                bool greaterthan = true;
                bool greaterthanequal = true;

                // Get random opponent hand values
                for (int i = 0; i < nopponents; i++)
                {
                    // Get Opponent hand info
                    ulong oppmask = Hand.RandomHand(deadmask, 2);
                    uint oppHandVal = Hand.Evaluate(oppmask | boardmask);

                    // Remove these opponent cards from future opponents
                    deadmask |= oppmask;

                    // Determine compare status
                    if (playerHandVal < oppHandVal)
                    {
                        greaterthan = greaterthanequal = false;
                        break;
                    }
                    else if (playerHandVal <= oppHandVal)
                    {
                        greaterthan = false;
                    }
                }

                // Calculate stats
                if (greaterthan)
                    win += 1.0;
                else if (greaterthanequal)
                    win += 0.5;

                count += 1.0;
            }

            // Return stats
            return (count == 0.0 ? 0.0 : win/count);
        }
    }
}

The resulting output is below. This table gives the approximate odds of winning, given the specified pocket hand against the number of specified opponents. Each cell was given 30 seconds of calculation time, so the results are probably good to several digits. However, I haven't verified that for opponent numbers greater then 1.

Pocket Cards vs Approximate Win Odds Given Opponent Count
123456789
As Ah85.20%73.50%63.91%55.91%49.24%43.59%38.75%34.66%31.14%
Ks Kh82.40%68.93%58.30%49.88%43.01%37.46%32.95%29.24%26.15%
Qs Qh79.93%64.98%53.58%44.79%37.92%32.58%28.35%24.98%22.28%
Js Jh77.46%61.22%49.25%40.32%33.63%28.59%24.70%21.71%19.40%
Ts Th75.01%57.63%45.27%36.41%30.00%25.27%21.82%19.17%17.20%
9s 9h72.06%53.70%41.21%32.65%26.70%22.50%19.47%17.26%15.61%
8s 8h69.16%50.01%37.67%29.56%24.09%20.38%17.77%15.91%14.55%
7s 7h66.23%46.56%34.48%26.85%21.96%18.69%16.44%14.87%13.70%
6s 6h63.29%43.29%31.60%24.59%20.19%17.36%15.44%14.09%13.10%
5s 5h60.32%40.17%29.01%22.54%18.63%16.16%14.50%13.33%12.43%
4s 4h57.02%36.88%26.41%20.69%17.37%15.34%13.93%12.98%12.21%
3s 3h53.69%33.77%24.12%19.14%16.35%14.67%13.56%12.75%12.08%
2s 2h50.33%30.82%22.10%17.86%15.62%14.24%13.30%12.59%11.99%
As Ks67.03%50.82%41.53%35.50%31.16%27.80%25.06%22.73%20.79%
As Kh65.31%48.29%38.64%32.41%27.94%24.48%21.68%19.33%17.30%
As Qs66.21%49.50%39.97%33.81%29.42%26.09%23.39%21.19%19.37%
As Qh64.44%46.89%36.94%30.56%26.04%22.59%19.82%17.56%15.66%
As Js65.37%48.27%38.56%32.36%27.98%24.72%22.12%20.03%18.30%
As Jh63.56%45.57%35.40%28.99%24.46%21.04%18.39%16.21%14.43%
As Ts64.59%47.15%37.35%31.12%26.84%23.65%21.16%19.15%17.48%
As Th62.74%44.38%34.08%27.62%23.16%19.85%17.26%15.20%13.50%
As 9s62.77%44.64%34.63%28.45%24.27%21.18%18.85%17.02%15.52%
As 9h60.76%41.65%31.19%24.70%20.36%17.19%14.77%12.86%11.34%
As 8s61.93%43.62%33.58%27.48%23.35%20.39%18.12%16.35%14.92%
As 8h59.87%40.53%30.03%23.64%19.36%16.30%13.95%12.13%10.65%
As 7s60.98%42.47%32.50%26.52%22.51%19.68%17.49%15.81%14.43%
As 7h58.83%39.32%28.86%22.60%18.44%15.47%13.24%11.51%10.13%
As 6s59.90%41.25%31.41%25.58%21.74%19.01%16.96%15.35%14.06%
As 6h57.67%37.99%27.67%21.56%17.57%14.75%12.65%11.02%9.74%
As 5s59.94%41.52%31.87%26.13%22.31%19.61%17.56%15.96%14.61%
As 5h57.71%38.26%28.13%22.14%18.20%15.37%13.27%11.62%10.31%
As 4s59.01%40.63%31.10%25.50%21.81%19.19%17.21%15.64%14.35%
As 4h56.72%37.29%27.29%21.43%17.61%14.91%12.87%11.30%10.04%
As 3s58.22%39.73%30.33%24.89%21.30%18.77%16.84%15.34%14.09%
As 3h55.86%36.33%26.47%20.75%17.06%14.46%12.51%10.97%9.74%
As 2s57.37%38.86%29.58%24.20%20.75%18.27%16.40%14.91%13.73%
As 2h54.93%35.37%25.57%19.99%16.42%13.90%12.02%10.52%9.33%
Ks Qs63.40%47.19%38.30%32.58%28.45%25.24%22.66%20.51%18.74%
Ks Qh61.47%44.48%35.28%29.39%25.15%21.84%19.21%17.02%15.21%
Ks Js62.58%45.98%36.94%31.18%27.08%23.95%21.45%19.44%17.76%
Ks Jh60.58%43.18%33.82%27.85%23.62%20.40%17.85%15.77%14.05%
Ks Ts61.81%44.88%35.73%30.02%25.95%22.93%20.54%18.61%17.01%
Ks Th59.73%42.02%32.54%26.57%22.41%19.25%16.80%14.83%13.22%
Ks 9s60.01%42.38%33.07%27.35%23.37%20.47%18.22%16.43%14.99%
Ks 9h57.81%39.34%29.61%23.63%19.57%16.57%14.26%12.44%10.95%
Ks 8s58.31%40.25%30.92%25.29%21.49%18.77%16.67%15.04%13.72%
Ks 8h56.01%37.03%27.27%21.43%17.53%14.71%12.57%10.91%9.58%
Ks 7s57.52%39.39%30.07%24.51%20.81%18.15%16.13%14.56%13.28%
Ks 7h55.20%36.07%26.33%20.59%16.75%14.00%11.95%10.35%9.10%
Ks 6s56.66%38.39%29.17%23.71%20.15%17.61%15.65%14.15%12.92%
Ks 6h54.21%35.02%25.37%19.72%16.03%13.39%11.43%9.90%8.71%
Ks 5s55.80%37.51%28.41%23.11%19.63%17.14%15.28%13.84%12.66%
Ks 5h53.29%34.05%24.54%19.02%15.44%12.92%11.02%9.57%8.42%
Ks 4s54.88%36.64%27.68%22.53%19.15%16.78%14.97%13.58%12.45%
Ks 4h52.33%33.10%23.73%18.37%14.90%12.47%10.68%9.28%8.18%
Ks 3s54.05%35.83%26.99%21.99%18.74%16.44%14.68%13.33%12.26%
Ks 3h51.43%32.20%22.98%17.77%14.43%12.09%10.36%9.03%7.96%
Ks 2s53.20%35.01%26.35%21.45%18.34%16.10%14.45%13.13%12.08%
Ks 2h50.50%31.34%22.24%17.20%13.98%11.75%10.11%8.79%7.79%
Qs Js60.26%44.30%35.80%30.31%26.35%23.30%20.90%18.96%17.36%
Qs Jh58.11%41.46%32.69%27.05%22.98%19.86%17.43%15.45%13.83%
Qs Ts59.46%43.23%34.65%29.19%25.26%22.36%20.04%18.19%16.69%
Qs Th57.30%40.31%31.43%25.81%21.81%18.78%16.42%14.58%13.07%
Qs 9s57.66%40.74%31.99%26.54%22.75%19.94%17.75%16.04%14.68%
Qs 9h55.37%37.67%28.55%22.92%19.04%16.14%13.93%12.21%10.81%
Qs 8s56.02%38.66%29.84%24.52%20.85%18.21%16.19%14.60%13.34%
Qs 8h53.61%35.41%26.23%20.74%16.99%14.29%12.22%10.64%9.39%
Qs 7s54.30%36.57%27.86%22.66%19.21%16.71%14.84%13.41%12.22%
Qs 7h51.76%33.15%24.07%18.74%15.18%12.64%10.75%9.32%8.18%
Qs 6s53.62%35.86%27.15%22.05%18.65%16.24%14.45%13.03%11.92%
Qs 6h51.02%32.37%23.29%18.05%14.57%12.11%10.28%8.89%7.82%
Qs 5s52.77%34.98%26.40%21.44%18.16%15.84%14.10%12.75%11.69%
Qs 5h50.12%31.44%22.50%17.34%14.00%11.67%9.92%8.59%7.55%
Qs 4s51.87%34.15%25.72%20.86%17.72%15.47%13.82%12.49%11.47%
Qs 4h49.14%30.49%21.73%16.73%13.49%11.25%9.58%8.30%7.32%
Qs 3s51.02%33.34%25.06%20.33%17.29%15.15%13.54%12.29%11.28%
Qs 3h48.23%29.64%21.01%16.14%13.03%10.87%9.29%8.07%7.11%
Qs 2s50.17%32.55%24.42%19.82%16.91%14.84%13.29%12.08%11.11%
Qs 2h47.31%28.76%20.29%15.59%12.61%10.53%9.02%7.86%6.94%
Js Ts57.52%42.05%33.98%28.75%24.98%22.13%19.88%18.13%16.70%
Js Th55.23%39.16%30.80%25.45%21.61%18.69%16.43%14.64%13.23%
Js 9s55.62%39.60%31.35%26.15%22.50%19.77%17.67%16.03%14.72%
Js 9h53.23%36.50%27.99%22.64%18.87%16.09%13.97%12.35%11.05%
Js 8s54.02%37.51%29.23%24.15%20.64%18.07%16.12%14.60%13.39%
Js 8h51.51%34.26%25.68%20.46%16.87%14.24%12.29%10.79%9.59%
Js 7s52.31%35.50%27.27%22.29%18.95%16.54%14.70%13.30%12.20%
Js 7h49.67%32.07%23.53%18.43%15.03%12.57%10.75%9.38%8.29%
Js 6s50.60%33.49%25.40%20.62%17.44%15.18%13.53%12.22%11.20%
Js 6h47.82%29.92%21.53%16.60%13.38%11.12%9.45%8.19%7.21%
Js 5s49.98%32.87%24.83%20.13%17.04%14.86%13.21%11.96%10.95%
Js 5h47.16%29.27%20.88%16.06%12.92%10.73%9.11%7.89%6.94%
Js 4s49.07%32.04%24.13%19.60%16.59%14.49%12.92%11.70%10.74%
Js 4h46.19%28.36%20.16%15.46%12.43%10.34%8.80%7.63%6.72%
Js 3s48.24%31.27%23.50%19.08%16.21%14.18%12.67%11.49%10.57%
Js 3h45.29%27.48%19.44%14.89%11.98%9.95%8.50%7.40%6.53%
Js 2s47.36%30.47%22.86%18.59%15.82%13.89%12.45%11.32%10.42%
Js 2h44.35%26.63%18.72%14.34%11.56%9.65%8.24%7.18%6.35%
Ts 9s54.04%38.90%31.10%26.09%22.54%19.89%17.83%16.26%15.02%
Ts 9h51.54%35.81%27.80%22.66%19.04%16.33%14.28%12.74%11.48%
Ts 8s52.33%36.83%29.02%24.13%20.73%18.21%16.33%14.87%13.70%
Ts 8h49.72%33.58%25.54%20.51%17.04%14.52%12.64%11.21%10.07%
Ts 7s50.62%34.79%27.04%22.30%19.04%16.68%14.93%13.55%12.50%
Ts 7h47.91%31.40%23.39%18.50%15.21%12.84%11.10%9.78%8.74%
Ts 6s48.95%32.85%25.17%20.56%17.49%15.26%13.66%12.38%11.37%
Ts 6h46.09%29.29%21.35%16.65%13.53%11.31%9.70%8.48%7.54%
Ts 5s47.22%30.94%23.40%19.01%16.12%14.07%12.57%11.38%10.45%
Ts 5h44.28%27.25%19.46%15.00%12.03%10.02%8.53%7.42%6.56%
Ts 4s46.53%30.35%22.91%18.60%15.75%13.75%12.26%11.12%10.21%
Ts 4h43.50%26.58%18.90%14.48%11.64%9.66%8.23%7.15%6.30%
Ts 3s45.68%29.58%22.28%18.10%15.36%13.45%12.03%10.93%10.06%
Ts 3h42.58%25.76%18.21%13.94%11.19%9.31%7.93%6.90%6.11%
Ts 2s44.84%28.82%21.66%17.61%14.99%13.16%11.79%10.74%9.89%
Ts 2h41.65%24.93%17.52%13.40%10.77%9.00%7.67%6.70%5.94%
9s 8s50.78%36.13%28.61%23.79%20.40%17.95%16.09%14.66%13.53%
9s 8h48.10%32.87%25.16%20.21%16.78%14.30%12.46%11.06%9.99%
9s 7s49.11%34.24%26.81%22.17%18.97%16.68%14.97%13.67%12.63%
9s 7h46.30%30.83%23.20%18.46%15.22%12.93%11.25%9.98%9.00%
9s 6s47.44%32.33%24.96%20.50%17.48%15.36%13.75%12.53%11.54%
9s 6h44.49%28.76%21.22%16.64%13.60%11.45%9.91%8.74%7.85%
9s 5s45.72%30.43%23.22%18.91%16.07%14.05%12.56%11.41%10.50%
9s 5h42.71%26.73%19.32%14.92%12.06%10.07%8.64%7.55%6.73%
9s 4s43.87%28.55%21.52%17.43%14.76%12.88%11.51%10.45%9.59%
9s 4h40.69%24.70%17.47%13.31%10.63%8.80%7.48%6.50%5.75%
9s 3s43.26%28.00%21.05%17.04%14.43%12.61%11.27%10.21%9.39%
9s 3h40.01%24.10%16.95%12.88%10.27%8.48%7.23%6.26%5.52%
9s 2s42.44%27.27%20.46%16.57%14.07%12.34%11.04%10.05%9.23%
9s 2h39.11%23.28%16.28%12.35%9.86%8.18%6.96%6.07%5.36%
8s 7s47.93%33.96%26.80%22.22%19.10%16.84%15.17%13.90%12.87%
8s 7h45.06%30.58%23.22%18.57%15.39%13.16%11.54%10.29%9.35%
8s 6s46.27%32.13%25.11%20.73%17.78%15.71%14.17%12.96%12.03%
8s 6h43.24%28.62%21.39%16.94%13.98%11.92%10.45%9.32%8.44%
8s 5s44.54%30.25%23.36%19.18%16.39%14.46%13.01%11.90%11.01%
8s 5h41.42%26.59%19.52%15.25%12.47%10.57%9.21%8.17%7.36%
8s 4s42.71%28.39%21.66%17.66%15.00%13.18%11.83%10.79%9.96%
8s 4h39.45%24.57%17.64%13.59%10.99%9.20%7.93%6.99%6.24%
8s 3s40.86%26.55%19.99%16.21%13.78%12.06%10.81%9.86%9.06%
8s 3h37.48%22.58%15.86%12.04%9.59%8.00%6.84%5.96%5.30%
8s 2s40.27%26.00%19.56%15.85%13.46%11.82%10.60%9.64%8.89%
8s 2h36.84%22.00%15.36%11.62%9.27%7.69%6.58%5.74%5.09%
7s 6s45.36%32.12%25.24%20.92%18.02%15.99%14.49%13.33%12.39%
7s 6h42.32%28.62%21.59%17.19%14.28%12.28%10.82%9.74%8.91%
7s 5s43.69%30.32%23.62%19.54%16.84%14.95%13.56%12.47%11.58%
7s 5h40.52%26.71%19.83%15.69%12.99%11.15%9.82%8.84%8.07%
7s 4s41.84%28.46%21.94%18.03%15.49%13.70%12.41%11.39%10.58%
7s 4h38.58%24.70%18.02%14.04%11.50%9.81%8.59%7.67%6.95%
7s 3s40.00%26.59%20.24%16.53%14.13%12.51%11.26%10.30%9.51%
7s 3h36.61%22.68%16.17%12.43%10.06%8.47%7.35%6.52%5.85%
7s 2s38.16%24.78%18.63%15.15%12.93%11.39%10.26%9.37%8.65%
7s 2h34.59%20.71%14.45%10.91%8.76%7.30%6.28%5.53%4.92%
6s 5s43.13%30.48%23.88%19.84%17.19%15.34%13.97%12.90%12.01%
6s 5h39.93%26.87%20.14%16.04%13.41%11.59%10.30%9.31%8.55%
6s 4s41.32%28.71%22.33%18.48%16.02%14.27%13.04%12.01%11.20%
6s 4h38.02%24.97%18.46%14.56%12.10%10.48%9.28%8.39%7.68%
6s 3s39.52%26.88%20.66%17.02%14.68%13.07%11.90%10.94%10.18%
6s 3h36.09%22.97%16.64%12.99%10.68%9.15%8.07%7.24%6.60%
6s 2s37.67%25.02%19.02%15.57%13.38%11.88%10.77%9.89%9.15%
6s 2h34.07%20.98%14.86%11.40%9.28%7.85%6.86%6.11%5.51%
5s 4s41.46%29.24%22.87%19.08%16.61%14.91%13.62%12.63%11.77%
5s 4h38.17%25.56%19.05%15.19%12.77%11.13%9.95%9.06%8.34%
5s 3s39.70%27.50%21.33%17.76%15.49%13.87%12.71%11.76%10.98%
5s 3h36.26%23.68%17.39%13.78%11.55%10.04%8.96%8.13%7.46%
5s 2s37.86%25.65%19.71%16.29%14.17%12.69%11.58%10.69%9.95%
5s 2h34.30%21.69%15.62%12.20%10.13%8.77%7.76%6.99%6.40%
4s 3s38.66%26.63%20.58%17.10%14.88%13.32%12.20%11.26%10.48%
4s 3h35.16%22.78%16.57%13.07%10.91%9.44%8.42%7.62%6.99%
4s 2s36.83%24.90%19.07%15.80%13.75%12.34%11.27%10.42%9.70%
4s 2h33.20%20.89%14.96%11.67%9.68%8.39%7.43%6.72%6.14%
3s 2s36.00%24.07%18.34%15.15%13.19%11.83%10.80%9.96%9.27%
3s 2h32.32%19.99%14.16%10.99%9.09%7.84%6.92%6.23%5.65%

Using multiple cores

In the "Calculating win odds" section, we showed that the conventional method for "As Ks" takes about 5 minutes on my laptop computer. However, modifying the code to support multiple cores is straightforward and yields some spectacular results.

Single Thread (s)Multiple Thread (s)Speedup (X)
Single Core (w/Hyperthreading)225.16 202.38 1.11
Dual Core163.69 80.89 2.02
Quad Core198.45 47.46 4.18
Dual Quad Core171.91 21.26 8.09

The following code was used to benchmark the "Multiple Threads" column in the previous table. This example uses a thread pool to keep all of the cores "active." This technique is fairly straightforward to implement and the result is a roughly linear increase in speed based on the number of cores. I believe that the following example is fairly self-explanatory, but here are some of the highlights.

  • CalculateOdds() – This is a method that is the equivalent of the inner loop in the "Calculating win odds" example.
  • Main():
    • Determines the number of opponent hands that we will feed to CalculateOdds; creates storage for each thread pool result.
    • Creates a thread pool entry for each opponent hand using BeginInvoke. The IAsyncResult is held for this entry and used later to wait for and collect the results.
    • Uses EndInvoke to wait for the thread items to complete and collects the results.
    • Takes the results and calculates the odds using the same equations as in the "Calculating win odds" example. Note that the returned result is exactly the same.
C#
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Threading;
using HoldemHand;

namespace WinOddsMultipleThreads
{    
    class Program    
    {        
        // Holds Calculated Results
        public struct Results
        {
            public long win, ties, count;
        }

        // Delegate used in threadpool
        delegate Results CalculateOddsDelegate(
            ulong pocket, ulong opp, ulong board, ulong dead);

        // Method called and inserted in the threadpool
        static Results CalculateOdds(
            ulong pocket, ulong opp, ulong board, ulong dead)
        {
            // Initialize local results
            Results results = new Results();
            results.win = results.ties = results.count = 0;

            // Loop through all possible boards and tally the results
            foreach (
                ulong boardmask in Hand.Hands(board, dead | opp | pocket, 5))
            {
                uint playerHandval = Hand.Evaluate(pocket | boardmask, 7);
                uint oppHandval = Hand.Evaluate(opp | boardmask, 7);

                if (playerHandval > oppHandval)
                    results.win++;
                else if (playerHandval == oppHandval)
                    results.ties++;
                results.count++;
            }

            // Return tally 
            return results;
        }
        
        // Converts a hand iteration specification into an array of hands.
        static ulong [] HandList(ulong shared, ulong dead, int ncards)
        {
            List result = new List();

            foreach (ulong mask in Hand.Hands(shared, dead, ncards))
            {
                result.Add(mask);
            }
            return result.ToArray();
        }

        
        static void Main(string[] args)        
        {
            // This code calculates the probablity of As Ks winning against
            // another random hand.            
            ulong pocketmask = Hand.ParseHand("As Ks"); // Hole hand
            ulong board = Hand.ParseHand("");           // No board cards yet
            long wins = 0, ties = 0, count = 0;         
                // Iterate through all possible opponent hole cards            
            ulong [] opphands = HandList(0UL, board | pocketmask, 2); 
                // Create Array of Opponent Hands
            
            // Create delegate
            CalculateOddsDelegate d = 
                new CalculateOddsDelegate(CalculateOdds);

            // Create results array
            IAsyncResult[] results = new IAsyncResult[opphands.Length];

            // start time
            double start = Hand.CurrentTime;

            // Put calculation requests into the threadpool
            for (int i = 0; i < opphands.Length; i++)
            {
                results[i] = d.BeginInvoke(
                    pocketmask, opphands[i], 0UL, 0UL, null, null);
            }

            // Collect results once the threads have completed
            for (int i = 0; i < opphands.Length; i++)
            {
                Results r = d.EndInvoke(results[i]);
                wins += r.win;
                ties += r.ties;
                count += r.count;
            }

            // Prints: Win 67.0446323092352%            
            Console.WriteLine("Win {0}%, Elapsed Time {1}",
                (((double)wins) + ((double)ties) / 2.0) / (
                (double)count) * 100.0,
                Hand.CurrentTime-start);
        }
    }
}

Demo programs

I've included several demo programs and examples in the downloadable project. This is a quick summary of each of the demo applications:

Benchmark

Benchmark

The benchmark program tests the responsiveness of several aspects of the hand evaluator. Here is a list of the benchmark choices:

  • Evaluate() - Attempts to measure just the evaluation speed for each of the 9 hand types.
  • EvaluateType() - Attempts to measure the hand type evaluation method. This method doesn't return a complete hand value, but just returns the hand type. This method is faster than full hand evaluation.
  • Inline Iterations - Measures iteration of multiple hands using hand-coded iterators rather than IEnumerable.
  • C# Iterations - Measures iteration of multiple hands using IEnumerable C# iterators. This method is slower, but more understandable.
  • Evaluate/Iterate - Measures both C# Iterations and Evaluate().
  • Thread Pool Evaluate/Iterate - Measures C# Iterations and Evaluate() but does it in a way that utilized up to 16 cores. This can be used to see the relative improvement of Evaluate/Iterate when split across cores.

Multi-Odds

MultiOdds

This application is similar to Hand Odds, but allows the number of random opponents to be selected. This application does not allow the Pocket Query Language to be used as arguments to the Pocket Hand and Opponent Hand input. That is an effort deferred for the future.

Acknowledgements

  • Wesley Tansey - For a lot of help with functions determining different variations of outs.
  • Matt Baker - For many discussions on outs and for providing the DiscountedOuts functions.
  • Scott Turner - For using and providing feedback on enhancements.
  • poker-eval - The core to my evaluation engine.
  • ZedGraph - I've used ZedGraph in several of the examples that graph results.
  • Malcolm Crowe - Author of the lexer generator/parser generator tools for C# that I used for the Pocket Query Language.

Licensing

This code is licensed using LGPL. See the comments in the source for more information. I use this license because it is the license referenced by the poker-eval code, which I use as my core evaluation engine.

Disclaimer

I could easily have spent another 6 months editing and tweaking the code and examples. I decided that I had spent enough time on this and had a "critical mass" of features available. So, here is my current code, warts and all.

I don't claim to be a great poker player or even a good one, for that matter. To paraphrase: those who can do, those who can't code. I don't recommend taking any poker advice I may have given (or implied to have given) in this article without independent verification.

I'd also like to encourage feedback. Poker programmers appear to be a somewhat secretive lot, for good reason I'm sure. I've learned a lot from the feedback I've been given over the last year or so. Please keep it coming.

History

First released May 2007

License

This article, along with any associated source code and files, is licensed under The GNU Lesser General Public License (LGPLv3)