• Bubble
  • Bubble
  • Line
Elixir Game - 0x06 Modifications to State - Modifying the User module and paving the way for our NPC
Andrei Clinciu Article AUthor
Andrei Clinciu
  • 2018-03-29T21:10:00Z
  • 8 min to read

Our User module works great for the purpose it has been created.

However we need to to prepare for the future when we'll implement our NPC's and battle system.
We will also probably need to store other details in a "global" repository in memory.
Yes, ETS works great for this, however that's for a future date.

We can copy paste the user module and modify it each time we need to store data, but there is an easier and better way.

Creating another Module DataStorage on which our User extends.
This way our application will make calls to the DataStorage module

DataStorage module

the DataStorage module is responsible for handling all our data in a special process. We will then implement the user model on top of that.

To view ALL The code in the Fossil Respository for the 0x06 Modifications to State click here

User Module

Our User Module has now become tiny. Apart from the documentation and tests which are required to see if everything works alright.
Our User Module can now work perfectly fine with whatever changes we will do to our Elixir Phoenix:Project Ideas:Life Beyond Apocalypse Zombie:DataStorage in the future without breaking any functionality.

Thanks to the Tests we did earlier we can find out if our implementation works by typing
mix test

Implementing a progressbar system to show our health and energy

GameUtilities.ex

   def progress_bar(current, maximum, char \\\\ "#", level \\\\ 25) do  \tcurrent_nr = round((current/maximum)*level)  \tmax_nr = round(maximum/maximum*level) - current_nr  \tfull_bar = String.duplicate(char, current_nr)  \tempty_bar = String.duplicate("-", max_nr)  \t"[#{full_bar}#{empty_bar}]"    end

Player Status Function


Previously our move_to misbehaved for certain moves. Now we've fixed it.

Our user should have a maximum of 30 items in his inventory. So when searching we should limit this.

We need a way to inform a user that he needs to rest when he goes below 25% of his max_energy.
We also need a way to inform him that he can't do anything else if he gets to 0.
IT would be great if we could combine all these with the decreasing of energy.. So you know, we have better code
We still have a slight issue..For our code to be modular we should return every data to the function above and it should return above and the calling function should decide how to show our text
But our :rest atom is different, it will be interpreted as an OK but we still need to show it. And probably within a different colour so that the user knows what's awaiting for him.
We could complicate each caller to handle the text. For now we should just show it directly and figure out later how to fix this.

Our movement did not use any energy so we not have to extend it to actually use energy.

Now we stumble on a problem.
Our use_energy function will decrease the enegy used.
But our user structure still has the old energy which will then be used to append the new x and y positions. This will mean that even though we decrease energy, it will be set back to it's old value.
This can only mean that we need to concieve a function that updates only the requested values without us having to call User.set multiple times in a row.
We can create 2 different fuctions set_many and get_many or we could use pattern matching and the power of functions elixir provides us to make it extremely simple.

 

data_storage.ex

#Server API

#Server APIdef new(data_structure) do     receive do        {:get, key, pid} ->          if  is_list(key) do            return_data = Map.take(data_structure,key)          else            return_data = Map.get(data_structure,key)          end          send pid, {:value, return_data}        {:get_struct,pid} ->          send pid, {:data_structure, data_structure}        {:set, key, value} ->            data_structure = Map.put(data_structure,key,value)         {:set, mapped_values} ->            data_structure = Map.merge(data_structure, mapped_values)        {:set_struct, structure} -> data_structure  = structure        {:incr,key,incr} -> data_structure =  Map.update(data_structure,key,incr, &(incr+&1))        {:append,key,item} ->  data_structure = Map.update(data_structure, key, [item], &(&1 ++ [item]))      end      new(data_structure)    end# ...@doc  """      Updates the structure with a map      %{key1: "value1", key2: "value2"}    """    def set(process, map) do      send(process, {:set, map})    end

 

Our get now has 2 different implementations depending if we receive a list or just a simple key.

 

TEST CASE

iex> User.start("Lord Praslea")iex> User.set(:energy,47)iex> User.set(:health,33)iex> User.get(~W/energy health/a)%{energy: 47, health: 33}

 

 

 

For our set it's a little bit different, we now have to implement 2 differnt set functions which verify if you transfer a key and value or just a map
We could use %{user | x: x, y: y} but i think it's better to use different functions this timeTEST CASE within the documentation for a certain module

@doc """Update multiple key, value pairs by using a map## Examplesiex> User.start("Lord Praslea")iex> User.set(%{energy: 25, health: 30, experience: 70})iex> User.get(:energy)25iex> User.get(:health)30iex> User.get(:experience)70"""

 

Use Energy code

 def use_energy(decrease_energy) do      user = User.get_struct()     {reason, msg} =  has_enough_energy(user, decrease_energy)     if reason in [:ok, :rest] do       User.incr(:energy, -decrease_energy)     end     {reason, msg}  end  def has_enough_energy(%User{energy: energy, max_energy: max_energy}, decrease) when    (energy - decrease) <=  (max_energy*0.25) and (energy - decrease) > 0  do      msg = "You are getting low on energy. You should find a safehouse to rest. \\#{energy - decrease} energy left."      IO.ANSI.format([:yellow, msg]) |> IO.puts    {:rest, msg }  end  def has_enough_energy(%User{energy: energy}, decrease) when    (energy - decrease) <= 0  do    {:not_enough_energy, "You don't have enough energy to perform this action." }  end    def has_enough_energy(%User{energy: energy}, decrease) when      (energy - decrease) > 0,  do:       {:ok, nil}

 

Move and decrease energy

 

Back to our move function with it's extra utility functions, here's how a new version looks like.
It may look like overkill and you might think well yeah, we could have used a simple IF statement. We opted however to use a few helper functions.
It really depends on what your preferences are, in this case an IF would have made us type less but for the sake of the example i've included both.
Remove from comments the one you would like to use.

 def move(location) do    IO.puts "Move to #{location}"    user = User.get_struct()    where_to = move_to(location,user)    case verify_bounds(where_to,generate_map()) do      {:ok, msg, {x,y}} ->        #  User.use_energy(-1)        #  |> move_the_player(msg, {x, y})        {reason, energy_msg}  = User.use_energy(1)        if reason in [:rest, :ok] do            move_the_player({reason, energy_msg}, msg, {x, y})        else          {:error, energy_msg}        end      {:error, msg} ->          IO.puts IO.ANSI.red  <> msg <> IO.ANSI.reset          {:error, msg}      end    end

Why writing tests is good. We might create a function and think it's OK.
First i have written it like this
(energy - decrease) <= (max_energy*0.25)

Search and decrease energy

Modifying our search function to fit the new way of decreasing makes us include a new if.
IF elixir had a specific statement to return early we could avoid the extra if and also avoiding creating a new subfunction.
We've modified the :false to :error in search and search_item together with the cli.exe file

  def search() do    if  length(User.get(:items)) < @item_limit do      {reason, energy_msg}  = User.use_energy(1)      if reason != :not_enough_energy do         search_item(GameUtilities.rand(1,10))       else         {:error, energy_msg}      end    else      {:error, "Before you start searching again, be sure you drop something since you're already carrying too many things (30 items limit)"}    end  end

in cli.ex

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

 

We're will now implement the part where using an item will decrease energy.

Use an item and decrease energy

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        {reason, energy_msg}  = User.use_energy(1)        if reason != :not_enough_energy do            User.set(item_type,value)            drop_item(item)         else           {:error, energy_msg}        end      end      {:ok, use_item_maximum_text(atom)         |> EEx.eval_string([item: item, user: user, maximum: maximum])}    end

 

Refactoring a little bit

 

As you can see by just adding the use_energy function call need to ad at least 4 extra lines to know when to return an error and to know when to do our action
Hmm, this doesn't seem efficient and is counter intuitive. We do this because we have multiple states and want to inform the user if he needs to rest, if he has enough energy or if it's ok.
Also because we want to prepare our app to be able to run in a commandline, client server environment and maybe via websocket and JSON?
Well, I think we can fix this.
There are 2 options
We can either just return :error or :ok. If the user needs to be informed of a rest, so be it. We will for now just PUT the message to the screen.
In the future if the user is in the commandline we will just update the user structure to point commandline, socket, websocket, json.. etc So we know the correct course of action for the :rest option
Otherwise we will just do an if and return the message
We could rename the use_energy function to verify_energy.. If we get :ok we can do our function, if we get :error we can just return the message
In case we succeed, we can then decrease the energy manually. We have to handle the energy verfification in one place and the decreasing in another place but our code will become easier to understand and to maintain in the long run.

user.ex

def verify_energy(decrease_energy) do  user = User.get_struct()  has_enough_energy(user, decrease_energy)  end  def use_energy(energy) do  User.incr(:energy, -energy)  end  def has_enough_energy(%User{energy: energy, max_energy: max_energy}, decrease) when(energy - decrease) <=  (max_energy*0.25) and (energy - decrease) > 0  do  msg = "You are getting low on energy. You should find a safehouse to rest. \\#{energy - decrease} energy left."  IO.ANSI.format([:yellow, msg]) |> IO.puts{:ok, msg }  end  def has_enough_energy(%User{energy: energy}, decrease) when(energy - decrease) <= 0  do{:error, "You don't have enough energy to perform this action." }  enddef has_enough_energy(%User{energy: energy}, decrease) when  (energy - decrease) > 0,  do:       {:ok, nil}

 

items.ex

 

def use_item(item) do  use_item_energy(User.verify_energy(1), item)enddef use_item_energy({:ok, _} ,item) do  use_item(item.type, item)enddef use_item_energy({:error, energy_msg} , _item) do\t{:error, energy_msg}enddef 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\t  User.use_energy(1)\t  User.set(item_type,value)\t  drop_item(item)  end  {:ok, use_item_maximum_text(atom)\t |> EEx.eval_string([item: item, user: user, maximum: maximum])}end

 

 

 

game_map.ex

def move(location) do#Logger.debug "Move to #{location}"user = User.get_struct()where_to = move_to(location,user){reason, energy_msg}  = User.verify_energy(1)if reason == :ok do  case verify_bounds(where_to,get_map()) do\t{:ok, msg, {x,y}} ->\t  User.use_energy(1)\t  move_the_player({reason, energy_msg}, msg, {x, y})\t  {:error, msg} ->\t\tIO.puts IO.ANSI.red  <> msg <> IO.ANSI.reset\t\t{:error, msg}\t  end\telse\t  {reason, energy_msg}\tend  end

 

 

 

 

Game map Refactoring by preparing for NPC

Our NPC's will move at random times.. THey will move in all of the 8 directions.
Our player only moves in 4 directions, we need to add 4 other directions.
Let's include north-west, north-east, south-west, south-east directions.
And also le'ts include the ability so that the user types in the location to go in that direction
We;ll also implement sorthand commands like nw, w, e, sw,se, ne,n, s.
And the numbers 1 all the way through 9 depending on the location on your numpad
This way movement becomes Number One thing you can do in the game.

YOU MIGHT HAVE NOTICED THAT SOMETIMES SOME TESTS FAIL. If you run mix test again they will succeed
This has to do with the state of our user since our tests are usually run in paralel..
We should for each test restart our User
It's usually the commandline tests that fail.

Download the full sourcecode here for 0x06 here

Ideas and comments

Andrei Clinciu
Andrei Clinciu

I'm a Full Stack Software Developer specializing in creating websites and applications which aid businesses to automate. Software which can help you simplify your life. Let's work together!

Building Great Software
The digital Revolution begins when you learn to automate with personalized software.

Find me on social media