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
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
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 ![]()
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
Last Post!
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?
Trending in Questions
Other Trending Topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #deployment
- #library
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #channels
- #elixirconf
- #exunit
- #discussion
- #code-sync
- #javascript
- #podcasts
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixir-ls
- #phoenix_html
- #iex
- #blog-post
- #graphql
- #genstage
- #ai
- #websockets
- #elixirconf-us
- #supervisor
- #advent-of-code
- #distillery
- #processes
- #api
- #forms
- #metaprogramming
- #security
- #hex









