bread-and-potions

This commit is contained in:
Danil Negrienko 2024-03-06 21:25:12 -05:00
parent af3b5c40f8
commit eab9b54f20
11 changed files with 459 additions and 0 deletions

View File

@ -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."
}

View File

@ -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}

View File

@ -0,0 +1,4 @@
# Used by "mix format"
[
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
]

24
elixir/bread-and-potions/.gitignore vendored Normal file
View File

@ -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

View File

@ -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/<FILE>.exs:LINENUM` - runs only a single test, the test from `<FILE>.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.

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,2 @@
ExUnit.start()
ExUnit.configure(exclude: :pending, trace: true, seed: 0)