EdvardM
4/23/2012 - 6:36 PM

My first attempt at poker kata /w Ruby

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