Brit
Brit

Reputation: 125

How do you check an array for a range in Ruby?

I'm writing a poker program, and I can't figure out how to handle straights.

Straight: All cards in a hand of 5 cards are consecutive values. ex. 2..6, 3..7, 4..8, 5..9, 6..T, 7..J, 8..Q, 9..K, T..A

cards = [2, 3, 4, 5, 6, 7, 8, 9, "T", "J", "Q", "K", "A"]

How can I check a hand, which is an array, for these combinations? Preferably I can check it to see if it's 5 in a row in the cards array.

Upvotes: 5

Views: 1075

Answers (7)

Mark Thomas
Mark Thomas

Reputation: 37517

I recommend writing classes to represent a Card (and maybe Deck and Hand too). Aim for an interface like this:

deck = Deck.new.shuffle!
hand = Hand.new(deck.draw 5)
hand.straight?
#=>false
puts hand
8♣ 8♦ T♠ 2♦ 7♦

The encapsulation of functionality gives you readability and makes it easy to extend (i.e. with suits)

Here's a more simplistic version, implemented as a single Card class. I did add suits though.

class Card
  include Enumerable #enables sorting
  attr_accessor :value, :suit

  @values = [2, 3, 4, 5, 6, 7, 8, 9, "T", "J", "Q", "K", "A"]
  @suits  = ["♣","♦","♥","♠"]

  def self.all
    @values.product(@suits).map{|c| Card.new c}
  end

  def self.straight?(cards)
    ["A", *@values].each_cons(5).include?(cards.map(&:value))
  end

  def self.flush?(cards)
    cards.map(&:suit).uniq.size == 1
  end

  def initialize(v)
    @value, @suit = *v
  end

  def <=>(other) #for sorting
    @values.index(value) <=> @values.index(other.value)
  end

  def to_s
    "#{value}#{suit}"
  end
end

This works as follows

deck = Card.all
puts deck
#=> 2♣ 2♦ 2♥ 2♠ 3♣ 3♦ 3♥ 3♠ 4♣ 4♦ 4♥ 4♠ 5♣ 5♦ 5♥ 5♠ 6♣ 6♦ 6♥ 6♠ 7♣ 7♦ 7♥ 7♠ 8♣ 8♦ 8♥ 8♠ 9♣ 9♦ 9♥ 9♠ T♣ T♦ T♥ T♠ J♣ J♦ J♥ J♠ Q♣ Q♦ Q♥ Q♠ K♣ K♦ K♥ K♠ A♣ A♦ A♥ A♠
hand = deck.sample 5
puts hand
#=> Q♥ 6♦ 2♣ T♠ Q♦
Card.straight?(hand)
#=>false

Upvotes: 1

Cary Swoveland
Cary Swoveland

Reputation: 110675

Edit 2: This is my absolutely final solution:

require 'set'
STRAIGHTS = ['A',*2..9,'T','J','Q','K','A'].each_cons(5).map(&:to_set)
  #=> [#<Set: {"A", 2, 3, 4, 5}>, #<Set: {2, 3, 4, 5, 6}>,
  #   ...#<Set: {9, "T", "J", "Q", "K"}>, #<Set: {"T", "J", "Q", "K", "A"}>]

def straight?(hand)
  STRAIGHTS.include?(hand.to_set)
end

STRAIGHTS.include?([6,3,4,5,2].to_set)
  # STRAIGHTS.include?(#<Set: {6, 3, 4, 5, 2}>)
  #=> true 

straight?([6,5,4,3,2])            #=> true 
straight?(["T","J","Q","K","A"])  #=> true 
straight?(["A","K","Q","J","T"])  #=> true
straight?([2,3,4,5,"A"])          #=> true 

straight?([6,7,8,9,"J"])          #=> false 
straight?(["J",7,8,9,"T"])        #=> false 

Edit 1: @mudasobwa upset the apple cart by pointing out that 'A',2,3,4,5 is a valid straight. I believe I've fixed my answer. (I trust he's not going to tell me that 'K','A',2,3,4 is also valid.)

I would suggest the following:

CARDS     = [2, 3, 4, 5, 6, 7, 8, 9, "T", "J", "Q", "K", "A"]
STRAIGHTS = CARDS.each_cons(5).to_a
  #=>[[2, 3, 4, 5, 6], [3, 4, 5, 6, 7], [4, 5, 6, 7, 8],
  #   [5, 6, 7, 8, 9], [6, 7, 8, 9, "T"], [7, 8, 9, "T", "J"],
  #   [8, 9, "T", "J", "Q"], [9, "T", "J", "Q", "K"],
  #   ["T", "J", "Q", "K", "A"]] 

def straight?(hand)
  (hand.map {|c| CARDS.index(c)}.sort == [0,1,2,3,12]) ||
  STRAIGHTS.include?(hand.sort {|a,b| CARDS.index(a) <=> CARDS.index(b)})
end

Upvotes: 4

Jordan Running
Jordan Running

Reputation: 106027

If we map each card to a value (9 is 9, "T" is 10, "J" is 11, etc.), then there are two facts that are true of all straights that we can use to solve our problem:

  1. All straights have exactly five unique card values
  2. The difference between the last and first cards' values is always 4

And so:

CARD_VALUES = {
    2 =>  2,    3 =>  3,    4 =>  4,
    5 =>  5,    6 =>  6,    7 =>  7,
    8 =>  8,    9 =>  9,  "T" => 10,
  "J" => 11,  "Q" => 12,  "K" => 13,
  "A" => 14
}

def is_straight?(hand)
  hand_sorted = hand.map {|card| CARD_VALUES[card] }
    .sort.uniq

  hand_sorted.size == 5 &&
    (hand_sorted.last - hand_sorted.first) == 4
end

This method (1) converts each card to its numeric value with map, then (2) sorts them, and then (3) throws out duplicates with uniq. To illustrate with various hands:

    hand |  4   A   T   A   2 |  2   2   3   3   4 |  5   6   4   8   7 |  3  6  2  8  7
---------+--------------------+--------------------+--------------------+----------------
 1. map  |  4  14  10  14   2 |  2   2   3   3   4 |  5   6   4   8   7 |  3  6  2  8  7
 2. sort |  2   4  10  14  14 |  2   2   3   3   4 |  4   5   6   7   8 |  2  3  6  7  8
 3. uniq |  2   4  10  14     |  2   3   4         |  4   5   6   7   8 |  2  3  6  7  8

Alternatively...

I originally posted the following solution, which isn't bad, but is definitely more convoluted:

If the hand is sorted, this is easy. You can use Enumerable#each_cons to check each possible straight.

CARDS = [ 2, 3, 4, 5, 6, 7, 8, 9, "T", "J", "Q", "K", "A" ]
hand = [ 4, 5, 6, 7, 8 ]

def is_straight?(hand)
  CARDS.each_cons(5).any? do |straight|
    hand == straight
  end
end

if is_straight?(hand)
  puts "Straight!"
else
  puts "Not straight!"
end
# => Straight!

each_cons(5) returns each consecutive set of 5 items, so in the above example hand is first compared to [ 2, 3, 4, 5, 6 ], then [ 3, 4, 5, 6, 7 ], and then [ 4, 5, 6, 7, 8 ], which is a match, so any? returns true.

Note that this is not the most efficient solution, but unless you need to check many thousands of hands per second, this is more than adequately performant.

If your hands aren't sorted yet, you'll need to do that first. The simplest way to do that is create a Hash that maps cards to a numeric value (as above) and then use sort_by:

def sort_hand(hand)
  hand.sort_by {|card| CARD_VALUES[card] }
end

hand = [ 4, "A", 2, "A", "T" ]
sort_hand(hand)
# => [ 2, 4, "T", "A", "A" ]

Upvotes: 4

hirolau
hirolau

Reputation: 13901

This is how I would write it:

hand  = [3,4,5,2,'A']


def is_straight(hand)

  # No need to check further if we do not have 5 unique cards.
  return false unless hand.uniq.size == 5

  # Note the A at beginning AND end to count A as 1 or 14.
  list_of_straights = 'A23456789TJQKA'.chars.each_cons(5)

  sorted_hand = hand.map(&:to_s).sort

  list_of_straights.any? do |straight| 
    straight.sort==sorted_hand
  end

end

puts is_straight(hand) #=> true  

Alternatively if you do not like all the sorting you could exchange the last part to:

  hand_as_stings = hand.map(&:to_s)

  list_of_straights.any? do |straight| 
    (straight-hand_as_stings).empty?
  end

Upvotes: 0

shivam
shivam

Reputation: 16506

Generate list of valid hands:

valid_hands = cards[0..8].each_with_index.map{|b,i| cards[i..i+4]}
#=> [[2, 3, 4, 5, 6], [3, 4, 5, 6, 7], [4, 5, 6, 7, 8], [5, 6, 7, 8, 9], [6, 7, 8, 9, "T"], [7, 8, 9, "T", "J"], [8, 9, "T", "J", "Q"], [9, "T", "J", "Q", "K"], ["T", "J", "Q", "K", "A"]]

Once you have the list of all valid hands, you can now check if provided hand is among any? of them (valid ones) or not:

if valid_hands.any? { |h| (h - hand).empty? } 
   puts "Valid hand"
else
   puts "Not Valid"
end

UPDATE

In-case 2, 3, 4, 5, "A", 2, 3, 4, "K", "A", 2, 3, "Q", "K", "A", 2, "J", "Q", "K", "A" are also considered as valid hands, calculate them as follows:

valid_hands = cards.each_with_index.map { |b,i| i < 9 ? cards[i..i+4] : cards[0..i-9] + cards[i..-1] }
# => [[2, 3, 4, 5, 6], [3, 4, 5, 6, 7], [4, 5, 6, 7, 8], [5, 6, 7, 8, 9], [6, 7, 8, 9, "T"], [7, 8, 9, "T", "J"], [8, 9, "T", "J", "Q"], [9, "T", "J", "Q", "K"], ["T", "J", "Q", "K", "A"], [2, "J", "Q", "K", "A"], [2, 3, "Q", "K", "A"], [2, 3, 4, "K", "A"], [2, 3, 4, 5, "A"]]

Upvotes: 1

Gavin
Gavin

Reputation: 4698

Step 0: Let's start with an empty class

class CardUtils
end

Step 1: Store values of card in Hash

Hash allows fast referencing of values of a card.

@@card_values = {
    'A' => 1,   2  => 2,   3  => 3, 4 => 4,  5 => 5,
     6  => 6,   7  => 7,   8  => 8, 9 => 9, 'T' => 10,
    'J' => 11, 'Q' => 12, 'K' => 13
}

Thus, you can reference the card value simply as below.

@@card_values['A']
# => 1

@@card_values[8]
# => 8

Step 2: Sort the hand

Apply sort! method to the hand with reference to the card values.

def self.sort(hand)
    hand.sort {|x,y| @@card_values[x] <=> @@card_values[y]}
end
#  => ["A", 2, 3, 4, 5, 6, 7, 8, 9, "T", "J", "Q", "K"] 

Step 3: Function that tells whether two cards are consecutive

def self.is_consecutive(x, y)
    val_x = @@card_values[x]
    val_y = @@card_values[y]

    val_x == val_y - 1 || val_x + 13 == val_y
end
# is_consecutive('A', 2)
#  => true
# is_consecutive('K', 'A')
#  => true
# is_consecutive('A', 3)
#  => false

Step 4: Check for 'straight'

It could be done with simple iteration.

def self.has_straight(hand)
    hand = sort(hand)

    max_consecutive_count = 0
    consecutive_count = 0

    hand.each_with_index do |curr, i|
        prev = hand[i - 1]

        if is_consecutive(prev, curr) then
            consecutive_count += 1
        else
            consecutive_count = 0
        end

        if consecutive_count > max_consecutive_count then
            max_consecutive_count = consecutive_count
        end
    end

    max_consecutive_count >= 5
end
# hand = [2, 3, 4, 5, 6, 7, 8, 9, "T", "J", "Q", "K", "A"]
# CardUtils.has_straight(hand)
#  => true

Final Result

class CardUtils
    @@card_values = {
        'A' => 1,   2  => 2,   3  => 3, 4 => 4,  5 => 5,
         6  => 6,   7  => 7,   8  => 8, 9 => 9, 'T' => 10,
        'J' => 11, 'Q' => 12, 'K' => 13
    }

    def self.is_consecutive(x, y)
        val_x = @@card_values[x]
        val_y = @@card_values[y]

        val_x == val_y - 1 || val_x + 13 == val_y
    end

    def self.sort(hand)
        hand.sort {|x,y| @@card_values[x] <=> @@card_values[y]}
    end

    def self.has_straight(hand)
        hand = sort(hand)

        max_consecutive_count = 0
        consecutive_count = 0

        hand.each_with_index do |curr, i|
            prev = hand[i - 1]

            if is_consecutive(prev, curr) then
                consecutive_count += 1
            else
                consecutive_count = 0
            end

            if consecutive_count > max_consecutive_count then
                max_consecutive_count = consecutive_count
            end
        end

        max_consecutive_count >= 5
    end
end

Upvotes: 0

Aleksei Matiushkin
Aleksei Matiushkin

Reputation: 121000

I did not want to participate, but I can’t keep silence looking at all these oversophisticated solutions around.

hand = [2, 5, 7, 'A', 'J'].map(&:to_s)

'23456789TJQKA' =~ hand.sort_by{|hc| '23456789TJQKA'.index(hc)}.join ||
   'A23456789TJQK' =~ hand.sort_by{|hc| 'A23456789TJQK'.index(hc)}.join

In a not lame hardcoded manner:

suit = '23456789TJQKA'

suit =~ hand.sort_by{|hc| suit.index(hc)}.join ||
   suit.rotate(-1) =~ hand.sort_by{|hc| suit.rotate(-1).index(hc)}.join

Upvotes: 1

Related Questions