diff --git a/poker_ex/lib/poker_ex/card.ex b/poker_ex/lib/poker_ex/card.ex index 038f809..1481071 100644 --- a/poker_ex/lib/poker_ex/card.ex +++ b/poker_ex/lib/poker_ex/card.ex @@ -1,4 +1,8 @@ defmodule PokerEx.Card do + @doc ~S""" + Holds functions that operate and symbolise a playing card. + """ + alias __MODULE__ @valid_suits [:spades, :hearts, :diamonds, :clubs] @@ -15,21 +19,85 @@ defmodule PokerEx.Card do when (is_integer(rank) and 2 <= rank and rank <= 10) or rank in [:jack, :queen, :king, :ace] + @doc ~S""" + Checks if a given card is valid. To be valid, it should be the card + string with a valid suit and rank, which are specified as module types. + + Valid suits: :hearts, :diamonds, :clubs, :spades + Valid ranks: Numbers 2-10, :jack, :queen, :king, :ace + + ## Examples + + iex> PokerEx.Card.valid? %PokerEx.Card{suit: :spades, rank: :ace} + true + + iex> PokerEx.Card.valid? %PokerEx.Card{suit: :non_existent, rank: 2} + false + + iex> PokerEx.Card.valid? %PokerEx.Card{suit: :diamonds, rank: 11} + false + """ @spec valid?(Card.t()) :: boolean def valid?(%Card{suit: suit, rank: rank}) when is_valid_rank(rank) and is_valid_suit(suit), do: true def valid?(_card), do: false + @doc ~S""" + Returns all valid suits as array. + """ @spec all_suits() :: [suit()] def all_suits(), do: @valid_suits + @doc ~S""" + Returns all cards with a given suit. This means all the numbers + 2 through 10, the jack, queen, king, and ace. + + ## Examples + + iex> PokerEx.Card.all_cards_for_suit :diamonds + [ + %PokerEx.Card{suit: :diamonds, rank: 2}, + %PokerEx.Card{suit: :diamonds, rank: 3}, + %PokerEx.Card{suit: :diamonds, rank: 4}, + %PokerEx.Card{suit: :diamonds, rank: 5}, + %PokerEx.Card{suit: :diamonds, rank: 6}, + %PokerEx.Card{suit: :diamonds, rank: 7}, + %PokerEx.Card{suit: :diamonds, rank: 8}, + %PokerEx.Card{suit: :diamonds, rank: 9}, + %PokerEx.Card{suit: :diamonds, rank: 10}, + %PokerEx.Card{suit: :diamonds, rank: :jack}, + %PokerEx.Card{suit: :diamonds, rank: :queen}, + %PokerEx.Card{suit: :diamonds, rank: :king}, + %PokerEx.Card{suit: :diamonds, rank: :ace} + ] + """ @spec all_cards_for_suit(suit()) :: [Card.t()] def all_cards_for_suit(suit) when is_valid_suit(suit) do [2, 3, 4, 5, 6, 7, 8, 9, 10, :jack, :queen, :king, :ace] |> Enum.map(fn rank -> %Card{suit: suit, rank: rank} end) end + @doc ~S""" + Makes a card struct from the given integers. Mostly used when parsing + the unicode glyph into a card. + + The jack, queen, king, and ace are 11, 13, 14, 1. + + ## Examples + + iex> PokerEx.Card.from_integers(0, 5) + {:ok, %PokerEx.Card{suit: :spades, rank: 5}} + + iex> PokerEx.Card.from_integers(5, 5) + {:error, "Invalid integer suit: 5"} + + iex> PokerEx.Card.from_integers(0, 1) + {:ok, %PokerEx.Card{suit: :spades, rank: :ace}} + + iex> PokerEx.Card.from_integers(0, 15) + {:error, "Invalid integer rank: 15"} + """ @spec from_integers(integer, integer) :: {:ok, Card.t()} | {:error, String.t()} def from_integers(suit, _rank) when suit < 0 @@ -38,7 +106,7 @@ defmodule PokerEx.Card do end def from_integers(_suit, rank) - when rank < 2 + when rank < 1 when rank > 0xE do {:error, "Invalid integer rank: #{rank}"} end @@ -46,18 +114,36 @@ defmodule PokerEx.Card do def from_integers(suit, rank) do suit = Enum.at(@valid_suits, suit) - case rank do + {: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} end + @doc ~S""" + Converts a unicode playing card glyph into a playing card struct. + + ## Examples + + iex> PokerEx.Card.from_string "🂸" + {:ok, %PokerEx.Card{suit: :hearts, rank: 8}} + + iex> PokerEx.Card.from_string "🃍" + {:ok, %PokerEx.Card{suit: :diamonds, rank: :queen}} + + iex> PokerEx.Card.from_string "Hello world" + {:error, "Not a valid card glyph!"} + + iex> PokerEx.Card.from_string "💩" + {:error, "Not a valid card glyph!"} + """ @spec from_string(String.t()) :: {:ok, Card.t()} | {:error, String.t()} def from_string(card_rep) do case card_rep do - <> -> + <> when codepoint > 0x1F0A0 and codepoint < 0x1F0DF -> offset = codepoint - ?🂠 suit = div(offset, 0x10) rank = rem(offset, 0x10) @@ -69,6 +155,43 @@ defmodule PokerEx.Card do end end + @doc ~S""" + Provides a helper function to easily generate a card. + Takes the short-hand notation for a suit, and then a rank. + + ## Examples + + iex> import PokerEx.Card + iex> ~p"CA" + %PokerEx.Card{suit: :clubs, rank: :ace} + iex> ~p"D10" + %PokerEx.Card{suit: :diamonds, rank: 10} + """ + @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 + + 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 + raise "Invalid card!" + end + + card + end + defimpl Inspect, for: Card do @suit_letters %{spades: "S", hearts: "H", diamonds: "D", clubs: "C"} @rank_letters %{ace: "A", jack: "J", queen: "Q", king: "K"} diff --git a/poker_ex/lib/poker_ex/deck.ex b/poker_ex/lib/poker_ex/deck.ex index 5577570..63f8446 100644 --- a/poker_ex/lib/poker_ex/deck.ex +++ b/poker_ex/lib/poker_ex/deck.ex @@ -3,12 +3,51 @@ defmodule PokerEx.Deck do @type deck() :: [Card.t()] + @doc ~S""" + Creates a new empty deck. This deck is sorted and contains all cards + 2-10, jack, queen, king, and ace for ranks spades, hearts, diamonds, and clubs. + + ## Examples + + iex> deck = PokerEx.Deck.new() + iex> PokerEx.Deck.valid? deck + true + iex> length deck + 52 + iex> Enum.take(deck, 3) + [ + %PokerEx.Card{suit: :spades, rank: 2}, + %PokerEx.Card{suit: :spades, rank: 3}, + %PokerEx.Card{suit: :spades, rank: 4} + ] + """ @spec new() :: deck() def new(), do: Card.all_suits() |> Enum.flat_map(&Card.all_cards_for_suit/1) @spec new_shuffled() :: deck() def new_shuffled(), do: new() |> Enum.shuffle() + @doc ~S""" + Returns true if a deck is valid. A deck is valid if, and only if, + it contains valid cards. There is no limitation on the size. + + ## Examples + + iex> PokerEx.Deck.valid? PokerEx.Deck.new() + true + + iex> PokerEx.Deck.valid? PokerEx.Deck.new_shuffled() + true + + iex> PokerEx.Deck.valid? ["Haha!" | PokerEx.Deck.new()] + false + + iex> PokerEx.Deck.valid? [%PokerEx.Card{suit: :diamonds, rank: 5}] + true + + iex> PokerEx.Deck.valid? [] + true + """ @spec valid?(deck()) :: boolean def valid?(deck), do: deck |> Enum.all?(&Card.valid?/1) end diff --git a/poker_ex/test/poker_ex/card_test.exs b/poker_ex/test/poker_ex/card_test.exs new file mode 100644 index 0000000..a53d6c6 --- /dev/null +++ b/poker_ex/test/poker_ex/card_test.exs @@ -0,0 +1,4 @@ +defmodule PokerEx.CardTest do + use ExUnit.Case + doctest PokerEx.Card +end diff --git a/poker_ex/test/poker_ex/deck_test.exs b/poker_ex/test/poker_ex/deck_test.exs new file mode 100644 index 0000000..6676ad7 --- /dev/null +++ b/poker_ex/test/poker_ex/deck_test.exs @@ -0,0 +1,4 @@ +defmodule PokerEx.DeckTest do + use ExUnit.Case + doctest PokerEx.Deck +end