This
is part of the Elixir Life Beyond Apocalypse Zombie Game creation
tutorial.
Refer to the introduction for more information.
So now that have thought about how our game will look like it’s time to
get started!
But where to start? It looks pretty complex and overwhelming at first
sight.
Let’s first create a basic User module to contain the basis structure of
our User.
We will then create a simple random map where we can move around.
Small and steady!
Let’s use mix to create a new project.
mix new life_beyond_apocalypse --sup
For now we save all files in lib/life_beyond_apocalypse, in later stages we will move our files to specialized folders.
user.exdefmodule User do defstruct name: "", health: 30, energy: 100, x: 5, y: 5, items: %{}, coins: 0, map: "" def map() domap = ~w"""#=#=#=#=#@@@@#=#=#=#=#@@@@#=#=#=#=#@@#@#=#=#=#=#@@@@#=#=#=#=#===##=#=#=#=#@@@@#=#=#=#=#@@@@"""Enum.map(map, fn (x) -> String.split(x," ", trim: true ) end) endend
We first create a structure map for the user which will contain various
information.
Then we write the map function.
Notice how we create a heredoc with a sigil
~w will create a list from our individual rows.
Afterwards we use Enum.map to split each row into a list of items. The
trim: true
option is just to make sure we don’t have an empty list
item at the end of each list.
Now we can handle the movement. However we have one little problem.
Elixir variables are immutable. Which means once declared we cannot
modify a variable. We thus need to create a new variable each time we
modify something in a old variable.
This means there is no global data which we can change. It’s a good
thing since we don’t pollute our programs with useless data.
But how do we solve the need to save data?
We could read and write to a file.
We could create a new process to store the data or use Agents and
Genserver but this is too advanced at the moment.
Or we could pass the User structure map to every function, returning us
a tuple with the new user map. This is the best solution for our current
needs.
Moving
def move(where_to, user) dolocation = move_to(where_to,user) case verify_bounds (location) do {:ok, msg, {x,y}} ->\tuser = Map.put(user, :x, x)\tuser = Map.put(user, :y, y) {:error, msg} ->\t IO.puts msg\tenduser end defp move_to("west", %User{x: x, y: y}) dox = x-1{x,y} end defp move_to("east", user) do x = user.x + 1 {x,user.y} end defp move_to("north", user) do y = user.y - 1 {user.x,y} end defp move_to("south", user) do y = user.y + 1{user.x,y}\t end #In case we get an invalid location, we just sit put defp move_to(where_to, user) do {user.x,user.y} end defp verify_bounds({x,y}) when x>=1 and y>=1 and 13 >= x and 7>=y do{:ok, "You moved to #{x},#{y}", {x,y}} end defp verify_bounds({_x,_y}) do{:error, "You are at the edge of the map"} end
Now we have a handful of functions.
We have a function move which takes 2 variables, our location as a
string "west","east","north"
And the user structure which gets returned as the function result.
IT then calls the move_to private function. But behold, we have 4
different move_to function definitions.
How can this work? It will surely give us an error or it will only know
the last compiled function (like some interpreted languages do).
Pattern matching is what makes Elixir great, since we can redefine the
same function multiple times to do various things.
For now what we’re doing is simple and may seem counter intuitive.
However it’s better to use multiple such functions instead of weird
if/else structures.
It makes the code readable and maintainable.
move_to
returns a tuple with the new location which we then send to
verify_bounds which has also 2 function definitions.
The first function definition verifies if we’re within bounds by using
when. When is actually a sort of if.
We hard code the maximum value for now to make things easier, later on
we will update it.
It returns a tuple containing the atom :ok with a text indicating the
movement.
The second definition of the function returns a tuple with the first value the error atom. And a text that we’re already at the edge of the map.
Clean and easy to separate our verifications.
Back to our move function we have a case for the bounds so we verify if
we can move or not.
Remembering that Elixir data is immutable we have to redefine the x and
y values.
We’re using Map.put for this, and since Map.put
returns a new map
object we need to overwrite our current user map.
By now you’re observing that we haven’t any IF/Else structures. This is
because Elixir and Erlang actually encourage you to define the same
function for each case you have.
Keeping the code cleaner and easier to understand.
We could have done everything within one function but we wouldn’t have
learned anything useful.
Let’s test it out
user = %User{name: "Andrei"}user = User.move("south",user)%User{coins: 0, energy: 100, health: 30, items: %{}, map: "", name: "Andrei",x: 5, y: 6}iex(100)> user = User.move("east",user)%User{coins: 0, energy: 100, health: 30, items: %{}, map: "", name: "Andrei",x: 6, y: 6}iex(101)> user = User.move("east",user)%User{coins: 0, energy: 100, health: 30, items: %{}, map: "", name: "Andrei",x: 7, y: 6}
So it seems to work and returns the proper values.
Let’s now create a function that shows the map with the location where
you are at in color.
By default iex comes with ANSI coloring. Depending on your operating
system it could be on/off.
The windows cmd.exe has different coloring than the Linux or Mac OS x
ones.
This won’t matter when we use telnet, because telnet can work with ANSI
coloring.
If you are however on windows and have coloring issues, try the cygwin
bash.
To turn coloring on use:Application.put_env(:elixir,:ansi_enabled,true)
We can use IO.ANSI.format to format the colouring.. We will also change
the texts we received with some colours.
But there is an issue.. working with lists and updating them becomes
very daunting.
Something that in a multidimensional array should be easy becomes
extremely difficult.
So we will create a few helper functions.
def show_map(%User{map: map, x: x, y: y} = user) do text = IO.ANSI.format_fragment([:bright, :green, get(user.map, x-1, y-1), :reset, :white]) map = set(map, x-1, y-1, text ) IO.puts IO.ANSI.white IO.puts map end 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
Let’s test it out again:
User.show_map(%User{})
** (Protocol.UndefinedError) protocol Enumerable not implemented for "". This protocol is implemented for: Date.Range, File.Stream, Function, GenEvent.Stream, HashDict, HashSet, IO.Stream, List, Map, MapSet, Range, Stream
Oh yeah, we forgot to add the user map. But while looking at the user map we saw that there are no newlines.. Let’s update the map() functon too.
def map() domap = ~w"""#=#=#=#=#@@@@#=#=#=#=#@@@@#=#=#=#=#@@#@#=#=#=#=#@@@@#=#=#=#=#===##=#=#=#=#@@@@#=#=#=#=#@@@@"""Enum.map(map, fn (x) -> String.split(x,"", trim: true ) end)|> Enum.map(fn (x) -> List.insert_at(x,-1,"\\n") end)end
Testing yet again, this time we’ve added the map to the user structure.
user = %User{name: "Andrei", map: User.map()}User.show_map(user)user = User.move("east",user)user = User.move("south",user)user = User.move("east",user)User.show_map(user)
Usually we should have written tests by now to test the functionality of our game. But we’re not yet done with how we arranged our code. Maybe we want to rename some functions.
One thing you should always do as a programmer is rewrite your code.
It’s something that should be done at all times to make the code more
readable and beautiful.
Sometimes you will see new ways to do various things and simplify the
code.. Or you might want to restructure it in some other way than
before.
We call this refactoring.
Notice that all we did up until now had to do with movement and map
stuff.
Why not move it over to it’s own Map module?
Our User module will be empty but we will add more things later!
You can look at the source code and conclude that it looks much better.
We’ve inlined the move_to functions.
We’ve also modified the show_map to read the map() function instead of
sending the map with the user structure map.
What we will do in the future is put a map_id and varying on that map we
will draw the map.
Move on to the next tutorial on how to use the 0x02 Command Line!