~sjm/kitties_village

23faadef32e3bb9fb2bcfc8ee7cad87393f4b6e5 — Sam Marshall 4 months ago 421dca1 master
convert game into a GenServer
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"},
}