Avatar of paulfioravanti

paulfioravanti's solution

to Poker in the Ruby Track

Published at May 16 2019 · 0 comments
Instructions
Test suite
Solution

Pick the best hand(s) from a list of poker hands.

See wikipedia for an overview of poker hands.


For installation and learning resources, refer to the Ruby resources page.

For running the tests provided, you will need the Minitest gem. Open a terminal window and run the following command to install minitest:

gem install minitest

If you would like color output, you can require 'minitest/pride' in the test file, or note the alternative instruction, below, for running the test file.

Run the tests from the exercise directory using the following command:

ruby poker_test.rb

To include color from the command line:

ruby -r minitest/pride poker_test.rb

Source

Inspired by the training course from Udacity. https://www.udacity.com/course/viewer#!/c-cs212/

Submitting Incomplete Solutions

It's possible to submit an incomplete solution so you can see how others have completed the exercise.

poker_test.rb

require 'minitest/autorun'
require_relative 'poker'

class PokerTest < Minitest::Test
  def test_one_hand
    high_of_jack = %w(4S 5S 7H 8D JC)
    game = Poker.new([high_of_jack])
    assert_equal [high_of_jack], game.best_hand
  end

  def test_highest_card
    skip
    high_of_8 = %w(4S 5H 6S 8D 2H)
    high_of_queen = %w(2S 4H 6C 9D QH)
    game = Poker.new([high_of_8, high_of_queen])
    assert_equal [high_of_queen], game.best_hand
  end

  def test_highest_card_10
    skip
    high_of_8 = %w(4D 5S 6S 8D 3C)
    high_of_10 = %w(2S 4C 7S 9H 10H)
    game = Poker.new([high_of_8, high_of_10])
    assert_equal [high_of_10], game.best_hand
  end

  def test_nothing_vs_one_pair
    skip
    high_of_king = %w(4S 5H 6C 8D KH)
    pair_of_4 = %w(2S 4H 6S 4D JH)
    game = Poker.new([high_of_king, pair_of_4])
    assert_equal [pair_of_4], game.best_hand
  end

  def test_two_pair
    skip
    pair_of_2 = %w(4S 2H 6S 2D JH)
    pair_of_4 = %w(2S 4H 6C 4D JD)
    game = Poker.new([pair_of_2, pair_of_4])
    assert_equal [pair_of_4], game.best_hand
  end

  def test_one_pair_vs_double_pair
    skip
    pair_of_8 = %w(2S 8H 6S 8D JH)
    fives_and_fours = %w(4S 5H 4C 8C 5C)
    game = Poker.new([pair_of_8, fives_and_fours])
    assert_equal [fives_and_fours], game.best_hand
  end

  def test_two_double_pair
    skip
    eights_and_twos = %w(2S 8H 2D 8D 3H)
    fives_and_fours = %w(4S 5H 4C 8S 5D)
    game = Poker.new([eights_and_twos, fives_and_fours])
    assert_equal [eights_and_twos], game.best_hand
  end

  def test_another_two_double_pair
    skip
    aces_and_twos = %w(2S AH 2C AD JH)
    queens_and_jacks = %w(JD QH JS 8D QC)
    game = Poker.new([aces_and_twos, queens_and_jacks])
    assert_equal [aces_and_twos], game.best_hand
  end

  def test_double_pair_vs_three
    skip
    eights_and_twos = %w(2S 8H 2H 8D JH)
    three_of_4 = %w(4S 5H 4C 8S 4H)
    game = Poker.new([eights_and_twos, three_of_4])
    assert_equal [three_of_4], game.best_hand
  end

  def test_two_three
    skip
    three_twos = %w(2S 2H 2C 8D JH)
    three_aces = %w(4S AH AS 8C AD)
    game = Poker.new([three_twos, three_aces])
    assert_equal [three_aces], game.best_hand
  end

  def test_three_vs_straight
    skip
    three_of_4 = %w(4S 5H 4C 8D 4H)
    straight = %w(3S 4D 2S 6D 5C)
    game = Poker.new([three_of_4, straight])
    assert_equal [straight], game.best_hand
  end

  def test_a_5_high_straight
    skip
    three_of_4 = %w(4S 5H 4C 8D 4H)
    straight_to_5 = %w(4D AH 3S 2D 5C)
    game = Poker.new([three_of_4, straight_to_5])
    assert_equal [straight_to_5], game.best_hand
  end

  def test_two_straights
    skip
    straight_to_8 = %w(4S 6C 7S 8D 5H)
    straight_to_9 = %w(5S 7H 8S 9D 6H)
    game = Poker.new([straight_to_8, straight_to_9])
    assert_equal [straight_to_9], game.best_hand
  end

  def test_5_high_straight_vs_other_straight
    skip
    straight_to_jack = %w(8H 7C 10D 9D JH)
    straight_to_5 = %w(4S AH 3S 2D 5H)
    game = Poker.new([straight_to_jack, straight_to_5])
    assert_equal [straight_to_jack], game.best_hand
  end

  def test_straight_vs_flush
    skip
    straight_to_8 = %w(4C 6H 7D 8D 5H)
    flush_to_7 = %w(2S 4S 5S 6S 7S)
    game = Poker.new([straight_to_8, flush_to_7])
    assert_equal [flush_to_7], game.best_hand
  end

  def test_two_flushes
    skip
    flush_to_8 = %w(3H 6H 7H 8H 5H)
    flush_to_7 = %w(2S 4S 5S 6S 7S)
    game = Poker.new([flush_to_8, flush_to_7])
    assert_equal [flush_to_8], game.best_hand
  end

  def test_flush_vs_full
    skip
    flush_to_8 = %w(3H 6H 7H 8H 5C)
    full = %w(4S 5H 4C 5D 4H)
    game = Poker.new([flush_to_8, full])
    assert_equal [full], game.best_hand
  end

  def test_two_fulls
    skip
    full_of_4_by_9 = %w(4H 4S 4D 9S 9D)
    full_of_5_by_8 = %w(5H 5S 5D 8S 8D)
    game = Poker.new([full_of_4_by_9, full_of_5_by_8])
    assert_equal [full_of_5_by_8], game.best_hand
  end

  def test_full_vs_square
    skip
    full = %w(4S 5H 4D 5D 4H)
    square_of_3 = %w(3S 3H 2S 3D 3C)
    game = Poker.new([square_of_3, full])
    assert_equal [square_of_3], game.best_hand
  end

  def test_two_square
    skip
    square_of_2 = %w(2S 2H 2C 8D 2D)
    square_of_5 = %w(4S 5H 5S 5D 5C)
    game = Poker.new([square_of_2, square_of_5])
    assert_equal [square_of_5], game.best_hand
  end

  def test_square_vs_straight_flush
    skip
    square_of_5 = %w(4S 5H 5S 5D 5C)
    straight_flush_to_10 = %w(7S 8S 9S 6S 10S)
    game = Poker.new([square_of_5, straight_flush_to_10])
    assert_equal [straight_flush_to_10], game.best_hand
  end

  def test_two_straight_flushes
    skip
    straight_flush_to_8 = %w(4H 6H 7H 8H 5H)
    straight_flush_to_9 = %w(5S 7S 8S 9S 6S)
    game = Poker.new([straight_flush_to_8, straight_flush_to_9])
    assert_equal [straight_flush_to_9], game.best_hand
  end

  def test_highest_card_down_to_fifth_card
    skip
    high_of_8_low_of_3 = %w(3S 5H 6S 8D 7H)
    high_of_8_low_of_2 = %w(2S 5D 6D 8C 7S)
    game = Poker.new([high_of_8_low_of_3, high_of_8_low_of_2])
    assert_equal [high_of_8_low_of_3], game.best_hand
  end

  def test_three_hand_with_tie
    skip
    spade_straight_to_9 = %w(9S 8S 7S 6S 5S)
    diamond_straight_to_9 = %w(9D 8D 7D 6D 5D)
    three_of_4 = %w(4D 4S 4H QS KS)
    hands = [spade_straight_to_9, diamond_straight_to_9, three_of_4]
    game = Poker.new(hands)
    assert_equal [spade_straight_to_9, diamond_straight_to_9], game.best_hand
  end
end
# frozen_string_literal: true

require "bigdecimal"
class Poker
  # Assign a value to a card of less than 1 dependent on its rank
  # so that:
  # - cards can be compared to each other
  # - the value can be tallied along with hand scores
  CARD_VALUE = ->(num) { BigDecimal(num) * BigDecimal("0.01") }
  private_constant :CARD_VALUE
  RANKS = %w[a 2 3 4 5 6 7 8 9 10 J Q K A].freeze
  private_constant :RANKS

  module Score
    module HighLowCard
      module_function

      def call(hand)
        min, max = hand.cards.minmax
        max.value + min.value / 2
      end
    end

    module OnePair
      POINTS = 1
      private_constant :POINTS

      module_function

      def call(hand)
        max_pair_card = Score.find_multicard(hand, 1).max
        max_pair_card ? POINTS + max_pair_card.value : 0
      end
    end

    module TwoPair
      POINTS = 2
      private_constant :POINTS

      module_function

      def call(hand)
        pair_cards = Score.find_multicard(hand, 1)
        pair_cards.length > 1 ? POINTS + pair_cards.max.value : 0
      end
    end

    module ThreeOfAKind
      POINTS = 3
      private_constant :POINTS

      module_function

      def call(hand)
        three_of_a_kind_card = Score.find_multicard(hand, 2).max
        three_of_a_kind_card ? POINTS + three_of_a_kind_card.value : 0
      end
    end

    module Straight
      ACES = ->(card) { card.rank == "A" }
      private_constant :ACES
      POINTS = 5
      private_constant :POINTS
      SINGLE_CARD_GAP = CARD_VALUE.call(1)
      private_constant :SINGLE_CARD_GAP

      module_function

      def call(hand)
        straight = straight(hand)
        straight ? POINTS + straight.last.value : 0
      end

      def straight(hand)
        cards = hand.cards.sort
        numbered_straight(cards) || ace_low_straight(cards)
      end

      def numbered_straight(cards)
        straight =
          cards
          .each_cons(2)
          .all? { |(card1, card2)| card2 - card1 == SINGLE_CARD_GAP }
        straight ? cards : nil
      end
      private_class_method :numbered_straight

      def ace_low_straight(cards)
        aces, other_cards = cards.partition(&ACES)
        return nil unless aces.length == 1

        ace_low = Card.new("a#{aces.first.suit}")
        numbered_straight(other_cards.prepend(ace_low))
      end
      private_class_method :ace_low_straight
    end

    module Flush
      POINTS = 8
      private_constant :POINTS

      module_function

      def call(hand)
        flush?(hand) ? POINTS + hand.cards.max.value : 0
      end

      def flush?(hand)
        hand.suits.uniq.one?
      end
    end

    module FullHouse
      POINTS = 13
      private_constant :POINTS

      module_function

      def call(hand)
        three_set, other_cards = split_hand(hand)
        return 0 if not_full_house?(three_set, other_cards, hand.ranks)

        throw(:halt, POINTS + three_set.first.value)
      end

      def split_hand(hand)
        hand
          .cards
          .sort
          .partition { |card| Score.multiple?(card, hand.ranks, 2) }
      end
      private_class_method :split_hand

      def not_full_house?(three_set, other_cards, ranks)
        three_set.empty? ||
          other_cards.none? { |card| Score.multiple?(card, ranks, 1) }
      end
    end

    module FourOfAKind
      POINTS = 21
      private_constant :POINTS

      module_function

      def call(hand)
        square_cards, _other_cards = split_hand(hand)
        if (square = square_cards.first)
          throw(:halt, POINTS + square.value)
        else
          0
        end
      end

      def split_hand(hand)
        hand
          .cards
          .partition { |card| Score.multiple?(card, hand.ranks, 3) }
      end
    end

    module StraightFlush
      POINTS = 34
      private_constant :POINTS

      module_function

      def call(hand)
        if (straight = Straight.straight(hand)) && Flush.flush?(hand)
          throw(:halt, POINTS + straight.last.value)
        else
          0
        end
      end
    end

    module_function

    def calculate(hand)
      catch(:halt) do
        constants.sum { |mod| const_get(mod).call(hand) }
      end
    end

    def find_multicard(hand, floor)
      hand
        .cards
        .each
        .with_object([hand.ranks, floor])
        .each_with_object([], &method(:add_multicard))
        .uniq
    end

    def add_multicard((card, (ranks, floor)), acc)
      acc << card if multiple?(card, ranks, floor)
    end

    def multiple?(card, ranks, floor)
      ranks.count(card.rank) > floor
    end
  end
  private_constant :Score

  class Card
    include Comparable

    # Lookahead to non-digit character in string
    SUIT = /(?=\D)/.freeze
    private_constant :SUIT

    attr_reader :rank, :suit

    def initialize(card)
      @rank, @suit = card.split(SUIT)
    end

    def to_s
      rank + suit
    end

    def value(rank = self.rank)
      RANKS.index(rank).then(&CARD_VALUE)
    end

    def -(other)
      value - value(other.rank)
    end

    private

    def hash
      value.hash
    end

    def eql?(other)
      value.eql?(value(other.rank))
    end

    def <=>(other)
      value <=> value(other.rank)
    end
  end
  private_constant :Card

  class Hand
    include Comparable

    attr_reader :cards

    def initialize(cards)
      @cards = cards.map { |card| Card.new(card) }
      @score = Score.calculate(self)
    end

    def to_a
      cards.map(&:to_s)
    end

    def ranks
      cards.map(&:rank)
    end

    def suits
      cards.map(&:suit)
    end

    protected

    attr_reader :score

    private

    def <=>(other)
      score <=> other.score
    end
  end
  private_constant :Hand

  def initialize(hands)
    @hands = hands.map { |hand| Hand.new(hand) }
  end

  def best_hand
    first_hand, *rest = hands
    initial_best_hands = initial_best_hands(first_hand)

    rest
      .each_with_object(initial_best_hands, &method(:compare_best_hand))
      .fetch(:best_hands)
  end

  private

  attr_reader :hands

  def initial_best_hands(first_hand)
    { best_hands: [first_hand.to_a], best_hand: first_hand }
  end

  def compare_best_hand(hand, acc)
    best_hand = acc[:best_hand]
    if hand == best_hand
      acc[:best_hands] << hand.to_a
    elsif hand > best_hand
      acc[:best_hands] = [hand.to_a]
      acc[:best_hand] = hand
    else
      acc
    end
  end
end

Community comments

Find this solution interesting? Ask the author a question to learn more.

What can you learn from this solution?

A huge amount can be learned from reading other people’s code. This is why we wanted to give exercism users the option of making their solutions public.

Here are some questions to help you reflect on this solution and learn the most from it.

  • What compromises have been made?
  • Are there new concepts here that you could read more about to improve your understanding?