Reputation: 125
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
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
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
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:
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) sort
s 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
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
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
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
Reputation: 4698
class CardUtils
end
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
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"]
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
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
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
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