From 23faadef32e3bb9fb2bcfc8ee7cad87393f4b6e5 Mon Sep 17 00:00:00 2001 From: Sam Marshall Date: Sat, 6 Feb 2021 22:07:29 +0000 Subject: [PATCH] convert game into a GenServer --- lib/kitty_village.ex | 49 ++++-- lib/kitty_village/building.ex | 16 +- lib/kitty_village/game.ex | 162 ++++++++++++++++-- lib/kitty_village/resource.ex | 11 +- lib/kitty_village/resource/wood.ex | 5 + lib/kitty_village/resource_conversion.ex | 36 ++++ .../resource_conversion/catnip_to_wood.ex | 17 ++ lib/kitty_village/view/building_button.ex | 11 +- mix.exs | 3 +- mix.lock | 2 + 10 files changed, 264 insertions(+), 48 deletions(-) create mode 100644 lib/kitty_village/resource/wood.ex create mode 100644 lib/kitty_village/resource_conversion.ex create mode 100644 lib/kitty_village/resource_conversion/catnip_to_wood.ex diff --git a/lib/kitty_village.ex b/lib/kitty_village.ex index f3c10be..df06a75 100644 --- a/lib/kitty_village.ex +++ b/lib/kitty_village.ex @@ -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 diff --git a/lib/kitty_village/building.ex b/lib/kitty_village/building.ex index 2f1fb7e..f613302 100644 --- a/lib/kitty_village/building.ex +++ b/lib/kitty_village/building.ex @@ -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 diff --git a/lib/kitty_village/game.ex b/lib/kitty_village/game.ex index ddc935d..9fda904 100644 --- a/lib/kitty_village/game.ex +++ b/lib/kitty_village/game.ex @@ -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 diff --git a/lib/kitty_village/resource.ex b/lib/kitty_village/resource.ex index 9b23e06..51fad30 100644 --- a/lib/kitty_village/resource.ex +++ b/lib/kitty_village/resource.ex @@ -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 diff --git a/lib/kitty_village/resource/wood.ex b/lib/kitty_village/resource/wood.ex new file mode 100644 index 0000000..22dc37d --- /dev/null +++ b/lib/kitty_village/resource/wood.ex @@ -0,0 +1,5 @@ +defmodule KittyVillage.Resource.Wood do + @behaviour KittyVillage.Resource + + def name, do: :wood +end diff --git a/lib/kitty_village/resource_conversion.ex b/lib/kitty_village/resource_conversion.ex new file mode 100644 index 0000000..6dba1c0 --- /dev/null +++ b/lib/kitty_village/resource_conversion.ex @@ -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 diff --git a/lib/kitty_village/resource_conversion/catnip_to_wood.ex b/lib/kitty_village/resource_conversion/catnip_to_wood.ex new file mode 100644 index 0000000..dabb1d5 --- /dev/null +++ b/lib/kitty_village/resource_conversion/catnip_to_wood.ex @@ -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 diff --git a/lib/kitty_village/view/building_button.ex b/lib/kitty_village/view/building_button.ex index f317927..5c8d9c5 100644 --- a/lib/kitty_village/view/building_button.ex +++ b/lib/kitty_village/view/building_button.ex @@ -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)}") ] ) ]) diff --git a/mix.exs b/mix.exs index befb54e..406ff09 100644 --- a/mix.exs +++ b/mix.exs @@ -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 diff --git a/mix.lock b/mix.lock index e736ef4..bd01201 100644 --- a/mix.lock +++ b/mix.lock @@ -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"}, } -- 2.45.2