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 · GitHub

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

First Post!

soup

soup

Hope this is helpful and that I haven’t misunderstood, otherwise apologies.

Consider these tests

  test "enqueue returns correct order" do
    {:ok, pid} = Queue.start_link()

    assert :ok = Queue.enqueue(pid, [1])
    assert Queue.size(pid) == 1
    assert :ok = Queue.enqueue(pid, [2])
    assert Queue.size(pid) == 2
    assert :ok = Queue.enqueue(pid, [3])
    assert Queue.size(pid) == 3
    assert Queue.flush(pid) == [1, 2, 3]
    assert Queue.size(pid) == 0

    assert :ok = Queue.enqueue(pid, [1])
    assert Queue.size(pid) == 1
    assert :ok = Queue.enqueue(pid, [2, 3])
    assert Queue.size(pid) == 3
    assert Queue.flush(pid) == [1, 2, 3]
    assert Queue.size(pid) == 0
  end
    test "minor hand" do
      deck = [10,10,1,1]
      expect = [1,1,10,10]
      # :: deal deck
      # p1      p2
      # [10, 1] [10, 1]
      #
      # :: play turn
      # p1      p2
      # [1]     [1]
      # 10 vs 10 <- even, war
      #
      # :: war
      # p1 size < 3, unable to war,
      # cards = flush + tied
      #       = [1] + [10, 10]
      #       = [1, 10, 10]
      #
      # p1     p2   float
      # []     [1]  [1, 10, 10]
      #
      # push_cards(p2, [1, 10, 10])
      #
      # :: push cards
      #
      # sorted = [1, 10, 10] (where 1 is actually 14)
      # enqueue p2 sorted
      #
      # -> [1, 1, 10, 10]
      # 1 <- held in hand cause p2 never flushed
      # 1 <- flush from p2
      # 10 <- collected tied
      # 10 <- collected tied
      assert War.deal(deck) == expect
    end
Check queue#flush and its test, spoiler

A queue should be FIFO, so if we enqueue 1 then 2, then 3, when we flush it (assuming you mean this to be “dequeue everything”), we should get 1 (first in, first out), then 2, then 3.

  test "flush/1" do
    {:ok, pid} = Queue.start_link()

    assert :ok = Queue.enqueue(pid, [1, 2, 3])
    assert Queue.size(pid) == 3
    assert Queue.flush(pid) == [3, 2, 1] # 🤔
    assert Queue.size(pid) == 0
  end
Check {:enqueue, elem}, does the queue look correct?, spoiler
def handle_cast({:enqueue, elem}, state) do
    %{queue: xs, size: x} = state
   {:noreply, %{state | size: x + 1, queue: Enum.reverse([elem | xs])}} # 🤔
 end

consider

elem, xs = 3, [1, 2]

 [3 | [1, 2]]
 = [3, 1, 2]
   |> reverse()
 = [2, 1, 3]

elem, xs = 4, [2, 1, 3]

 [4 | [2, 1, 3]]
 = [4, 2, 1, 3]
   |> reverse()
 = [3, 1, 2, 4]

Something doesn’t seem quite right ey?

patch, spoiler

Note this patch does not make your tests pass, but see the comment, I could not be bothered to manually play out the hand and check the expected result.

diff --git a/lib/queue.ex b/lib/queue.ex
index 07baee9..843e7e6 100644
--- a/lib/queue.ex
+++ b/lib/queue.ex
@@ -84,19 +84,22 @@ defmodule Queue do
     {:reply, elem, state}
   end
 
-  def handle_call({:dequeue, many}, _from, state) do
-    {elems, state} = deq_many(state, many)
+  def handle_call({:dequeue, count}, _from, state) do
+    {elems, state} = deq_many(state, count)
 
-    {:reply, Enum.reverse(elems), state}
+    {:reply, elems, state}
   end
 
   defp deq_many(state, n) do
-    Enum.reduce(1..n, {[], state}, fn
-      _, {elems, state} ->
-        {elem, state} = deq(state)
+    {elems, state} =
+      Enum.reduce(1..n, {[], state}, fn
+        _, {elems, state} ->
+          {elem, state} = deq(state)
+          {[elem | elems], state}
+      end)
 
-        {[elem | elems], state}
-    end)
+    # Opinion: the reverse is an impl detail of deq_many, should not be exposed
+    {Enum.reverse(elems), state}
   end
 
   defp deq(state) do
@@ -111,15 +114,16 @@ defmodule Queue do
   @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])
-
+    # FIFO, so given xs = [1, 2], elems = [3, 4]
+    # this should be equivalent to enqueue(3), enqueue(4)
+    # which should give us [1,2,3,4], so a simple concat is fine.
+    xs = xs ++ elems
     {: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])}}
+    # You could also use List.insert_at(xs, elem, -1)
+    {:noreply, %{state | size: x + 1, queue: xs ++ [elem]}}
   end
 end
diff --git a/test/queue_test.exs b/test/queue_test.exs
index 586185f..f8d2354 100644
--- a/test/queue_test.exs
+++ b/test/queue_test.exs
@@ -81,4 +81,24 @@ defmodule QueueTest do
     assert Queue.flush(pid) == [3, 2, 1]
     assert Queue.size(pid) == 0
   end
+
+  test "enqueue returns correct order" do
+    {:ok, pid} = Queue.start_link()
+
+    assert :ok = Queue.enqueue(pid, [1])
+    assert Queue.size(pid) == 1
+    assert :ok = Queue.enqueue(pid, [2])
+    assert Queue.size(pid) == 2
+    assert :ok = Queue.enqueue(pid, [3])
+    assert Queue.size(pid) == 3
+    assert Queue.flush(pid) == [1, 2, 3]
+    assert Queue.size(pid) == 0
+
+    assert :ok = Queue.enqueue(pid, [1])
+    assert Queue.size(pid) == 1
+    assert :ok = Queue.enqueue(pid, [2, 3])
+    assert Queue.size(pid) == 3
+    assert Queue.flush(pid) == [1, 2, 3]
+    assert Queue.size(pid) == 0
+  end
 end
diff --git a/test/war_test.exs b/test/war_test.exs
index 04d34b8..602857b 100644
--- a/test/war_test.exs
+++ b/test/war_test.exs
@@ -2,9 +2,51 @@ defmodule WarTest do
   use ExUnit.Case
 
   describe "War" do
+    test "minor hand" do
+      deck = [10,10,1,1]
+      expect = [1,1,10,10]
+
+      # :: deal deck
+      # p1      p2
+      # [10, 1] [10, 1]
+      #
+      # :: play turn
+      # p1      p2
+      # [1]     [1]
+      # 10 vs 10 <- even, war
+      #
+      # :: war
+      # p1 size < 3, unable to war,
+      # cards = flush + tied
+      #       = [1] + [10, 10]
+      #       = [1, 10, 10]
+      #
+      # p1     p2   float
+      # []     [1]  [1, 10, 10]
+      #
+      # push_cards(p2, [1, 10, 10])
+      #
+      # :: push cards
+      #
+      # sorted = [1, 10, 10] (where 1 is actually 14)
+      # enqueue p2 sorted
+      #
+      # -> [1, 1, 10, 10]
+      # 1 <- held in hand cause p2 never flushed
+      # 1 <- flush from p2
+      # 10 <- collected tied
+      # 10 <- collected tied
+
+      assert War.deal(deck) == expect
+    end
+
     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]
+      # this test fails, but its possible that the test is incorrect, it
+      # appears that the first 3 cards in the deck are the last 3 cards in the
+      # winners hand, with the rest of the game placed beneath them, which
+      # seems a plausible state given the code and "able to war" checks.
       assert War.deal(t1) == r1
     end

As an aside, using a GenServer for this looks a bit like trying to treat them as OOP Objects, where the data structure should probably be represented by a struct and module with supporting functions. This also lets you use pattern matching when writing your game functions (you cant match on a gen servers state outside of the genserver for example).

This is 100% ok if your simply using it to learn bits of elixir (I am aware the first GenServer tutorial represents a stack) or have a larger abstraction in mind (eg an email send queue probably would be a genserver), but best practices … well, imagine if any List spawned a GenServer to match it, things would get a bit messy!

Hope that helps.

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

Last Post!

sunnyro95

sunnyro95

Hey Waheed, curious about this. In part of your code, you use player2_deck, and player1_deck but haven’t defined player1_deck and player2_deck is essentially not used. What’s supposed to happen with these two?

Where Next?

Trending in Questions Top

jonnycharles
I’m in search of an Elixir library that offers PDF generation capabilities similar to Ruby’s Prawn. While there have been discussions abo...
New
spammy
I’m looking to build a personal workflow to quickly deploy web applications written in elixir/phoenix, for local consumption (ie not on t...
New
silverdr
Using Phoenix.LiveView.TagEngine as an EEx.Engine is deprecated! To compile HEEx, use Phoenix.LiveView.TagEngine.compile/2 instead. Sta...
New
saveman71
Hello ! We want new/edit form pages to POST/PUT to their own URL rather than the resources REST defaults (post /things, put /things/:id)...
New
dli
Before I dive in myself, did anyone successfully sprinkle Hologram into their existing LiveView app? Looking for hints regarding: Addi...
New
bottlenecked
Hi all, I wanted to ask how the community is dealing with post-release steps. Today we have Ecto migrations, which make sure that the db...
New
michallepicki
I am using Oban and occasionally, shortly after a deployment, a handful of jobs can fail because of dependency on other parts of the syst...
New

Other Trending Topics Top

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
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
Damirados
Hello everyone. After busy few months I am happy to announce v0.1.0 of Emerge &amp; Solve. They are GUI (Emerge) and State management (S...
New
ausimian
Emily is an Elixir library that runs Nx computations on Apple’s MLX. Install it as the default Nx backend and Nx, defn, Axon, Nx.Serving,...
New
type1fool
I just stumbled on a newly redesigned elixir-lang.org. :tada: It looks like @Software_Mansion did the work, and I think it is generally a...
New
akoutmos
@hugobarauna and I (Alex Koutmos) have been hard at work on writing a book on Nerves that takes you from simply blinking LEDs to building...
New

We're in Beta

About us Mission Statement