Is there a better way to code cards game deal?

I managed to write a simple script to distribute cards to players.

num_of_cards_per_player = 2
num_of_players = 2
cards = ["Ace", 1, 2, 3, 4, 5, 6, 7]
initial_state = %{players: %{}, cards: cards}

1..num_of_cards_per_player
|> Enum.reduce(initial_state, fn _, proxy ->
  1..num_of_players
  |> Enum.reduce(proxy, fn i, acc ->
    [h | tail] = acc.cards

    acc
    |> Map.update(:players, acc.players, fn existing ->
      existing |> Map.update("player_#{i}", [h], fn items -> [h | items] end)
    end)
    |> Map.put(:cards, tail)
  end)
end)

# output
# %{cards: [4, 5, 6, 7], players: %{"player_1" => [2, "Ace"], "player_2" => [3, 1]}}

I wonder if is a better way to do it.

Maybe with for

for _i <- 1..num_of_cards_per_player, j <- 1..num_of_players, reduce: initial_state do
  acc ->
    [h | tail] = acc.cards

    acc
    |> Map.update(:players, acc.players, fn existing ->
      existing |> Map.update("player_#{j}", [h], fn items -> [h | items] end)
    end)
    |> Map.put(:cards, tail)
end
1 Like

update_in can help with nested puts/updates:

acc
|> update_in([:players, "player_#{j}"], fn
  nil -> [h]
  items -> [h | items]
end)

destructuring sometimes helps with code management:

for _ <- 1..num_of_cards_per_player, j <- 1..num_of_players, reduce: initial_state do
  %{cards: [card | remaining_cards], players: players} ->
    updated_players = Map.update(players, "player_#{j}", [card], fn deck -> [card | deck] end)

    %{cards: remaining_cards, players: updated_players}
end

Note that none of the solutions so far handle the draw deck being empty (they all expect to match a non-empty [head | tail] shape)

1 Like
num_of_cards_per_player = 2
num_of_players = 2
cards = ["Ace", 1, 2, 3, 4, 5, 6, 7]

{to_deal, rest} = Enum.split(cards, num_of_cards_per_player * num_of_players)

new_players =
  to_deal
  |> Stream.chunk_every(num_of_players)
  |> Stream.take(num_of_cards_per_player)
  |> Enum.zip_reduce([], fn cards, acc -> [Enum.reverse(cards) | acc] end)
  |> Enum.reverse()
  |> Enum.with_index(1)
  |> Map.new(fn {cards, id} -> {"player_#{id}", cards} end)

%{cards: rest, players: new_players}

I dunno about “better”, but it was interesting to see a place where zip_reduce solved the problem.

This uses chunk_every to collect groups containing num_of_players cards, then zip_reduce to collect “all the first card”, “all the second card”, “all the third card”, etc

3 Likes

Enum.zip_with would be even cleaner.

1 Like

Now, I can see the power of for comprehension and update_in.
This is cool! thanks @jerdew @hlx

It is a bit challenging at first, but after I replaced Stream with Enum and append |> dbg()

to_deal #=> ["Ace", 1, 2, 3]
|> Enum.chunk_every(num_of_players) #=> [["Ace", 1], [2, 3]]
|> Enum.take(num_of_cards_per_player) #=> [["Ace", 1], [2, 3]]
|> Enum.zip_reduce([], fn cards, acc -> [Enum.reverse(cards) | acc] end) #=> [[3, 1], [2, "Ace"]]
|> Enum.reverse() #=> [[2, "Ace"], [3, 1]]
|> Enum.with_index(1) #=> [{[2, "Ace"], 1}, {[3, 1], 2}]
|> Map.new(fn {cards, id} -> {"player_#{id}", cards} end) #=> %{"player_1" => [2, "Ace"], "player_2" => [3, 1]}

Cool!

That part could be

|> Enum.zip_with(fn cards -> cards end)
1 Like