One of the Etudes for Elixir exercises is to create a War game that uses the spawn
, send
, and receive
primitives, but I went all out and made it an OTP GenServer.
I run the game in iex with:
{:ok, dealer} = War.Dealer.start
War.Dealer.add_handler(dealer, War.Printer)
War.Dealer.do_round(dealer) # repeat until end of game
Right now the Dealer is responsible for creating and cleaning up the other Gen things that it creates, but I'm wondering if I should have used a supervision tree or not. I thought it'd be nice to make this an application, but I'm not sure if I can start the same Application multiple times, which is something I can do with an anonymous GenServer, like it is now. I'm also coming from Node where EventEmitters are king. I decided that GenEvent would also be the best way to do output, so that anybody who wants to listen can. Was that wrong?
This is the biggest thing I've created in Elixir so far, so it'd be nice to know if I'm doing anything unidiomatic or if anything is hard to read.
Note: War.Dealer
's init
function allows for the setting of ran
, which is the datatype Erlang's :random
module takes for setting state. This is so that tests can be deterministic.
defmodule War do
defmodule Card do
defstruct suit: :spade, face: :ace, value: 0
@type t :: %War.Card{suit: atom, face: atom | 2..10, value: number}
def suits(), do: [:diamond] #[:diamond, :heart, :club, :spade]
def faces() do
#[:ace, 2, 3, 4, 5, 6, 7, 8, 9, 10, :jack, :queen, :king]
[:ace, 2, 3, 4]
|> Enum.with_index()
end
def make_deck(suits \\ suits(), faces \\ faces()) do
for suit <- suits, {face, value} <- faces do
%War.Card{suit: suit, face: face, value: value}
end
end
def compare(c1, c2) do
v1 = c1.value
v2 = c2.value
cond do
v1 == v2 -> :eq
v1 < v2 -> :lt
v1 > v2 -> :gt
end
end
end
defmodule Dealer do
use GenServer
# API
def start(args \\ [], opts \\ []) do
GenServer.start(__MODULE__, args, opts)
end
def do_round(dealer) do
GenServer.cast(dealer, :round)
dealer
end
def add_handler(dealer, handler) do
GenServer.cast(dealer, {:add_handler, handler})
dealer
end
# Implementation
def init(args) do
suits = args[:suits] || War.Card.suits
faces = args[:faces] || War.Card.faces
ran = args[:ran] || :erlang.now()
:random.seed(ran)
deck = War.Card.make_deck(suits, faces) |> Enum.shuffle()
{p1_deck, p2_deck} = Enum.split(deck, div(length(deck), 2))
{:ok, emitter} = GenEvent.start
{:ok, p1} = War.Player.start_link(p1_deck)
{:ok, p2} = War.Player.start_link(p2_deck)
{:ok, [
deck: [],
p1: p1,
p2: p2,
emitter: emitter,
]}
end
def handle_cast({:add_handler, handler}, dealer) do
GenEvent.add_handler(dealer[:emitter], handler, [])
{:noreply, dealer}
end
def handle_cast(:round, dealer) do
p1_card = War.Player.request_card(dealer[:p1])
p2_card = War.Player.request_card(dealer[:p2])
case {p1_card, p2_card} do
{:out_of_cards, :out_of_cards} -> win(dealer, :tie)
{:out_of_cards, _p2_card} -> win(dealer, :p2)
{_p1_card, :out_of_cards} -> win(dealer, :p1)
{p1_card, p2_card} -> battle(dealer, p1_card, p2_card)
end
end
defp win(dealer, winner) do
GenEvent.notify(dealer[:emitter], {:game_over, winner})
{:stop, :normal, dealer}
end
defp battle(dealer, p1_card, p2_card) do
GenEvent.notify(dealer[:emitter], {:play_card, {:p1, p1_card}})
GenEvent.notify(dealer[:emitter], {:play_card, {:p2, p2_card}})
case War.Card.compare(p1_card, p2_card) do
:gt ->
GenEvent.notify(dealer[:emitter], {:win_round, :p1})
War.Player.give_cards(dealer[:p1], Enum.shuffle([p1_card | [p2_card | dealer[:deck]]]))
{:noreply, dealer}
:lt ->
GenEvent.notify(dealer[:emitter], {:win_round, :p2})
War.Player.give_cards(dealer[:p2], Enum.shuffle([p1_card | [p2_card | dealer[:deck]]]))
{:noreply, dealer}
:eq ->
GenEvent.notify(dealer[:emitter], :war)
dealer = Dict.update!(dealer, :deck, &[p1_card | [p2_card | &1]])
war(dealer)
end
end
defp war(dealer) do
p1_cards = War.Player.request_cards(dealer[:p1], 4)
p2_cards = War.Player.request_cards(dealer[:p2], 4)
case {p1_cards, p2_cards} do
{:out_of_cards, :out_of_cards} ->
win(dealer, :tie)
{:out_of_cards, _p2_cards} ->
GenEvent.notify(dealer[:emitter], {:out_of_cards, :p1})
win(dealer, :p2)
{_p1_cards, :out_of_cards} ->
GenEvent.notify(dealer[:emitter], {:out_of_cards, :p1})
win(dealer, :p1)
{[p1_card | p1_bet], [p2_card | p2_bet]} ->
GenEvent.notify(dealer[:emitter], :bet_three_cards)
dealer = Dict.update!(dealer, :deck, &(List.flatten([p1_bet, p2_bet, &1])))
battle(dealer, p1_card, p2_card)
end
end
def terminate(_pid, :normal, dealer) do
GenEvent.stop(dealer[:emitter])
:ok
end
end
defmodule Player do
use GenServer
# API
def start_link(deck \\ []) do
GenServer.start_link(__MODULE__, deck)
end
def request_card(player) do
case GenServer.call(player, {:request_cards, 1}) do
:out_of_cards -> :out_of_cards
[card] -> card
end
end
def request_cards(player, n), do: GenServer.call(player, {:request_cards, n})
def give_cards(player, cards), do: GenServer.cast(player, {:give_cards, cards})
# Implementation
def init(deck) do
{:ok, deck}
end
def handle_call({:request_cards, n}, _from, deck) do
{top, deck} = Enum.split(deck, n)
if (length(top) == n) do
{:reply, top, deck}
else
{:reply, :out_of_cards, deck}
end
end
def handle_cast({:give_cards, cards}, deck) do
{:noreply, deck ++ cards}
end
end
defmodule Printer do
use GenEvent
def handle_event(message, self) do
IO.inspect(message)
{:ok, self}
end
end
end