DOTA 2 on Rails: Digging Deeper
This article is the second in my “Dota 2 on Rails” series. In the first part we discussed what Dota is, how to connect to Steam API, what data it provides. The application in part 1 presents basic statistics for an authenticated user. The post gained a bit of interest as some readers were even inspired to create their own Dota 2 statistics websites – that’s really great.
In this article we will continue working with the demo app, so if you wish to follow along, the starting code can be found on this branch. I will show you what other data can you fetch with dota gem, how to work with live and scheduled matches, how the gem operates, and how testing is organized.
The final version of the code can be found on GitHub.
The working demo is accessible via the same link sitepoint-dota.herokuapp.com.
Fetching Players’ Ability Upgrades
The dota gem, which we are going to use throughout this article, is gradually evolving – today we are proud to announce a couple of new features. The first one is the support for fetching ability upgrades data.
What is an ability upgrade? As you remember, every player in Dota 2 controls their own hero which has a set of unique abilities (skills). As a hero is leveling up, they can learn new abilities or improve existing ones – that’s what an ability upgrade is. The cool thing is that the Steam API presents an array for each player containing an ability id, time, and level when the ability was learned. How about using this data in our app?
First of all, create and apply a new migration:
$ rails g migration add_ability_upgrades_to_players ability_upgrades:text
$ rake db:migrate
This attribute has to be serialized because we want to store an array of hashes:
models/player.rb
[...]
serialize :ability_upgrades
[...]
Now, modify the method that loads players:
models/match.rb
[...]
def load_players!(radiant, dire)
[...]
self.players.create({
ability_upgrades: player.ability_upgrades.map {
|ability_upgrade| {id: ability_upgrade.ability.id,
name: ability_upgrade.ability.name,
image: ability_upgrade.ability.image_url(:hp1),
level: ability_upgrade.level,
time: parse_duration(ability_upgrade.time)}
}.sort_by {|ability_upgrade| ability_upgrade[:level]},
[...]
})
end
[...]
Under the hood, an array containing instances of the AbilityUpgrade
class is returned. ability_upgrade.ability
is an instance of the Ability
class that has attributes like id
, name
, full_name
, and image_url
. full_name
returns the ability’s name with the hero’s name (like “Antimage Mana Break”), whereas name
returns only the name of the ability.
ability.image_url(:hp1)
means that we want to fetch a URL the the ability’s small image (90×90) – other arguments can be passed as well.
By the way, you can easily find an ability by its id using the Dota.api.abilities(id)
method. All abilities are cached in the ability.yml file. The downside here is that this file has to be constantly updated, because, as new Dota versions are released, abilities may be added or reworked.
sort_by
sorts the array by level (ascending).
ability_upgrade.time
returns a number of seconds since the game started when the ability was learned. I am using parse_duration
to format it like 00:00:00
. This method was already used inside models/user.rb file, so let’s extract it into a separate file:
lib/utils.rb
module Utils
def parse_duration(d)
hr = (d / 3600).floor
min = ((d - (hr * 3600)) / 60).floor
sec = (d - (hr * 3600) - (min * 60)).floor
hr = '0' + hr.to_s if hr.to_i < 10
min = '0' + min.to_s if min.to_i < 10
sec = '0' + sec.to_s if sec.to_i < 10
hr.to_s + ':' + min.to_s + ':' + sec.to_s
end
end
and include it:
models/user.rb
require './lib/utils'
class User < ActiveRecord::Base
include Utils
[...]
end
models/match.rb
require './lib/utils'
class Match < ActiveRecord::Base
include Utils
[...]
end
Don’t forget to remove this method from user.rb.
Okay, lastly, let’s display this new info on the show
page:
views/matches/show.html.erb
<% page_header "Match #{@match.uid} <small>#{@match.started_at}</small>" %>
<h2 class="<%= @match.winner.downcase %>"><%= @match.winner %> won</h2>
<ul>
<li><strong>Mode:</strong> <%= @match.mode %></li>
<li><strong>Type:</strong> <%= @match.match_type %></li>
<li><strong>Duration:</strong> <%= @match.duration %></li>
<li><strong>First blood:</strong> <%= @match.first_blood %></li>
</ul>
<%= render 'details_table', players: @players[true], team: 'radiant' %>
<%= render 'details_table', players: @players[false], team: 'dire' %>
Here I’ve renamed the _players_table.html.erb partial to _details_table.html.erb and reduced code duplication. This partial contains the following code:
views/matches/_details_table.html.erb
<h3 class="<%= team %>">Team <%= team.titleize %></h3>
<table class="table table-hover table-striped info-table">
<tr>
<th>Player ID</th>
<th>Hero</th>
<th>Level</th>
<th>Items</th>
<th>Kills</th>
<th>Deaths</th>
<th>Assists</th>
<th><abbr title="Last hits">LH</abbr></th>
<th><abbr title="Denies">DN</abbr></th>
<th>Gold (spent)</th>
<th><abbr title="Gold per minute">GPM</abbr></th>
<th><abbr title="Experience per minute">XPM</abbr></th>
<th><abbr title="Hero damage">HD</abbr></th>
<th><abbr title="Tower damage">TD</abbr></th>
<th><abbr title="Hero healing">HH</abbr></th>
</tr>
<% players.each do |player| %>
<tr>
<td>
<% if player.abandoned_or_not_connected? %>
<abbr class="text-muted" title="<%= player.status.to_s.titleize %>"><%= player.uid %></abbr>
<% else %>
<%= player.uid %>
<% end %>
</td>
<td><%= render 'player_hero', hero: player.hero %></td>
<td><%= player.level %></td>
<td><%= render 'items', items: player.items %></td>
<td><%= player.kills %></td>
<td><%= player.deaths %></td>
<td><%= player.assists %></td>
<td><%= player.last_hits %></td>
<td><%= player.denies %></td>
<td><%= player.gold %> (<%= player.gold_spent %>)</td>
<td><%= player.gpm %></td>
<td><%= player.xpm %></td>
<td><%= player.hero_damage %></td>
<td><%= player.tower_damage %></td>
<td><%= player.hero_healing %></td>
</tr>
<% end %>
</table>
<h4 class="<%= team %>">Builds</h4>
<table class="table table-hover table-striped info-table">
<tr>
<th>Hero</th>
<% (1..25).each do |level| %>
<th><%= level %></th>
<% end %>
</tr>
<% @players[true].each do |player| %>
<tr>
<td><%= render 'player_hero', hero: player.hero %></td>
<% player.ability_upgrades.each do |ability| %>
<td class="text-center">
<%= image_tag ability[:image], alt: ability[:name], title: ability[:name] %><br/>
<small class="text-muted"><%= ability[:time] %></small>
</td>
<% end %>
</tr>
<% end %>
</table>
The first table remains intact and the second one presents the hero build (in Dota 2 the maximum level is 25).
You may want to play with layout and styling here, but, all in all, this functionality is now working – go ahead and try it! If one day you notice that this no longer works, it probably means that some new ability has been added which the dota gem don’t know about yet. In this case, feel free to update the ability.yml file and send your PR :).
Additional Units
Another new feature in the dota gem is the ability to fetch information about additional units under a player’s control. In general, a player only controls one hero, however, under some circumstances he can summon or subjugate other units (the classic example is Lone Druid’s Spirit Bear).
The Steam API provides information which additional units a player had under their control at the end of the game. This info is pretty minimalistic (only the unit’s name and items, if any), but still useful. Go ahead and apply a new migration:
$ rails g migration add_additional_units_to_players additional_units:text
$ rake db:migrate
Serialize the attribute:
models/player.rb
[...]
serialize :additional_units
[...]
and, again, modify the load_players!
method:
models/match.rb
[...]
def load_players!(radiant, dire)
[...]
self.players.create({
additional_units: player.additional_units.map {
|unit| {name: unit.name,
items: parse_items(unit.items)}
},
[...]
})
end
[...]
additional_units
is a simple method that returns an array containing instances of the Unit
class class.
parse_items
is a helper method that deletes empty slots (each hero and some units have 6 inventory slots) and produces a hash with the items info:
models/match.rb
[...]
private
def parse_items(items)
items.delete_if {
|item| item.name == "Empty"
}.map {
|item| {id: item.id, name: item.name, image: item.image_url}
}
end
Don’t forget to simplify the following lines:
models/match.rb
items: player.items.delete_if {
|item| item.name == "Empty"
}.map {
|item| {id: item.id, name: item.name, image: item.image_url}
},
to
models/match.rb
items: parse_items(player.items),
Now display the new info:
views/matches/_details_table.html.erb
<h3 class="<%= team %>">Team <%= team.titleize %></h3>
<table class="table table-hover table-striped info-table">
<tr>
<th>Player ID</th>
<th>Hero</th>
<th>Level</th>
<th>Items</th>
<th>Kills</th>
<th>Deaths</th>
<th>Assists</th>
<th><abbr title="Last hits">LH</abbr></th>
<th><abbr title="Denies">DN</abbr></th>
<th>Gold (spent)</th>
<th><abbr title="Gold per minute">GPM</abbr></th>
<th><abbr title="Experience per minute">XPM</abbr></th>
<th><abbr title="Hero damage">HD</abbr></th>
<th><abbr title="Tower damage">TD</abbr></th>
<th><abbr title="Hero healing">HH</abbr></th>
</tr>
<% players.each do |player| %>
<tr>
<td>
<% if player.abandoned_or_not_connected? %>
<abbr class="text-muted" title="<%= player.status.to_s.titleize %>"><%= player.uid %></abbr>
<% else %>
<%= player.uid %>
<% end %>
</td>
<td><%= render 'player_hero', hero: player.hero %></td>
<td><%= player.level %></td>
<td><%= render 'items', items: player.items %></td>
<td><%= player.kills %></td>
<td><%= player.deaths %></td>
<td><%= player.assists %></td>
<td><%= player.last_hits %></td>
<td><%= player.denies %></td>
<td><%= player.gold %> (<%= player.gold_spent %>)</td>
<td><%= player.gpm %></td>
<td><%= player.xpm %></td>
<td><%= player.hero_damage %></td>
<td><%= player.tower_damage %></td>
<td><%= player.hero_healing %></td>
</tr>
<% if player.additional_units.any? %>
<% player.additional_units.each do |unit| %>
<tr class="text-muted small">
<td></td>
<td><%= unit[:name] %></td>
<td></td>
<td><%= render 'items', items: unit[:items] %></td>
</tr>
<% end %>
<% end %>
<% end %>
</table>
[...]
We are simply adding another row per each additional unit a player has under control. _items.html.erb is the partial that was introduced in the previous article.
If this functionality stops working, this probably means that a new item was introduced and the dota gem needs to be updated. You can easily fetch an up-to-date list of all items by visiting http://api.steampowered.com/IEconItems_570/GetSchemaURL/v0001/?key=
URL. However, sometimes this info also may not be 100% accurate (judging by the discussions on dev forum).
Towers and Barracks Status
As you probably remember, each team has its own base to defend. However before the enemy can enter your base they have to destroy three towers on one of the lanes. These towers are called Tier 1, Tier 2, and Tier 3. Tier 3 is located near the entrance to the base; two barracks (for ranged and melee units) are located there as well. Apart from these towers, there are two more guards at the main building that have to be destroyed to win the game.
The Steam API provides the status of towers and barracks and we are going to use this information as well. Status used to be represented in a binary format where each digit corresponded to a specific building; 0 meant that the building was destroyed and 1 meant that the building was standing by the end of the game. However, now the Steam API simply returns a decimal number for status. So, how are we dealing with it in the dota gem?
First of all, there are two arrays containing towers and barracks. Then there is a special method to convert the decimal number to a more user-friendly format. It takes the number and converts it to binary using to_s(2)
. By passing a number as an argument to the to_s
method you set the base and that’s really a neat trick.
In some cases Steam API may return status like 5
which is 101
in binary format, meaning that leading zeroes were stripped. However, this is not enough for us, so append missing zeroes using rjust
.
As a result, this method returns a hash with towers and barracks names as keys and true
or false
as values.
Armed with this knowledge, we can now apply the new migration:
$ rails g migration add_towers_status_and_barracks_status_to_matches towers_status:text barracks_status:text
$ rake db:migrate
Serialize these attributes:
models/match.rb
[...]
serialize :towers_status
serialize :barracks_status
[...]
and modify the load_matches!
method:
models/user.rb
[...]
def load_matches!(count)
[...]
new_match = self.matches.create({
[...]
towers_status: {
radiant: parse_buildings(match_info.radiant.tower_status),
dire: parse_buildings(match_info.dire.tower_status)
},
barracks_status: {
radiant: parse_buildings(match_info.radiant.barracks_status),
dire: parse_buildings(match_info.dire.barracks_status)
}
[...]
end
[...]
Here is the parse_buildings
method:
models/user.rb
[...]
def parse_buildings(arr)
arr.keep_if {|k, v| v }.keys
end
[...]
Basically, we are only keeping the names of the buildings that were still standing at the end of the game. I am not saying that this is the best format to store this data, so you might choose some other, especially if you want to visualize this on a small Dota map (like it is done in Dotabuff).
On to the view:
views/matches/_details_table.html.erb
[...]
<h4 class="<%= team %>">Remaining buildings</h4>
<h5>Towers</h5>
<ul>
<% @match.towers_status[team.to_sym].each do |b| %>
<li><%= b.to_s.titleize %></li>
<% end %>
</ul>
<h5>Barracks</h5>
<ul>
<% @match.barracks_status[team.to_sym].each do |b| %>
<li><%= b.to_s.titleize %></li>
<% end %>
</ul>
titleize
is used to convert the building’s system name to a user friendly format. Feel free to refactor this further.
Likes and Dislikes
The last piece of information we are going to display is the likes and dislikes counts for the match. This is really easy to do. Apply a migration:
$ rails g migration add_likes_and_dislikes_to_matches likes:integer dislikes:integer
$ rake db:migrate
Modify the load_matches!
method:
models/user.rb
[...]
def load_matches!(count)
[...]
new_match = self.matches.create({
[...]
likes: match_info.positive_votes,
dislikes: match_info.negative_votes,
[...]
end
[...]
And tweak the view:
views/matches/show.html.erb
<% page_header "Match #{@match.uid} <small>#{@match.started_at}</small>" %>
<h2 class="<%= @match.winner.downcase %>"><%= @match.winner %> won</h2>
<p>
<i class="glyphicon glyphicon-thumbs-up"></i> <%= @match.likes %>
<i class="glyphicon glyphicon-thumbs-down"></i> <%= @match.dislikes %>
</p>
[...]
Pizza cake!
Live and Scheduled Matches
You can work with live matches just like with the finished ones, only a few things change.
Use the live_matches
method to fetch all live matches, passing either league_id
or match_id
to narrow the scope.
LiveMatch
is a child of the BasicMatch
class, therefore its instance responds to a bunch of the same methods. Raw info provided by the Steam API, however, has some strange inconsistencies. For example, to get the league ID for a finished match you’d access the leagueid
key. However, for a live match, the key is league_id
. Really strange.
Live matches have a set of their own useful methods like roshan_timer
or spectators_count
.
Information about participating teams is fetched in the same way: by calling radiant
or dire
. However, you can also call the score
and series_wins
method on each team object. If you are wondering what the complete?
method does, it simply tells if all players on one side belong to the same e-sports team.
Use radiant.players
or dire.players
to get an array of players. These objects, however, are instances of a separate LiveMatch::Player
class that has some additional methods, in addition to the common ones. For example, use player.position_x
and player.position_y
to track the current player position, or player.respawn_timer
to track how soon the dead hero will be respawned.
On the client side, you may implement some kind of a script to constantly poll for changes and update these values (by the way, I’ve written a couple of posts about polling, like this one.)
On the other hand, methods like hero_damage
or additional_units
are not available for live matches. Read more here.
Scheduled matches are fetched via scheduled_matches
, which accepts to
and from
params (timestamp has to be passed as a value). Returned objects are instances of the ScheduledMatch
class, which is not a child of BasicMatch
and, therefore, has its own methods. You can only fetch the league and match IDs, information about participating teams (instances of the Team
class), start time, description, and whether this is the final match of the competition. Read more here.
Testing
In conclusion, I want to say a couple of words about how testing in the dota gem is implemented. It uses RSpec and VCR to replay interactions with the Steam API.
The idea behind VCR is pretty simple: on the first run your automated tests access the API and perform all the required interactions. VCR records these requests and API responses in special YAML files (looking like this) that are called cassettes. These cassettes contain all the information about request and response like status, headers, and body. Cassettes are then employed during the next test runs.
Here is the example of using a VCR cassette. Inside the let
method, VCR.use_cassette
is called with the cassette’s name as an argument (that corresponds to the YAML file name). API interaction is simulated and then the required portion of the response is fetched (live_matches.first
in this case). test_client
is a simple method that returns a dota client.
If you’ve never heard of VCR, I really encourage you to read more as it comes in really handy when testing gems that interact with various services.
Conclusion
Welcome to the end of the article! We had a look at various data presented by the Steam API, discussed how the dota gem works, and how its testing is implemented. I hope you’ve enjoyed this series :).
If you’ve build you own app based on this demo, please share it in the comments. Thanks for staying with me and see you soon!