Created
April 23, 2012 18:36
-
-
Save edvardm/2472946 to your computer and use it in GitHub Desktop.
My first attempt at poker kata /w Ruby
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
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
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