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










Showing Posts 1 to 10- Show Best Posts
- Show All (oldest first)
- Show All (newest first)
soup
Hope this is helpful and that I haven’t misunderstood, otherwise apologies.
Consider these tests
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.
Check {:enqueue, elem}, does the queue look correct?, spoiler
consider
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.
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
Listspawned aGenServerto match it, things would get a bit messy!Hope that helps.
Sebb
Do I understand the rules right, that there are no decisions the players can make?
zoedsoupe
Thank you so much for your comment! It really helped me with the
Queuedata structure.So a converted it to a simple module:
and this is my new
war.eximplementation: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
zoedsoupe
yeah, no decisions
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
What is
x?Consider
What does
p1contain?Hint
Remember before all our state was maintained in a genserver, but now we must maintain it ourselves.
Hint
Look at
push_cardsand 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
caseand the^pin operator…Sebb
OK, so what when the game is a state where there will never be a winner?
soup
Woke up realizing this wont get you there because the player is modified in the call so you’ll have to find another way.
zoedsoupe
when both players runs out of cards at the same time
zoedsoupe
So what I made was to add a key
:nameto theQueuestruct, to identify which queue I’m dealing of.push_cards/2returns the final new queue, and then I pattern match its name to know which player was the turn winner!New
queue.ex(only diifs):and the
play_game/2:Although I now have a infinite loop haha. At least the
Queueseems to be correctly implemented nowSebb
I think its possible that this will end in a loop. Image you only play with four cards:
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…