# Deciding if a hand forms a gin or not?

I am trying to write a little function to determine whether a given hand of cards forms a gin.

Here are the rules:

• A hand contains 10 cards from a standard 52-card deck.
• A run is formed if it contains at least 3 cards or more of the same suit and their value increases without gaps.
• A set is formed if it contains at least 3 or more cards of the same value.
• A hand is a gin if every card in the hand either belongs to a set or a run.

I am curious to see alternative ways of coding this in idiomatic Elixir.

What have you tried?

1 Like

â€śAlternativesâ€ť means thereâ€™s a first reference implementation against which they will be compared.

I havenâ€™t provided any code not to influence anyone.

My solution is a simple backtracking algorithm that more or less goes like the following:

I take the first card in the hand and then try to find a set of 4, if found, then remove the members of the set and recurse into the remaining. If not found, then try to find a set of 3, if found, remove them and recurse into the remaining. If not, then try to find a run, if found, recurse into the remaining, if not then return false.

However, I quickly realised that matching a set of 4 whenever I can doesnâ€™t work for some cases where one member of the set can help to complete a straight, so I ended up trying out the permutations of the set. I have later on made a similar observation about matching the longest runs doesnâ€™t help when the last elements of the run can actually complete a set, etc.

I ended up special casing so many things in the end, which prompted me to see what people could come up with.

I hope this gives some direction.

You will not. Letâ€™s see your try at it and discuss it.

My code is like the following:

``````  @spec is_gin(hand :: list(Card.t())) :: boolean()
def is_gin(hand) do
sorted = hand |> Enum.sort(&(&2.value > &1.value))
is_gin?(sorted, [])
end

defp is_gin?([], _), do: true

defp is_gin?([h | rest], used) do
others = Enum.filter(rest, &(&1.value == h.value))

cond do
length(others) == 2 ->
is_gin?(rest -- others, [[h | others] | used])

length(others) == 3 ->
[s1, s2, s3] = others

# try permutations
is_gin?(rest -- [s1, s2], [[h, s1, s2] | used]) or
is_gin?(rest -- [s2, s3], [[h, s2, s3] | used]) or
is_gin?(rest -- [s1, s3], [[h, s1, s3] | used]) or
is_gin?(rest -- [s1, s2, s3] ++ [h], [[s1, s2, s3] | used])

[] ->
case find_run(h, rest) do
nil ->
false

run ->
for size <- Enum.to_list(length(run)..3) do
slice = run |> Enum.reverse() |> Enum.take(size)
is_gin?(rest -- slice, [slice | used])
end
|> Enum.any?(& &1)
end
end
end

defp find_run(_, rest) when length(rest) < 2, do: nil

defp find_run(card, rest) do
candidates =
Enum.filter(rest, &Card.same_suit?(card, &1))
|> Enum.sort(&(&2.value > &1.value))

run =
for c <- candidates, reduce: [card] do
acc ->
prev = hd(acc)
if c.value - 1 == prev.value, do: [c | acc], else: acc
end

if length(run) >= 3, do: run, else: nil
end
``````

I used the following test case to verify that it is working as expected.

``````    test "various gins" do
hands = [
{[h(1), s(1), d(1), s(5), h(5), c(5), s(2), s(3), s(4), c(1)], true},
{[h(6), s(6), d(6), c(1), c(2), c(3), c(4), c(6)], true},
{[h(1), h(2), h(3), h(4), h(6)], false},
{[h(1), h(2), h(3), h(4), h(5), h(6), h(7), h(8), h(9), h(10)], true},
{[c(1), h(2), h(3), h(4), h(5), h(6), h(7), h(8), h(9), h(10)], false}
]

for {hand, expected} <- hands do
assert GameState.is_gin(hand) == expected
end
end
``````

The tricky part is dealing with cards that could be in either a set or a run - for instance if you have 5/6/7/8 of diamonds, 5 of clubs and 5 of hearts.

Iâ€™d implement this check by first rewriting it into a recursive definition:

``````* A hand is a gin if:
* there is a set of 4 cards, and one of the following:
* the remaining cards are a gin
* the remaining cards plus one of the 4 from the set are a gin
* there is a set of 3 cards, and one of the following:
* the remaining cards are a gin
* the whole hand is a gin ignoring this set of 3 cards
* there is a run of at least 3 cards, and:
* the remaining cards are a gin
* the hand is empty
``````

Some of these clauses are trickier than others:

• the case where a candidate set of four cards isnâ€™t used as a set is ignored, since having four runs would require 12 cards!
• the â€śnot a setâ€ť branch for 3 cards needs to track which values have been tried as a set, or else the definition will loop forever

Thereâ€™s a corresponding clause in `GinCalc.gin?` for all the bullet points above:

``````defmodule GinCalc do
def gin?(hand, ignored \\ [])
def gin?([], _), do: true
def gin?(hand, ignored) do
case split_set(hand, ignored) do
{run, rest} when length(run) == 4 ->
if gin?(rest, ignored) do
true
else
Enum.any?(run, &gin?([&1 | rest], ignored))
end
{[{_, v} | _] = run, rest} when length(run) == 3 ->
if gin?(rest, ignored) do
true
else
gin?(hand, [v | ignored])
end
{nil, rest} ->
case split_run(rest) do
{nil, _} -> false
{_, without_run} -> gin?(without_run, ignored)
end
end
end

def split_set(hand, ignored) do
grouped = Enum.group_by(hand, &elem(&1, 1))

set_key =
grouped
|> Map.keys()
|> Enum.reject(& &1 in ignored)
|> Enum.find(fn k -> length(grouped[k]) > 2 end)

if set_key do
set = grouped[set_key]

{set, hand -- set}
else
{nil, hand}
end
end

def split_run(hand) do
sorted = Enum.sort(hand)

candidate_run =
Enum.reduce_while(sorted, [], fn
{suit, value}, [{suit, prev_value} | _] = acc when value == prev_value + 1 ->
{:cont, [{suit, value} | acc]}
_, acc when length(acc) > 2 ->
{:halt, acc}
{suit, value}, _ ->
{:cont, [{suit, value}]}
end)

if length(candidate_run) > 2 do
{candidate_run, hand -- candidate_run}
else
{nil, hand}
end
end
end

GinCalc.split_set([{:h, 5}, {:h, 6}, {:h, 7}, {:s, 5}, {:d, 5}], []) |> IO.inspect()
GinCalc.gin?([{:h, 5}, {:s, 5}, {:d, 5}, {:c, 5}, {:h, 6}, {:h, 7}]) |> IO.inspect()
``````

I used tuples to represent cards because they `Enum.sort` in a reasonable way (grouped into suits, then by number) and are easy to pattern-match, but using structs would only require light modifications.

2 Likes

You might find this approach to ranking poker hands helpful. Playing Poker with Elixir (pt. 1)

1 Like

Nice! It looks better than mine.

I like the usage of `reduce_while` for halting the loop early. I might steal that

Thanks! Thatâ€™s exactly what I was looking for! Pity the blog doesnâ€™t exist anymore.