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.

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.

Do I understand the rules right, that there are no decisions the players can make?

Thank you so much for your comment! It really helped me with the Queue data structure.

So a converted it to a simple module:

defmodule Queue do
  defstruct list: [], size: 0

  def new do
    struct!(__MODULE__)
  end

  def enqueue(%Queue{} = q, elems) when is_list(elems) do
    List.foldl(elems, q, &enq/2)
  end

  def enqueue(%Queue{} = q, elem) do
    enq(elem, q)
  end

  defp enq(elem, %Queue{list: xs, size: x} = q) do
    %{q | list: List.insert_at(xs, -1, elem), size: x + 1}
  end

  def dequeue(%Queue{} = q, n) do
    {elems, q} =
      Enum.reduce(1..n, {[], q}, fn
        _, {elems, state} ->
          {elem, state} = deq(state)
          {[elem | elems], state}
      end)

    {Enum.reverse(elems), q}
  end

  def dequeue(%Queue{} = q) do
    deq(q)
  end

  defp deq(%Queue{list: []} = q) do
    {nil, q}
  end

  defp deq(%Queue{list: xs, size: x} = q) do
    {hd(xs), %{q | list: tl(xs), size: x - 1}}
  end

  def size(%Queue{} = q) do
    q.size
  end

  def front(%Queue{} = q) do
    List.first(q.list)
  end

  def rear(%Queue{} = q) do
    List.last(q.list)
  end

  def flush(%Queue{} = q) do
    {q.list, new()}
  end

  def inspect(%Queue{} = q) do
    q.list
  end
end

and this is my new war.ex implementation:

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
    {d1, d2} = deal_deck(deck)

    p1 = Queue.enqueue(Queue.new(), d1)
    p2 = Queue.enqueue(Queue.new(), d2)

    winner = play_game(p1, p2)

    Queue.size(winner)
    |> then(&Queue.dequeue(winner, &1))
    |> then(fn {elems, _q} ->
      Enum.map(elems, &remove_ace_weight/1)
    end)
  end

  defp deal_deck(deck) do
    List.foldr(deck, {[], []}, &deal_player/2)
  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 play_game(p1, p2) do
    if winner = maybe_get_winner(p1, p2) do
      winner
    else
      {turn_winner, cards} = play_turn(p1, p2)
      push_cards(turn_winner, cards)
      play_game(p1, p2)
    end
  end

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

    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
    cond do
      !able_to_war?(p1) ->
        {cards, p2} = Queue.flush(p2)
        {p2, cards ++ tied}

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

      true ->
        {[d1, n1], p1} = Queue.dequeue(p1, 2)
        {[d2, n2], p2} = Queue.dequeue(p2, 2)
        play_turn(p1, p2, n1, n2, tied ++ [d1, d2])
    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

it seems to infinite loop into even decks…

I also thought those test cases are wrong, so I’ll confirm it

Now I’ll try to run manually the program or debug in more depth to try understand the errors

yeah, no decisions

1 Like

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…

2 Likes

OK, so what when the game is a state where there will never be a winner?

Woke up realizing this wont get you there because the player is modified in the call so you’ll have to find another way.

when both players runs out of cards at the same time

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

1 Like

I think its possible that this will end in a loop. Image you only play with four cards:

player_1  player_2 
[1, 2]    [2, 1]    # player 2 get the cards [1, 2]
[2]       [1, 1, 2] # player 1 get the cards [2, 1]
[2, 1]    [1, 2]    # back to start

hmm maybe its possible to find a rule to sort the cards in the pot, which never leads to a loop but still is fair…

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:

3 Likes

Yes, that true… The problem is that the game description does not cover any draw state, only turn winners so I imagine that the first player to run out of cards will be ever the loser, considering those tests cases…

This is the complete assign description:

Preamble

In this project, you will simulate a game of War. War is a simple card game between two players. A description of the game and its rules can be found here:
https://bicyclecards.com/how-to-play/war
Given the above description, there are still some situations that are ambiguous. They will be clarified in the game description below.
You will complete this project in three of the four languages we study in this course. The languages chosen are up to you. In addition to the general requirements of the project, each language comes with its own language-specific constraints. These specify the format of the input and output, as well as submission instructions for each language.
Aside from these requirements, anything you do inside your program is up to you. Use as many helper functions or methods as you want, use any syntax you find useful whether we covered it in class or not. There is one exception to this: you may not use any functionality that is not part of the base installation of each language. No 3rd party libraries. Your submission will be tested using out-of-the-box installations of each language.

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.

Technical details:

Input: The input to your program, representing a shuffled deck of cards, will be a permutation of 52 integers, where each integer between 1-13 occurs four times. The integers in this permutation correspond to cards according to the following table (four kings, four tens, four threes, and so on). Notice that we don’t bother representing the suit because the game of War doesn’t require it.
The game: Your program will deal two piles from the input permutation. How you represent your piles is completely up to you. Once the piles are dealt, “play” the game in your program until one player runs out of cards. Once again, how you manage your piles during the game is completely up to you. Keep going until one player runs out of cards.
When cards are added to the bottom of a player’s pile, they should be added in decreasing order by rank. That is, first place the highest ranked card on the bottom, then place the next highest ranked card beneath that. This is true of wars as well. If a player wins six cards as a result of a war, those cards should be added to the bottom starting with the highest rank and ending with the smallest. Ace has the highest rank, Two has the lowest.
Output: Your program will return the pile of the winning player. This pile should contain all 52 integers from the original input permutation and be in the correct order according to how the game played out.

Oh i was getting infinite loop because I was throwing away the state of the turn loser… Now we get back to state 0, test cases fails with little differences and I still didn’t get what is happening

  1) test War deal_1 (WarTest)
     test/war_test.exs:5
     Assertion with == failed
     code:  assert War.deal(t1) == r1
     left:  [
              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,
              2,
              2,
              1,
              3,
              1,
              2,
              1,
              2
            ]
     right: [
              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
            ]
     stacktrace:
       test/war_test.exs:8: (test)



  2) test War deal_4 (WarTest)
     test/war_test.exs:23
     Assertion with == failed
     code:  assert War.deal(t4) == r4
     left:  [
              1,
              1,
              1,
              13,
              13,
              13,
              13,
              12,
              12,
              12,
              12,
              11,
              11,
              11,
              11,
              10,
              10,
              10,
              10,
              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,
              1,
              9
            ]
     right: [
              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
            ]
     stacktrace:
       test/war_test.exs:26: (test)



  3) test War deal_2 (WarTest)
     test/war_test.exs:11
     Assertion with == failed
     code:  assert War.deal(t2) == r2
     left:  [
              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,
              1,
              2
            ]
     right: [
              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
            ]
     stacktrace:
       test/war_test.exs:14: (test)

..

  4) test War deal_3 (WarTest)
     test/war_test.exs:17
     Assertion with == failed
     code:  assert War.deal(t3) == r3
     left:  [
              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,
              1,
              2
            ]
     right: [
              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
            ]
     stacktrace:
       test/war_test.exs:20: (test)



  5) test War deal_5 (WarTest)
     test/war_test.exs:29
     Assertion with == failed
     code:  assert War.deal(t5) == r5
     left:  [
              1,
              1,
              1,
              13,
              13,
              13,
              13,
              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,
              1,
              12
            ]
     right: [
              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
            ]
     stacktrace:
       test/war_test.exs:32: (test)


Finished in 0.09 seconds (0.00s async, 0.09s sync)
18 tests, 5 failures

This may avoid a loop.
I think a draw will still be possible, so the assignment is not complete…?

Do you use the Queue on purpose? This could be solved in a simpler way with just two lists and a recursion.

I would manually play some hands with a smaller deck to verify and use those as your initial tests, as parsing out those 52 card stacks can be obtuse.

I also wondered about your able_to_war? check, if you read the end of the game description, I think it hints at another interpretation. The winner will be the same, but the cards in the final hand would be different. Truly only guessing though.

spoiler

“If, during a war, a player runs out of cards, this counts as a loss as well.”

Meaning play to exhaustion, instead of giving up if you have less than 3 cards? Maybe? I’ve never played the real game.

I really don’t know! actually, I’m doing this assignment as a freelancer job so I don’t have direct access to whom wrote the assignment

I used a Queue to simplify war implementation, so that wouldn’t have so many ++/2 and [x | xs] in the whole program. i thought it would be simpler to manage state via a Queue

Yeah, i’ll try to run manually or via debugger with smaller decks…

Hmmm :thinking: I don’t know, I’m confused at this time haha, I’ll try it too

take a look at github.com/bstakes/war-cardgame or github.com/Bovojon/BlackJack-War-Games/blob/master. both fails for tests cases so I imagine these test cases are wrong or some rule is missing

so its not homework. Then I can do this. Its probably not correct anyway. Or the sorting rule is not fair.

defmodule WarTest do
  use ExUnit.Case

  import Enum
  import War

  test "test" do
    assert :player_1 == play({[4, 4, 3, 3], [1, 1, 2, 2]})
    assert :player_1 == play({[4, 4, 1, 1], [2, 2, 3, 3]})
    assert :player_2 == play({[4, 3, 2, 1], [4, 4, 3, 3]})
    assert :player_2 == play({[2, 2, 3, 3], [4, 4, 1, 1]})
    assert :draw == play({[1, 2], [2, 1]})
    assert :draw == play({[1, 2], [1, 2]})
    assert :draw == play({[2, 1], [2, 1]})

    n_runs = 100_00
    %{player_1: player_1_wins} = 1..n_runs |> map(fn _ -> deal() |> play() end) |> frequencies()
    assert player_1_wins / (n_runs / 2) - 1 < 0.01
  end
end

defmodule War do
  import Enum

  @trace false

  def play(decks) do
    round_(:normal, decks, [])
  end

  defp round_(state, {deck_1, deck_2}, pot) do
    if @trace, do: dbg({state, {deck_1, deck_2}, pot})
    round(state, {deck_1, deck_2}, pot)
  end

  defp round(_, {[], []}, _), do: :draw
  defp round(_, {_, []}, _), do: :player_1
  defp round(_, {[], _}, _), do: :player_2

  defp round(:war, {[c, c1 | d1], [c, c2 | d2]}, pot) do
    round_(:war, {d1, d2}, pot ++ [c, c1, c, c2])
  end

  defp round(:war, {[c11, c12 | d1], [c21, c22 | d2]}, pot) do
    round_(:normal, decks(c11, c21, d1, d2, pot ++ [c11, c12, c21, c22]), [])
  end

  defp round(_, {[c | d1], [c | d2]}, pot) do
    round_(:war, {d1, d2}, pot ++ [c, c])
  end

  defp round(_, {[c1 | d1], [c2 | d2]}, pot) do
    round_(:normal, decks(c1, c2, d1, d2, [c1, c2]), pot)
  end

  defp decks(c1, c2, d1, d2, pot) do
    pot = sort(pot, :desc)
    if c1 > c2, do: {d1 ++ pot, d2}, else: {d1, d2 ++ pot}
  end

  @set_of_cards to_list(2..14)
  defp deal(set_of_cards \\ @set_of_cards) do
    set_of_cards
    |> List.duplicate(4)
    |> List.flatten()
    |> shuffle()
    |> split(length(set_of_cards) * 2)
  end
end