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.