idursun

idursun

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.

Showing Posts 1 to 9

joey_the_snake

joey_the_snake

What have you tried?

dimitarvp

dimitarvp

“Alternatives” means there’s a first reference implementation against which they will be compared.

Where’s your code?

idursun

idursun OP

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.

dimitarvp

dimitarvp

You will not. Let’s see your try at it and discuss it.

idursun

idursun OP

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
al2o3cr

al2o3cr

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.

dpreston

dpreston

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

idursun

idursun OP

Nice! It looks better than mine.

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

idursun

idursun OP

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

— All posts loaded —

Where Next? Top

Trending in Questions Top

RSP87
I’m working on a project that simulates the bumbl example in the programming phoenix book. It acts almost like an email client. We have a...
New
nseaSeb
Hello, I know there is an approach for handling lists that allows for optimized traversal, but I can’t recall the specific method (somet...
New
kpanic
Hi everyone, I am toying with the idea of building a “match maker” for giving personal help to people that wants to start coding. I sta...
New
brecabral
Documentation While reading the Scoped Routes section, I noticed that the documentation currently refers to a problem without explainin...
New
velrest
So my question is quite simple and i have found no conclusive answer on forum, google or AI. Should we use :erlang.float for Integer to ...
New
asweet-confluent
I recently noticed that Elixir’s Logger defaults its primary log level to :debug when no :logger, :level application configuration is pre...
New
apz
I’m new to elixir and just tried to install the elixirLS extension for VScode(ium) and it is throwing some errors that I would like help ...
New

Other Trending Topics Top

GenericJam
Edit: 2026 May 15 - This post is archived. Mob is alive!! Main docs: mob v0.7.11 — Documentation A bit of explanation for the slightly c...
New
JesseHerrick
Hey, I’m Jesse and I’m the main contributor behind Dexter, a full-featured, lightning-fast Elixir LSP optimized for large codebases. It s...
New
mudasobwa
I am happy to introduce the very α version of the new programming language compiled to BEAM. Welcome Cure. It has literally three kille...
New
marciok
Hi there! We created Gust: A task orchestrator inspired by Airflow. For those who have never heard about Aiflow, it’s a Python-based wor...
New
mhanberg
Hi everyone! The first release candidate for the Expert language server project is now available! We’ve published a press release detai...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews