zoedsoupe

zoedsoupe

Trying to implement War card game in Elixir

I’m trying to implement the war game in Elixir using a Queue

This is the brief game description:

The game starts with a shuffled deck of cards. The deck will be passed into your program already shuffled (details below). The cards are dealt in an alternating fashion to each player, so that each player has 26 cards.
In each round, both players reveal the top card of their pile. The player with the higher card (by rank) wins both cards, placing them at the bottom of their pile. Aces are considered high, meaning the card ranks in ascending order are 2-10, Jack, Queen, King, Ace.
If the revealed cards are tied, there is war! Each player turns up one card face down followed by one card face up. The player with the higher face-up card takes both piles (six cards – the two original cards that were tied, plus the four cards from the war). If the turned-up cards are again the same rank, each player places another card face down and turns another card face up. The player with the higher card takes all 10 cards, and so on.
When one player runs out of cards, they are the loser, and the other the winner. If, during a war, a player runs out of cards, this counts as a loss as well.

This is my Queue implementation:

defmodule Queue do
  use GenServer

  @intial_state %{
    size: 0,
    queue: [],
  }

  # Client - Public and high level API

  def start_link do
    GenServer.start_link(__MODULE__, @intial_state)
  end

  def enqueue(pid, elems) do
    GenServer.cast(pid, {:enqueue, elems})
  end

  def dequeue(pid) do
    GenServer.call(pid, :dequeue)
  end

  def dequeue(pid, many) do
    GenServer.call(pid, {:dequeue, many})
  end

  def size(pid) do
    GenServer.call(pid, :size)
  end

  def front(pid) do
    GenServer.call(pid, :front)
  end

  def rear(pid) do
    GenServer.call(pid, :rear)
  end

  def flush(pid) do
    GenServer.call(pid, :flush)
  end

  # Server - Public but internal API
  # handle_cast - handle the demand asynchronously
  # handle_call - handle the demand eagerly

  @impl true
  def init(init) do
    {:ok, init}
  end

  @impl true
  def handle_call(:size, _from, state) do
    {:reply, state.size, state}
  end

  def handle_call(:front, _from, state) do
    %{queue: xs} = state

    case xs do
      [] -> {:reply, nil, state}
      [x | _] -> {:reply, x, state}
    end
  end

  def handle_call(:rear, _from, state) do
    %{queue: xs} = state

    case xs do
      [] -> {:reply, nil, state}
      xs -> {:reply, List.last(xs), state}
    end
  end

  def handle_call(:flush, _from, state) do
    {elems, state} = deq_many(state, state.size)

    {:reply, elems, state}
  end

  def handle_call(:dequeue, _from, state) do
    {elem, state} = deq(state)

    {:reply, elem, state}
  end

  def handle_call({:dequeue, many}, _from, state) do
    {elems, state} = deq_many(state, many)

    {:reply, Enum.reverse(elems), state}
  end

  defp deq_many(state, n) do
    Enum.reduce(1..n, {[], state}, fn
      _, {elems, state} ->
        {elem, state} = deq(state)

        {[elem | elems], state}
    end)
  end

  defp deq(state) do
    %{queue: xs, size: size} = state

    case xs do
      [] -> {nil, state}
      [x | xs] -> {x, %{state | queue: xs, size: size - 1}}
    end
  end

  @impl true
  def handle_cast({:enqueue, elems}, state) when is_list(elems) do
    %{queue: xs, size: x} = state

    xs = List.foldr(elems, xs, &[&1 | &2])

    {:noreply,
      %{state | size: x + length(elems), queue: xs}}
  end

  def handle_cast({:enqueue, elem}, state) do
    %{queue: xs, size: x} = state

    {:noreply, %{state | size: x + 1, queue: Enum.reverse([elem | xs])}}
  end
end

This is my current implementation so far of war game (it fails with timeout):

defmodule War do
  @doc """
  The main module to the challenge.
  This module exposes a deal/1 function to play the game.

  You can run all tests executing `elixir war.ex`.
  """
  require Integer

  # prefers to use a weight as we don't represent the cards
  @ace_weight 14

  def deal(deck) do
    {deck_1, deck_2} = deal_deck(deck)

    {:ok, player_1} = Queue.start_link()
    {:ok, player_2} = Queue.start_link()

    :ok = Queue.enqueue(player_1, deck_1)
    :ok = Queue.enqueue(player_2, deck_2)

    winner = play_game(player_1, player_2)

    Queue.size(winner)
    |> then(&Queue.dequeue(winner, &1))
    |> Enum.map(&remove_ace_weight/1)
  end

  defp deal_deck(deck) do
    List.foldr(deck, {[], []}, &deal_player/2)
  end

  defp play_game(p1, p2) do
    if winner = maybe_get_winner(p1, p2) do
      winner
    else
      case play_turn(p1, p2) do
        {winner, []} ->
          winner

        {turn_winner, cards} ->
          push_cards(turn_winner, cards)
          play_game(p1 ,p2)
      end
    end
  end

  defp play_turn(p1, p2, x \\ nil, y \\ nil, tied \\ []) do
    x = x || Queue.dequeue(p1)
    y = y || Queue.dequeue(p2)
    cards = [x, y]

    cond do
      x > y -> {p1, cards ++ tied}
      x < y -> {p2, cards ++ tied}
      x == y -> war(p1, p2, cards ++ tied)
    end
  end

  defp war(p1, p2, tied) do
    [x, y] = Enum.take(tied, 2)
    tied = Enum.drop(tied, 2)

    cond do
      !able_to_war?(p1) ->
        cards = tied ++ Queue.flush(p1)
        push_cards(p2, cards)
        {p2, []}


      !able_to_war?(p2) ->
        cards = tied ++ Queue.flush(p2)
        push_cards(p1, cards)
        {p1, []}

      true ->
        {turn_winner, cards} = play_turn(p1, p2, x, y, tied)
        push_cards(turn_winner, cards)
        play_game(p1, p2)
    end
  end

  defp deal_player(card, {p1, p2}) do
    if length(p1) == length(p2) do
      {[apply_ace_weight(card) | p1], p2}
    else
      {p1, [apply_ace_weight(card) | p2]}
    end
  end

  defp able_to_war?(player) do
    Queue.size(player) > 3
  end

  # The game ends when a player losses all their cards
  # so their Stack is empty
  defp maybe_get_winner(player_1, player_2) do
    cond do
      Queue.size(player_1) == 0 -> player_2
      Queue.size(player_2) == 0 -> player_1
      true -> nil
    end
  end

  defp apply_ace_weight(card) do
    (card == 1 && @ace_weight) || card
  end

  defp remove_ace_weight(card) do
    (card == @ace_weight && 1) || card
  end

  # Cards won from a war needs to be pushed in descending order
  defp push_cards(player, cards) do
    cards = Enum.sort(cards, :desc)

    Queue.enqueue(player, cards)
  end
end

And these are the tests cases:

defmodule WarTest do
  use ExUnit.Case

  describe "War" do
    test "deal_1" do
      t1 = [1,1,1,1,13,13,13,13,11,11,11,11,12,12,12,12,10,10,10,10,9,9,9,9,7,7,7,7,8,8,8,8,6,6,6,6,5,5,5,5,4,4,4,4,3,3,3,3,2,2,2,2]
      r1 = [1,1,1,1,13,13,13,13,12,12,12,12,11,11,11,11,10,10,10,10,9,9,9,9,8,8,8,8,7,7,7,7,6,6,6,6,5,5,5,5,4,4,4,4,3,3,3,3,2,2,2,2]
      assert War.deal(t1) == r1
    end

    test "deal_2" do
      t2 = [1,13,1,13,1,13,1,13,12,11,12,11,12,11,12,11,10,9,10,9,10,9,10,9,8,7,8,7,8,7,8,7,6,5,6,5,6,5,6,5,4,3,4,3,4,3,4,3,2,2,2,2]
      r2 = [4,3,2,2,2,2,4,3,4,3,4,3,6,5,6,5,6,5,6,5,8,7,8,7,8,7,8,7,10,9,10,9,10,9,10,9,12,11,12,11,12,11,12,11,1,13,1,13,1,13,1,13]
      assert War.deal(t2) == r2
    end

    test "deal_3" do
      t3 = [13,1,13,1,13,1,13,1,11,12,11,12,11,12,11,12,9,10,9,10,9,10,9,10,7,8,7,8,7,8,7,8,5,6,5,6,5,6,5,6,3,4,3,4,3,4,3,4,2,2,2,2]
      r3 = [4,3,2,2,2,2,4,3,4,3,4,3,6,5,6,5,6,5,6,5,8,7,8,7,8,7,8,7,10,9,10,9,10,9,10,9,12,11,12,11,12,11,12,11,1,13,1,13,1,13,1,13]
      assert War.deal(t3) == r3
    end

    test "deal_4" do
      t4 = [10,11,12,13,1,2,3,4,5,6,7,8,9,10,11,12,13,1,2,3,4,5,6,7,8,9,10,11,12,13,1,2,3,4,5,6,7,8,9,10,11,12,13,1,2,3,4,5,6,7,8,9]
      r4 = [1,1,13,12,9,5,11,4,9,3,8,7,7,2,13,10,12,5,10,4,9,6,8,3,1,1,13,12,7,5,11,4,9,3,8,6,7,2,13,10,12,5,11,11,10,8,6,4,6,3,2,2]
      assert War.deal(t4) == r4
    end

    test "deal_5" do
      t5 = [1,2,3,4,5,6,7,8,9,10,11,12,13,1,2,3,4,5,6,7,8,9,10,11,12,13,1,2,3,4,5,6,7,8,9,10,11,12,13,1,2,3,4,5,6,7,8,9,10,11,12,13]
      r5 = [1,10,13,8,11,9,8,7,11,8,13,7,13,6,12,6,9,5,8,5,7,4,7,4,11,6,12,10,6,3,2,2,12,5,9,3,10,4,9,2,10,3,5,2,1,1,1,13,12,11,4,3]
      assert War.deal(t5) == r5
    end

    defp create_deck do
      deck =
        for n <- 1..13, _ <- 1..4 do
          n
        end

      Enum.shuffle(deck)
    end

    test "should return the same number of cards after deal" do
      deck = create_deck()

      assert length(deck) == 52
      assert length(War.deal(deck)) == 52
    end

    test "should remove the ace weight after a win" do
      deck = create_deck()

      assert Enum.all?(War.deal(deck), &(&1 != 14))
    end
  end
end

I also made a public repo of this challenge: GitHub - zoedsoupe/war.ex: War card game implemented in Elixir

I’m trying to understand why my code fails and how I could improve it to achieve the desired result.

Most Liked

sodapopcan

sodapopcan

Based on actually playing War as a kid on boring school trips this tracks. We’d often get frustrated and resort to “mega wars” where we would put 3 cards face down in the event of a tie just to try and make the game end :sweat_smile:

soup

soup

I can see two issues,

One is an easy to make copy-paste mistake in the first two lines of play_turn.

The other is a mistake regarding immutability. (These might read as trick questions, they don’t intend to be mean.)

Consider

x = [1,2,3]
x ++ [4, 5]

What is x?

Consider

p1 = Queue.new()
Queue.enqueue(p1, 1)

What does p1 contain?

Hint

Remember before all our state was maintained in a genserver, but now we must maintain it ourselves.

Hint

Look at push_cards and when you call it, maybe we are losing some state here?

Fixing this might need some broader redesign, but you can make a hack-job fix with case and the ^ pin operator…

zoedsoupe

zoedsoupe

yeah, no decisions

zoedsoupe

zoedsoupe

So what I made was to add a key :name to the Queue struct, to identify which queue I’m dealing of.

push_cards/2 returns the final new queue, and then I pattern match its name to know which player was the turn winner!

New queue.ex (only diifs):

defmodule Queue do
  @fields ~w(list size name)a
  @enforce_keys ~w(name)a
  defstruct @fields

  def new(name, list \\ [])

  def new(name, []) do
    struct!(__MODULE__, name: name, list: [], size: 0)
    f
  end

  def new(name, list) do
    struct!(__MODULE__, name: name, list: list, size: length(list))
  end

and the play_game/2:

defp play_game(p1, p2) do
    if winner = maybe_get_winner(p1, p2) do
      winner
    else
      {turn_winner, cards} = play_turn(p1, p2)

      case push_cards(turn_winner, cards) do
        %{name: :player_1} = turn_winner -> play_game(turn_winner, p2)
        %{name: :player_2} = turn_winner -> play_game(p1, turn_winner)
      end
    end
  end

Although I now have a infinite loop haha. At least the Queue seems to be correctly implemented now

Sebb

Sebb

OK, great naming… not.

|> split(length(deck) * 2)

The game starts with a shuffled deck of cards. The deck will be passed into your program already shuffled (details below). The cards are dealt in an alternating fashion to each player, so that each player has 26 cards.

I did just split the deck in two, because the alternated dealing does not add randomness. But if you get the deck from the test you have to do it, otherwise this cant succeed.

Where Next?

Popular in Questions Top

sergio
In Ruby, I can go: User.find_by(email: "foobar@email.com").update(email: "hello@email.com") How can I do something similar in Elixir? ...
New
JorisKok
I have a server on AWS, and was running a load test using artillery. When looking at the Phoenix dashboard I see the Ports going to 100% ...
New
Werner
Hi, I’m using Ubuntu 18.04 and after updating to OTP-24.0 yesterday i have this warning when I run “mix local.hex”: 14:57:30.512 [warn] ...
New
jononomo
I am trying to figure out how Mix knows whether the environment is test, dev, or prod -- where is this set? Thanks.
New
lk-geimfari
What is most correct way to open, read and parse JSON file with poison? For example if we have example.json file in root of some projec...
New
mathew4509
I have a list say x = ["23gh", "56kh", "97mh"] I would like to pass each element to Val in each iteration. Say, in iteration 1 -------...
New
stefanluptak
Hello everybody, usually, I use a 29" ultra-wide monitor for VSCode which can easily accomodate explorer (files panel) + file with code ...
New
johnnyicon
Hi all, I've just started learning Elixir and Phoenix Framework, so please pardon my n00bness at this stage. I'm trying to use Postg...
New
baxterw3b
Hi guys, i’m new in the Elixir world, and i have to say, that i love it! i’m having some problem to understand anonymous functions with ...
New
wernerlaude
In AR this is so simple @articles = current_user.articles How to do in Ecto? def index(conn, _params) do current_user = conn.assig...
New

Other popular topics Top

Brian
What is the proper way to load a module from a file in to IEX? In the python world, doing something like this pretty standard: from ....
New
pmjoe
I have a relationship of love and hate with Elixir. Lots of things are just absolutely right, but there are some things that are kind of ...
New
gshaw
What is the idiomatic way of matching for not nil in Elixir? E.g., First way: defp halt_if_not_signed_in(conn, signed_in_account) when...
New
jerry
Good day to you all. I have been struggling to get a query involving like and ilike to work. Can anyone assist me on this, please? pro...
New
nsuchy
Hi. I’ve noticed that Windows Powershell has it’s own IEX command and you cannot access Elixir’s IEX due to the conflict. This isn’t a cr...
New
hariharasudhan94
I would like to know what is the best IDE for elixir development?
New
minhajuddin
I have seen a lot of code which picks the first element from a list using Enum.at(0) instead of List.first. Is there a reason why people ...
New
chensan
I have a User schema with a :from_id field set to type :string: defmodule TweetBot.Repo.Migrations.CreateUsers do use Ecto.Migration ...
New
shahryarjb
Hello, I have map which I want to convert it to string like this: the map: %{last_name: "tavakkoli", name: "shahryar"} the string I ne...
New
fireproofsocks
Forgive me if this is obvious, but how does one delete a database record WITHOUT selecting it first? https://hexdocs.pm/ecto/Ecto.Repo.h...
New

We're in Beta

About us Mission Statement