From d5e418c0d8a056a442aaa615480f0012d86ae510 Mon Sep 17 00:00:00 2001 From: Liru Date: Sat, 21 Apr 2018 13:31:41 -0400 Subject: [PATCH] Finished step five: Output --- lib/zombie_survivor/game.ex | 95 ++++++++++--------------- lib/zombie_survivor/game_state.ex | 113 ++++++++++++++++++++++++++++++ lib/zombie_survivor/survivor.ex | 13 ++-- test/game_test.exs | 99 +++++++++++++++++++++++--- 4 files changed, 248 insertions(+), 72 deletions(-) create mode 100644 lib/zombie_survivor/game_state.ex diff --git a/lib/zombie_survivor/game.ex b/lib/zombie_survivor/game.ex index 91c94b1..16683a1 100644 --- a/lib/zombie_survivor/game.ex +++ b/lib/zombie_survivor/game.ex @@ -1,67 +1,25 @@ defmodule ZombieSurvivor.Game do - alias ZombieSurvivor.{Game, Survivor} - - defmodule State do - alias __MODULE__, as: Game - - @type t :: %__MODULE__{ - survivors: %{String.t() => Survivor.t()}, - history: [String.t()] - } - @type history_type :: - :start - | :new_survivor - | :new_equipment - | :wounded - | :death - | :levelup - | :game_levelup - | :end - @type history :: {history_type, any} - - defstruct survivors: %{}, history: [] - - @spec new() :: Game.t() - def new(), do: %Game{} - - @spec add_survivor(Game.t(), Survivor.t()) :: Game.t() - def add_survivor(game, survivor) do - name = survivor.name - - if Map.has_key?(game, name) do - game - else - %{game | survivors: Map.put(game.survivors, name, survivor)} - end - end - - @spec ended?(Game.t()) :: boolean - def ended?(%Game{survivors: survivors}) when map_size(survivors) == 0, do: false - - def ended?(game) do - Enum.all?(game.survivors, fn {_, survivor} -> - Survivor.dead?(survivor) - end) - end - - @spec level(Game.t()) :: ZombieSurvivor.level() - def level(game) do - game.survivors - |> Enum.reject(fn {_, s} -> Survivor.dead?(s) end) - |> Enum.reduce(0, fn {_, s}, acc -> max(s.experience, acc) end) - |> ZombieSurvivor.level() - end - end + alias ZombieSurvivor.{Game.State, Survivor} use GenServer def new, do: start_link() - def add_survivor(pid, survivor), do: GenServer.cast(pid, {:add_survivor, survivor}) + def add_history(pid, tuple), do: GenServer.call(pid, {:add_history, tuple}) + def add_survivor(pid, survivor), do: GenServer.call(pid, {:add_survivor, survivor}) def ended?(pid), do: GenServer.call(pid, :ended?) + + def give_equipment(pid, survivor, item), + do: GenServer.call(pid, {:give_equipment, survivor, item}) + def history(pid), do: GenServer.call(pid, :history) + + def kill_zombies(pid, survivor, count), + do: GenServer.call(pid, {:kill_zombies, survivor, count}) + def level(pid), do: GenServer.call(pid, :level) def survivors(pid), do: GenServer.call(pid, :survivors) + def wound(pid, survivor), do: GenServer.call(pid, {:wound, survivor}) ## Server callbacks @@ -71,18 +29,39 @@ defmodule ZombieSurvivor.Game do @impl GenServer def init(:ok) do - {:ok, State.new()} + s = State.new() + {:ok, %{s | history: [{:start, DateTime.utc_now()} | s.history]}} end @impl GenServer + def handle_call({:add_history, tuple}, _from, state) do + s = %{state | history: [tuple | state.history]} + {:reply, s, s} + end + + def handle_call({:add_survivor, survivor}, _from, state) do + s = State.add_survivor(state, survivor) + {:reply, s, s} + end + def handle_call(:ended?, _from, state) do {:reply, State.ended?(state), state} end + def handle_call({:give_equipment, survivor, item}, _from, state) do + s = State.give_equipment(state, survivor, item) + {:reply, s, s} + end + def handle_call(:history, _from, state) do {:reply, state.history, state} end + def handle_call({:kill_zombies, survivor, count}, _from, state) do + s = State.kill_zombies(state, survivor, count) + {:reply, s, s} + end + def handle_call(:level, _from, state) do {:reply, State.level(state), state} end @@ -91,8 +70,8 @@ defmodule ZombieSurvivor.Game do {:reply, state.survivors, state} end - @impl GenServer - def handle_cast({:add_survivor, survivor}, state) do - {:noreply, State.add_survivor(state, survivor)} + def handle_call({:wound, survivor}, _from, state) do + s = State.wound_survivor(state, survivor) + {:reply, s, s} end end diff --git a/lib/zombie_survivor/game_state.ex b/lib/zombie_survivor/game_state.ex new file mode 100644 index 0000000..6f69ca9 --- /dev/null +++ b/lib/zombie_survivor/game_state.ex @@ -0,0 +1,113 @@ +defmodule ZombieSurvivor.Game.State do + alias __MODULE__ + alias ZombieSurvivor.Survivor + + @type t :: %__MODULE__{ + survivors: %{String.t() => Survivor.t()}, + history: [String.t()] + } + @type history_type :: + :start + | :new_survivor + | :new_equipment + | :wounded + | :death + | :levelup + | :game_level + | :end + + defstruct survivors: %{}, history: [] + + @spec new() :: State.t() + def new(), do: %State{} + + @spec add_survivor(State.t(), Survivor.t()) :: State.t() + def add_survivor(game, survivor) do + name = survivor.name + + if Map.has_key?(game, name) do + game + else + %{game | survivors: Map.put(game.survivors, name, survivor)} + |> add_history({:new_survivor, survivor.name}) + end + end + + @spec ended?(State.t()) :: boolean + def ended?(%State{survivors: survivors}) when map_size(survivors) == 0, do: false + + def ended?(game) do + Enum.all?(game.survivors, fn {_, survivor} -> + Survivor.dead?(survivor) + end) + end + + @spec give_equipment(State.t(), Survivor.t(), String.t()) :: State.t() + def give_equipment(game, survivor, item) do + name = survivor.name + + new_survivors = Map.update!(game.survivors, name, &Survivor.add_equipment(&1, item)) + + %{game | survivors: new_survivors} + |> add_history({:new_equipment, {name, item}}) + end + + @spec level(State.t()) :: ZombieSurvivor.level() + def level(game) do + game.survivors + |> Enum.reject(fn {_, s} -> Survivor.dead?(s) end) + |> Enum.reduce(0, fn {_, s}, acc -> max(s.experience, acc) end) + |> ZombieSurvivor.level() + end + + @spec kill_zombies(State.t(), Survivor.t(), non_neg_integer) :: State.t() + def kill_zombies(game, survivor, count) do + name = survivor.name + + old_game_level = level(game) + + survivors = game.survivors + + old_survivor_level = Survivor.level(survivors[name]) + new_survivors = Map.update!(survivors, name, &Survivor.kill_zombies(&1, count)) + new_survivor_level = Survivor.level(new_survivors[name]) + + g = %{game | survivors: new_survivors} + new_game_level = level(g) + + g + |> add_history({:levelup, name}, old_survivor_level != new_survivor_level) + |> add_history({:game_level, new_game_level}, old_game_level != new_game_level) + end + + @spec wound_survivor(State.t(), Survivor.t()) :: State.t() + def wound_survivor(game, survivor) do + name = survivor.name + + old_game_level = level(game) + + survivors = game.survivors + new_survivors = Map.update!(survivors, name, &Survivor.wound(&1)) + + game = %{game | survivors: new_survivors} + + new_game_level = level(game) + + game + |> add_history({:wounded, name}, !Survivor.dead?(new_survivors[name])) + |> add_history({:death, name}, Survivor.dead?(new_survivors[name])) + |> add_history({:game_level, new_game_level}, old_game_level != new_game_level) + |> add_history({:end, DateTime.utc_now()}, ended?(game)) + end + + ## Private + + @spec add_history(State.t(), tuple, boolean) :: State.t() + defp add_history(state, entry, comp \\ true) do + if comp do + %{state | history: [entry | state.history]} + else + state + end + end +end diff --git a/lib/zombie_survivor/survivor.ex b/lib/zombie_survivor/survivor.ex index 7968f26..d6b3fa7 100644 --- a/lib/zombie_survivor/survivor.ex +++ b/lib/zombie_survivor/survivor.ex @@ -5,10 +5,11 @@ defmodule ZombieSurvivor.Survivor do name: String.t(), wounds: non_neg_integer, equipment: [String.t()], - experience: non_neg_integer + experience: non_neg_integer, + game: pid } - defstruct name: "", wounds: 0, equipment: [], experience: 0 + defstruct name: "", wounds: 0, equipment: [], experience: 0, game: nil @spec new([{atom, any}]) :: Survivor.t() def new(opts \\ []), do: struct(__MODULE__, opts) @@ -26,8 +27,12 @@ defmodule ZombieSurvivor.Survivor do @spec wound(Survivor.t(), non_neg_integer) :: Survivor.t() def wound(survivor, num \\ 1) do - %{survivor | wounds: survivor.wounds + num} - |> discard_equipment() + if dead?(survivor) do + survivor + else + %{survivor | wounds: survivor.wounds + num} + |> discard_equipment() + end end @spec kill_zombies(Survivor.t(), non_neg_integer) :: Survivor.t() diff --git a/test/game_test.exs b/test/game_test.exs index 1d3896e..a9f4fa0 100644 --- a/test/game_test.exs +++ b/test/game_test.exs @@ -34,9 +34,8 @@ defmodule GameTest do end test "ensures that two survivors with the same name can't exist", %{game: game} do - game - |> Game.add_survivor(@new_survivor) - |> Game.add_survivor(@new_survivor) + Game.add_survivor(game, @new_survivor) + Game.add_survivor(game, @new_survivor) assert Map.size(Game.survivors(game)) == 1 end @@ -45,21 +44,18 @@ defmodule GameTest do describe "ended?/1" do test "returns true if all its survivors are dead", %{game: game} do # TODO: Property test, add many dead survivors - game - |> Game.add_survivor(@dead_survivor) + Game.add_survivor(game, @dead_survivor) assert Game.ended?(game) - game - |> Game.add_survivor(%{@dead_survivor | name: "Zambee"}) + Game.add_survivor(game, %{@dead_survivor | name: "Zambee"}) assert Game.ended?(game) end test "returns false if at least one survivor is alive", %{game: game} do - game - |> Game.add_survivor(@new_survivor) - |> Game.add_survivor(@dead_survivor) + Game.add_survivor(game, @new_survivor) + Game.add_survivor(game, @dead_survivor) refute Game.ended?(game) end @@ -105,4 +101,87 @@ defmodule GameTest do assert Game.level(game) == :yellow end end + + describe "game history" do + test "begins by recording the time the game began", %{game: game} do + assert {:start, _} = hd(Game.history(game)) + end + + test "notes that a survivor has been added", %{game: game} do + s = @new_survivor + Game.add_survivor(game, s) + assert {:new_survivor, s.name} in Game.history(game) + + s2 = %{@new_survivor | name: "Bob"} + Game.add_survivor(game, s2) + assert {:new_survivor, s2.name} in Game.history(game) + end + + test "notes that a survivor acquires a piece of equipment", %{game: game} do + # TODO: Property test: check item names + s = @new_survivor + + Game.add_survivor(game, s) + Game.give_equipment(game, s, "Cheese") + + log = Game.history(game) + + # IO.inspect history + assert {:new_equipment, {s.name, "Cheese"}} in log + end + + test "notes that a survivor is wounded", %{game: game} do + s = @new_survivor + + Game.add_survivor(game, s) + Game.wound(game, s) + log = Game.history(game) + + assert {:wounded, s.name} in log + end + + test "notes that a survivor dies", %{game: game} do + Game.add_survivor(game, @new_survivor) + Game.add_survivor(game, %{@new_survivor | name: "Bob"}) + Game.wound(game, @new_survivor) + Game.wound(game, @new_survivor) + log = Game.history(game) + + assert {:death, @new_survivor.name} in log + end + + test "notes that a survivor levels up", %{game: game} do + Game.add_survivor(game, @new_survivor) + Game.kill_zombies(game, @new_survivor, 10) + + log = Game.history(game) + + assert {:levelup, @new_survivor.name} in log + end + + test "notes that the game level changes", %{game: game} do + Game.add_survivor(game, @new_survivor) + Game.add_survivor(game, %{@new_survivor | name: "Bob"}) + Game.kill_zombies(game, @new_survivor, 10) + + log = Game.history(game) + + assert {:game_level, :yellow} in log + + Game.wound(game, @new_survivor) + Game.wound(game, @new_survivor) + log = Game.history(game) + + assert {:game_level, :blue} in log + end + + test "notes that the game ends", %{game: game} do + Game.add_survivor(game, @new_survivor) + Game.wound(game, @new_survivor) + Game.wound(game, @new_survivor) + log = Game.history(game) + + assert {:end, _} = hd(log) + end + end end