Posted on 11 Apr 2018
I was watching The Hitchhiker’s Guide to the Unexpected (YouTube link) by Fred Hebert and in that there is a neat exercise of writing out your supervision tree on a whiteboard and seeing how things would fail. With this you could better determine what happens to your application as things go wrong.
I decided this would be a good exercise to do on ExVenture. This is a fairly long post that goes through the full supervision tree for ExVenture.
You can see ExVenture in action on MidMUD.
Supervision Tree
This is the supervision tree that ExVenture ships with now. There are roughly 3 levels in the photo.
First Level
This is the top level directly underneath the application. It contains, in start up order:
Data.Repo
- the Ecto repo
Web.Supervisor
- the Phoenix supervisor
Game.Registries
- a collection of Registry
s
Game.Supervisor
- a the top level supervisor of the game
- A ranch listener is also started at this level, but it spins off into the ranch application
At this level the supervision strategy is rest_for_one
. This is fine because if the Repo dies the rest of the app should be rebooted, something went wrong. As we’ll find later on the loads process with an ID to fetch from the database to ensure a clean state is fetched on process restarts (if something crashes.)
Second Level - Web.Supervisor
This supervisor is mostly sitting on top of the Phoenix Endpoint
along with a few process monitors for the TelnetChannel
and a Cachex cache. It is handled by a one_for_one
strategy. This is fine as none of them are really connected to the other, this supervision level is mostly to break sections up for my benefit.
Second Level - Game.Supervisor
This supervisor contains the “world” along with supporting processes. In start up order:
Game.Config
- an agent that caches game configuration
Game.Caches
- a supervisor of Cachex caches along with GenServer processes that are related to caching
Game.Server
- a tiny process that used to do more, but now keeps player telemetry up to date
Game.Session.Supervisor
- the supervisor for player sessions
Game.Channel
- a gen server that tracks player sessions and which channels they are joined to, inspired by Phoenix Channels
Game.World
- the supervisor that supervises the game world, see more below
Game.Insight
- a small GenServer that tracks bad command parsing
Game.Help.Agent
- an agent that load internal game help
This level has one_for_one
as its strategy. At this level most sub-trees are fairly separate and can handle rebooting (to my knowledge) without interfering with other sub-trees.
Third Level - Game.World
This is the heart of the app. It contains everything the user interacts with in the game. Its direct children are Zone.Supervisor
supervisors. This level has a strategy of one_for_one
. This is fine because each zone is self contained and can reboot on its own.
Zone.Supervisor
This level has in startup order:
Game.Zone
- the zone’s state, which tracks what rooms/npcs/shops are online
Game.Room.Supervisor
- A supervisor of rooms that belong to the zone
Game.NPC.Supervisor
- A supervisor of NPCs that belong to the zone
Game.Shop.Supervisor
- A supervisor of shops that belong to the zone
The reboot here is one_for_all
. If any of these processes die something bad happened and the whole zone should restart. To further go into this, the Zone process tracks processes inside the sibling supervisors and if that dies then the rest should go as well. If the supervisors at this level died something really bad beneath them happened and the rest should be restarted.
When the sibling supervisors start they are started with the zone id. With this they figure out which children should be loaded at boot. Tese supervisors start processes as transient
because they may be terminated normally and should not be rebooted, e.g. if someone deletes a spawner for an NPC then the process will be terminated cleanly.
The sibling supervisors are also a one_for_one
strategy. This is fine as each process under them are fairly self contained and separated mostly for programmer benefit, this could probably be a big bag of processes directly under the Zone.Supervisor
.
Take Aways
While doing this I was able to rework some of the tree. I pushed Game.Config
further up the tree since that seems important. I also pushed more GenServers into the Cache
sub-tree since they were similar.
One of the other reasons I did this was to figure out how to split up the app on separate nodes. This exercise taught me that it’s currently not as easy as I was hoping. I figure the Web
tree could be pulled off without doing much of anything, yet I found out that the Game
tree is connected in a few spots that prevent it from immediately being pulled off. This would have been an annoying lesson to learn as I did that, now I know before hand and can fix the problems I found first.
In going multi-node, each of the first level would be good as a separate OTP app in an umbrella app. I had previously started with that but the application was too new for that to be useful. If I split them up again, I can boot nodes that are just for web, just for telnet connections, or just the world. I think this is a next step for going multinode.
I hope this was useful reading through seeing why I picked what I did and also finding out I had a few things ordered wrong. I hope you go through your own apps and try out a similar exercise on them.
Posted on 27 Mar 2018
The last month of ExVenture had a lot of game improvements in addition to a lot of tweaks and bug fixes. I say game improvements because mechanics were updated in regards to leveling and gaining stat improvements.
The documentation website is exventure.org. You can see the latest additions here on MidMUD, my running instance of ExVenture. There is also a public Trello board now.
Also check out The MUD Coder’s Guild, it’s a slack team devoted to developing MUDs.
Honing your stats
The biggest change (so big I reset characters on MidMUD) is being able to hone your basic stats. I got rid of your class boosting your stat a certain amount each level along with your level being added. It now increases each stat by 1 each level, and the top two stats get an extra point. The top two stats are chosen that level by the skills you used. If you use something that uses a lot of strength, your strength will go up faster.
To further customize your character, you can hone
them. This uses your gained experience points and “spends” them on increasing a stat by 1 (or 5 if it’s health/skill.) See more information on the help for hone.
Web Client
The web client got a very big update, commands are now clickable! As part of a semantic color update, I start sending {command}help config{/command}
instead of {white}help config{/white}
. This lets the web interface know that you can click those. The game can also send a different command to send when clicking a command, so you can have display text and the command itself.
A tooltip also displays:
Announcements
I added a small “blog” to the home page that lets admins post announcements about the game. You can sticky posts and write in markdown. There is an atom feed that is linked from the homepage so players can subscribe without knowing the feed URL. You can also simply not publish them to preview them on the home page as an admin.
Here is a sample announcement.
Hint system & Configuration
A hint system was introduced as a simple form of tutorial. For instance, after receiving your first tell each sign in you will see a message such as You can reply with reply my message.
With this I introduced a player configuration system to disable these messages.
Also in the config system is disabling regeneration notices, changing your prompt, and setting the pager size. See more information at MidMUD’s help.
Say improvements
I played some of the tutorial from Discworld Mud and I was inspired to add in a say to and whisper set of commands. Whispering sends your message to only the character it is directed at and everyone else sees only that you’re whispering to each other. Say to directs a message at someone in the room for all to see.
Later, I was looking over the cheatsheet for CurryMUD and saw all the cool emote and say features it has. This inspired me to add in speaking at someone (in a cleaner fashion) and adverb phrases. I am very happy with how this feature turns out. You can be much more expressive in local chatter now. Adverb phrases also carries over to channel chat.
Bug Fixes
Several process crashing bugs were found, luckily no player was disconnected because of them. The worst one was sending in not an integer into quest info
. Ecto was not happy about that. Other bugs include:
- Running was missing up/down
- Display gold if only gold was in the room, no other items
- Remove command only worked for chest items
- Configuring the prompt broke sign up
- NPC combat target bug, you could trick the NPC into not attacking you when you entered a room
- Updating an NPC crashed the NPC
Small Tweaks
- Skills have an effect whitelist now, no damaging yourself while performing a healing command
- Items have the same whitelist on a use
- Movement in/out
- Skills have a cool down
- AFK status
- Name map layers in the admin panel
- Movement should indicate direction for other players, “Guard left north”, this is also clickable!
- Global broadcast when someone signs in or out
- Start of a real templating engine for messages inside the game
Next Month
I didn’t do the extra detail of the world like I had hoped last night, but I am pretty happy with what did get done instead. I think in the next month I want to have more game elements in place. Maybe data defined damage types.
Posted on 05 Mar 2018
The other week I started playing around with the @external_resource tag in Elixir. I wanted to do something similar to a translations file for the admin panel in ExVenture for help text. I didn’t want to default to a YAML file as I’ve heard the elixir community isn’t thrilled with it, so I took a look around to see what I could do.
What I ended up with was a macro that could compile a text file into Elixir functions. It also live reloads with the Phoenix code reloader in development because of @external_resource
, this is really cool to see working.
This is the file format I wanted to end up with. Keys and values essentially separated by new lines.
room.ecology:
Room ecology changes the color of the map "icon" in the map grid.
room.feature:
Room features are appended to the end of a room's description.
You can see the full file here.
Help Macro
The end result of the macro will give us an API as follows:
iex> Web.Help.get("room.feature")
"Room features are appended to the end of a room's description."
This works via a macro (full file here) that loads the file and compiles it into quoted functions. The import sections are copied below:
defmacro __using__(file) do
help_file = Path.join(:code.priv_dir(:ex_venture), file)
{:ok, help_text} = File.read(help_file)
quotes = generate_gets(help_text)
quotes = Enum.reverse([default_get() | quotes])
[external_resource(help_file) | quotes]
end
defp generate_gets(help_text) do
help_text
|> String.split("\n")
|> Enum.reject(&(&1 == ""))
|> Enum.map(&String.trim/1)
|> convert_to_map()
|> Enum.map(fn {key, val} ->
quote do
def get(unquote(key)) do
unquote(val)
end
end
end)
end
defp external_resource(help_file) do
quote do
@external_resource unquote(help_file)
end
end
The __using__
macro reads the file and generates the get quotes and the external resource quote. The external_resource/1
function is how the code live reloads when editing the text file. The generate_gets/1
function parses the help file to generate a list of quoted functions, something I’ve never tried before. It was pretty cool to see you can return a list of quoted functions and get the same result.
Conclusion
It is really cool to see the live reloading work. This may or may not be the best way to do what I want, but I had a lot of fun writing this. I think if these help files grow to be hundreds of keys long I will want to change up how this is parsing, but for now it works out well.
Lastly, you can check out more ExVenture at exventure.org and see my running instance at midmud.com.
Posted on 26 Feb 2018
The last month of ExVenture was a lot of small tweaks here and there, along with a revamp of the skills system.
The documentation website is exventure.org. You can see the latest additions here on MidMUD, my running instance of ExVenture. There is also a public Trello board now.
Skills
Skills used to be directly attached to classes. You only had what the class had. Now they are split apart from classes. You can train as many skills as you have experience for. You train them from NPCs around the world.
Skills can also be global and can still be attached to classes. When they are global or attached to a class, players will automatically train them when they become the appropriate level to unlock it. Races can also have skills now, and act in the same manner.
Rooms
There are a few tweaks to rooms this month. They can now have features, which let players look further into details of a room. These features show up at the end of a room description and will highlight a keyword a user might look
at to view more information.
The admin panel also had rooms spruced up, so they look more like they do in game.
Mail system
The mail system lets you send mail now. You can view it in game and also via the web site. If you are offline and have an email on your account, you will receive an email notification. You will also receive an in-game notification of new mail.
Handle crashes
I worked on handling crashes better. Now all of the processes that are “in” a room are linked together. When an NPC or player enters a room, they link against it. This way if any of them die, then they all die together. When they die, they load from the database to make sure they have a well known good copy of state.
Players will see a brief “Session crashed, restoring…” message and that is all they should notice (aside from losing up to 15 seconds of save data.) This works across telnet and websocket connections.
New commands
There are several new commands available. You can give
items to other players and NPCs. Quests can tie into this as well.
The recall
command is new. It will return you to the zone’s graveyard, but only if you have 90% of your movement left.
Lastly, there are social commands that you can add via the admin panel. They can be with or without a target.
Security
The security of the application has been increased. Telnet logins require a one time password after signing into the website.
When signing into the website, you can now add two factor authentication to you account.
Small Tweaks
- Ran the elixir formatter over the codebase
- Display flags for users in the who list
- Push more events through the
notify
systems
- Stop ticking every 2 seconds, have systems react when they notice work to be done
- Send email after signing up
- Rearrange the admin sidebar
- Switch sessions to the dynamic supervisor
- Refactor the
{:user, session, user}
tuple out of the codebase
- Simple state machine for the telnet color formatter
- Keep newlines when wrapping text
- Damage is randomized, +/- 25%
- Dyanmic channels
- Web notifications for in game messaging
say/random
npc action, to pick a random message each tick
Next Month
For next month, I am hoping to fill in the world detail more. I am thinking senses, lighting, and time are next. Making things more dynamic, such as damage types, is something I want to continue doing. I also need to do a refresh of the documention, some of the new features are missing and screenshots are generally out of date.
Posted on 02 Feb 2018
For my project ExVenture I wanted to have a very nice admin interface and with that comes filtering of tables. This is what I came up with, which I think has been very extendable and worked out well.
This is what we’ll end up with:
Web.Admin.ItemController
Let’s first start at a controller to see what it looks like there. The controller is incredibly simple, pulling out the filter parameters and passing it into a web specific module, Web.Item. You can view the full file here on github.
def index(conn, params) do
%{page: page, per: per} = conn.assigns
filter = Map.get(params, "item", %{})
%{page: items, pagination: pagination} = Item.all(filter: filter, page: page, per: per)
conn |> render("index.html", items: items, filter: filter, pagination: pagination)
end
This also includes my pagination module, but I will be glossing over that part. We will see it again in the Web.Item
module.
Web.Item
Next let’s look at the Web.Item module. This module contains the all/1
function used above. It implements the Filter’s behaviour that requires a single function filter_on_attribute/2
.
The map of filters passed in from the controller is looped over and passed into this function of the module. Inside the filter_on_attribute/2
function you can tweak the query to do whatever is needed to perform a filter based on that attributes.
defmodule Web.Item do
alias Data.Item
alias Web.Filter
@behaviour Filter
@doc """
Load all items
"""
@spec all(Keyword.t()) :: [Item.t()]
def all(opts \\ []) do
opts = Enum.into(opts, %{})
Item
|> order_by([i], i.id)
|> preload([:item_aspects])
|> Filter.filter(opts[:filter], __MODULE__)
|> Pagination.paginate(opts)
end
@impl Filter
def filter_on_attribute({"level_from", level}, query) do
query |> where([i], i.level >= ^level)
end
def filter_on_attribute({"level_to", level}, query) do
query |> where([i], i.level <= ^level)
end
def filter_on_attribute({"tag", value}, query) do
query |> where([n], fragment("? @> ?::varchar[]", n.tags, [^value]))
end
def filter_on_attribute(_, query), do: query
end
You can see here that I’m doing a fragment to look into an array of strings. You could also do a join or other more complicated query manipulation.
Web.Filter
Finally the Web.Filter module itself. This is very simple, it defines a callback and a single function. The function also requires the using module to be passed in as the last argument. This is very simple with the __MODULE__
macro.
defmodule Web.Filter do
@moduledoc """
Filter an `Ecto.Query` by a map of parameters. Modules
that use this should follow it's behaviour.
"""
@doc """
This will be reduced from the query params that are
passed into `filter/3`.
"""
@callback filter_on_attribute(
{attribute :: String.t(), value :: String.t()},
query :: Ecto.Query.t()
) :: Ecto.Query.t()
@doc """
Common elements of filtering a query
"""
@spec filter(Ecto.Query.t(), map(), atom()) :: Ecto.Query.t()
def filter(query, nil, _), do: query
def filter(query, filter, module) do
filter
|> Enum.reject(&(elem(&1, 1) == ""))
|> Enum.reduce(query, &module.filter_on_attribute/2)
end
end
There are two other small things that this does, it pattern matches on a nil map and simply passes the query through. Lastly it filters out values that are an empty string since that is an empty form field.
The Template
I won’t go into detail about this, but you can view it on github.
Conclusion
This has worked out really well in crafting my admin panel. Adding a new filter is as simple as adding the template code and a single function, no extra magic needed.