Elixir Game - 0x03 Saving state in Processes

Elixir Game - 0x03 Saving state in Processes

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})
  end
end

 

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} ->
		send pid, {:value, Map.get(user,key)}
	  {:get_struct,pid} ->
		send pid, {:user, user}
	  {:set,key,value} ->
	   user = Map.put(user,key,value)
	  {:set_struct, struct} -> user = struct
	end
	new(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) do
	send(:user_pid, {:get, key, self()})
	receive do
	  {:value, value} -> value
	  5000 -> {:error, "Did not respond on time"}
	end
  end

  def get_struct() do
	send(:user_pid, {:get_struct, self()})
	receive do
	  {:user, user} -> user
		5000 -> {:error, "Did not respond on time"}
	end
  end

  def set(key,value) do
	send(:user_pid, {:set, key, value})
  end

  def set_struct(user) do
	send(:user_pid, {:set_struct, user})
  end
end

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"
  end

end

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)
          )
        end
end

 

 

 

Download the full sourcecode here for 0x03 here

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

You might enjoy these similar articles:

Be the first to comment!

The comments section is closed.

Subscribe to my awesome newsletter!








What to expect: Ultimate Knowledge regarding Business Efficiency, Personalized Marketing Experience, Software Development and Cyber Security tips and tricks. 1-3 mails per month. Unsubscribe any time. See the privacy policy to learn how we take care of your information.