0
\$\begingroup\$

Hello everyone and thanks in advance for taking the time to read my post!!

In the process of learning Ruby, I'm writing a video poker machine that analyzes hands represented by arrays of Cards, like so: [ card, card, card, card, card ]

You may know that in draw poker, one can hold certain cards and toss the rest to the dealer, to be replaced by drawing new cards from the deck in the hopes of improving one's hand (hence the name). It's natural to figure that holds would be represented by an array of boolean values. To wit: [ true, true, false, false, false ] would mean holding the first two cards in a five-card hand and discarding the rest. What I'm trying to write is a method that will give me an array of all possible holds (an array of arrays) for a given hand size.

I've found that Ruby is terrific (though a bit slow) for permutations and combinations, i.e., all possible unique hands from a 52-card deck. All I have to do for that is call deck.combination( hand_size ).to_a. But unfortunately neither .permutation nor .combination is really giving me what I need for a method to produce possible holds.

High school math (which I'm terrible at, by the way) tells me that when determining possible unique n-combinations of two possible values, the size of the universe of all possible n-combinations will be two to the power of n - meaning there are 32 possible holds for a five-card poker hand.

After slaughtering a snow-white bull for the gods at midnight under a full moon, I've come up with the following solution using binary numbers. This method loops through a range from 0...2 ** hand_size, turns each element of the range into a binary number string with a hand_size number of leading zeroes using .rjust, then .maps each character in the string to an array of hand_size elements using a conditional and ternary; if the character is "1" it maps a true to the array, otherwise the character is a "0" and it maps false to the array.

def possible_holds( hand_size )
   result = []
   for each_hold in 0...2 ** hand_size do
      as_binary = each_hold.to_s( 2 ).rjust( hand_size, "0" )
      result << ( 0...hand_size ).to_a.map{ | card | as_binary[ card ] == "1" ? true : false }
   end
   result
end

This clunky little method actually works very well, but... it just doesn't seem very rubinic. I would love to hear clever suggestions as to how to improve it. I have this nagging feeling that this can all be done with a neat, simple method/enumerable or two... but no matter how many virgins I sacrifice I just can't seem to come up with anything better.

Thanks in advance for your help and happy coding!!

\$\endgroup\$
7
  • \$\begingroup\$ If n = 5, a = n.times.to_a.combination(2).to_a #=> [[0, 1], [0, 2], [0, 3], [0, 4], [1, 2], [1, 3], [1, 4], [2, 3], [2, 4], [3, 4]] are the 10 (n*(n-1)/2) ways of holding 2 cards in a 5-card hand. Each element gives the indices of the two cards to be kept. If the hand were hand = ['4D', 'AS', 'KH', '6C', 'QD'], the possible pairs of cards to keep would be a.map { |b| a.values_at(*b) } #=> [["4D", "AS"], ["4D", "KH"], ["4D", "6C"], ["4D", "QD"], ["AS", "KH"], ["AS", "6C"], ["AS", "QD"], ["KH", "6C"], ["KH", "QD"], ["6C", "QD"]]. \$\endgroup\$ Dec 17, 2020 at 0:50
  • \$\begingroup\$ What's your actual end-goal here? If you want to manage the hand, you can simply #pop or #delete the elements you want to remove, and replace them with new cards. I'm not sure what you're gaining by adding a layer of indirection with mapping the cards to boolean values first, or what you're actually trying to calculate. \$\endgroup\$
    – CodeGnome
    Dec 17, 2020 at 0:51
  • \$\begingroup\$ That's wonderful Cary!! \$\endgroup\$
    – josh-frank
    Dec 17, 2020 at 0:52
  • \$\begingroup\$ Hi Todd, eventually I'd like to make a hash with each hold as a key corresponding to a value of the best hand possible with that hold. \$\endgroup\$
    – josh-frank
    Dec 17, 2020 at 0:53
  • \$\begingroup\$ Cheers @steenslag that's a great point, I suppose it'd be better to calculate the average of all possible hands for a given hold and pick the highest \$\endgroup\$
    – josh-frank
    Dec 17, 2020 at 1:27

3 Answers 3

1
\$\begingroup\$

Here's a demonstration of how you might solve this problem the Ruby way:

SUITS = %w[ H D C S ]
FACES = %w[ 2 3 4 5 6 7 8 9 T J Q K A ]

CARDS = SUITS.flat_map { |s| FACES.map { |f| "#{f}#{s}" } }

def holds(hand)
  hand.length.times.flat_map do |n|
    hand.combination(n).to_a
  end << hand
end

holds(CARDS.shuffle.take(5))

Where you get results like this:

[[],
 ["AD"],
 ["5H"],
 ["QS"],
 ["4H"],
 ["5D"],
 ["AD", "5H"],
 ["AD", "QS"],
 ["AD", "4H"],
 ["AD", "5D"],
 ["5H", "QS"],
 ["5H", "4H"],
 ["5H", "5D"],
 ["QS", "4H"],
 ["QS", "5D"],
 ["4H", "5D"],
 ["AD", "5H", "QS"],
 ["AD", "5H", "4H"],
 ["AD", "5H", "5D"],
 ["AD", "QS", "4H"],
 ["AD", "QS", "5D"],
 ["AD", "4H", "5D"],
 ["5H", "QS", "4H"],
 ["5H", "QS", "5D"],
 ["5H", "4H", "5D"],
 ["QS", "4H", "5D"],
 ["AD", "5H", "QS", "4H"],
 ["AD", "5H", "QS", "5D"],
 ["AD", "5H", "4H", "5D"],
 ["AD", "QS", "4H", "5D"],
 ["5H", "QS", "4H", "5D"],
 ["AD", "5H", "QS", "4H", "5D"]]

Personally I'd hold the pair of 5s and the Ace.

For educational purposes here's a more Ruby-esque form of your original code:

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

It's kind of messy due to the binary string to boolean conversion.

Tip: Try to avoid "casting a boolean to a boolean" as in patterns like x ? true : false or if x; true; else; false like you had in your code. That reduces down to just x. The x == '1' test already returns a true or false value. There is no maybe or kind_of response.

\$\endgroup\$
2
  • 1
    \$\begingroup\$ I can't thank you enough for this wonderful response!! I am coming from Java which is shaping my thinking but it seems like I need to shift my thinking from using an array of booleans to picking specific cards by name, since that will enable me to make more powerful use of Ruby's combination method. Your tip about casting boolean to boolean is a priceless pearl of wisdom!! Again and again: THANK YOU \$\endgroup\$
    – josh-frank
    Dec 17, 2020 at 3:44
  • \$\begingroup\$ If you change hand.length.times to 0..hand.length you don't need << hand. \$\endgroup\$ Jan 31, 2021 at 19:06
1
\$\begingroup\$

This is a variant of @tadman's answer, just to show there are different ways to achieve the same results.

SUITS = %w[ H D C S ]
FACES = %w[ 2 3 4 5 6 7 8 9 T J Q K A ]
CARDS = SUITS.product(FACES).map { |s,f| "#{f}#{s}" }
  #=> ["2H", "3H", "4H", "5H", "6H", "7H", "8H", "9H", "TH", "JH", "QH", "KH",
  #    "AH", "2D", "3D", "4D", "5D", "6D", "7D", "8D", "9D", "TD", "JD", "QD",
  #    "KD", "AD", "2C", "3C", "4C", "5C", "6C", "7C", "8C", "9C", "TC", "JC",
  #    "QC", "KC", "AC", "2S", "3S", "4S", "5S", "6S", "7S", "8S", "9S", "TS",
  #    "JS", "QS", "KS", "AS"]

See Array#product.


def holds(hand)
  hand.repeated_combination(hand.size).map(&:uniq).uniq
end
hand = CARDS.sample(5)
  #=> ["TC", "8S", "4H", "6H", "JD"]
arr = holds(hand)
  #=> [["TC"], ["TC", "8S"],...,["6H"], ["6H", "JD"], ["JD"], []]

We can better see what we have by sorting arr by size:

arr.sort_by(&:size)
  #=> [[],
  #    ["8S"], ["TC"], ["4H"], ["6H"], ["JD"],
  #    ["4H", "6H"], ["8S", "4H"], ["8S", "6H"], ["8S", "JD"], ["TC", "JD"],
  #    ["TC", "6H"], ["TC", "4H"], ["6H", "JD"], ["4H", "JD"], ["TC", "8S"],
  #    ["TC", "8S", "4H"], ["TC", "8S", "JD"], ["TC", "4H", "6H"],
  #    ["TC", "4H", "JD"], ["TC", "6H", "JD"], ["8S", "4H", "6H"],
  #    ["8S", "4H", "JD"], ["8S", "6H", "JD"], ["4H", "6H", "JD"],
  #    ["TC", "8S", "6H"],
  #    ["TC", "4H", "6H", "JD"], ["TC", "8S", "4H", "6H"],
  #    ["TC", "8S", "6H", "JD"], ["TC", "8S", "4H", "JD"],
  #    ["8S", "4H", "6H", "JD"],
  #    ["TC", "8S", "4H", "6H", "JD"]]

See Array#repeated_combination and Array#sample.

Let's look at the first part of the calculation.

enum = hand.repeated_combination(hand.size)
  #=> #<Enumerator: ["TC", "8S", "4H", "6H", "JD"]:repeated_combination(5)>
arr = enum.to_a
  #=> [["TC", "TC", "TC", "TC", "TC"], ["TC", "TC", "TC", "TC", "8S"],
  #    ... 
  #    ["TC", "TC", "TC", "8S", "8S"], ["TC", "TC", "TC", "8S", "4H"],
  #    ...
  #    ["TC", "TC", "8S", "8S", "8S"], ["TC", "TC", "8S", "8S", "4H"],
  #    ...
  #    ["TC", "8S", "8S", "8S", "8S"], ["TC", "8S", "8S", "8S", "4H"],
  #    ...
  #    ["8S", "8S", "8S", "8S", "8S"], ["8S", "8S", "8S", "8S", "4H"],
  #    ...
  #    ["4H", "4H", "4H", "4H", "4H"], ["4H", "4H", "4H", "4H", "6H"],
  #    ...
  #    ["6H", "6H", "6H", "6H", "6H"], ["6H", "6H", "6H", "6H", "JD"],
  #    ["6H", "6H", "6H", "JD", "JD"], ["6H", "6H", "JD", "JD", "JD"],
  #    ["6H", "JD", "JD", "JD", "JD"],
  #    ["JD", "JD", "JD", "JD", "JD"]]
arr.size
  #=> 126

This becomes:

arr.map(&:uniq)
  #=> [["TC"], ["TC", "8S"],
  #    ... 
  #    ["TC", "8S"], ["TC", "8S", "4H"],
  #    ...
  #    ["TC", "8S"], ["TC", "8S", "4H"],
  #    ...
  #    ["TC", "8S"], ["TC", "8S", "4H"],
  #    ...
  #    ["8S"], ["8S", "4H"],
  #    ...
  #    ["4H"], ["4H", "6H"],
  #    ...
  #    ["6H"], ["6H", "JD"],
  #    ["6H", "JD"], ["6H", "JD"],
  #    ["6H", "JD"],
  #    ["JD"]]

The final .uniq returns an array to which an empty array is appended.

This is grossly inefficient relative to other calculations that could be performed to achieve the same result, but we are only dealing with hand sizes of 5, not 10,000, so who cares?


As for the OP's method possible_holds, we could write it as follows:

def possible_holds(hand_size)
  (0..2 ** hand_size - 1).map { |n| hand_size.times.map { |i| n[i] == 1 } } 
end
arr = possible_holds(5)
  #=> [[false, false, false, false, false], [true, false, false, false, false],
  #    [false, true,  false, false, false], [true, true,  false, false, false],
  #    [false, false, true,  false, false], [true, false, true,  false, false],
  #    [false, true,  true,  false, false], [true, true,  true,  false, false],
  #    [false, false, false, true,  false], [true, false, false, true,  false],
  #    [false, true,  false, true,  false], [true, true,  false, true,  false],
  #    [false, false, true,  true,  false], [true, false, true,  true,  false],
  #    [false, true,  true,  true,  false], [true, true,  true,  true,  false],
  #    [false, false, false, false, true],  [true, false, false, false, true],
  #    [false, true,  false, false, true],  [true, true,  false, false, true],
  #    [false, false, true,  false, true],  [true, false, true,  false, true],
  #    [false, true,  true,  false, true],  [true, true,  true,  false, true],
  #    [false, false, false, true,  true],  [true, false, false, true,  true],
  #    [false, true,  false, true,  true],  [true, true,  false, true,  true],
  #    [false, false, true,  true,  true],  [true, false, true,  true,  true],
  #    [false, true,  true,  true,  true],  [true, true,  true,  true,  true]]

See Integer#[].

\$\endgroup\$
0
\$\begingroup\$

Analyzing a Single Hand with User-Selected Discards

I may be completely misunderstanding your problem. I'm sure there's already an algorithm out there somewhere for calculating the odds of a given hand, or you could construct a Monte Carlo simulation. That said, it seems like this might get you where you're trying to go by building a result set of all possible combinations based on the cards remaining in your hand after selecting the ones to discard.

Using Ruby 2.7.2:

MAX_HAND_SIZE = 5

suit = (2..10).to_a + %w[J Q K A]
deck = suit * 4
deck.shuffle!

hand = deck.pop MAX_HAND_SIZE
#=> [3, 7, 2, 7, "J"]

discards = []
discards +=
  [3, 2, "J"].map { hand.delete_at hand.find_index _1 }

combos =
  deck.
  combination(MAX_HAND_SIZE - hand.size).
  to_a.
  map { _1.append *hand }.
  uniq

Calling combos.size shows that, with this particular shuffle and this particular starting hand, you have 1,956 unique hands when drawing from the remaining deck. You can then analyze those possible hands for strength or value in whatever way you were planning to do before.

This approach neatly side-steps the indirection of trying to map your intended discards to a Boolean value, but still allows you to calculate all the potential hands available when refilling from the remaining deck.

All Possible Permutations of a Hand

If you're trying to do something more complex, like trying to determine all possible variations of a 1..3 card draw or a 0..5 card draw, the problem quickly becomes much more complex. In trying to answer your question, I found myself with what seemed like a good answer until I realized that you would probably have to run something like:

permuted_hand = (2..4).map { |i| hand.permutation(i).to_a }

on each drawn hand, and then generate potential combinations based on dropping each of the permutations from that hand. This quickly got out of hand (if you'll pardon the pun) both in terms of logic, runtime, and combinations, so I didn't pursue it any further.

In comments, you stated:

[E]ventually I'd like to make a hash with each hold as a key corresponding to a value of the best hand possible with that hold.

Even if you were able to calculate that for a given hand in a given shuffled deck, I suspect that this would be hard to generalize outside the current round of play without a very large statistical sample and a lot more math. This sort of work has certainly been done before for other games of chance, so there's bound to be some research out there, but it may be more of a statistical or mathematical modeling question than strictly a programming one. If you don't get a good answer here on Stack Overflow, it may be worth kicking the tires on your mathematical or statistical model on a math-related stack instead.

\$\endgroup\$
1
  • \$\begingroup\$ Hi Todd, sorry for my delay in replying but I so appreciate this detailed, thoughtful analysis!!! I actually took a very similar path in getting possible hands from a deck: first: hold_cards.each{ | hold_card | deck.delete( hold_card ) } to strip the deck of hold cards, then calling deck.combination( hand_size - hold_cards.length ).to_a.map{ | hand | hand.concat( hold_cards ) } to generate smaller-sized hand combos and then mapping hold cards to each combo! And I also found Ruby's ranges wonderful useful for generating decks!! Ruby is a poker fanatic's dream!! \$\endgroup\$
    – josh-frank
    Dec 18, 2020 at 4:12

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy