diff --git a/poker_ex/lib/poker_ex/card.ex b/poker_ex/lib/poker_ex/card.ex index 1481071..0a9750c 100644 --- a/poker_ex/lib/poker_ex/card.ex +++ b/poker_ex/lib/poker_ex/card.ex @@ -8,7 +8,7 @@ defmodule PokerEx.Card do @valid_suits [:spades, :hearts, :diamonds, :clubs] @type suit() :: :hearts | :diamonds | :clubs | :spades - @type rank() :: integer | :jack | :queen | :king | :ace + @type rank() :: integer() | :jack | :queen | :king | :ace @type t() :: %Card{suit: suit(), rank: rank()} defstruct [:suit, :rank] @@ -37,7 +37,7 @@ defmodule PokerEx.Card do iex> PokerEx.Card.valid? %PokerEx.Card{suit: :diamonds, rank: 11} false """ - @spec valid?(Card.t()) :: boolean + @spec valid?(Card.t()) :: boolean() def valid?(%Card{suit: suit, rank: rank}) when is_valid_rank(rank) and is_valid_suit(suit), do: true @@ -78,6 +78,24 @@ defmodule PokerEx.Card do |> Enum.map(fn rank -> %Card{suit: suit, rank: rank} end) end + @spec compare(Card.t(), Card.t()) :: :lt | :eq | :gt + def compare(first, second) do + case [first.rank, second.rank] + |> Enum.map( + &case &1 do + :jack -> 11 + :queen -> 12 + :king -> 13 + :ace -> 14 + n -> n + end + ) do + [f, s] when f < s -> :lt + [f, s] when f == s -> :eq + [f, s] when f > s -> :gt + end + end + @doc ~S""" Makes a card struct from the given integers. Mostly used when parsing the unicode glyph into a card. @@ -98,7 +116,7 @@ defmodule PokerEx.Card do iex> PokerEx.Card.from_integers(0, 15) {:error, "Invalid integer rank: 15"} """ - @spec from_integers(integer, integer) :: {:ok, Card.t()} | {:error, String.t()} + @spec from_integers(integer(), integer()) :: {:ok, Card.t()} | {:error, String.t()} def from_integers(suit, _rank) when suit < 0 when suit >= 4 do @@ -114,13 +132,14 @@ defmodule PokerEx.Card do def from_integers(suit, rank) do suit = Enum.at(@valid_suits, suit) - {:ok, case rank do - n when n >= 2 and n <= 10 -> %Card{suit: suit, rank: n} - 0x1 -> %Card{suit: suit, rank: :ace} - 0xB -> %Card{suit: suit, rank: :jack} - 0xD -> %Card{suit: suit, rank: :queen} - 0xE -> %Card{suit: suit, rank: :king} - end} + {:ok, + case rank do + n when n >= 2 and n <= 10 -> %Card{suit: suit, rank: n} + 0x1 -> %Card{suit: suit, rank: :ace} + 0xB -> %Card{suit: suit, rank: :jack} + 0xD -> %Card{suit: suit, rank: :queen} + 0xE -> %Card{suit: suit, rank: :king} + end} end @doc ~S""" @@ -169,23 +188,26 @@ defmodule PokerEx.Card do """ @spec sigil_p(atom() | String.t(), [char()]) :: Card.t() def sigil_p(<>, []) do - suit = case suit do - ?S -> :spades - ?H -> :hearts - ?D -> :diamonds - ?C -> :clubs - end + suit = + case suit do + ?S -> :spades + ?H -> :hearts + ?D -> :diamonds + ?C -> :clubs + end + + rank = + case rank do + "A" -> :ace + "K" -> :king + "Q" -> :queen + "J" -> :jack + n -> String.to_integer(n) + end - rank = case rank do - "A" -> :ace - "K" -> :king - "Q" -> :queen - "J" -> :jack - n -> String.to_integer(n) - end - card = %Card{suit: suit, rank: rank} - if not valid? card do + + if not valid?(card) do raise "Invalid card!" end diff --git a/poker_ex/lib/poker_ex/combination.ex b/poker_ex/lib/poker_ex/combination.ex new file mode 100644 index 0000000..73b6d71 --- /dev/null +++ b/poker_ex/lib/poker_ex/combination.ex @@ -0,0 +1,194 @@ +defmodule PokerEx.Combination do + alias PokerEx.Card, as: Card + + @type high_card() :: {:high_card, Card.t()} + @type pair() :: {:pair, Card.t()} + @type two_pair() :: {:two_pair, {Card.t(), Card.t()}} + @type three_of_a_kind() :: {:three_of_a_kind, Card.t()} + @type straight() :: {:straight, {Card.rank(), Card.rank()}} + @type flush() :: {:flush, Card.suit()} + @type full_house() :: {:full_house, {Card.t(), Card.t()}} + @type four_of_a_kind() :: {:four_of_a_kind, Card.t()} + @type straight_flush() :: {:straight_flush, {Card.suit(), Card.rank(), Card.rank()}} + @type royal_flush() :: {:royal_flush, Card.suit()} + + @type score() :: + high_card() + | pair() + | two_pair() + | three_of_a_kind() + | straight() + | flush() + | full_house() + | four_of_a_kind() + | straight_flush() + | royal_flush() + + @spec straight?(ranks: [Card.rank()]) :: boolean + defp straight?(ranks) when length(ranks) < 5, do: false + + defp straight?(ranks) do + ranks + |> Enum.map( + &case &1 do + :jack -> 11 + :queen -> 12 + :king -> 13 + :ace -> 14 + n -> n + end + ) + |> Enum.chunk_every(2, 1, :discard) + |> Enum.map( + &case &1 do + [14, 2] -> true + [a, b] -> b - a == 1 + _ -> false + end + ) + |> Enum.all?() + end + + @spec n_equals(%{Card.t() => integer()}, integer()) :: [Card.t()] + defp n_equals(frequencies, n) do + frequencies + |> Enum.filter(fn {_key, val} -> val == n end) + |> Enum.map(fn {key, _val} -> key end) + |> Enum.sort({:desc, Card}) + end + + @spec straight([Card.t()]) :: straight() | nil + defp straight(sorted_hand) when length(sorted_hand) < 5, do: nil + + defp straight(sorted_hand) do + case sorted_hand + |> Enum.uniq() + |> Enum.drop_while(&(&1.rank != :ace)) + |> Enum.concat(sorted_hand) + |> Enum.map(& &1.rank) + |> Enum.chunk_every(5, 1, :discard) + |> Enum.find(&straight?/1) do + nil -> nil + [begin, _, _, _, final] -> {:straight, {begin, final}} + end + end + + @spec flush([Card.t()]) :: flush() | nil + defp flush(hand) when length(hand) < 5, do: nil + + defp flush(hand) do + case hand + |> Enum.map(& &1.suit) + |> Enum.frequencies() + |> Enum.find(fn {_, freq} -> freq >= 5 end) do + nil -> nil + {suit, _freq} -> {:flush, suit} + end + end + + @spec straight_flush([Card.t()]) :: straight_flush() | nil + defp straight_flush(sorted_hand) do + case {straight(sorted_hand), flush(sorted_hand)} do + {{:straight, {begin, final}}, {:flush, suit}} -> {:straight_flush, {suit, begin, final}} + _ -> nil + end + end + + @spec royal_flush([Card.t()]) :: royal_flush() | nil + defp royal_flush(sorted_hand) do + case straight_flush(sorted_hand) do + {:straight_flush, {suit, begin, _}} when begin == 10 -> {:royal_flush, suit} + _ -> nil + end + end + + @spec pair(%{Card.t() => integer()}) :: pair() | nil + defp pair(frequencies) do + case n_equals(frequencies, 2) do + [] -> nil + [p | _] -> {:pair, p} + end + end + + @spec two_pair(%{Card.t() => integer()}) :: two_pair() | nil + defp two_pair(frequencies) do + case n_equals(frequencies, 2) do + [f | [s | _]] -> {:two_pair, {f, s}} + _ -> nil + end + end + + @spec three_of_a_kind(%{Card.t() => integer()}) :: three_of_a_kind() | nil + defp three_of_a_kind(frequencies) do + case n_equals(frequencies, 3) do + [] -> nil + [tok | _] -> {:three_of_a_kind, tok} + end + end + + @spec four_of_a_kind(%{Card.t() => integer()}) :: four_of_a_kind() | nil + defp four_of_a_kind(frequencies) do + case n_equals(frequencies, 4) do + [] -> nil + [fok | _] -> {:four_of_a_kind, fok} + end + end + + @spec full_house(%{Card.t() => integer()}) :: full_house() | nil + defp full_house(frequencies) do + case {three_of_a_kind(frequencies), pair(frequencies)} do + {{:three_of_a_kind, tok}, {:pair, p}} -> {:full_house, {tok, p}} + _ -> nil + end + end + + @doc ~S""" + Finds the highest combination that can be made using the hand and the cards + that are present. + + ## Examples + + iex> import PokerEx.Card + iex> PokerEx.Combination.evaluate_hand [~p"CA", ~p"D2", ~p"H3", ~p"S4", ~p"C5"] + {:straight, {:ace, 5}} + + iex> import PokerEx.Card + iex> PokerEx.Combination.evaluate_hand [~p"C10", ~p"CJ", ~p"CQ", ~p"CK", ~p"CA"] + {:royal_flush, :clubs} + + iex> import PokerEx.Card + iex> PokerEx.Combination.evaluate_hand [~p"CA", ~p"CA", ~p"CA", ~p"CK", ~p"CK"] + {:full_house, {%PokerEx.Card{suit: :clubs, rank: :ace}, %PokerEx.Card{suit: :clubs, rank: :king}}} + + iex> import PokerEx.Card + iex> PokerEx.Combination.evaluate_hand [~p"CA", ~p"CA", ~p"CA", ~p"D2", ~p"D3"] + {:three_of_a_kind, %PokerEx.Card{suit: :clubs, rank: :ace}} + + iex> import PokerEx.Card + iex> PokerEx.Combination.evaluate_hand [~p"CA", ~p"CA", ~p"D2", ~p"D3", ~p"D4"] + {:pair, %PokerEx.Card{suit: :clubs, rank: :ace}} + + iex> import PokerEx.Card + iex> PokerEx.Combination.evaluate_hand [~p"CA", ~p"CK", ~p"DJ", ~p"D2", ~p"H7"] + {:high_card, %PokerEx.Card{suit: :clubs, rank: :ace}} + """ + @spec evaluate_hand([Card.t()]) :: score() + def evaluate_hand(hand) do + sorted_hand = hand |> Enum.sort({:asc, Card}) + freq = hand |> Enum.frequencies() + + [ + royal_flush(sorted_hand), + straight_flush(sorted_hand), + four_of_a_kind(freq), + full_house(freq), + flush(sorted_hand), + straight(sorted_hand), + three_of_a_kind(freq), + two_pair(freq), + pair(freq), + {:high_card, sorted_hand |> List.last()} + ] + |> Enum.find(&(&1 != nil)) + end +end diff --git a/poker_ex/lib/poker_ex/deck.ex b/poker_ex/lib/poker_ex/deck.ex index 63f8446..ab63298 100644 --- a/poker_ex/lib/poker_ex/deck.ex +++ b/poker_ex/lib/poker_ex/deck.ex @@ -48,6 +48,6 @@ defmodule PokerEx.Deck do iex> PokerEx.Deck.valid? [] true """ - @spec valid?(deck()) :: boolean + @spec valid?(deck()) :: boolean() def valid?(deck), do: deck |> Enum.all?(&Card.valid?/1) end diff --git a/poker_ex/test/poker_ex/combination_test.exs b/poker_ex/test/poker_ex/combination_test.exs new file mode 100644 index 0000000..af44500 --- /dev/null +++ b/poker_ex/test/poker_ex/combination_test.exs @@ -0,0 +1,4 @@ +defmodule PokerEx.CombinationTest do + use ExUnit.Case + doctest PokerEx.Combination +end