4
\$\begingroup\$

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
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.