The Data Science Lab

Just for Fun: A Five-Card Poker Library Using C#

Chances are if you've had many coding interviews you've been presented with a poker problem. Here's a great take from Dr. James McCaffrey of Microsoft Research.

This article presents C# code for a library of five-card poker functions. Programmatically working with five-card poker hands is a surprisingly tricky, but interesting task -- and one that has useful lessons related to software design principles.

A good way to see where this article is headed is to take a look at the screenshot of a demo run in Figure 1. The demo begins by creating and displaying two Card objects: Card c1 = As (the Ace of spades), Card c2 = Td (the Ten of diamonds).

Next, the demo creates and classifies three five-card Hand objects. The first is displayed as h1 = 7c-8d-9h-Ts-Jc (Seven of clubs, Eight of diamonds, Nine of hearts, Ten of spades, Jack of clubs) and is classified as string type Straight (numeric type 4). The second Hand object is h2 = 6d-6h-6s-Ac-Ah, classified as string type FullHouse (numeric type 6). The third Hand is 5c-5d-9c-9d-Qh, string type TwoPair (numeric type 2). As you'll see, classifying poker hand types is somewhat more difficult than you might expect.

Figure 1: Five-Card Poker Library in Action
[Click on image for larger view.] Figure 1: Five-Card Poker Library in Action

The most significant part of the demo program compares Hand objects. The first comparison is displayed as Hand.Compare(h1, h2) = -1 which means that Hand h1 (Straight) is less than Hand h2 (FullHouse) in poker hand ranking. The second Hand comparison is Hand.Compare(h2, h3) = 1 which means Hand h2 (FullHouse) is greater than Hand h3 (TwoPair). Comparing two poker hands is much more difficult than you might expect.

The demo concludes by creating a 52-card SingleDeck object, shuffling the deck, and then dealing out a five-card Hand object and removing the next 43 cards, leaving 52 - (5 + 43) = 4 cards in the deck (3c Ts Jh 8s).

This article assumes you have intermediate or better programming skill and that you have at least a rudimentary knowledge of poker (but doesn't assume you're an expert). The demo is implemented using C#, but you should be able to refactor the demo code to another C-family language if you wish.

The source code for the demo program is too long to be presented in its entirety in this article. The complete code is available in the accompanying file download, and is also available online. All normal error checking has been removed to keep the main ideas as clear as possible.

The Demo Program
To create the demo program I used Visual Studio 2022 (Community Free Edition). I created a new C# console application and checked the "Place solution and project in the same directory" option. I specified .NET version 8.0. I named the project Poker. I checked the "Do not use top-level statements" option to avoid the program entry point shortcut syntax.

The demo has no significant .NET dependencies, and any relatively recent version of Visual Studio with .NET (Core) or the older .NET Framework will work fine. You can also use the Visual Studio Code program if you like.

After the template code loaded into the editor, I right-clicked on file Program.cs in the Solution Explorer window and renamed the file to the slightly more descriptive PokerProgram.cs. I allowed Visual Studio to automatically rename class Program.

The overall program structure of the demo is presented in Listing 1. There are three classes: Card, Hand, and SingleDeck. Most of the demo program code is dedicated to helper functions that are used to implement the Hand.Compare() method.

Listing 1: Overall Demo Program Structure

using System;
using System.IO;
using System.Collections.Generic;

namespace Poker
{
  internal class PokerProgram
  {
    static void Main(string[] args)
    {
      Console.WriteLine("Begin five-card poker with C# ");

      // create and display two Card objects
      // create, show, compare three Hand objects
      // create, show, use a SingleDeck object

      Console.WriteLine("End demo ");
      Console.ReadLine();
    } // Main
  } // Program

  public class Card : IComparable<Card>
  {
    public int rank; 
    public int suit; 

    public Card(int rank, int suit) { . . }
    public Card(string c) { . . }
    public override string ToString() { . . }
  }

  public class Hand
  {
    public List<Card> cards;

    public Hand(List<Card> lst) { . . }
    public Hand(Card c1, Card c2, Card c3,
      Card c4, Card c5) { . . }
    public Hand(string s) { . . }
    public override string ToString() { . . }
    public string ToTerseString() { . . }

    public string GetHandTypeStr() { . . }
    public int GetHandTypeInt() { . . }
    // 12 static helper methods like IsStraight()

    public static int Compare(Hand h1, Hand h2) { . . }
    // 9 static helpers like BreakTieFlush()
  }

  public class SingleDeck
  {
    public List<Card> deck;
    private Random rnd;
    public int currCardIdx;

    public SingleDeck(int seed) { . . }
    public void Shuffle() { . . }
    public Hand DealHand() { . . }
    public List<Card> DealListCards(int nCards) { . . }
    public string DealHandString() { . . }
    public void Show() { . . }
  }
} // ns

Creating Card Objects
The demo code that creates and displays two Card objects is:

Card c1 = new Card(14, 3);  // As
Console.Write("Card c1 = ");
Console.WriteLine(c1.ToString());
Card c2 = new Card("Td");
Console.Write("Card c2 = ");
Console.WriteLine(c2);

Card c1 is instantiated by supplying explicit rank and suit values as integers. Card c2 is instantiated by supplying a two-character string "Td". In most situations, you won't need to explicitly create Card objects. Notice that because the Card class implements a ToString() method, the explicit .ToString() call is not required in Console.WriteLine() statements.

Creating Hand Objects
The first of three Hand objects is created and displayed like so:

Hand h1 = new Hand("7cTsJc8d9h");
Console.WriteLine("Hand h1: ");
Console.WriteLine(h1);
Console.Write(h1.GetHandTypeStr() + " (");  // Straight
Console.WriteLine(h1.GetHandTypeInt() + ")");  // 4

A Hand object has two internal types. The GetHandTypeStr() method returns one of 10 strings: "RoyalFlush", "StraightFlush", "FourKind", "FullHouse", "Flush", "Straight", "ThreeKind", "TwoPair", "OnePair", "HighCard". Technically, a royal flush is just a special type of straight flush, but among poker players, the two hand types are usually considered distinct.

One of the many design alternatives is to pre-compute the hand type at instantiation and return the type as a property instead of using an explicit method. The GetHandTypeInt() method returns one of 10 integers: 9 (for "RoyalFlush"), 8 ("StraightFlush"), . . 1 ("OnePair"), 0 ("HighCard").

The second Hand object is created and displayed using these statements:

Hand h2 = new Hand(new Card("6s"), new Card("Ah"),
  new Card("6h"), new Card("Ac"), new Card("6d"));
Console.WriteLine("Hand h2: ");
Console.WriteLine(h2);
Console.Write(h2.GetHandTypeStr() + " (");  // FullHouse
Console.WriteLine(h2.GetHandTypeInt() + ")");  // 6

The overloaded Hand() constructor accepts five individual Card objects. The third Hand object is created and displayed as:

List<Card> lst = new List<Card>();
lst.Add(new Card("5c")); lst.Add(new Card("5d"));
lst.Add(new Card("9c")); lst.Add(new Card("9d"));
lst.Add(new Card("Qh"));
Hand h3 = new Hand(lst);
Console.WriteLine("Hand h3: ");
Console.WriteLine(h3);
Console.Write(h3.GetHandTypeStr() + " (");  // TwoPair
Console.WriteLine(h3.GetHandTypeInt() + ")");  // 2

The Hand object is created from a List<Card> collection. One of the subtleties of software library design is deciding on how many constructors to implement. Too few constructors can hamper using the library, but too many constructors can bloat the library.

The Hand objects are compared using these statements:

int cmp1 = Hand.Compare(h1, h2);
Console.Write("Hand.Compare(h1, h2) = ");
Console.WriteLine(cmp1);
int cmp2 = Hand.Compare(h2, h3);
Console.Write("Hand.Compare(h2, h3) = ");
Console.WriteLine(cmp2);

The static Hand.Compare(h1, h2) method returns -1 if h1 is less than h2, +1 if h1 is greater than h2, and 0 if h1 is equivalent to h2. Being equivalent is not the same as being identical. For example, "2c-3d-4h-5s-6c" is equivalent in poker hand rankings to "2d-3h-4s-5c-6d".

A significantly different interface option is to implement the hand comparison code as a method associated with a Hand object. For example, int result = h1.Compare(h2). This interface style is common, but I don't like it because it establishes one of two hands being compared as a reference hand. In my mind, a Boolean comparison accepts two arguments with equal importance.

Creating a Deck Object
The demo program creates a deck of 52 cards like so:

Console.WriteLine("Creating and shuffling deck ");
SingleDeck d1 = new SingleDeck(seed: 0);
d1.Shuffle();
d1.Show();
Hand h4 = d1.DealHand();
Console.WriteLine("Dealing Hand from deck: ");
Console.WriteLine(h4);

A SingleDeck object is a List<Card> collection. The Shuffle() method puts the cards in a random order using the Fisher-Yates shuffle algorithm.

The demo concludes by removing 43 cards:

Console.WriteLine("Removing 43 cards from deck");
List<Card> listCards = d1.DealListCards(43);
Console.WriteLine("Deck is now: ");
d1.Show();
Console.WriteLine("End demo ");

The demo code does not do any error checking to guarantee that the SingleDeck object has enough cards to meet the DealListCards() request. Depending on how you use the demo code, you can add error checking as needed.

Implementing a Card Class
Over the years, I have implemented a poker library many times. The number of design choices is huge but all begin with the design of a Card class. The Card class code is presented in Listing 2. The first thing to note is that the Card class inherits from the IComparable interface. This means the class must implement a CompareTo() method to satisfy the interface contract. This allows a .NET collection of Card objects to be automatically sorted using the built-in Sort() method.

Listing 2: The Card Class

public class Card : IComparable<Card>
{
  public int rank; 
  public int suit; 
  // 0,1 not used. 11=J, 12=Q, 13=K, 14=A
  // 0=clubs, 1=diamonds, 2=hearts, 3=spades

  public Card(int rank, int suit)
  {
    this.rank = rank; this.suit = suit;
  }

  public Card(string c)
  {
    char rnk = c[0]; char sut = c[1];
    if (rnk == 'T') this.rank = 10;
    else if (rnk == 'J') this.rank = 11;
    else if (rnk == 'Q') this.rank = 12;
    else if (rnk == 'K') this.rank = 13;
    else if (rnk == 'A') this.rank = 14;
    else this.rank = int.Parse(rnk.ToString());

    if (sut == 'c') this.suit = 0;
    else if (sut == 'd') this.suit = 1;
    else if (sut == 'h') this.suit = 2;
    else if (sut == 's') this.suit = 3;
  }

  public override string ToString()
  {
    string rnk = ""; string sut = "";
    if (this.rank == 10) rnk = "T";
    else if (this.rank == 11) rnk = "J";
    else if (this.rank == 12) rnk = "Q";
    else if (this.rank == 13) rnk = "K";
    else if (this.rank == 14) rnk = "A";
    else rnk = this.rank.ToString();

    if (this.suit == 0) sut = "c";
    else if (this.suit == 1) sut = "d";
    else if (this.suit == 2) sut = "h";
    else if (this.suit == 3) sut = "s";

    return rnk + sut;
  }

  public int CompareTo(Card other)
  {
    // sort cards in a hand from low (Two) to high (Ace)
    if (this.rank.CompareTo(other.rank) == 0)
    {
      return this.suit.CompareTo(other.suit);
    }
    return this.rank.CompareTo(other.rank);
  }
} // class Card

Each Card object has a rank field and a suit field. The possible rank values are 2 = Two, 3 = Three, . . 10 = Ten, 11 = Jack, 12 = Queen, 13 = King, 14 = Ace. Rank values of 0 and 1 are not allowed. One of the many tricky issues when working with a poker library is that the Ace can be the highest rank in most hands, such as OnePair but can also be the lowest rank in a Straight containing Ace, Two, Three, Four, Five.

The suit values are 0 = clubs, 1 = diamonds, 2 = hearts, 3 = spades. In poker, suits have no inherent ranking, so they are arranged in alphabetical order.

All of the poker library functions depend on the rank and suit definitions, so you should not modify them. The library code defines a simple ToString() override method that is used to display a Card in a two-character sequence, such as "2c" or "As". You can modify ToString() or implement a ToLongString() method to display cards as "TwoClubs", or "AceSpades".

The Card class CompareTo() method calls the built-in int.CompareTo() method. Card objects are first compared by rank, then in case of a tie when int.CompareTo() returns 0, compared by suit. The result is that Card objects are ordered as 2c, 2d, 2h, 2s, 3c, . . Ah, As.

Implementing a Hand Class
The demo code implements a five-card poker Hand class as a .NET generic collection List<Card> object named this.cards. There are three constructors. The first constructor accepts an existing List<Card> collection:

public Hand(List<Card> lst)
{
  this.cards = new List<Card>();
  for (int i = 0; i < 5; ++i)
    this.cards.Add(lst[i]);
  this.cards.Sort();
}

After copying from the List argument, the this.cards List is sorted using the built-in Sort() method, which, behind the scenes, calls the Card.CompareTo() method. All the logic of the poker library assumes that the cards in a Hand are sorted from low card to high. Notice that normal error checking, such as verifying the List argument has Count = 5, are omitted to keep the size of the library a bit smaller. Adding error checking typically doubles the number of lines of code in many libraries.

The second Hand constructor accepts five individual Card objects. The third Hand constructor accepts a string argument like "Js3h7d7cAd":

public Hand(string s) // s like "Js3h7d7cAd"
{
  this.cards = new List<Card>();
  this.cards.Add(new Card(s.Substring(0, 2)));
  this.cards.Add(new Card(s.Substring(2, 2)));
  this.cards.Add(new Card(s.Substring(4, 2)));
  this.cards.Add(new Card(s.Substring(6, 2)));
  this.cards.Add(new Card(s.Substring(8, 2)));
  this.cards.Sort();
}

Comparing Two Poker Hands
At first thought, comparing two poker hands seems like a relatively easy problem. You could just compute the integer hand types for the two hands and then compare those values. For example, a hand that has type FullHouse (6) is greater than a hand that has type ThreeKind (3). This works for most pairs of random hands. But when the two hand types are equal, such as TwoPair (integer type = 2), then the tie must be broken. This is surprisingly difficult.

The demo code has nine helper functions with names like BreakTieFlush() and BreakTieTwoPair(). There is no need for a BreakTieRoyalFlush() because all royal flush hands are equivalent in ranking. To break a tie between two hands that are both TwoPair, you must determine what the two pairs are in each hand, and also the kicker card in case both hands have the same two pairs. The logic of the various break-tie functions is very tricky.

A Hand object that is sorted and is type TwoPair can have one of three patterns: LLHHK, LLKHH or KLLHH where L is the lower-rank pair, H is the higher rank pair, and K is the kicker. The first pattern can be identified in several ways but the clearest approach looks like cards[0].rank == cards[1].rank && cards[1].rank != cards[2].rank && cards[2].rank == cards[3].rank && cards[3].rank != cards[4].rank. But you can get cute and observe that the first pattern can be uniquely identified by just the last condition, cards[3].rank != cards[4].rank.

An inexperienced programmer will lean toward the cute approach, thinking it's more efficient. An experienced programmer will lean toward the brute force approach, thinking it's safer. A very experienced programmer will ask if the code will be called only a few times or inside a loop and called millions of times.

After the TwoPair pattern has been determined, you can compute the ranks of the low-rank pair, the high-rank pair, and the kicker. If you have this information for both Hand objects, the comparison takes the form of if h1HighPairRank < h2HighPairRank return -1 else if h1HighPairRank > h2highPairRank return +1 else if . . . And you have to remember that a return value of 0 is possible.

Using the Poker Library
The poker library can be used in many ways. One example is computing probabilities using a simulation. Mechanical and electromechanical poker games have been popular for well over 100 years. The image in Figure 2 shows a fascinating machine manufactured by the Charles Fey Company at the turn of the last century.

Figure 2: An Antique Mechanical Five-Card Poker Machine (image courtesy of Bill Petrochuk, from the Coin Operated Collectors Association)
[Click on image for larger view.] Figure 2: An Antique Mechanical Five-Card Poker Machine (image courtesy of Bill Petrochuk, from the Coin Operated Collectors Association)

The machine has five reels:

reels[0] = "2d 3c 4s 6h 7d 8c 9s Qd Kc As"
reels[1] = "2h 3d 4c 5h 7h 8d 9c Qh Kd Ac"
reels[2] = "3h 4d 5d 6s 8h 9d Tc Js Kh Ad"
reels[3] = "2s 4h 5c 6c 7s 9h Td Jc Qs Ah"
reels[4] = "2c 3s 5s 6d 7c 8s Th Jd Qc Ks"

Although it's possible to compute the probability of each of the 10 possible hand types using standard mathematical probability, using that approach would be extremely difficult. Instead, writing a simple simulation is quite easy. In pseudo-code:

loop n trials times
  randomly select one card from each reel
  construct hand from the five cards
  determine the hand type
  increment counter for the hand type
end-loop
divide each counter by n

I ran such a simulation with 10,000,000 trials and got these results:

                machine     standard deck
HighCard        0.43135600  0.50117700
OnePair         0.45633230  0.42256900
TwoPair         0.06447860  0.04753900
ThreeKind       0.03819820  0.02112800
Straight        0.00337820  0.00392500
Flush           0.00177490  0.00196500
FullHouse       0.00328960  0.00144100
FourKind        0.00108450  0.00024010
StraightFlush   0.00008790  0.00001390
RoyalFlush      0.00001980  0.00000154

Interestingly, the antique machine has a better chance of giving OnePair, TwoPair, and ThreeKind (better than the worst HighCard hand type but hands with low payouts) than random hands from a standard deck of 52 cards, but a lower chance of giving hands that deliver a significant payout. This configuration provides users with positive feedback with small payouts to encourage them to keep playing, but avoids large payouts.

Wrapping Up
Several of the teams I have worked with over the years used a hypothetical poker library as the basis for job interview questions. There are dozens of design alternatives that trade off performance, maintainability, efficiency, and other factors. This characteristic allows the interviewers to evaluate the job candidate's software design ability, and communication skills. Interestingly, in all the years I saw poker hands used as the basis of job interview questions, I don't recall any job candidate not being familiar with poker. I suspect there's a connection between people who like mathematical-based games and people who like to write computer programs.

comments powered by Disqus

Featured

Subscribe on YouTube