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