Task

Create a program to parse a single five card poker hand and rank it according to this list of poker hands.



A poker hand is specified as a space separated list of five playing cards.

Each input card has two characters indicating face and suit.



Example

2d       (two of diamonds).


Faces are:    a, 2, 3, 4, 5, 6, 7, 8, 9, 10, j, q, k

Suits are:    h (hearts),   d (diamonds),   c (clubs),   and   s (spades),   or

alternatively,   the unicode card-suit characters:     ♥ ♦ ♣ ♠ 



Duplicate cards are illegal.

The program should analyze a single hand and produce one of the following outputs:

 straight-flush
 four-of-a-kind
 full-house
 flush
 straight
 three-of-a-kind
 two-pair
 one-pair
 high-card
 invalid



Examples

   2♥ 2♦ 2♣ k♣ q♦:   three-of-a-kind
   2♥ 5♥ 7♦ 8♣ 9♠:   high-card
   a♥ 2♦ 3♣ 4♣ 5♦:   straight
   2♥ 3♥ 2♦ 3♣ 3♦:   full-house
   2♥ 7♥ 2♦ 3♣ 3♦:   two-pair
   2♥ 7♥ 7♦ 7♣ 7♠:   four-of-a-kind 
   10♥ j♥ q♥ k♥ a♥:  straight-flush
   4♥ 4♠ k♠ 5♦ 10♠:  one-pair
   q♣ 10♣ 7♣ 6♣ q♣:  invalid

The programs output for the above examples should be displayed here on this page.



Extra credit

 use the playing card characters introduced with Unicode 6.0 (U+1F0A1 - U+1F0DE).
 allow two jokers
 use the symbol   joker
 duplicates would be allowed (for jokers only)
 five-of-a-kind would then be the highest hand


More extra credit examples

   joker  2♦  2♠  k♠  q♦:     three-of-a-kind
   joker  5♥  7♦  8♠  9♦:     straight
   joker  2♦  3♠  4♠  5♠:     straight
   joker  3♥  2♦  3♠  3♦:     four-of-a-kind
   joker  7♥  2♦  3♠  3♦:     three-of-a-kind
   joker  7♥  7♦  7♠  7♣:     five-of-a-kind
   joker  j♥  q♥  k♥  A♥:     straight-flush
   joker  4♣  k♣  5♦ 10♠:     one-pair
   joker  k♣  7♣  6♣  4♣:     flush
   joker  2♦  joker  4♠  5♠:  straight
   joker  Q♦  joker  A♠ 10♠:  straight
   joker  Q♦  joker  A♦ 10♦:  straight-flush
   joker  2♦  2♠  joker  q♦:  four-of-a-kind



Related tasks

 Playing cards
 Card shuffles
 Deal cards_for_FreeCell
 War Card_Game
 Go Fish


defmodule Card do
  @faces   ~w(2 3 4 5 6 7 8 9 10 j q k a)
  @suits   ~w(♥ ♦ ♣ ♠)                          # ~w(h d c s)
  @ordinal @faces |> Enum.with_index |> Map.new
 
  defstruct ~w[face suit ordinal]a
 
  def new(str) do
    {face, suit} = String.split_at(str, -1)
    if face in @faces and suit in @suits do
      ordinal = @ordinal[face]
      %__MODULE__{face: face, suit: suit, ordinal: ordinal}
    else
      raise ArgumentError, "invalid card: #{str}"
    end
  end
 
  def deck do
    for face <- @faces, suit <- @suits, do: "#{face}#{suit}"
  end
end
 
defmodule Hand do
  @ranks ~w(high-card one-pair two-pair three-of-a-kind straight flush
            full-house four-of-a-kind straight-flush five-of-a-kind)a |>
         Enum.with_index |> Map.new
  @wheel_faces ~w(2 3 4 5 a)
 
  def new(str_of_cards) do
    cards = String.downcase(str_of_cards) |>
            String.split([" ", ","], trim: true) |>
            Enum.map(&Card.new &1)
    grouped = Enum.group_by(cards, &(&1.ordinal)) |> Map.values
    face_pattern = Enum.map(grouped, &(length &1)) |> Enum.sort
    {consecutive, wheel_faces} = consecutive?(cards)
    rank = categorize(cards, face_pattern, consecutive)
    rank_num = @ranks[rank]
    tiebreaker = if wheel_faces do
                   for ord <- 3..-1, do: {1,ord}
                 else
                   Enum.map(grouped, &{length(&1), hd(&1).ordinal}) |>
                   Enum.sort |> Enum.reverse
                 end
    {rank_num, tiebreaker, str_of_cards, rank}
  end
 
  defp one_suit?(cards) do
    Enum.map(cards, &(&1.suit)) |> Enum.uniq |> length == 1
  end
 
  defp consecutive?(cards) do
    sorted = Enum.sort_by(cards, &(&1.ordinal))
    if Enum.map(sorted, &(&1.face)) == @wheel_faces do
      {true, true}
    else
      flag = Enum.map(sorted, &(&1.ordinal)) |>
             Enum.chunk(2,1) |>
             Enum.all?(fn [a,b] -> a+1 == b end)
      {flag, false}
    end
  end
 
  defp categorize(cards, face_pattern, consecutive) do
    case {consecutive, one_suit?(cards)} do
      {true, true}  -> :"straight-flush"
      {true, false} -> :straight
      {false, true} -> :flush
      _ ->  case face_pattern do
              [1,1,1,1,1] -> :"high-card"
              [1,1,1,2]   -> :"one-pair"
              [1,2,2]     -> :"two-pair"
              [1,1,3]     -> :"three-of-a-kind"
              [2,3]       -> :"full-house"
              [1,4]       -> :"four-of-a-kind"
              [5]         -> :"five-of-a-kind"
            end
    end
  end
end
 
test_hands = """
2♥ 2♦ 2♣ k♣ q♦
2♥ 5♥ 7♦ 8♣ 9♠
a♥ 2♦ 3♣ 4♣ 5♦
2♥ 3♥ 2♦ 3♣ 3♦
2♥ 7♥ 2♦ 3♣ 3♦
2♥ 6♥ 2♦ 3♣ 3♦
10♥ j♥ q♥ k♥ a♥
4♥ 4♠ k♠ 2♦ 10♠
4♥ 4♠ k♠ 3♦ 10♠
q♣ 10♣ 7♣ 6♣ 4♣
q♣ 10♣ 7♣ 6♣ 3♣
9♥ 10♥ q♥ k♥ j♣
2♥ 3♥ 4♥ 5♥ a♥
2♥ 2♥ 2♦ 3♣ 3♦
"""
hands = String.split(test_hands, "\n", trim: true) |> Enum.map(&Hand.new(&1))
IO.puts "High to low"
Enum.sort(hands) |> Enum.reverse |>
Enum.each(fn hand -> IO.puts "#{elem(hand,2)}: \t#{elem(hand,3)}" end)
 
# Extra Credit 2. Examples:
IO.puts "\nExtra Credit 2"
extra_hands = """
joker  2♦  2♠  k♠  q♦
joker  5♥  7♦  8♠  9♦
joker  2♦  3♠  4♠  5♠
joker  3♥  2♦  3♠  3♦
joker  7♥  2♦  3♠  3♦
joker  7♥  7♦  7♠  7♣
joker  j♥  q♥  k♥  A♥
joker  4♣  k♣  5♦ 10♠
joker  k♣  7♣  6♣  4♣
joker  2♦  joker  4♠  5♠
joker  Q♦  joker  A♠ 10♠
joker  Q♦  joker  A♦ 10♦
joker  2♦  2♠  joker  q♦
"""
deck = Card.deck
String.split(extra_hands, "\n", trim: true) |>
Enum.each(fn hand ->
  [a,b,c,d,e] = String.split(hand) |>
                Enum.map(fn c -> if c=="joker", do: deck, else: [c] end)
  cards_list = for v<-a, w<-b, x<-c, y<-d, z<-e, do: "#{v} #{w} #{x} #{y} #{z}"
  best = Enum.map(cards_list, &Hand.new &1) |> Enum.max
  IO.puts "#{hand}:\t#{elem(best,3)}"
end)