Finished step five: Output
This commit is contained in:
parent
7e20cac9c1
commit
d5e418c0d8
|
|
@ -1,67 +1,25 @@
|
||||||
defmodule ZombieSurvivor.Game do
|
defmodule ZombieSurvivor.Game do
|
||||||
alias ZombieSurvivor.{Game, Survivor}
|
alias ZombieSurvivor.{Game.State, 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
|
|
||||||
|
|
||||||
use GenServer
|
use GenServer
|
||||||
|
|
||||||
def new, do: start_link()
|
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 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 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 level(pid), do: GenServer.call(pid, :level)
|
||||||
def survivors(pid), do: GenServer.call(pid, :survivors)
|
def survivors(pid), do: GenServer.call(pid, :survivors)
|
||||||
|
def wound(pid, survivor), do: GenServer.call(pid, {:wound, survivor})
|
||||||
|
|
||||||
## Server callbacks
|
## Server callbacks
|
||||||
|
|
||||||
|
|
@ -71,18 +29,39 @@ defmodule ZombieSurvivor.Game do
|
||||||
|
|
||||||
@impl GenServer
|
@impl GenServer
|
||||||
def init(:ok) do
|
def init(:ok) do
|
||||||
{:ok, State.new()}
|
s = State.new()
|
||||||
|
{:ok, %{s | history: [{:start, DateTime.utc_now()} | s.history]}}
|
||||||
end
|
end
|
||||||
|
|
||||||
@impl GenServer
|
@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
|
def handle_call(:ended?, _from, state) do
|
||||||
{:reply, State.ended?(state), state}
|
{:reply, State.ended?(state), state}
|
||||||
end
|
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
|
def handle_call(:history, _from, state) do
|
||||||
{:reply, state.history, state}
|
{:reply, state.history, state}
|
||||||
end
|
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
|
def handle_call(:level, _from, state) do
|
||||||
{:reply, State.level(state), state}
|
{:reply, State.level(state), state}
|
||||||
end
|
end
|
||||||
|
|
@ -91,8 +70,8 @@ defmodule ZombieSurvivor.Game do
|
||||||
{:reply, state.survivors, state}
|
{:reply, state.survivors, state}
|
||||||
end
|
end
|
||||||
|
|
||||||
@impl GenServer
|
def handle_call({:wound, survivor}, _from, state) do
|
||||||
def handle_cast({:add_survivor, survivor}, state) do
|
s = State.wound_survivor(state, survivor)
|
||||||
{:noreply, State.add_survivor(state, survivor)}
|
{:reply, s, s}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -5,10 +5,11 @@ defmodule ZombieSurvivor.Survivor do
|
||||||
name: String.t(),
|
name: String.t(),
|
||||||
wounds: non_neg_integer,
|
wounds: non_neg_integer,
|
||||||
equipment: [String.t()],
|
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()
|
@spec new([{atom, any}]) :: Survivor.t()
|
||||||
def new(opts \\ []), do: struct(__MODULE__, opts)
|
def new(opts \\ []), do: struct(__MODULE__, opts)
|
||||||
|
|
@ -26,9 +27,13 @@ defmodule ZombieSurvivor.Survivor do
|
||||||
|
|
||||||
@spec wound(Survivor.t(), non_neg_integer) :: Survivor.t()
|
@spec wound(Survivor.t(), non_neg_integer) :: Survivor.t()
|
||||||
def wound(survivor, num \\ 1) do
|
def wound(survivor, num \\ 1) do
|
||||||
|
if dead?(survivor) do
|
||||||
|
survivor
|
||||||
|
else
|
||||||
%{survivor | wounds: survivor.wounds + num}
|
%{survivor | wounds: survivor.wounds + num}
|
||||||
|> discard_equipment()
|
|> discard_equipment()
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
@spec kill_zombies(Survivor.t(), non_neg_integer) :: Survivor.t()
|
@spec kill_zombies(Survivor.t(), non_neg_integer) :: Survivor.t()
|
||||||
def kill_zombies(survivor, count \\ 1) do
|
def kill_zombies(survivor, count \\ 1) do
|
||||||
|
|
|
||||||
|
|
@ -34,9 +34,8 @@ defmodule GameTest do
|
||||||
end
|
end
|
||||||
|
|
||||||
test "ensures that two survivors with the same name can't exist", %{game: game} do
|
test "ensures that two survivors with the same name can't exist", %{game: game} do
|
||||||
game
|
Game.add_survivor(game, @new_survivor)
|
||||||
|> Game.add_survivor(@new_survivor)
|
Game.add_survivor(game, @new_survivor)
|
||||||
|> Game.add_survivor(@new_survivor)
|
|
||||||
|
|
||||||
assert Map.size(Game.survivors(game)) == 1
|
assert Map.size(Game.survivors(game)) == 1
|
||||||
end
|
end
|
||||||
|
|
@ -45,21 +44,18 @@ defmodule GameTest do
|
||||||
describe "ended?/1" do
|
describe "ended?/1" do
|
||||||
test "returns true if all its survivors are dead", %{game: game} do
|
test "returns true if all its survivors are dead", %{game: game} do
|
||||||
# TODO: Property test, add many dead survivors
|
# TODO: Property test, add many dead survivors
|
||||||
game
|
Game.add_survivor(game, @dead_survivor)
|
||||||
|> Game.add_survivor(@dead_survivor)
|
|
||||||
|
|
||||||
assert Game.ended?(game)
|
assert Game.ended?(game)
|
||||||
|
|
||||||
game
|
Game.add_survivor(game, %{@dead_survivor | name: "Zambee"})
|
||||||
|> Game.add_survivor(%{@dead_survivor | name: "Zambee"})
|
|
||||||
|
|
||||||
assert Game.ended?(game)
|
assert Game.ended?(game)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "returns false if at least one survivor is alive", %{game: game} do
|
test "returns false if at least one survivor is alive", %{game: game} do
|
||||||
game
|
Game.add_survivor(game, @new_survivor)
|
||||||
|> Game.add_survivor(@new_survivor)
|
Game.add_survivor(game, @dead_survivor)
|
||||||
|> Game.add_survivor(@dead_survivor)
|
|
||||||
|
|
||||||
refute Game.ended?(game)
|
refute Game.ended?(game)
|
||||||
end
|
end
|
||||||
|
|
@ -105,4 +101,87 @@ defmodule GameTest do
|
||||||
assert Game.level(game) == :yellow
|
assert Game.level(game) == :yellow
|
||||||
end
|
end
|
||||||
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
|
end
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue