From eab9b54f20ebe8cf8b03e0b9af0446bcb5042955 Mon Sep 17 00:00:00 2001 From: Danylo Negrienko Date: Wed, 6 Mar 2024 21:25:12 -0500 Subject: [PATCH] bread-and-potions --- .../bread-and-potions/.exercism/config.json | 21 +++ .../bread-and-potions/.exercism/metadata.json | 1 + elixir/bread-and-potions/.formatter.exs | 4 + elixir/bread-and-potions/.gitignore | 24 ++++ elixir/bread-and-potions/HELP.md | 75 ++++++++++ elixir/bread-and-potions/HINTS.md | 31 ++++ elixir/bread-and-potions/README.md | 82 +++++++++++ elixir/bread-and-potions/lib/rpg.ex | 58 ++++++++ elixir/bread-and-potions/mix.exs | 28 ++++ elixir/bread-and-potions/test/rpg_test.exs | 133 ++++++++++++++++++ elixir/bread-and-potions/test/test_helper.exs | 2 + 11 files changed, 459 insertions(+) create mode 100644 elixir/bread-and-potions/.exercism/config.json create mode 100644 elixir/bread-and-potions/.exercism/metadata.json create mode 100644 elixir/bread-and-potions/.formatter.exs create mode 100644 elixir/bread-and-potions/.gitignore create mode 100644 elixir/bread-and-potions/HELP.md create mode 100644 elixir/bread-and-potions/HINTS.md create mode 100644 elixir/bread-and-potions/README.md create mode 100644 elixir/bread-and-potions/lib/rpg.ex create mode 100644 elixir/bread-and-potions/mix.exs create mode 100644 elixir/bread-and-potions/test/rpg_test.exs create mode 100644 elixir/bread-and-potions/test/test_helper.exs diff --git a/elixir/bread-and-potions/.exercism/config.json b/elixir/bread-and-potions/.exercism/config.json new file mode 100644 index 0000000..57ee829 --- /dev/null +++ b/elixir/bread-and-potions/.exercism/config.json @@ -0,0 +1,21 @@ +{ + "authors": [ + "angelikatyborska" + ], + "contributors": [ + "neenjaw" + ], + "files": { + "solution": [ + "lib/rpg.ex" + ], + "test": [ + "test/rpg_test.exs" + ], + "exemplar": [ + ".meta/exemplar.ex" + ] + }, + "language_versions": ">=1.10", + "blurb": "Learn about protocols by developing your own role-playing video game." +} diff --git a/elixir/bread-and-potions/.exercism/metadata.json b/elixir/bread-and-potions/.exercism/metadata.json new file mode 100644 index 0000000..15942dd --- /dev/null +++ b/elixir/bread-and-potions/.exercism/metadata.json @@ -0,0 +1 @@ +{"track":"elixir","exercise":"bread-and-potions","id":"67297c81721e4ff98b3b7d962328ddfd","url":"https://exercism.org/tracks/elixir/exercises/bread-and-potions","handle":"negrienko","is_requester":true,"auto_approve":false} \ No newline at end of file diff --git a/elixir/bread-and-potions/.formatter.exs b/elixir/bread-and-potions/.formatter.exs new file mode 100644 index 0000000..d2cda26 --- /dev/null +++ b/elixir/bread-and-potions/.formatter.exs @@ -0,0 +1,4 @@ +# Used by "mix format" +[ + inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] +] diff --git a/elixir/bread-and-potions/.gitignore b/elixir/bread-and-potions/.gitignore new file mode 100644 index 0000000..d7c8d40 --- /dev/null +++ b/elixir/bread-and-potions/.gitignore @@ -0,0 +1,24 @@ +# The directory Mix will write compiled artifacts to. +/_build/ + +# If you run "mix test --cover", coverage assets end up here. +/cover/ + +# The directory Mix downloads your dependencies sources to. +/deps/ + +# Where third-party dependencies like ExDoc output generated docs. +/doc/ + +# Ignore .fetch files in case you like to edit your project deps locally. +/.fetch + +# If the VM crashes, it generates a dump, let's ignore it too. +erl_crash.dump + +# Also ignore archive artifacts (built via "mix archive.build"). +*.ez + +# Ignore package tarball (built via "mix hex.build"). +numbers-*.tar + diff --git a/elixir/bread-and-potions/HELP.md b/elixir/bread-and-potions/HELP.md new file mode 100644 index 0000000..341edef --- /dev/null +++ b/elixir/bread-and-potions/HELP.md @@ -0,0 +1,75 @@ +# Help + +## Running the tests + +From the terminal, change to the base directory of the exercise then execute the tests with: + +```bash +$ mix test +``` + +This will execute the test file found in the `test` subfolder -- a file ending in `_test.exs` + +Documentation: + +* [`mix test` - Elixir's test execution tool](https://hexdocs.pm/mix/Mix.Tasks.Test.html) +* [`ExUnit` - Elixir's unit test library](https://hexdocs.pm/ex_unit/ExUnit.html) + +## Pending tests + +In test suites of practice exercises, all but the first test have been tagged to be skipped. + +Once you get a test passing, you can unskip the next one by commenting out the relevant `@tag :pending` with a `#` symbol. + +For example: + +```elixir +# @tag :pending +test "shouting" do + assert Bob.hey("WATCH OUT!") == "Whoa, chill out!" +end +``` + +If you wish to run all tests at once, you can include all skipped test by using the `--include` flag on the `mix test` command: + +```bash +$ mix test --include pending +``` + +Or, you can enable all the tests by commenting out the `ExUnit.configure` line in the file `test/test_helper.exs`. + +```elixir +# ExUnit.configure(exclude: :pending, trace: true) +``` + +## Useful `mix test` options + +* `test/.exs:LINENUM` - runs only a single test, the test from `.exs` whose definition is on line `LINENUM` +* `--failed` - runs only tests that failed the last time they ran +* `--max-failures` - the suite stops evaluating tests when this number of test failures +is reached +* `--seed 0` - disables randomization so the tests in a single file will always be ran +in the same order they were defined in + +## Submitting your solution + +You can submit your solution using the `exercism submit lib/rpg.ex` command. +This command will upload your solution to the Exercism website and print the solution page's URL. + +It's possible to submit an incomplete solution which allows you to: + +- See how others have completed the exercise +- Request help from a mentor + +## Need to get help? + +If you'd like help solving the exercise, check the following pages: + +- The [Elixir track's documentation](https://exercism.org/docs/tracks/elixir) +- The [Elixir track's programming category on the forum](https://forum.exercism.org/c/programming/elixir) +- [Exercism's programming category on the forum](https://forum.exercism.org/c/programming/5) +- The [Frequently Asked Questions](https://exercism.org/docs/using/faqs) + +Should those resources not suffice, you could submit your (incomplete) solution to request mentoring. + +If you're stuck on something, it may help to look at some of the [available resources](https://exercism.org/docs/tracks/elixir/resources) out there where answers might be found. \ No newline at end of file diff --git a/elixir/bread-and-potions/HINTS.md b/elixir/bread-and-potions/HINTS.md new file mode 100644 index 0000000..0dc2042 --- /dev/null +++ b/elixir/bread-and-potions/HINTS.md @@ -0,0 +1,31 @@ +# Hints + +## General + +- Read about protocols in the [official Getting Started guide][getting-started-protocols] or on [elixirschool.com][elixir-school-protocols]. + +## 1. Define edibility + +- There is a [special macro][kernel-defprotocol] for defining protocols. +- Protocols consist of one or more function headers. +- A function header is like a function definition, but without the `do` block. It defines the function name and its arguments, but not what it does. + +## 2. Make loaves of bread edible + +- There is a [special macro][kernel-defimpl] for implementing a protocol for a given data type. +- Implementing a protocol means writing a function definition for each function header from the protocol. + +## 3. Make mana potions edible + +- There is a [special macro][kernel-defimpl] for implementing a protocol for a given data type. +- Implementing a protocol means writing a function definition for each function header from the protocol. + +## 4. Make poisons edible + +- There is a [special macro][kernel-defimpl] for implementing a protocol for a given data type. +- Implementing a protocol means writing a function definition for each function header from the protocol. + +[getting-started-protocols]: https://hexdocs.pm/elixir/protocols.html +[elixir-school-protocols]: https://elixirschool.com/en/lessons/advanced/protocols/ +[kernel-defprotocol]: https://hexdocs.pm/elixir/Kernel.html#defprotocol/2 +[kernel-defimpl]: https://hexdocs.pm/elixir/Kernel.html#defimpl/3 \ No newline at end of file diff --git a/elixir/bread-and-potions/README.md b/elixir/bread-and-potions/README.md new file mode 100644 index 0000000..0610a86 --- /dev/null +++ b/elixir/bread-and-potions/README.md @@ -0,0 +1,82 @@ +# Bread And Potions + +Welcome to Bread And Potions on Exercism's Elixir Track. +If you need help running the tests or submitting your code, check out `HELP.md`. +If you get stuck on the exercise, check out `HINTS.md`, but try and solve it without using those first :) + +## Introduction + +## Protocols + +Protocols are a mechanism to achieve polymorphism in Elixir when you want behavior to vary depending on the data type. + +Protocols are defined using `defprotocol` and contain one or more function headers. + +```elixir +defprotocol Reversible do + def reverse(term) +end +``` + +Protocols can be implemented using `defimpl`. + +```elixir +defimpl Reversible, for: List do + def reverse(term) do + Enum.reverse(term) + end +end +``` + +A protocol can be implemented for any existing Elixir data type or for a struct. + +When a protocol function is invoked, the appropriate implementation gets automatically chosen based on the type of the first argument. + +## Instructions + +You're developing your own role-playing video game. In your game, there are _characters_ and _items_. One of the many actions that you can do with an item is to make a character eat it. + +Not all items are edible, and not all edible items have the same effects on the character. Some items, when eaten, turn into a different item (e.g. if you eat an apple, you are left with an apple core). + +To allow for all that flexibility, you decided to create an `Edible` protocol that some of the items can implement. + +## 1. Define edibility + +Create the `RPG.Edible` protocol. The protocol has one function - `eat`. The `eat` function accepts an item and a character and returns a by-product and a character. + +## 2. Make loaves of bread edible + +Implement the `RPG.Edible` protocol for the `RPG.LoafOfBread` item. When eaten, a loaf of bread gives the character 5 health points and has no by-product. + +```elixir +RPG.Edible.eat(%RPG.LoafOfBread{}, %RPG.Character{health: 31}) +# => {nil, %RPG.Character{health: 36, mana: 0}} +``` + +## 3. Make mana potions edible + +Implement the `RPG.Edible` protocol for the `RPG.ManaPotion` item. When eaten, a mana potion gives the character as many mana points as the potion's strength, and produces an empty bottle. + +```elixir +RPG.Edible.eat(%RPG.ManaPotion{strength: 13}, %RPG.Character{mana: 50}) +# => {%RPG.EmptyBottle{}, %RPG.Character{health: 100, mana: 63}} +``` + +## 4. Make poisons edible + +Implement the `RPG.Edible` protocol for the `RPG.Poison` item. When eaten, a poison takes away all the health points from the character, and produces an empty bottle. + +```elixir +RPG.Edible.eat(%RPG.Poison{}, %RPG.Character{health: 3000}) +# => {%RPG.EmptyBottle{}, %RPG.Character{health: 0, mana: 0}} +``` + +## Source + +### Created by + +- @angelikatyborska + +### Contributed to by + +- @neenjaw \ No newline at end of file diff --git a/elixir/bread-and-potions/lib/rpg.ex b/elixir/bread-and-potions/lib/rpg.ex new file mode 100644 index 0000000..499c4e9 --- /dev/null +++ b/elixir/bread-and-potions/lib/rpg.ex @@ -0,0 +1,58 @@ +defmodule RPG do + defmodule Character do + defstruct health: 100, mana: 0 + end + + defmodule LoafOfBread do + defstruct [] + end + + defmodule ManaPotion do + defstruct strength: 10 + end + + defmodule Poison do + defstruct [] + end + + defmodule EmptyBottle do + defstruct [] + end + + # Add code to define the protocol and its implementations below here... + defprotocol Edible do + @moduledoc """ + Protocol definition for interaction between Characters and Items + """ + @type item() :: LoafOfBread.t() | ManaPotion.t() | Poison.t() | EmptyBottle.t() + @spec eat(item(), Character.t()) :: {nil | item(), Character.t()} + def eat(item, character) + end + + defimpl Edible, for: LoafOfBread do + @moduledoc """ + Edible implimentation for LoafOfBread + """ + @spec eat(LoafOfBread.t(), Character.t()) :: {nil, Character.t()} + def eat(%LoafOfBread{}, %Character{health: health} = character), + do: {nil, %{character | health: health + 5}} + end + + defimpl Edible, for: ManaPotion do + @moduledoc """ + Edible implimentation for ManaPotion + """ + @spec eat(ManaPotion.t(), Character.t()) :: {EmptyBottle.t(), Character.t()} + def eat(%ManaPotion{strength: strength}, %Character{mana: mana} = character), + do: {%EmptyBottle{}, %{character | mana: mana + strength}} + end + + defimpl Edible, for: Poison do + @moduledoc """ + Edible implimentation for Poison + """ + @spec eat(Poison.t(), Character.t()) :: {EmptyBottle.t(), Character.t()} + def eat(%Poison{}, %Character{} = character), + do: {%EmptyBottle{}, %{character | health: 0}} + end +end diff --git a/elixir/bread-and-potions/mix.exs b/elixir/bread-and-potions/mix.exs new file mode 100644 index 0000000..c8fb4bb --- /dev/null +++ b/elixir/bread-and-potions/mix.exs @@ -0,0 +1,28 @@ +defmodule RPG.MixProject do + use Mix.Project + + def project do + [ + app: :bread_and_potions, + version: "0.1.0", + # elixir: "~> 1.10", + start_permanent: Mix.env() == :prod, + deps: deps() + ] + end + + # Run "mix help compile.app" to learn about applications. + def application do + [ + extra_applications: [:logger] + ] + end + + # Run "mix help deps" to learn about dependencies. + defp deps do + [ + # {:dep_from_hexpm, "~> 0.3.0"}, + # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"} + ] + end +end diff --git a/elixir/bread-and-potions/test/rpg_test.exs b/elixir/bread-and-potions/test/rpg_test.exs new file mode 100644 index 0000000..77da893 --- /dev/null +++ b/elixir/bread-and-potions/test/rpg_test.exs @@ -0,0 +1,133 @@ +defmodule RPGTest do + use ExUnit.Case + + alias RPG.{Edible, Character, LoafOfBread, ManaPotion, Poison, EmptyBottle} + + defmodule NewItem do + defstruct [] + end + + describe "Edible" do + @tag task_id: 1 + test "is a protocol" do + assert Edible.__protocol__(:functions) == [eat: 2] + end + + @tag task_id: 1 + test "cannot eat a completely new item" do + assert_raise Protocol.UndefinedError, fn -> + Edible.eat(%NewItem{}, %Character{}) + end + end + end + + describe "LoafOfBread" do + @tag task_id: 2 + test "implements the Edible protocol" do + {:consolidated, modules} = Edible.__protocol__(:impls) + assert LoafOfBread in modules + end + + @tag task_id: 2 + test "eating it increases health" do + character = %Character{health: 50} + {_byproduct, %Character{} = character} = Edible.eat(%LoafOfBread{}, character) + assert character.health == 55 + end + + @tag task_id: 2 + test "eating it has no byproduct" do + character = %Character{} + {byproduct, %Character{}} = Edible.eat(%LoafOfBread{}, character) + assert byproduct == nil + end + + @tag task_id: 2 + test "eating it does not affect mana" do + character = %Character{mana: 77} + {_byproduct, %Character{} = character} = Edible.eat(%LoafOfBread{}, character) + assert character.mana == 77 + end + end + + describe "ManaPotion" do + @tag task_id: 3 + test "implements the Edible protocol" do + {:consolidated, modules} = Edible.__protocol__(:impls) + assert ManaPotion in modules + end + + @tag task_id: 3 + test "eating it increases mana" do + character = %Character{mana: 10} + {_byproduct, %Character{} = character} = Edible.eat(%ManaPotion{strength: 6}, character) + assert character.mana == 16 + {_byproduct, %Character{} = character} = Edible.eat(%ManaPotion{strength: 9}, character) + assert character.mana == 25 + end + + @tag task_id: 3 + test "eating it produces an empty bottle" do + character = %Character{} + {byproduct, %Character{}} = Edible.eat(%ManaPotion{}, character) + assert byproduct == %EmptyBottle{} + end + + @tag task_id: 3 + test "eating it does not affect health" do + character = %Character{health: 4} + {_byproduct, %Character{} = character} = Edible.eat(%ManaPotion{strength: 6}, character) + assert character.health == 4 + end + end + + describe "Poison" do + @tag task_id: 4 + test "implements the Edible protocol" do + {:consolidated, modules} = Edible.__protocol__(:impls) + assert Poison in modules + end + + @tag task_id: 4 + test "eating it brings health down to 0" do + character = %Character{health: 120} + {_byproduct, %Character{} = character} = Edible.eat(%Poison{}, character) + assert character.health == 0 + end + + @tag task_id: 4 + test "eating it produces an empty bottle" do + character = %Character{} + {byproduct, %Character{}} = Edible.eat(%Poison{}, character) + assert byproduct == %EmptyBottle{} + end + + @tag task_id: 4 + test "eating it does not affect mana" do + character = %Character{mana: 99} + {_byproduct, %Character{} = character} = Edible.eat(%Poison{}, character) + assert character.mana == 99 + end + end + + @tag task_id: 4 + test "eating many things one after another" do + items = [ + %LoafOfBread{}, + %ManaPotion{strength: 10}, + %ManaPotion{strength: 2}, + %LoafOfBread{} + ] + + character = %Character{health: 100, mana: 100} + + character = + Enum.reduce(items, character, fn item, character -> + {_, character} = Edible.eat(item, character) + character + end) + + assert character.health == 110 + assert character.mana == 112 + end +end diff --git a/elixir/bread-and-potions/test/test_helper.exs b/elixir/bread-and-potions/test/test_helper.exs new file mode 100644 index 0000000..e8677a3 --- /dev/null +++ b/elixir/bread-and-potions/test/test_helper.exs @@ -0,0 +1,2 @@ +ExUnit.start() +ExUnit.configure(exclude: :pending, trace: true, seed: 0)