Added combination checks.

This commit is contained in:
Hans Goor 2024-08-15 20:35:16 +02:00
parent 4210670e9e
commit 1e2b24d616
Signed by: eyedevelop
SSH key fingerprint: SHA256:Td89veptDEwCV8J3fjqnknNk7SbwzedYhauyC2nFBYg
4 changed files with 246 additions and 26 deletions

View file

@ -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(<<suit::utf8, rank::binary>>, []) 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

View file

@ -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

View file

@ -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

View file

@ -0,0 +1,4 @@
defmodule PokerEx.CombinationTest do
use ExUnit.Case
doctest PokerEx.Combination
end