~sjm/kitties_village

7f359bb83c0430bd8257446b5db56436fa7b1f7a — Sam Marshall 4 months ago d2cf3c9
update with extra abstraction
M lib/kitty_village.ex => lib/kitty_village.ex +35 -12
@@ 1,5 1,5 @@
defmodule KittyVillage do
  @doc """
  @moduledoc """
  KittyVillage is the (hopefully temporary) TUI entrypoint for a kitties game
  """



@@ 7,19 7,38 @@ defmodule KittyVillage do

  import Ratatouille.View

  alias KittyVillage.Building
  alias KittyVillage.Resource

  alias KittyVillage.Building.Field

  alias KittyVillage.Resource.Catnip

  @type building_list :: %{Building.building_name() => number}
  @type resource_list :: %{Resource.resource_name() => number}
  @type state :: %{
          :resources => resource_list,
          :buildings => building_list,
          :stats => %{:max_resources => %{Catnip.name() => number}}
        }

  def init(_context),
    do: %{
      resources: %{
        catnip: 0
        Catnip.name() => 10.0
      },
      buildings: %{
        field: 0
        Field.name() => 0.0
      },
      stats: %{
        max_resources: %{Catnip.name() => 10.0}
      }
    }

  def update(model, msg) do
    case msg do
      {:event, %{ch: ?c}} -> increase_resource(model, :catnip)
      {:event, %{ch: ?c}} -> Resource.add_one(Catnip, model)
      {:event, %{ch: ?f}} -> KittyVillage.Building.buy(Field, model)
      _ -> model
    end
  end


@@ 27,13 46,17 @@ defmodule KittyVillage 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)
               ),
        column(
          [size: 4],
          render_resources(model.resources)
        ),
        column([size: 8], [
          panel([
            if KittyVillage.Building.Field.current_cost(1) < model.resources.catnip do
              label(content: "Catnip Field(f): #{model.buildings.field}")
            if Field.visible(model) do
              render_catnip_field(
                model.buildings.catnip_field,
                KittyVillage.Building.cost(Field, model)
              )
            end
          ])
        ])


@@ 43,11 66,11 @@ defmodule KittyVillage do

  defp render_resources(resources) do
    panel([
      label(content: "Catnip(c): #{resources.catnip}")
      label(content: "Catnip(c): #{Float.round(resources.catnip, 2)}")
    ])
  end

  defp increase_resource(model, :catnip) do
    update_in(model.resources.catnip, fn catnip -> catnip + 1 end)
  defp render_catnip_field(count, current_cost) do
    KittyVillage.View.BuildingButton.render("Catnip Field", "f", count, current_cost)
  end
end

M lib/kitty_village/building.ex => lib/kitty_village/building.ex +62 -8
@@ 1,20 1,74 @@
defmodule KittyVillage.Building do
  defstruct catnip_field: %{
              base_price: 10,
              price_ratio: 1.12,
              increase: {:catnip, 0.125},
              built: 0
            }
  @type building_name :: :catnip_field

  @doc """
  The name of this building
  """
  @callback name :: building_name

  @doc """
  Given the current count of buildings of this type, return the cost of the next building.
  """
  @callback current_cost(number) :: number
  @callback current_cost(number) :: [{KittyVillage.Resource.name(), number}]

  @doc """
  Determine the effects of this building, given the number of buildings
  """
  @callback effect(number) :: [KittyVillage.Effect.t]
  @callback effect(number) :: [KittyVillage.Effect.t()]

  def cost(b, %{buildings: buildings}) do
    cost(b, buildings)
  end

  def cost(b, buildings) do
    b.current_cost(buildings[b.name()])
  end

  @doc """
  can the village afford this building?
  """
  def available(b, %{resources: resources} ) do
    b.current_cost(0)
    |> ensure_cost(resources)
  end

  def buy(b, %{buildings: buildings} = model) do
    make_purchase(cost(b, buildings), model, b.name())
  end

  defp make_purchase(cost, %{resources: resources, buildings: buildings} = model, building_name) do
    if ensure_cost(cost, resources) do
      %{
        model
        | resources: deduct_cost(cost, resources),
          buildings: increment(buildings, building_name)
      }
    else
      model
    end
  end

  defp increment(buildings, building_name) do
    Map.put(buildings, building_name, buildings[building_name] + 1)
  end

  defp deduct_cost([], resources) do
    resources
  end

  defp deduct_cost([{resource, amount} | rest], resources) do
    deduct_cost(rest, Map.put(resources, resource, resources[resource] - amount))
  end

  defp ensure_cost([], _resources) do
    true
  end

  defp ensure_cost([{resource, amount} | rest], resources) do
    if resources[resource] >= amount do
      ensure_cost(rest, resources)
    else
      false
    end
  end
end

M lib/kitty_village/building/field.ex => lib/kitty_village/building/field.ex +11 -2
@@ 1,19 1,28 @@
defmodule KittyVillage.Building.Field do
  @behaviour KittyVillage.Building

  alias KittyVillage.Resource.Catnip

  @base_price 10
  @price_ratio 1.12
  @base_nip_increase 0.125

  @impl true
  def name, do: :catnip_field

  @impl true
  def current_cost(count) do
    :math.pow(@price_ratio, count) * @base_price
    [{Catnip.name(), :math.pow(@price_ratio, count) * @base_price}]
  end

  @impl true
  def effect(count) do
    [
      {:increase, :catnip, @base_nip_increase * count}
      {:increase, Catnip.name(), @base_nip_increase * count}
    ]
  end

  def visible(%{stats: %{ max_resources: %{ catnip: max }}}) do
    max <= current_cost(0)
  end
end

M lib/kitty_village/effect.ex => lib/kitty_village/effect.ex +1 -2
@@ 5,7 5,6 @@ defmodule KittyVillage.Effect do

  @type t :: increase_resource

  @type resource_name :: :catnip

  @type increase_resource :: {:increase, resource_name, number}
  @type increase_resource :: {:increase, KittyVillage.Resource.resource_name, number}
end

A lib/kitty_village/resource.ex => lib/kitty_village/resource.ex +26 -0
@@ 0,0 1,26 @@
defmodule KittyVillage.Resource do
  @type resource_name :: :catnip

  @doc """
  The name of the resource
  """
  @callback name :: resource_name

  def add_one(resource, %{resources: resources, stats: %{max_resources: max_resources}} = model) do
    resources = Map.put(resources, resource.name(), resources[resource.name()] + 1)
    max_resources = update_max_resources(resource, resources[resource.name()], max_resources)

    %{model | resources: resources, stats: %{model.stats | max_resources: max_resources}}
  end

  defp update_max_resources(resource, current_count, max_resources) do
    count =
      if current_count > max_resources[resource.name()] do
        current_count
      else
        max_resources[resource.name()]
      end

    Map.put(max_resources, resource.name(), count)
  end
end

A lib/kitty_village/resource/catnip.ex => lib/kitty_village/resource/catnip.ex +5 -0
@@ 0,0 1,5 @@
defmodule KittyVillage.Resource.Catnip do
  @behaviour KittyVillage.Resource

  def name, do: :catnip
end

A lib/kitty_village/view/building_button.ex => lib/kitty_village/view/building_button.ex +17 -0
@@ 0,0 1,17 @@
defmodule KittyVillage.View.BuildingButton do
  import Ratatouille.View

  def render(button_name, activate, count, cost) do
    row do
      column([size: 12], [
               label(content: "#{button_name}(#{activate})"),
               label(content: cost_text(cost)),
               label(content: "built: #{count}")
             ])
    end
  end

  defp cost_text([{:catnip, price}]) do
    "catnip: #{price}"
  end
end

M mix.exs => mix.exs +2 -1
@@ 24,7 24,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"}
      {:ratatouille, "~> 0.5.1"},
      {:focus, "~> 0.3.5"}
    ]
  end
end

M mix.lock => mix.lock +1 -0
@@ 2,5 2,6 @@
  "asciichart": {:hex, :asciichart, "1.0.0", "6ef5dbeab545cb7a0bdce7235958f129de6cd8ad193684dc0953c9a8b4c3db5b", [:mix], [], "hexpm", "edc475e4cdd317599310fa714dbc1f53485c32fc918e23e95f0c2bbb731f2ee2"},
  "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"},
  "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"},
}

A test/kitty_village/building/field_test.exs => test/kitty_village/building/field_test.exs +17 -0
@@ 0,0 1,17 @@
defmodule KittyVillage.Building.FieldTest do
  use ExUnit.Case

  test "fields have correct effect" do
    assert [{:increase, :catnip, 0.125 }] = KittyVillage.Building.Field.effect(1)
    assert [{:increase, :catnip, 0.250 }] = KittyVillage.Building.Field.effect(2)
  end

  test "cost scales correctly" do
    round_result =
      Lens.make_lens(1)
      |> Focus.fix_over(&Float.round(&1, 1))

    assert [catnip: 10.0] = Enum.map(KittyVillage.Building.Field.current_cost(0), round_result)
    assert [catnip: 11.2] = Enum.map(KittyVillage.Building.Field.current_cost(1), round_result)
  end
end

M test/kitty_village_test.exs => test/kitty_village_test.exs +0 -4
@@ 1,8 1,4 @@
defmodule KittyVillageTest do
  use ExUnit.Case
  doctest KittyVillage

  test "greets the world" do
    assert KittyVillage.hello() == :world
  end
end