Статьи

DOTA 2 on Rails: копать глубже

dota2-логотип

Эта статья является второй в моей серии «Dota 2 on Rails». В первой части мы обсудили, что такое Dota, как подключаться к Steam API, какие данные он предоставляет. Приложение в части 1 представляет базовую статистику для аутентифицированного пользователя. Эта публикация вызвала небольшой интерес, поскольку некоторые читатели были даже вдохновлены созданием своих собственных сайтов статистики по Dota 2 – это действительно здорово.

В этой статье мы продолжим работу с демонстрационным приложением, поэтому, если вы хотите следовать, начальный код можно найти в этой ветке . Я покажу вам, какие другие данные вы можете получить с помощью dota gem, как работать с живыми и запланированными матчами, как работает гем и как организовано тестирование.

Окончательную версию кода можно найти на GitHub .

Рабочая демоверсия доступна по той же ссылке sitepoint-dota.herokuapp.com .

Получение улучшений способностей игроков

Dota Gem, который мы будем использовать в этой статье, постепенно развивается – сегодня мы с гордостью представляем несколько новых функций. Первым из них является поддержка выборки обновлений данных.

Что такое повышение способности? Как вы помните, каждый игрок в Dota 2 контролирует своего собственного героя, который обладает набором уникальных способностей (навыков). По мере прокачки героя они могут изучать новые способности или улучшать уже существующие – вот что такое модернизация способностей. Круто то, что Steam API представляет массив для каждого игрока, содержащий идентификатор способности, время и уровень, когда способность была изучена. Как насчет использования этих данных в нашем приложении?

Прежде всего, создайте и примените новую миграцию:

$ rails g migration add_ability_upgrades_to_players ability_upgrades:text $ rake db:migrate 

Этот атрибут должен быть сериализован, потому что мы хотим хранить массив хэшей:

модели / player.rb

 [...] serialize :ability_upgrades [...] 

Теперь измените метод загрузки игроков:

модели / 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 [...] 

Под капотом возвращается массив, содержащий экземпляры класса AbilityUpgrade . ability_upgrade.ability – это экземпляр класса Ability который имеет такие атрибуты, как id , name , full_name и image_url . full_name возвращает имя способности с именем героя (например, «Antimage Mana Break»), тогда как name возвращает только имя способности.

ability.image_url(:hp1) означает, что мы хотим извлечь URL для небольшого изображения способности (90 × 90) – другие аргументы также могут быть переданы.

Кстати, вы можете легко найти способ по его идентификатору, используя метод Dota.api.abilities(id) . Все способности кэшируются в файле способность . Недостатком здесь является то, что этот файл должен постоянно обновляться, потому что, по мере выпуска новых версий Dota, возможности могут быть добавлены или переработаны.

sort_by сортирует массив по уровню (по возрастанию).

ability_upgrade.time возвращает количество секунд с момента начала игры, когда способность была изучена. Я использую parse_duration чтобы отформатировать его как 00:00:00 . Этот метод уже использовался в файле models / user.rb , поэтому давайте выделим его в отдельный файл:

Библиотека / 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 

и включите это:

модели / user.rb

 require './lib/utils' class User < ActiveRecord::Base include Utils [...] end 

модели / match.rb

 require './lib/utils' class Match < ActiveRecord::Base include Utils [...] end 

Не забудьте удалить этот метод из user.rb.

Хорошо, наконец, давайте отобразим эту новую информацию на странице show :

просмотры / матчи / 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' %> 

Здесь я переименовал частичный _players_table.html.erb в _details_table.html.erb и уменьшил дублирование кода. Эта часть содержит следующий код:

просмотры / матчи / _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> 

Первая таблица остается неповрежденной, а вторая представляет сборку героя (в Dota 2 максимальный уровень – 25).

Возможно, вы захотите поиграть с макетом и стилем, но в целом эта функциональность теперь работает – попробуйте и попробуйте! Если однажды вы заметите, что это больше не работает, это, вероятно, означает, что была добавлена ​​какая-то новая способность, о которой еще не знают самоцветы. В этом случае не стесняйтесь обновить файлility.yml и отправить свой PR :).

Дополнительные блоки

Еще одна новая функция в Dota Gem – это возможность получать информацию о дополнительных юнитах под контролем игрока. В общем, игрок контролирует только одного героя, однако при некоторых обстоятельствах он может вызывать или подчинять другие юниты (классический пример – Дух-медведь Одинокого друида ).

Steam API предоставляет информацию о том, какие дополнительные юниты игрок контролировал в конце игры. Эта информация довольно минималистична (только имя юнита и предметы, если есть), но все же полезна. Продолжайте и примените новую миграцию:

 $ rails g migration add_additional_units_to_players additional_units:text $ rake db:migrate 

Сериализуйте атрибут:

модели / player.rb

 [...] serialize :additional_units [...] 

и, опять же, измените load_players! метод:

модели / 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 – это простой метод, который возвращает массив, содержащий экземпляры класса класса Unit .

parse_items – это вспомогательный метод, который удаляет пустые слоты (каждый герой и некоторые юниты имеют 6 слотов инвентаря) и создает хэш с информацией об элементах:

модели / 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 

Не забудьте упростить следующие строки:

модели / match.rb

 items: player.items.delete_if { |item| item.name == "Empty" }.map { |item| {id: item.id, name: item.name, image: item.image_url} }, 

в

модели / match.rb

 items: parse_items(player.items), 

Теперь отобразите новую информацию:

просмотры / матчи / _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> [...] 

Мы просто добавляем еще один ряд на каждую дополнительную единицу, которую игрок контролирует. _items.html.erb – это фрагмент , который был представлен в предыдущей статье.

Если эта функциональность перестает работать, это, вероятно, означает, что был введен новый элемент, и нужно обновить самоцвет dota. Вы можете легко получить актуальный список всех товаров, посетив http://api.steampowered.com/IEconItems_570/GetSchemaURL/v0001/?key= URL. Однако иногда эта информация также может быть не на 100% точной (судя по дискуссиям на форуме разработчиков ).

Состояние башен и казарм

Как вы, наверное, помните, у каждой команды есть своя база для защиты. Однако, прежде чем противник сможет войти на вашу базу, он должен разрушить три башни на одной из дорожек. Эти башни называются Tier 1, Tier 2 и Tier 3. Tier 3 расположен около входа в базу; там же находятся две казармы (для дальнего и ближнего боя). Помимо этих башен, в главном здании есть еще два охранника, которых нужно уничтожить, чтобы выиграть игру.

Steam API предоставляет статус башен и казарм, и мы собираемся использовать эту информацию также. Состояние раньше представлялось в двоичном формате, где каждая цифра соответствовала определенному зданию; 0 означало, что здание было разрушено, а 1 означало, что здание стояло к концу игры. Однако теперь Steam API просто возвращает десятичное число для статуса. Итак, как мы справляемся с этим в дота-джеме?

Прежде всего, есть два массива, содержащие башни и казармы. Тогда есть специальный метод для преобразования десятичного числа в более удобный формат. Он берет число и преобразует его в двоичный файл, используя to_s(2) . to_s число в качестве аргумента методу to_s вы устанавливаете базу, и это действительно изящный прием.

В некоторых случаях Steam API может возвращать статус, такой как 5 который равен 101 в двоичном формате, что означает, что начальные нули были удалены. Однако этого нам недостаточно, поэтому добавьте недостающие нули с помощью rjust .

В результате этот метод возвращает хеш с именами башен и казарм в качестве ключей и значениями true или false качестве значений.

Вооружившись этими знаниями, теперь мы можем применить новую миграцию:

 $ rails g migration add_towers_status_and_barracks_status_to_matches towers_status:text barracks_status:text $ rake db:migrate 

Сериализуйте эти атрибуты:

модели / match.rb

 [...] serialize :towers_status serialize :barracks_status [...] 

и измените load_matches! метод:

модели / 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 [...] 

Вот метод parse_buildings :

модели / user.rb

 [...] def parse_buildings(arr) arr.keep_if {|k, v| v }.keys end [...] 

По сути, мы сохраняем только названия зданий, которые все еще стояли в конце игры. Я не говорю, что это лучший формат для хранения этих данных, поэтому вы можете выбрать другие, особенно если вы хотите визуализировать это на небольшой карте Dota (как это делается в Dotabuff).

На взгляд:

просмотры / матчи / _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 используется для преобразования имени системы здания в удобный для пользователя формат. Не стесняйтесь рефакторинг это дальше.

Симпатии и антипатии

Последняя информация, которую мы собираемся отобразить, это количество лайков и антипатий в матче. Это действительно легко сделать. Применить миграцию:

 $ rails g migration add_likes_and_dislikes_to_matches likes:integer dislikes:integer $ rake db:migrate 

Изменить load_matches! метод:

модели / user.rb

 [...] def load_matches!(count) [...] new_match = self.matches.create({ [...] likes: match_info.positive_votes, dislikes: match_info.negative_votes, [...] end [...] 

И настроить вид:

просмотры / матчи / 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> [...] 

Торт с пиццей!

Живые и запланированные матчи

Вы можете работать с живыми матчами, как с готовыми, только некоторые вещи меняются.

Используйте метод live_matches чтобы получить все живые совпадения, передавая league_id или match_id чтобы сузить область видимости.

LiveMatch является дочерним BasicMatch класса BasicMatch , поэтому его экземпляр отвечает на множество одних и тех же методов. Однако необработанная информация, предоставляемая API Steam, имеет некоторые странные несоответствия. Например, чтобы получить идентификатор лиги для завершенного матча, вам нужно получить доступ к leagueid . Тем не менее, для живого матча ключ league_id . Действительно странно

Живые матчи имеют набор собственных полезных методов, таких как roshan_timer или spectators_count .

Информация об участвующих командах выбирается таким же образом: по radiant или по телефону. Однако вы также можете вызвать метод series_wins и series_wins для каждого командного объекта. Если вам интересно, что в complete? метод делает, он просто сообщает, все ли игроки на одной стороне принадлежат к одной и той же киберспортивной команде.

Используйте radiant.players или dire.players чтобы получить множество игроков. Эти объекты, однако, являются экземплярами отдельного LiveMatch::Player который имеет некоторые дополнительные методы, в дополнение к обычным . Например, используйте player.position_x и player.position_y чтобы отслеживать текущую позицию игрока, или player.respawn_timer чтобы отслеживать, как скоро мертвый герой будет возрожден.

На стороне клиента вы можете реализовать какой-то скрипт для постоянного опроса изменений и обновления этих значений (кстати, я написал пару постов об опросе, как этот ).

С другой стороны, такие методы, как hero_damage или hero_damage , недоступны для живых матчей. Узнайте больше здесь .

Запланированные совпадения выбираются с помощью scheduled_matches , который принимает параметры to и to (отметка времени должна быть передана как значение). Возвращенные объекты являются экземплярами класса ScheduledMatch , который не является дочерним по отношению к BasicMatch и, следовательно, имеет свои собственные методы. Вы можете получить только идентификаторы лиги и матчей, информацию об участвующих командах (экземпляры класса Team ), время начала, описание и информацию о том, является ли это финальным матчем соревнования. Узнайте больше здесь .

тестирование

В заключение хочу сказать пару слов о том, как реализовано тестирование в dota gem. Он использует RSpec и VCR для воспроизведения взаимодействий со Steam API.

Идея VCR довольно проста: при первом запуске ваши автоматизированные тесты получают доступ к API и выполняют все необходимые взаимодействия. Видеомагнитофон записывает эти запросы и ответы API в специальные файлы YAML (выглядящие так), которые называются кассетами . Эти кассеты содержат всю информацию о запросе и ответе, такую ​​как статус, заголовки и тело. Кассеты затем используются во время следующих тестовых прогонов.

Вот пример использования кассеты VCR. Внутри метода let VCR.use_cassette вызывается с именем кассеты в качестве аргумента (что соответствует имени файла YAML). Взаимодействие API моделируется, а затем выбирается необходимая часть ответа (в данном случае live_matches.first ). test_client – это простой метод, который возвращает Dota-клиент.

Если вы никогда не слышали о видеомагнитофоне, я настоятельно рекомендую вам прочитать больше, поскольку он очень удобен при тестировании драгоценных камней, взаимодействующих с различными сервисами.

Вывод

Добро пожаловать в конец статьи! Мы рассмотрели различные данные, представленные API-интерфейсом Steam, обсудили, как работает dota gem и как осуществляется его тестирование. Надеюсь, вам понравился этот сериал :).

Если вы создали собственное приложение на основе этой демонстрации, поделитесь им в комментариях. Спасибо, что остаетесь со мной и до скорой встречи!