Skip to content

Instantly share code, notes, and snippets.

@edvardm
Created April 23, 2012 18:36
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save edvardm/2472946 to your computer and use it in GitHub Desktop.
Save edvardm/2472946 to your computer and use it in GitHub Desktop.
My first attempt at poker kata /w Ruby
require 'rspec'
class Array
include Comparable
def <=>(other)
if self[0] == other[0]
self[1..-1] <=> other[1..-1]
else
self[0] <=> other[0]
end
end
end
class Card
CARD_VALUES = {'a' => 14, 'k' => 13, 'q' => 12, 'j' => 11, 't' => 10}
CARD_NAMES = {14 => 'Ace', 13 => 'King', 12 => 'Queen', 11 => 'Jack'}
def self.name_for(val); CARD_NAMES.fetch(val, val.to_s); end
attr_reader :value, :suit
def initialize(c)
@suit, v = c.scan(/(\w)(\w+)/).flatten
@value = CARD_VALUES.fetch(v, v.to_i)
end
end
class Game
def initialize(p1, p2)
@p1 = p1
@p2 = p2
end
def fmt_winner(p, other)
f = p.score.f
hand_name = p.score.hand_name
fmt = "%s wins. - with %s"
fmt % [p.name, f ? hand_name % f.call(p.score.cards, other.score.cards) : hand_name]
end
def result
case @p1.score <=> @p2.score
when 1; fmt_winner(@p1, @p2)
when -1; fmt_winner(@p2, @p1)
else; 'Tie.'
end
end
# % Card.name_for(cs.first)
end
class PokerHand
class Score
include Comparable
attr_reader :cards, :hand_value, :hand_name, :f
def initialize(rev_cards, hand_value, hand_name, f)
@cards = rev_cards
@hand_value = hand_value
@hand_name = hand_name
@f = f
end
def <=>(o); @hand_value <=> o.hand_value; end
end
attr_reader :name
def initialize(name, *cs)
@name = name
@cards = cs.map { |c| Card.new(c.to_s) }
end
def score
cs = rev_values(@cards)
seq, hand_name, f = if p = straight_flush(@cards)
[[9] << p, 'straight flush']
elsif p = four(cs)
[[8] << p, 'four of a kind']
elsif p = full_house(cs)
[[7] << p, 'full house']
elsif p = flush(@cards)
[[6] << p, 'flush']
elsif p = straight(cs)
[[5] << p, 'straight']
elsif p = three(cs)
[[4] + [p] + (cs - [p]), 'three of a kind']
elsif ps = two_pairs(cs)
[[3] + ps + (cs - ps), 'two pairs']
elsif p = pair(cs)
[[2] + [p] + (cs-[p]), 'pair']
else
[[1] + cs, 'high card: %s', lambda {|cs1, cs2| Card.name_for((cs1 - cs2).first) }]
end
Score.new(cs, seq, hand_name, f)
end
private
def rev_values(cards); cards.map(&:value).sort.reverse; end
def all_with_count(n, vs); count_occurrences(vs).select {|_, v| v == n}; end
def first_with_count(n, vs)
v, _ = count_occurrences(vs).detect {|_, v| v == n}
v
end
def count_occurrences(vs)
vs.inject(Hash.new {|h,k| h[k] = 0}) { |acc, v| acc[v] += 1; acc }
end
# different hands
def pair(vs); first_with_count(2, vs); end
def three(vs); first_with_count(3, vs); end
def four(vs); first_with_count(4, vs); end
def two_pairs(vs)
rs = all_with_count(2, vs)
rs.size == 2 ? rs.map { |v, _| v } : nil
end
def straight(vs)
vs.include?(14) ? straight_p(vs) || straight_p(vs - [14] + [1]) : straight_p(vs)
end
def straight_p(vs)
vs.max - vs.min == 4 ? vs.max : nil
end
def flush(cs)
cs.all? {|c| c.suit == cs.first.suit} ? cs.map(&:value).max : nil
end
def full_house(vs)
s1 = three(vs)
s2 = s1 ? pair(vs - [s1]) : nil
s1 && s2 ? s1 : nil
end
def straight_flush(cs)
s1 = flush(cs)
s2 = s1 ? straight(cs.map(&:value)) : nil
(s1 && s2) ? s2 : nil
end
end
describe 'Poker' do
def score(*a)
PokerHand.new('player', *a).score.hand_value
end
describe "comparing hands" do
def cards(*a)
PokerHand.new(*a)
end
def game(p1, p2)
Game.new(p1, p2).result
end
it "should win high king with high ace" do
w = cards('White', :h2, :d3, :s5, :c9, :dk)
b = cards('Black', :c2, :h3, :s4, :c8, :ha)
game(w, b).should == 'Black wins. - with high card: Ace'
end
it "should win flush with a full house" do
w = cards('White', :s2, :s8, :sa, :sq, :s3)
b = cards('Black', :h2, :s4, :d4, :d2, :h4)
game(w, b).should == 'Black wins. - with full house'
end
it "should win with better second card if high cards are equal" do
w = cards('White', :h2, :d3, :s5, :c9, :dk)
b = cards('Black', :c2, :h3, :s4, :c8, :hk)
game(w, b).should == 'White wins. - with high card: 9'
end
it "should be a tie with equal hands" do
game(
cards('W', :h2, :d3, :s5, :c9, :dk),
cards('B', :d2, :h3, :c5, :s9, :hk)
).should == 'Tie.'
end
end
end
@edvardm
Copy link
Author

edvardm commented Apr 23, 2012

I know the order of cards is odd (first suit, then value), but I wanted to use symbols to save precious keystrokes, and :2d is not a valid symbol in Ruby. Moreover, when I'm rewriting the kata in Haskell that notation has another benefit, because strings are lists there :-P

@edvardm
Copy link
Author

edvardm commented Apr 24, 2012

Second attempt. Read the spec a bit more carefully (eg. 10 is represented by the symbol 't') and reports actual winner now (as well as the winning card in case of high card). Note the amount of cruft needed compared to the first version to just report the winner name as well as winning card.

Next step to make it more clean in OOP style would be to create separate class for different Hands, I suppose, eg. FullHouse etc, because the logic to show the winning card is specific to outcome. However, even though a tad hackish, I still preferred the anonymous lambda in the end to pick the winning card.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment