M lib/kitty_village.ex => lib/kitty_village.ex +31 -18
@@ 8,40 8,51 @@ defmodule KittyVillage do
import Ratatouille.View
alias KittyVillage.Game
- alias KittyVillage.Building
- alias KittyVillage.Resource
-
alias KittyVillage.View.BuildingButton
alias KittyVillage.Building.Field
alias KittyVillage.Resource.Catnip
- def init(_context),
- do: Game.fresh_state()
+ alias KittyVillage.ResourceConversion.CatnipToWood
+
+ def run do
+ Ratatouille.run(__MODULE__)
+ end
- def subscribe(_model) do
- Ratatouille.Runtime.Subscription.interval(200, :tick)
+ def init(_context) do
+ KittyVillage.Game.start_link(nil)
+ %{}
end
def update(model, msg) do
case msg do
- {:event, %{ch: ?c}} -> Resource.add_one(Catnip, model)
- {:event, %{ch: ?f}} -> Building.buy(Field, model)
- :tick -> Game.tick(model)
- _ -> model
+ {:event, %{ch: ?c}} ->
+ :ok = Game.add_resource(Catnip)
+ model
+
+ {:event, %{ch: ?f}} ->
+ :ok = Game.build(Field)
+ model
+
+ {:event, %{ch: ?w}} ->
+ :ok = Game.convert(CatnipToWood)
+ model
+
+ _ ->
+ model
end
end
- def render(model) do
+ def render(_model) do
view top_bar: bar([label(content: "You are a kitten in a catnip forest")]) do
row([
column(
[size: 4],
- render_resources(model.resources)
+ render_resources(Game.resources())
),
column(size: 8) do
- render_buildings(model)
+ render_buildings()
end
])
end
@@ 49,11 60,13 @@ defmodule KittyVillage do
defp render_resources(resources) do
panel([
- label(content: "catnip: #{Float.round(resources.catnip, 2)}")
+ # TODO: storage + colors
+ label(content: "catnip: #{Float.round(resources.catnip, 2)}"),
+ label(content: "wood: #{Float.round(resources.wood, 2)}")
])
end
- defp render_buildings(model) do
+ defp render_buildings() do
panel([
row do
column([size: 6], [
@@ 62,8 75,8 @@ defmodule KittyVillage do
end
])
end,
- if Field.visible(model) do
- BuildingButton.render("Catnip Field", "f", Field, model)
+ if Game.building_visible(Field) do
+ BuildingButton.render("Catnip Field", "f", Field)
end
])
end
M lib/kitty_village/building.ex => lib/kitty_village/building.ex +11 -5
@@ 16,6 16,12 @@ defmodule KittyVillage.Building do
"""
@callback effect(number) :: [KittyVillage.Effect.t()]
+
+
+
+ @doc """
+ the number of a specific building
+ """
def count(b, model) do
model.buildings[b.name()]
end
@@ 33,7 39,7 @@ defmodule KittyVillage.Building do
"""
def available(b, %{resources: resources} ) do
b.current_cost(0)
- |> ensure_cost(resources)
+ |> can_afford?(resources)
end
def buy(b, %{buildings: buildings} = model) do
@@ 41,7 47,7 @@ defmodule KittyVillage.Building do
end
defp make_purchase(cost, %{resources: resources, buildings: buildings} = model, building_name) do
- if ensure_cost(cost, resources) do
+ if can_afford?(cost, resources) do
%{
model
| resources: deduct_cost(cost, resources),
@@ 64,13 70,13 @@ defmodule KittyVillage.Building do
deduct_cost(rest, Map.put(resources, resource, resources[resource] - amount))
end
- defp ensure_cost([], _resources) do
+ def can_afford?([], _resources) do
true
end
- defp ensure_cost([{resource, amount} | rest], resources) do
+ def can_afford?([{resource, amount} | rest], resources) do
if resources[resource] >= amount do
- ensure_cost(rest, resources)
+ can_afford?(rest, resources)
else
false
end
M lib/kitty_village/game.ex => lib/kitty_village/game.ex +146 -16
@@ 1,15 1,22 @@
defmodule KittyVillage.Game do
@moduledoc """
- This module is the coordination point for much of the game
+ This module is the genserver and coordination
+ point for much of the game.
"""
alias KittyVillage.Building
alias KittyVillage.Resource
+ alias KittyVillage.ResourceConversion
alias KittyVillage.Effect
+ alias KittyVillage.View.BuildingButton
+
alias KittyVillage.Building.Field
alias KittyVillage.Resource.Catnip
+ alias KittyVillage.Resource.Wood
+
+ alias KittyVillage.ResourceConversion.CatnipToWood
@type building_list :: %{Building.building_name() => number}
@type resource_list :: %{Resource.resource_name() => number}
@@ 19,23 26,105 @@ defmodule KittyVillage.Game do
:stats => %{:max_resources => %{Catnip.name() => number}}
}
- @spec fresh_state :: state
- def fresh_state(),
- do: %{
- resources: %{
- Catnip.name() => 10.0
- },
- buildings: %{
- Field.name() => 0.0
- },
- stats: %{
- max_resources: %{Catnip.name() => 10.0}
- }
- }
+ ### GenServer impl ###
+
+ use GenServer
+
+ def start_link(_) do
+ GenServer.start_link(__MODULE__, nil, name: __MODULE__)
+
+ Periodic.start_link(
+ every: 200,
+ run: fn -> send(__MODULE__, :tick) end
+ )
+ end
+
+ @impl true
+ def init(_) do
+ {:ok, fresh_state()}
+ end
+
+ @impl true
+ def handle_info(:tick, state) do
+ {:noreply, tick(state)}
+ end
+
+ @impl true
+ def handle_call({:resource, :add, resource, amt}, _from, state) do
+ {:reply, :ok, Resource.add(state, resource, amt)}
+ end
+
+ @impl true
+ def handle_call({:build, building}, _from, state) do
+ {:reply, :ok, Building.buy(building, state)}
+ end
+
+ @impl true
+ def handle_call({:convert, conversion}, _from, state) do
+ {:reply, :ok, ResourceConversion.convert(state, conversion, conversion.minimum())}
+ end
+
+ @impl true
+ def handle_call({:get, :resources}, _from, state) do
+ {:reply, state.resources, state}
+ end
+
+ @impl true
+ def handle_call({:visible, :building, building}, _from, state) do
+ {:reply, building.visible(state), state}
+ end
+
+ @impl true
+ def handle_call({:cost, :building, building}, _from, state) do
+ {:reply, Building.cost(building, state), state}
+ end
+
+ @impl true
+ def handle_call({:count, :building, building}, _from, state) do
+ {:reply, Building.count(building, state), state}
+ end
+
+ @impl true
+ def handle_call({:afford, :building, building}, _reply, state) do
+ {:reply, Building.can_afford?(Building.cost(building, state), state.resources), state}
+ end
+
+ def add_resource(resource, amt \\ 1) do
+ GenServer.call(__MODULE__, {:resource, :add, resource, amt})
+ end
+
+ def build(building) do
+ GenServer.call(__MODULE__, {:build, building})
+ end
+
+ def convert(conversion) do
+ GenServer.call(__MODULE__, {:convert, conversion})
+ end
+
+ def resources() do
+ GenServer.call(__MODULE__, {:get, :resources})
+ end
+
+ def building_visible(building) do
+ GenServer.call(__MODULE__, {:visible, :building, building})
+ end
+
+ def can_afford_building?(building) do
+ GenServer.call(__MODULE__, {:afford, :building, building})
+ end
+
+
+
+ def building_cost(building) do
+ GenServer.call(__MODULE__, {:cost, :building, building})
+ end
+
+ def building_count(building) do
+ GenServer.call(__MODULE__, {:count, :building, building})
+ end
- @doc "calculate and apply the effects on state for a single game tick"
@spec tick(state) :: state
- def tick(state) do
+ defp tick(state) do
# building effects
effects =
state.buildings
@@ 47,7 136,48 @@ defmodule KittyVillage.Game do
Enum.reduce(effects, state, fn el, acc -> Effect.apply_effect(el, acc) end)
end
+ @spec fresh_state :: state
+ defp fresh_state() do
+ %{
+ resources: %{
+ Catnip.name() => 0.0,
+ Wood.name() => 0.0
+ },
+ buildings: %{
+ Field.name() => 0.0
+ },
+ stats: %{
+ max_resources: %{Catnip.name() => 0.0}
+ }
+ }
+
+ KittyVillage.Game.TestState.wood_start()
+ end
+
defp key_to_building(:catnip_field) do
Field
end
end
+
+defmodule KittyVillage.Game.TestState do
+ @moduledoc "Initial states in various states of completion for the game"
+
+ alias KittyVillage.Building.Field
+ alias KittyVillage.Resource.Catnip
+ alias KittyVillage.Resource.Wood
+
+ def wood_start do
+ %{
+ resources: %{
+ Catnip.name() => 100.0,
+ Wood.name() => 0.0
+ },
+ buildings: %{
+ Field.name() => 10.0
+ },
+ stats: %{
+ max_resources: %{Catnip.name() => 100.0}
+ }
+ }
+ end
+end
M lib/kitty_village/resource.ex => lib/kitty_village/resource.ex +8 -3
@@ 1,5 1,5 @@
defmodule KittyVillage.Resource do
- @type resource_name :: :catnip
+ @type resource_name :: :catnip | :wood
@doc """
The name of the resource
@@ 13,13 13,18 @@ defmodule KittyVillage.Resource do
"#{resource_name}: #{state.resources[resource_name]}"
end
- def add_one(resource, %{resources: resources, stats: %{max_resources: max_resources}} = model) do
- resources = Map.put(resources, resource.name(), resources[resource.name()] + 1)
+ def add(%{resources: resources, stats: %{max_resources: max_resources}} = model, resource, amt \\ 1) do
+ resources = Map.put(resources, resource.name(), resources[resource.name()] + amt)
max_resources = update_max_resources(resource, resources[resource.name()], max_resources)
%{model | resources: resources, stats: %{model.stats | max_resources: max_resources}}
end
+ @doc "Remove a single resource"
+ def sub(state, resource, amt \\ 1) do
+ put_in(state.resources[resource.name()], state.resources[resource.name()] - amt)
+ end
+
defp update_max_resources(resource, current_count, max_resources) do
count =
if current_count > max_resources[resource.name()] do
A lib/kitty_village/resource/wood.ex => lib/kitty_village/resource/wood.ex +5 -0
@@ 0,0 1,5 @@
+defmodule KittyVillage.Resource.Wood do
+ @behaviour KittyVillage.Resource
+
+ def name, do: :wood
+end
A lib/kitty_village/resource_conversion.ex => lib/kitty_village/resource_conversion.ex +36 -0
@@ 0,0 1,36 @@
+defmodule KittyVillage.ResourceConversion do
+ @moduledoc """
+ A behaviour for a conversion from one Resource to another
+ """
+
+ alias KittyVillage.Game
+ alias KittyVillage.Resource
+
+ @callback from :: Resource
+ @callback to :: Resource
+
+ @doc "the minimum of the from resource that must be available"
+ @callback minimum :: number
+
+ @doc "the conversion ratio"
+ @callback ratio(Game.state()) :: number
+
+ def convert(state, conversion, amt \\ 1) do
+ if conversion.minimum() > amt do
+ state
+ else
+ from = conversion.from()
+ to = conversion.to()
+ ratio = conversion.ratio(state)
+
+ from_amt = state.resources[from.name()]
+ to_amt = conversion.minimum() * ratio
+
+ if from_amt > conversion.minimum() do
+ state |> Resource.sub(from, conversion.minimum()) |> Resource.add(to, to_amt)
+ else
+ state
+ end
+ end
+ end
+end
A lib/kitty_village/resource_conversion/catnip_to_wood.ex => lib/kitty_village/resource_conversion/catnip_to_wood.ex +17 -0
@@ 0,0 1,17 @@
+defmodule KittyVillage.ResourceConversion.CatnipToWood do
+ @base_ratio 0.01
+ @behaviour KittyVillage.ResourceConversion
+
+ alias KittyVillage.Resource.Catnip
+ alias KittyVillage.Resource.Wood
+
+ def from, do: Catnip
+ def to, do: Wood
+
+ def minimum, do: 100
+
+ def ratio(_state) do
+ # this will eventually change based on certain research
+ @base_ratio
+ end
+end
M lib/kitty_village/view/building_button.ex => lib/kitty_village/view/building_button.ex +6 -5
@@ 1,15 1,16 @@
defmodule KittyVillage.View.BuildingButton do
import Ratatouille.View
+ alias KittyVillage.Game
alias KittyVillage.Building
- def render(button_name, activate, building, model) do
+ def render(button_name, activate, building) do
row do
column([size: 6], [
panel(
[
- color:
- if model.resources.catnip > Building.cost(building, model) do
+ background:
+ if Game.can_afford_building?(building) do
:white
else
:yellow
@@ 17,8 18,8 @@ defmodule KittyVillage.View.BuildingButton do
],
[
label(content: "#{button_name}(#{activate})"),
- label(content: cost_text(Building.cost(building, model))),
- label(content: "built: #{Building.count(building, model)}")
+ label(content: cost_text(Game.building_cost(building))),
+ label(content: "built: #{Game.building_count(building)}")
]
)
])
M mix.exs => mix.exs +2 -1
@@ 25,7 25,8 @@ defmodule KittyVillage.MixProject do
# {:dep_from_hexpm, "~> 0.3.0"},
# {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}
{:ratatouille, "~> 0.5.1"},
- {:focus, "~> 0.3.5"}
+ {:focus, "~> 0.3.5"},
+ {:parent, "~> 0.12.0"}
]
end
end
M mix.lock => mix.lock +2 -0
@@ 3,5 3,7 @@
"elixir_make": {:hex, :elixir_make, "0.6.2", "7dffacd77dec4c37b39af867cedaabb0b59f6a871f89722c25b28fcd4bd70530", [:mix], [], "hexpm", "03e49eadda22526a7e5279d53321d1cced6552f344ba4e03e619063de75348d9"},
"ex_termbox": {:hex, :ex_termbox, "1.0.2", "30cb94c2585e28797bedfc771687623faff75ab0eb77b08b3214181062bfa4af", [:make, :mix], [{:elixir_make, "~> 0.4", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "ca7b14d1019f96466a65ba08bd6cbf46e8b16f87339ef0ed211ba0641f304807"},
"focus": {:hex, :focus, "0.3.5", "ba9f2c3cc6ea9398db66ebbb859ba22d91341d64f6f771e87f7679b5bff1c6a1", [:mix], [], "hexpm", "962390084c8fffc134bb23e38dbd51da63e7e05462fd91ca16be478b513fc982"},
+ "parent": {:hex, :parent, "0.12.0", "e7d4f144fdb041cd637acb28a8a7680d23e48407e14a7b91b70da2a87c694b96", [:mix], [{:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "912cfdb2d6dae45361065afa31c20753bf3827228272fcb1d7d2538bce157946"},
"ratatouille": {:hex, :ratatouille, "0.5.1", "0f80009fa9534e257505bfe06bff28e030b458d4a33ec2427f7be34a6ef1acf7", [:mix], [{:asciichart, "~> 1.0", [hex: :asciichart, repo: "hexpm", optional: false]}, {:ex_termbox, "~> 1.0", [hex: :ex_termbox, repo: "hexpm", optional: false]}], "hexpm", "b2394eb1cc662eae53ae0fb7c27c04543a6d2ce11ab6dc41202c5c4090cbf652"},
+ "telemetry": {:hex, :telemetry, "0.4.2", "2808c992455e08d6177322f14d3bdb6b625fbcfd233a73505870d8738a2f4599", [:rebar3], [], "hexpm", "2d1419bd9dda6a206d7b5852179511722e2b18812310d304620c7bd92a13fcef"},
}