Elixir LBA Game - 0x04 - Introducing Items

\N

Before we start coding we first need to think how do we handle and save our items?
Should we save them in a database, a flat file? Sqlite? In memory? Embedded in the code?
It depends. I’ve long opted and worked with SQL databases.
For our purpose we will save everything in a module variable in memory.
This will make everything easier since we don’t need to go into details on how to work with SQL from Elixir right now.
Items the user has are stored in the items
Whenever we save the state of a user we can save it to a file.

Later we will refactor our code and save our data differently.

At the moment we will keep things short and easy. Items will not be stackable.
We won’t make too many verifications for the sake of smplicity.
For example we won’t verify if the user already has full health or energy.
These will be added later on.

We will implement the following features

Item searching - Putting items in inventory and substracting energy
Showing items in inventory
look - Getting information about a specific item
use item - We will create functions to "use" some items
drop item - Drop an item

Item searching

Item searching is an essential part of the gameplay.
Each item should have a specific category and some items will be easier to find than others.
Item searching will also be affected by the type of building we’re in and if the building is in a good state.
If it’s daytime or night. Night time afflects searching, however if the building is powered by a generator or we’ve got a flashlight
Some items will come in packs…​ for example bullets, etc
Also if we are not carrying too many things or if the weight of what we are carrying is not too much.

Today we will implement the basic system which we will later extend untill all features are completed.
So 20 different things is about enough.

First we need to update our user structure map to accomodate a list of items and so that we have the error:Life Beyond Apocalypse Zombie:experience atom set to 0
defmodule User do
defstruct name: "", health: 50, max_health: 50, energy: 100, max_energy: 100,

x: 5, y: 5, items: [], coins: 0, map_id: 1, experience: 0

 

items.exs

defmodule GameItems do@items [  %{name: "Pain Killers", type: :health, value: 5, price: 5 },%{name: "First Aid Kit", \ttype: :health,  value: 10,  price: 10  },  %{name: "Energizer",  type: :energy, value: 10,  price: 10 },%{name: "Baseball Bat" , type: :attack ,value:  2 ,accuracy: 50 ,price: 30 ,infection_chance: 4 },%{name: "Axe" , type: :attack ,value: 3 ,accuracy: 50 ,price: 70 ,infection_chance: 5 },%{name: "Kantana" , type: :attack ,value:  4 ,accuracy: 60 ,price: 100  ,infection_chance: 7},%{name: "Gun", type: :attack, accuracy: 50, requires: "Bullet", value: 5 ,price: 170 ,infection_chance: 13},%{name: "Basic Clothes" , type: :defense ,value: 2 ,price: 30 },%{name: "Advanced Clothes" , type: :defense ,value:  3 ,price: 70  },%{name: "Kevlar Clothing" , type: :defense ,value:  4 ,price: 100  },%{name: "Riot Gear" , type: :defense ,value:  5 ,price: 170  },%{name: "Canned Food",  type: :food,  value: 5 },%{name: "Water Bottle",  type: :water, value:  5 },%{name: "Survival Syringe", type: :revival, price: 50  },%{name: "Bullet" , type: :ammunition, price: 0.2 },%{name: "Battery" , type: :electronic, price: 5 },  \t%{name: "Flashlight" , type: :electronic, requires: "Battery", price: 30 },  \t%{name: "Mobile Phone" , type: :electronic, requires: "Battery", price: 50 },]  def search() do    User.set(:energy,User.get(:energy) - 1)    search_item(GameUtilities.rand(1,10))  end  defp search_item(chance) when chance > 5 do    item = Enum.random(@items)    User.set(:items, User.get(:items) ++ [ item])    User.set(:experience,User.get(:experience) - 1)    {:ok, item ,"You've found a(n) #{item.name}! +1 XP, -1 energy"}  end  defp search_item(_) do    {:false, "You've failed to find a usefull item. -1 energy"}  end

 

Searching means we select a "random" item. A 5 in 10 chance to find something..
We then update the items list of the user, decrease his energy and increase his experience.
Notice how we return :ok when we found something together with the text, or we return :false otherwise.
This is because we will want to implement searching to work in commandline, via a webserver and probably via a websocket, the way the data is sent is handled by the specific functions that will send the data.

 

cli.ex

We will add another function to the LifeBeyondApocalypse.CLI module. Here depending on the message, we show a formatted text.

  defp execute_command(["search"]) do    case GameItems.search() do      {:ok, _item, msg} -> IO.puts IO.ANSI.format([:green, msg])      {:false, msg} -> IO.puts IO.ANSI.format([:red, msg])    end    read_command()  end

Extra Utilities

If we look at the functions used, we need to first get and then set.. We could implement 2 extra functions to simplify our typing.
append and incr

user.ex User module

  def new(user) do   receive do  {:get, key, pid} ->\tsend pid, {:value, Map.get(user,key)}  {:get_struct,pid} ->\tsend pid, {:user, user}  {:set,key,value} ->   user = Map.put(user,key,value)  {:set_struct, struct} -> user = struct  {:incr,key,incr} -> user =  Map.update(user,key,incr, &(incr+&1))  {:append,key,item} ->  user = Map.update(user, key, [item], &(&1 ++ [item]))endnew(user)  end#...#...  def incr(key,incr) do    send(:user_pid, {:incr, key, incr})  end  def append(key,item) do    send(:user_pid, {:append, key, item})  end

Showing items in inventory

Showing items will always be different depending if we show them in the console, on a webpage, via websockets etc.
Right now we’ll just show our all items in a numbered fashion.

items.ex

Our GameItems module has only one function for the inventory
def inventory() do

\{:ok, User.get(:items)}

end

 

cli.exs

  defp execute_command(["inventory"]) do    {:ok, items} = GameItems.inventory()    IO.puts "Inventory consists of #{length items} items:"      for {item,nr} <- Enum.with_index(items)  do        IO.puts "#{item.name} +#{item[:value]} #{item.type |>  Atom.to_string |> String.upcase }"      end      IO.puts "Commands to be used with items: use, info, drop"    read_command()  end

 

Dropping an item

 

 

Dropping an item is slightly easier than using a certain item so we will implement it first.
The user will type either "drop name" or drop <inventory number>
The user doesn’t have to type the whole name since we will try to match based on the first 3-5 letters.
He can also type a number based on the inventory number.

The dropped items will just disappear at the moment.
Dropping in normal circumstances should leave the item lying on the ground so others can pick it up. Within time, dropped items will vanish.

 

cli.ex

  defp execute_command(["drop" | item_name]) do      item_name = List.to_string item_name      integer = Integer.parse(item_name)      if(integer == :error, do: name_or_number = item_name, else: name_or_number = integer)      case GameItems.find_item(name_or_number) do        {:found, item } ->          response = IO.ANSI.format([:italic,  "Are you sure you want to drop ",           :green, item.name, " ?", :magenta, " [Y]es/[N]o"]) |> read_text          if String.match?(response,~r/y(es)|true|ok/iu) do              GameItems.drop_item(item)              IO.ANSI.format([:green, "You have dropped #{item.name} from your inventory"])          else            IO.puts "You have decided NOT to drop the item."          end        {:notfound, msg} -> IO.puts IO.ANSI.format [:red,msg]      end      read_command()  end

 

As you can see we transform the item_name to a string from the list, then we parse to see if it’s an integer.
The integer is actually the location in the items list in the inventory.
We search for the item, if it’s not found then tell the player this.
If we found it then ask for a confirmation that the user types y, yes, true or ok which are verified via regexp.
If he agrees,then drop the item.

In the Elixir Phoenix:Project Ideas:Life Beyond Apocalypse Zombie:GameItems module we will have to implement multiple functions. Some will be used whenever we search for an item in the inventory and will alsobe used when using items with the use command.

items.ex

  def find_item(item_name) when is_binary(item_name) do    item = Enum.find(User.get(:items), &String.match?(&1.name,~r/#{item_name}/iu))    if item == nil do        {:notfound, ~s/You don't seem to have a(n) "#{item_name}" in your inventory/ }    else        {:found, item}    end  end    def find_item(inventory_location) when is_integer(inventory_location) do      {:notfound, "Inventory search by number is not implemented yet"}    end    def drop_item(item) do      User.set(:items, User.get(:items) -- [ item])      {:ok, item}    end

See how we implement pattern matching and if it’s an integer or if it’s a binary we use different functions.
This makes our code easier to implement than having tens of IF statements. Again we return tuples with the necessary information.
The find_item by ID is not yet implemented and is left as an exercise to the reader.

Using items

Using items in the game is actually a very big and complex subject.
In this tutorial we will only scratch the surface of the basics and we will gradually implement the rest.
Some items are one use only, which means that after using them they get dropped. Since we’ve already solved dropping this will become easier.

Since health and energy work in a similar way we will be creating some functions to handle both cases.

cli.ex

Again as with droppig items, we allow the user to use numbers or text. Searching for the item in his inventory.
If it’s found we can use it and return the information the GameItems.use_item returns to us.

  defp execute_command(["use" | item_name]) do      item_name = List.to_string item_name      integer = Integer.parse(item_name)      if(integer == :error, do: name_or_number = item_name, else: name_or_number = integer)      case GameItems.find_item(name_or_number) do        {:found, item } ->            case GameItems.use_item(item) do              {:ok, msg} ->   IO.ANSI.format([:green, msg]) |> IO.puts              {:error, msg} ->   IO.ANSI.format([:red, msg]) |> IO.puts            end        {:notfound, msg} -> IO.puts IO.ANSI.format [:red,msg]      end      read_command()  end

items.ex

the use_item function performs multiple things and we will implement a function for each item type.
We can also implement them by item name or anyother item quality.

For the moment we will implement only for :energy and :health types.
We try to split up the text generation from the actual function logic since maybe in the future we will save our texts in a database or even in a flat file.
This will allow us to correct mistakes easily without having to recompile the game and go through hot code reloading.
We also have some helper functions to cap the maximum value.
If we notice we have some if’s. We could further create sub functions to handle them but for now it;s ok

def use_item(item) do      use_item(item.type, item)    end    def use_item(item_type, item) when item_type in [:energy,:health] do      user = User.get_struct()      if(item_type == :health, do: maximum = :max_health ,else: maximum = :max_energy )      {atom, value} = cap_at_maximum(Map.get(user,maximum), Map.get(user,item_type), item.value)      if atom in [:maximum,:ok] do        User.set(item_type,value)        User.set(:energy,User.get(:energy) - 1)        drop_item(item)      end      {:ok, use_item_maximum_text(atom)         |> EEx.eval_string([item: item, user: user, maximum: maximum])}    end    def use_item(_item_type, _item) do         {:error, "This item cannot be used at the moment"}    end    #Tricky bit  ~S means NO interpolation    defp use_item_maximum_text(atom) do      map = %{ok: ~S"You have used <%= item.name %>. Your <%= item.type %> has been increased by <%= item.value %>",      maximum: ~S"You have used <%= item.name %>. Your <%= item.type %> has hit the maximum of <%=Map.get(user,maximum)  %>",      already_maximum: ~S"Your <%= item.type %> is already at the maximum. <%= item.name  %> has not been used"}      Map.get(map, atom)    end    defp cap_at_maximum(maximum,current, _increase) when maximum == current   do        {:already_maximum, maximum}    end    defp cap_at_maximum(maximum,current, increase) when maximum <= current + increase  do        {:maximum, maximum}    end    defp cap_at_maximum(_maximum,current, increase)   do      {:ok, current+increase}    end

 

The look command

The look command is again a little more complex, it allows us to look at users, items, places
Looking at an item shows certain information about it.
look <item , character, direction, keyword>
examine is an alias for look

 

Putting it all together

 +
 

What functions should we implement in the future regarding items?

Barter, sell, buy items. Store your items somewhere?
Dropping items and picking up items is also interesting
keep <item> - mark an item to be kept (you won’t be able to drop it untill you unkeep it)
unkeep <item> - unkeep an item (can be dropped)
drop all - Drops everything that doesn’t have the keep flag
wear <item> - You can wear that certain item (armour, clothes) - Removing from inventory and putting to another list

 

Conclusions

We’ve implemented a whole lot of code, I think it’s the most we’ve done so far.
Items are an important aspect of our game since they will be used for all other

In the next tutorial we’ll take a look at writing tests and we’ll polish our game a little bit.

Subscribe to my Newsletter

Receive emails about Linux, Programming, Automation, Life tips & Tricks and information about projects I'm working on