• Bubble
  • Bubble
  • Line
Elixir Game - 0x03 Saving state in Processes
Andrei Clinciu Article AUthor
Andrei Clinciu
  • 2018-03-08T19:43:00Z
  • 7 min to read

This is part of the Elixir Life Beyond Apocalypse Zombie Game creation tutorial.

In the last tutorial(Elixir - 0x02 Command Line Fun - LBA Game) we created a command-line application.
Due to the immutability of Elixir and Erlang we had to pass the user structure map to every function.
However, passing the user struct form one function to another only makes things more complex and makes everything error prone.

Elixir has a solution for this.

Storing data in Elixir can be done by creating a new process and storing the data there.
We will be using spawn, send and receive for the whole thing.
Later on we will upgrade our codebase to use Agents.

Normally when we work with processes we always need to work with a PID. This means that whenever we want to send information to the process (to get back a response) we need to pass the pid.
This makes it again a little bit difficult since we would need to pass the pid instead of the user struct.
However thanks to Elixir we can name our processes! This can help us a lot.

We update user.ex as follows

defmodule User do  defstruct name: "", health: 30, energy: 100, x: 5, y: 5, items: %{}, coins: 0, map_id: 1#Server API  def new(user) do  user =  receive do      {:get, key, pid} ->        send pid, {:value, Map.get(user,key)}        user      {:update,key,value} ->        Map.put(user,key,value)    end    new(user)  end#Client API  def start(player_name) do       pid = spawn(User, :new, [%User{name: player_name}])      Process.register(pid, :user)  end  def get(key) do    send(:user, {:get, key, self()})    receive do      {:value, value} -> value    end  end  def update(key,value) do    send(User, {:update, key, value})  endend

 

So, whenever we call start a new process will be created and it will be registered to the atom of :user. Experiment by changing this to :user_process or whatever atom you like.
The spawn command then creates a new process by running the User.new function by passing it a User struct.

 

The new() command listens for commands with the receive function.
Whenever we have a tuple with :get we send back a response with the value of the key requested.
Note that we're expecting a pid as the last argument, otherwise the process wouldn't know where to send it back to!
The :update clause doesn't require a pid since we;re just updating the data and we won't send anything back.

We then define 2 API functions which will do the work for us when getting and updating data.
Note that whenever we run get() we're sending a packet to the :user process and then expect a response with a value.
If we would create a real live application we would certainly need to catch errors and/or put a timeout for the receive command.
But for now this is enough.

Be aware that even though we use the same module for both processes new is run in the :user process, get and update are ran in our current process.
This means that the client is our current process and :user is the server process.

Before we integrate this in our CLI module let's test it in iex.
iex -S mix


It seems to work just fine.
However, we forgot to implement a function to retrieve and update the whole user structure map.
Let's do this now.
I've renamed the update function to set so we know they're setters and getters. I've also added a timeout after 5 seconds with an error tuple in the case someting fails.

defmodule User do  defstruct name: "", health: 30, energy: 100, x: 5, y: 5, items: %{}, coins: 0, map_id: 1#Server API  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 = structendnew(user)  end#Client API  def start(player_name) do   pid = spawn(User, :new, [%User{name: player_name}])  Process.register(pid, :user_pid)  end  def get(key) dosend(:user_pid, {:get, key, self()})receive do  {:value, value} -> value  5000 -> {:error, "Did not respond on time"}end  end  def get_struct() dosend(:user_pid, {:get_struct, self()})receive do  {:user, user} -> user\t5000 -> {:error, "Did not respond on time"}end  end  def set(key,value) dosend(:user_pid, {:set, key, value})  end  def set_struct(user) dosend(:user_pid, {:set_struct, user})  endend

Running it:

We've implemented a way to save the User state.

We also have a way to save the game state in the future. All we need to do is just copy the User module to a new module and modify the structure.
Or we could create a general module to handle this.

Either way, we can now refactor the GameMap and CLI modules. You will get used to hearing that we're going to refactor our code.
Refactoring is sometimes viewed as a necessary evil by some programmers who think their code is perfect.
However, code refactoring is a necessity in programming.
In other languages whenever you have a function that is longer than 20 lines of code then you should rewrite it.
Lucky for us, Elixir makes it easy to write readable and good code.
IF you ever have more than 20 lines of code in Elixir then probably you're doing someting wrong or could simplify it.

LifeBeyondAlpocalypse.CLI in CLI.ex
Notice how we've removed references to the user struct?
We start the :user state process in the main function and that's it. The rest should be handled by the correct functions.
This way each module has it's own role.

defmodule LifeBeyondApocalypse.CLI do  @tag ~S""" _     _  __        ____                             _| |   (_)/ _| ___  | __ )  ___ _   _  ___  _ __   __| || |   | | |_ / _ \\ |  _ \\ / _ \\ | | |/ _ \\| '_ \\ / _` || |___| |  _|  __/ | |_) |  __/ |_| | (_) | | | | (_| ||_____|_|_|  \\___| |____/ \\___|\\__, |\\___/|_| |_|\\__,_|                               |___/    _                          _   / \\   _ __   ___   ___ __ _| |_   _ _ __  ___  ___  / _ \\ | '_ \\ / _ \\ / __/ _` | | | | | '_ \\/ __|/ _ \\ / ___ \\| |_) | (_) | (_| (_| | | |_| | |_) \\__ \\  __//_/   \\_\\ .__/ \\___/ \\___\\__,_|_|\\__, | .__/|___/\\___|        |_|                      |___/|_|"""  @commands %{    "quit" => "Quits the game",    "help" => "?<topic>? - Shows help screen and help topics about various commands",    "move" => "<location> - Moves to location. Valid options are: (w)est, (e)ast, (s)outh, (n)orth ",    "map" => "Shows the map with your current location colored in",  }  def main(_args) do    IO.puts(@tag)    name =  read_text("What is your name dear adventurer?")    IO.puts "Welcome to LifeBeyondApocalypse #{name}!"    User.start(name)    read_command("To get started type in a command, or help")  end  defp read_text(text) do     IO.gets("\\n#{text} > ")     |> String.trim  end  defp read_command(text \\\\ "") do    IO.gets("\\n#{text} > ")    |> String.trim    |> String.downcase    |> String.split(" ")    |>  execute_command  end  defp execute_command(["quit"]) do    IO.puts "\\nThanks for playing Life Beyond Apocalypse. Have a nice day!"  end  defp execute_command(["help"]) do    print_help_message()    read_command()  end  defp execute_command(["move" | location]) do    GameMap.move(List.to_string(location) )    read_command()  end  defp execute_command(["map"]) do    GameMap.show_map()      read_command()  end  defp execute_command(_unknown) do    IO.puts("\\nUnknown command. Try help <topic>.")    print_help_message()    read_command()  end  defp print_help_message() do    IO.puts("\\nLife Beyond Apocalypse supports the following commands:\\n")    @commands    |> Enum.map(fn({command, description}) -> IO.puts("  #{command} - #{description}") end)    IO.puts "Type help <command> to find out more about a specific command"  endend

But it's not yet working properly untill we modify the game_map.ex file with the GameMap module.
For the move_to function we could remove the user and ask the process for the details or just pass the user value, since they're private functions we chose to pass the user structure we received earilier.

defmodule GameMap do  import GameUtilities  def generate_map() do    map = ~w"""    #=#=#=#=#@@@@    #=#=#=#=#@@@@    #=#=#=#=#@@#@    #=#=#=#=#@@@@    #=#=#=#=#===#    #=#=#=#=#@@@@    #=#=#=#=#@@@@    """    {x, y} = {length(map), String.length(Enum.at(map,1))}    map = Enum.map(map, fn (x) -> String.split(x,"", trim: true ) end)    |> Enum.map(fn (x) -> List.insert_at(x,-1,"\\n") end)    %{map: map, x: x, y: y}  end  def move(location) do    user = User.get_struct()    where_to = move_to(location,user)    case verify_bounds(where_to,generate_map()) do      {:ok, msg, {x,y}} ->        IO.puts IO.ANSI.format([:green, msg])         User.set_struct(%User{ user | x: x, y: y})        {:error, msg} ->          IO.puts IO.ANSI.red  <> msg <> IO.ANSI.reset        end      end      defp move_to("west", %User{x: x, y: y}),  do: {x-1,y}      defp move_to("east", user),  do:     {user.x + 1 ,user.y}      defp move_to("north", user),  do: {user.x, user.y - 1 }      defp move_to("south", user),  do:       {user.x,user.y + 1}      #In case we get an invalid location, we just sit put    #  defp  move_to(_where_to, user), do:    {user.x,user.y}      defp verify_bounds({x,y}, %{x: max_x, y: max_y}) when x>=1  and y>=1 and max_x>= x and max_y>=y  do        {:ok, "You moved to #{x},#{y}", {x,y}}      end      defp verify_bounds({_x,_y},_mapinfo) do        {:error, "You are at the edge of the map and can't move further in this direction."}      end      def show_map() do        %User{x: x, y: y} = User.get_struct()        map = generate_map()        text = IO.ANSI.format_fragment([:bright, :green, get(map.map,   x - 1,    y - 1), :reset, :white])        IO.write  IO.ANSI.format([:white, set(map.map, x - 1, y - 1, text  )])      end    end

 

We've also moved the get and set list utilities to it's own module. Note the import GameUtilities in the GameMap module

 

defmodule GameUtilities do        def get(arr, x, y) do          arr |> Enum.at(x) |> Enum.at(y)        end        def set(arr, x, y, value) do          List.replace_at(arr, x,          List.replace_at(Enum.at(arr, x), y, value)          )        endend

 

 

 

Download the full sourcecode here for 0x03 here

In the next tutorial we'll advance once again and add more features.

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