December 16: Possible hands and holds in a Ruby poker machine ♠♥♦♣

Josh Frank
6 min readDec 16, 2020

--

Video poker machines, like these at the Borgata in Atlantic City, New Jersey, must calculate millions of combinations of hands and hold cards — how does Ruby make this easy? (Photo credit: Atlantic City Weekly)

I’m not a gambling man — I work too hard for my money to give it away — but I am an avid poker player. I love poker because it’s a game of skill disguised as a game of chance. The engine of poker, its beating heart, is a complex and nuanced probability puzzle. The world’s top poker players aren’t unusually lucky or rich or obsessed with winning big money — they‘re just skilled at calculating lots of odds and analyzing large probability universes, and doing so quickly, accurately, under pressure, and while distracted or multitasking. (It should come as no surprise that many high-rolling regulars of the professional poker circuit, like Haseeb Qureshi and Paul Philips, are software engineers.)

The world’s first electronic Video Poker machine, Draw Poker, featured in a 1982 sales brochure (Photo credit: International Game Technologies)

Background/history and domain model

Motivated by my love of poker, I’ve decided to write a poker machine in Ruby using the skills I’m acquiring during my time at Flatiron. It’s inspired by the world’s first electronic poker machine, the IGT 701, which was introduced in 1982 and employed a 6502 processor just like contemporary arcade games and home video game systems like the NES and Game Boy. (Mechanical poker machines, most of which use spinning wheels just like old-fashioned slot machines, are even older, dating to the inter-war period.) The IGT 701 simulates Jacks or Better five-card draw, also known as California draw, and even today most Vegas/Atlantic City video poker machines are based on this five-card variety of poker. It’s called Jacks or Better because the lowest-scoring hand is a pair of Jacks; pairs of non-face cards have no score or value.

In my Ruby poker machine, hands are represented by arrays of cards, to wit: [ card, card, card, card, card ]

Generating possible hands

Ruby is excellent (though a bit slow) for permutations and combinations, and this makes generating hand possibilities a breeze. In another language like Java, getting all possible_hands from a 52-card deck would take dozens of lines of code and possibly a helper method for recursion. But in Ruby it’s as simple as invoking:

deck.combination( hand_size ).to_a

Note the to_a method at the end: that’s because .combination returns an Enumerator instance, not an array.

Draw poker is so named because cards can be discarded and drawn from the deck in the hopes of improving one’s hand, typically after a round of betting. The cards that aren’t discarded are referred to as hold cards. So, how do we generate all possible hands for a given set of hold cards? Ruby’s powerful and flexible enumerable methods make it easy:

def possible_hands( deck, hand_size, hold_cards = [] )
hold_cards.each{ | hold_card | deck.delete( hold_card ) }
deck.combination( hand_size - hold_cards.length ).to_a.map{ | hand | hand.concat( hold_cards ) }
end

What’s going on here? First, in line 1, we’re removing our chosen hold cards from the deck using .each and .delete. Then, in the first half of line 2, we call .combination.to_a with an argument of hand_size — hold_cards.length, to generate smaller combinations without our hold cards using the deck we stripped in line 1. Finally, we finish the job by .mapping our hold cards onto each .combination we generated from the stripped deck, using the .concat method.

What’s especially wonderful about this method is that it works even without any hold cards! In that case, Ruby will just skip line 1 and the second half of line 2 and call .combination( hand_size ) on the full, unstripped deck. Neato!

Generating possible holds

A particular configuration of hold cards for a hand is called a hold. Suppose we represent a hold as another array of cards; for example, for a hand [ card_one, card_two, card_three, card_four, card_five ], [ card_four, card_five ] would mean holding the last two cards and discarding the rest.

Generating possible_holds in this model is a bit more complicated than generating possible_hands, but it can be done in a relatively straightforward manner (on one line) by .mapping our .combinations:

def possible_holds( hand, number_of_hold_cards )
hand.size.times.to_a.combination( number_of_hold_cards ).to_a.map{ | hold | hold.map{ | index | hand[ index ] } }
end

First, we turn the number of cards in our hand to an array of each card’s index, using .size.times.to_a. Next we call .combination.to_a using number_of_hold_cards as an argument, giving us an array of possible card indices. Then finally, for each .combination , we .map the indices to actual cards in our hand!

Generating possible holds: a more naïve solution using binary numbers and .to_s( 2 )

Another way to represent a hold is with an array of boolean values; for example, when hand_size.eq?( 5 ), then [ true, true, false, false, false ] would mean holding the first two cards and discarding the rest.

What if I want to write a method that will give me an array of all possible_holds (an array of arrays) for a given hand_size? Unfortunately, neither .permutation nor .combination is what I need for a method to produce possible holds… but there is a clever solution!

High school math (which I’m terrible at, by the way) tells me that when determining possible combinations of size n from two possible values, the size of the universe of all possible combinations will be two to the power of n - meaning there are 32 possible holds for a five-card poker hand. This gets really interesting when we make use of the Integer.to_s( 2 ) method, which takes a base-10 integer (the kind we’re all familiar with from high school math) and converts it to that integer’s binary (base 2) representation as a string. For example:

24.to_s( 2 ) # => "11000" 

That looks an awful lot like how we want to represent a hold, doesn’t it? In fact, we can represent each number in the range 0...2 ** hand_size as a binary string using .to_s( 2 ) and, with some clever .mapping, use this to scrape out boolean holds:

def possible_holds( hand_size )
( 2 ** hand_size ).times.map{ | each_hold | each_hold.to_s( 2 ).rjust( hand_size, '0' ).chars.map { | character | character == '1' } }
end

This looks like gobbledygook — what’s happening here? Let’s break it down line by line:

  1. We .map over each possible hold by number— 0 to 2 ** hand_size;
  2. We turn each_hold into a string of zeroes and ones using .rjust, which we use because .to_s( 2 ) does not give us leading zeroes: if we create a binary string without leading zeroes, we’ll get a bunch of nil values;
  3. For each_hold string in the range 0...2 ** hand_size, we nest another .map inside our first .map over each character using a conditional to turn '1’ into true and anything else into false!

Unfortunately, this hold configuration is terribly inefficient because it essentially casts a boolean to another boolean. It’s really just a holdover from when I first wrote a poker hand odds calculator in Java, which lacks Ruby’s powerful native enumerables and thus favors logic like this. So it should be treated as a thought experiment only.

Conclusion: the bigger picture

The power and flexibility of Ruby’s .combination and .permutation methods, combined with Ruby’s enumerables, will enable me to create a robust and flexible video poker machine. For example, I can now generate all possible_hands for a given set of hold cards, then find the hand with the .max score; in other words, the hold with the highest possible chance of winning my bet. I can also create an array of possible_holds and then an array of possible_hands for each possible hold — enabling me to enumerate through all possible_holds to determine which hold for a given hand is the smartest move.

These methods and the logic behind them also have powerful uses for domains beyond video poker. Imagine a chess bot that can analyze a chess board and generate possible_moves, using it to determine the best move (this is, in fact, exactly the idea behind many basic chess engines). Or even imagine a program that examines a representation of a protein with a possible_protein_folds method, and analyzes each individual fold to find new lifesaving drugs. Using .combination and .permutation in clever, out-of-the-box ways yields but a clue to the power of combinatorics with Ruby!

--

--

Josh Frank
Josh Frank

Written by Josh Frank

Oh geez, Josh Frank decided to go to Flatiron? He must be insane…

No responses yet