Статьи

Паровой DOTA на Рельсах

ss_09f21774b2309fcb67a2d9f8b385b47c48e985ff.600x338

После написания пары «серьезных» статей о построении систем аутентификации с помощью Rails я решил сделать перерыв и поиграть в некоторые игры. Я просто гуглил по онлайн играм и наткнулся на Dota 2 . Dota 2 — чрезвычайно популярная (более 10 000 000 уникальных игроков в прошлом месяце) боевая арена, разработанная Valve.

Игра довольно сложная, и некоторые веб-сервисы, такие как DotaBuff, были созданы, чтобы помочь игрокам отслеживать их успехи. После посещения DotaBuff и просмотра всех его прекрасных таблиц и графиков я просто не мог удержаться от поиска информации об API Dota 2. Рубин должен поделиться с вами своими исследованиями!

В этой статье мы собираемся поработать с Dota 2 API. Мы создадим приложение, которое заимствует основные функции из DotaBuff: пользователь входит в систему через Steam и видит список своих последних матчей и статистику. Я укажу все ошибки, с которыми я столкнулся при разработке этого приложения, и дам несколько советов о том, как это можно реализовать в производстве.

Исходный код доступен на GitHub .

Рабочую демоверсию можно найти на сайте pointpoint-dota.herokuapp.com .

Что такое дота?

Согласно Википедии, Dota 2 — это бесплатная многопользовательская онлайновая видеоигра на боевой арене. Это отдельное продолжение модификации Защита Древних для Warcraft III: Ледяной Трон. Его механика довольно сложна, поэтому я просто дам вам краткий обзор.

Первоначально DotA (Защита Древних) была пользовательской картой для Warcraft III, разработанной человеком под названием IceFrog. Через пару лет она стала настолько популярной, что Valve решила нанять IceFrog и создать автономную версию игры, которую они назвали Dota 2. Она была выпущена в 2013 году и теперь считается одной из самых популярных онлайн-игр в мире. Мир. Проводятся многие профессиональные соревнования с денежными призами, а сотни людей наблюдают за трансляциями опытных игроков.

Говоря об игровом процессе, есть две команды (Сияющая, добрая и Сильная, злая) с (как правило) 5 игроками в каждой. В начале игры каждый игрок выбирает одного героя из пула, содержащего более 100 героев, и входит в игру. У каждой команды есть своя база, и конечная цель — уничтожить главное здание на вражеской базе, защищая свое. Игроки могут покупать различные артефакты (каждый герой может взять до 6 предметов одновременно), убивать крипов (существ, управляемых ИИ) или вражеских героев, чтобы заработать золото и опыт. Каждый герой обладает уникальным набором способностей, которые игроки используют, чтобы помочь своим товарищам по команде или разрушить врагов. Это Дота в двух словах.

Давайте продолжим и посмотрим, что хорошего в Dota 2 API.

Препараты

Как всегда, мы сделаем некоторые приготовления, прежде чем перейти к интересной части. Создайте новое приложение Rails под названием Doter:

$ rails new Doter -T 

В этой статье я буду использовать Rails 4.2.1, но такое же решение можно реализовать с помощью Rails 3.

Если вы хотите следовать, подключите самоцвет bootstrap-sass для стилизации:

Gemfile

 [...] gem 'bootstrap-sass' [...] 

Бегать

 $ bundle install 

и поместите эти строки в application.scss :

application.scss

 @import 'bootstrap-sprockets'; @import 'bootstrap'; @import 'bootstrap/theme'; 

Теперь настройте макет следующим образом:

просмотров / макеты / application.html.erb

 <nav class="navbar navbar-inverse"> <div class="container"> <div class="navbar-header"> <%= link_to 'Doter', root_path, class: 'navbar-brand' %> </div> <div id="navbar"> </div> </div> </nav> <div class="container"> <% flash.each do |key, value| %> <div class="alert alert-<%= key %>"> <%= value %> </div> <% end %> <div class="page-header"><h1><%= yield :page_header %></h1></div> <%= yield %> </div> 

Добавьте вспомогательный метод page_header для предоставления содержимого для блока yield :

application_helper.rb

 module ApplicationHelper def page_header(text) content_for(:page_header) { text.to_s.html_safe } end end 

Наконец, настройте маршруты. Я хотел бы отображать совпадения пользователей на главной странице сайта, поэтому создайте пустой MatchesController :

matches_controller.rb

 class MatchesController < ApplicationController end 

Соответствующая модель будет добавлена ​​чуть позже. Теперь актуальные маршруты:

конфиг / routes.rb

 [...] root to: 'matches#index' [...] 

Не забудьте создать представление для действия index :

просмотры / матчи / index.html.erb

 <% page_header 'Your matches' %> 

Отлично, приготовления сделаны, и мы можем двигаться вперед!

Аутентификация через Steam

Steam, как и многие другие веб-платформы, поддерживает протокол OAuth, и вы можете аутентифицировать своего пользователя, чтобы получить его данные, такие как uID, псевдоним, аватар и т. Д. (Вы можете прочитать мою статью Rails Authentication с OAuth 2.0 и статью OmniAuth, чтобы узнать больше об этом протоколе ). Вам не нужно делать это, чтобы фактически выполнять вызовы Steam API, но нам нужен идентификатор пользователя, чтобы показать список совпадений. Поэтому мы будем использовать камень https://github.com/reu/omniauth-steam от Rodrigo Navarro, который добавляет стратегию аутентификации Steam в OmniAuth:

Gemfile

 [...] gem 'omniauth-steam' [...] 

Не забудь бежать

 $ bundle install 

Теперь создайте файл инициализатора Omniauth:

конфиг / Инициализаторы / omniauth.rb

 Rails.application.config.middleware.use OmniAuth::Builder do provider :steam, ENV['STEAM_KEY'] end 

Хорошо, где этот ключ? Зайдите на эту страницу (вам потребуется аккаунт Steam для продолжения) и просто зарегистрируйте новый ключ. Из всех веб-платформ, поддерживающих OAuth, Steam предоставляет самый быстрый способ регистрации вашего приложения.

Теперь давайте решим, какую пользовательскую информацию мы хотим сохранить. На самом деле, Steam предоставляет довольно минималистичный набор данных, поэтому у нас не так много вариантов для выбора. Я собираюсь придерживаться следующего:

  • uid ( string , индекс, уникальный) — уникальный идентификатор пользователя.
  • nickname ( string ) — ник пользователя (Steam также предоставляет имя).
  • avatar_url ( string ) — ссылка на аватар пользователя. Кстати, мы не можем выбрать размер аватара.
  • profile_url ( string ) — URL профиля пользователя в Steam.

Запустите следующую команду:

 $ rails g model User uid:string nickname:string avatar_url:string profile_url:string 

Откройте файл миграции и добавьте эту строку:

дб / Миграция / xxx_create_users.rb

 [...] add_index :users, :uid, unique: true [...] 

после метода create_table .

Применить миграцию:

 $ rake db:migrate 

Добавьте несколько маршрутов:

конфиг / routes.rb

 [...] match '/auth/:provider/callback', to: 'sessions#create', via: :all delete '/logout', to: 'sessions#destroy', as: :logout [...] 

Первый — это маршрут обратного вызова, по которому пользователь будет перенаправлен после успешной аутентификации. Здесь мне пришлось использовать match потому что по какой-то причине Steam, похоже, отправляет POST-запрос вместо GET (это то, что делают большинство других платформ). Второй маршрут будет использоваться для выхода пользователя из системы.

Теперь контроллер для обработки этих маршрутов. Я начну с действия по create :

sessions_controller.rb

 class SessionsController < ApplicationController skip_before_filter :verify_authenticity_token, only: :create def create begin @user = User.from_omniauth request.env['omniauth.auth'] rescue flash[:error] = "Can't authorize you..." else session[:user_id] = @user.id flash[:success] = "Welcome, #{@user.nickname}!" end redirect_to root_path end end 

Этот skip_before_filter — еще одна ошибка с аутентификацией через Steam. Он отправляет запрос POST, но, конечно, он не предоставляет токен CSRF. Поэтому по умолчанию вы получите сообщение о том, что кто-то пытается отправить вредоносные данные. Таким образом, мы должны пропустить эту проверку для действия create .

Само действие create просто. request.env['omniauth.auth'] данные пользователя, хранящиеся в request.env['omniauth.auth'] и создайте нового пользователя или найдите существующего на основе указанных данных (мы from_omniauth метод from_omniauth ). Если все в порядке, сохраните идентификатор пользователя в сеансе, установите приветственное сообщение и перенаправьте на главную страницу.

Теперь метод from_omniauth :

модели / user.rb

 [...] class << self def from_omniauth(auth) info = auth['info'] # Convert from 64-bit to 32-bit user = find_or_initialize_by(uid: (auth['uid'].to_i - 76561197960265728).to_s) user.nickname = info['nickname'] user.avatar_url = info['image'] user.profile_url = info['urls']['Profile'] user.save! user end end [...] 

Найдите пользователя по его uID или создайте нового, а затем просто загрузите все необходимые данные. Но что это 76561197960265728 номер 76561197960265728 ? Это третья ошибка. При аутентификации через Steam возвращается 64-битный uID пользователя. Однако, когда список Dota 2 совпадает, вместо него используются 32-битные идентификаторы пользователя. Конечно, есть причина для этого, но я не смог найти никакого объяснения. В любом случае нам нужно преобразовать 64-битный идентификатор в 32-битный, и самый простой способ сделать это — вычесть это большое число. Не волнуйтесь, Ruby позаботится о номерах BigInt для нас прозрачно, поэтому вам не нужно выполнять никаких дополнительных действий.

Действие destroy еще проще:

sessions_controller.rb

 def destroy if current_user session.delete(:user_id) flash[:success] = "Goodbye!" end redirect_to root_path end 

Нам нужен метод current_user чтобы проверить, вошел ли пользователь в систему:

application_controller.rb

 [...] private def current_user return nil unless session[:user_id] @current_user ||= User.find_by(id: session[:user_id]) end helper_method :current_user [...] 

helper_method гарантирует, что этот метод также может быть вызван из представлений.

Изменить макет:

просмотров / макеты / application.html.erb

 [...] <nav class="navbar navbar-inverse"> <div class="container"> <div class="navbar-header"> <%= link_to 'Doter', root_path, class: 'navbar-brand' %> </div> <div id="navbar"> <% if current_user %> <ul class="nav navbar-nav pull-right"> <li><%= image_tag current_user.avatar_url, alt: current_user.nickname %></li> <li><%= link_to 'Log Out', logout_path, method: :delete %></li> </ul> <% else %> <ul class="nav navbar-nav"> <li><%= link_to 'Log In via Steam', '/auth/steam' %></li> </ul> <% end %> </div> </div> </nav> [...] 

Я уже упоминал, что Steam не позволяет изменять размер аватара, поэтому мы должны применить некоторые стили, чтобы он выглядел красиво:

application.scss

 [...] nav { img { max-width: 48px; } } 

Теперь вы можете перезагрузить сервер и увидеть все это в действии!

Интеграция с API Steam

Следующий шаг — интеграция с API Steam. Если вам нужен доступ к основному веб-API Steam, вы можете использовать steam-web-api от Olga Grabek или steam-api от Brian Haberer. Однако, поскольку мы собираемся работать конкретно с материалами, связанными с Dota 2, было бы неплохо иметь отдельный гем, который предоставляет несколько удобных методов. К счастью, есть винодельческий камень от Vinni Carlo Canos, который предоставляет множество полезных функций (хотя он еще не закончен на 100%). Прежде чем продолжить, вы можете прочитать о Dota 2 API. К сожалению, пока нет страницы с исчерпывающей и актуальной документацией, но вот некоторые ресурсы:

Когда вы будете готовы, поместите новый драгоценный камень в Gemfile:

Gemfile

 [...] gem 'dota', github: 'vinnicc/dota', branch: 'master' [...] 

На момент написания статьи мне пришлось указать основную ветвь, потому что недавно мы обнаружили, что некоторые изменения были внесены в Dota 2 API, и они учтены в последнем запросе на получение, однако новая версия гема еще не выпущена. Кстати, я хотел предупредить вас, что некоторые аспекты Dota 2 API могут время от времени меняться, и вы должны следить за этим.

Бегать

 $ bundle install 

Создайте инициализатор:

конфиг / Инициализаторы / dota.rb

 Dota.configure do |config| config.api_key = ENV['STEAM_KEY'] end 

Поместите ключ Steam, который вы получили ранее в этом файле.

По сути, это все. Теперь вы готовы выполнять запросы API.

Получение пользовательских совпадений

Я хочу получить список совпадений пользователей, как только они аутентифицируются через Steam. Однако, прежде чем мы продолжим, я хотел бы отметить одну вещь. Решение, описанное в этой статье, реализовано только в демонстрационных целях. Если вы хотите создать реальное приложение, похожее на DotaBuff, вам придется реализовать более сложную систему с некоторым фоновым процессом, который постоянно проверяет, есть ли какие-либо новые совпадения, сыгранные конкретным пользователем (или даже если есть какие-либо новые совпадения на все) и загружает соответствующие данные. Затем, когда пользователь входит в систему, вы просто предоставляете список всех совпадений из локальной базы данных, а не отправляете синхронный вызов в API Dota 2. Загрузка информации даже для 40 матчей может занять до 1 минуты — а у пользователей обычно их сотни.

Это потребует огромного количества ресурсов (только представьте, сколько матчей играется каждый день), и, очевидно, я не могу позволить себе создать такую ​​среду. Однако, когда у вас есть достаточное количество данных, вы можете отображать действительно интересную статистическую информацию, например, предоставляемую DotaBuff.

Хорошо, продолжаем, какую информацию об одном матче мы можем получить? Вот список доступных методов, которые вы можете использовать. Мы будем хранить следующее:

  • uid ( string , индекс) — уникальный идентификатор совпадения.
  • winner ( string ) — Какая команда выиграла матч. На самом деле, это будет либо «Сияющий», либо «Страшный».
  • first_blood ( string ) — Когда произошла первая кровь (первое убийство героя). Dota 2 API возвращает количество секунд с начала матча, но вместо этого мы будем хранить отформатированное значение. Не стесняйтесь сделать этот столбец целым и хранить вместо него необработанное значение.
  • started_at ( datetime ) — когда матч начался.
  • mode ( string ) — режим соответствия. Вот список всех доступных режимов. Мы собираемся сохранить его заголовок, но вы также можете сохранить его идентификатор (Dota 2 API обеспечивает оба).
  • match_type ( string ) — тип совпадения. Вот список всех доступных типов. Опять же, вы можете сохранить его название или идентификатор. Не вызывайте этот type столбца, потому что он зарезервирован для наследования одной таблицы!
  • duration ( string ) — Продолжительность матча. Опять же, API предоставляет количество секунд, но мы будем хранить отформатированное значение.
  • user_id ( integer , index) — user_id ключ для установления отношения один-ко-многим между совпадениями и пользователями.

Создайте и примените миграцию:

 $ rails g model Match uid:string winner:string started_at:datetime mode:string match_type:string duration:string user:references $ rake db:migrate 

Убедитесь, что у вас есть эти строки в файлах модели:

модели / user.rb

 [...] has_many :matches [...] 

модели / match.rb

 [...] belongs_to :user [...] 

На фактической загрузке матча. Я хочу сделать это, как только пользователь вошел в систему:

sessions_controller.rb

 [...] def create begin @user = User.from_omniauth request.env['omniauth.auth'] rescue flash[:error] = "Can't authorize you..." else @user.load_matches!(1) session[:user_id] = @user.id flash[:success] = "Welcome, #{@user.nickname}!" end redirect_to root_path end [...] 

1 — количество совпадений для загрузки. Вот это load_matches! метод:

модели / user.rb

 [...] def load_matches!(count) matches_arr = Dota.api.matches(player_id: self.uid, limit: count) if matches_arr && matches_arr.any? matches_arr.each do |match| unless self.matches.where(uid: match.id).any? match_info = Dota.api.matches(match.id) new_match = self.matches.create({ uid: match.id, winner: match_info.winner.to_s.titleize, first_blood: parse_duration(match_info.first_blood), started_at: match_info.starts_at, mode: match_info.mode, cluster: match_info.cluster, duration: parse_duration(match_info.duration), match_type: match_info.type }) end end end end [...] 

Прежде всего, загрузите совпадения для предоставленного проигрывателя (не забывайте, что здесь мы используем 32-битный идентификатор Steam, а не 64-битный). Конечно, метод match на самом деле не загружает все — у него есть опция limit установленная по умолчанию на 100, но мы перезаписываем ее своим собственным значением. Этот метод принимает некоторые другие параметры, подробнее здесь .

Затем мы проверяем, загружено ли совпадение с этим идентификатором, и, если нет, загружаем его данные. Почему мы должны снова вызывать метод matches , но на этот раз с точным идентификатором совпадения? Суть в том, что первый вызов вернет ограниченное количество полей по соображениям производительности, поэтому для получения полной информации нам нужно запросить конкретное совпадение.

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

модели / user.rb

 [...] private 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 [...] 

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

matches_controller.rb

 [...] def index @matches = current_user.matches.order('started_at DESC') if current_user end [...] 

и сделать их

просмотры / матчи / index.html.erb

 <% page_header 'Your matches' %> <% if @matches && @matches.any? %> <table class="table table-striped table-hover"> <% @matches.each do |match| %> <tr> <td> <%= link_to match.started_at, match_path(match) %> </td> </tr> <% end %> </table> <% end %> 

Я решил предоставить минимальный набор данных на этой странице и использовать действие show для отображения более подробной информации. Давайте введем новый маршрут:

конфиг / routes.rb

 [...] resources :matches, only: [:index, :show] [...] 

Действие контроллера:

matches_controller.rb

 [...] def show @match = Match.find_by(id: params[:id]) end [...] 

А теперь мнение:

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

Здесь мы предоставляем всю доступную информацию. Было бы неплохо использовать разные цвета для команд Radiant и Dire, поэтому я представил два CSS-класса:

application.scss

 [...] $radiant: #92A524; $dire: #C23C2A; .dire { color: $dire; } .radiant { color: $radiant; } [...] 

Это довольно мило, но не очень информативно. Пользователь выиграл матч? В какого героя они играли? Какие предметы у них были? Сколько убийств у них было? Было бы здорово отобразить эту информацию, так что давайте перейдем к следующему шагу и усовершенствуем наше приложение!

Загрузка игроков

Мы можем ответить на все эти вопросы, просто загрузив информацию об игроках, участвующих в матче. Методы radiant и dire вызываемые в экземпляре match возвращают массив объектов player, каждый из которых имеет свои собственные методы . Здесь доступно много информации, и мы собираемся хранить большую ее часть:

  • match_id ( integer , index) — match_id ключ для установления отношения один ко многим.
  • uid ( string ) — 32-битный уникальный идентификатор игрока.
  • hero ( text ) — герой игрока. Это будет сериализованный атрибут, хранящий идентификатор героя, имя и ссылку на его изображение.
  • level ( integer ) — уровень героя к концу матча.
  • kills ( integer ) — убийства игрока.
  • deaths ( integer ) — смерти игрока.
  • assists ( integer ) — ассисты игрока.
  • last_hits ( integer ) — последние попадания игрока (сколько крипов они убили).
  • denies ( integer ) — отрицание игрока (сколько крипов союзников он отрицает, не позволяя врагам получить золото за их убийство).
  • gold ( integer ) — количество золота, которое игрок имел к концу матча.
  • gpm ( integer ) — золото за минуту.
  • xpm ( integer ) — xpm опыта за минуту.
  • status ( string ) — статус игрока к концу матча. Возможно, они остались до конца игры, по какой-то причине были оставлены, благополучно оставлены (например, при обнаружении плохого сетевого соединения) или никогда не подключались к игре.
  • gold_spent ( integer ) — общее количество золота, потраченное игроком во время матча.
  • hero_damage ( integer ) — Общее количество урона, нанесенного вражеским героям.
  • tower_damage ( integer ) — Общее количество урона, нанесенного вражеским башням.
  • hero_healing ( integer ) — Общее количество исцеления союзных героев.
  • items ( text ) — Сериализированный атрибут, содержащий массив предметов, которые игрок имел к концу игры. Мы будем хранить идентификатор каждого предмета, название и ссылку на его изображение.
  • slot ( integer ) — слот игрока в команде (от 1 до 5).
  • radiant ( boolean ) — указывает на команду игрока. В простейшем сценарии есть только две возможности ( radiant или dire , однако кажется, что вы можете быть отмечены как observer ), поэтому я использовал логический атрибут, но вы можете использовать свой собственный способ хранения этой информации.

Это много данных для хранения! Создайте и примените соответствующую миграцию:

 $ rails g model Player match:references uid:string hero:text level:integer kills:integer deaths:integer assists:integer last_hits:integer denies:integer gold:integer gpm:integer xpm:integer status:string gold_spent:integer hero_damage:integer tower_damage:integer hero_healing:integer items:text slot:integer radiant:boolean $ rake db:migrate 

Добавьте следующие строки в наши файлы моделей:

модели / match.rb

 [...] has_many :players, dependent: :delete_all [...] 

модели / player.rb

 [...] belongs_to :match serialize :hero serialize :items [...] 

load_matches! метод как это:

модель / user.rb

 [...] def load_matches!(count) matches_arr = Dota.api.matches(player_id: self.uid, limit: count) if matches_arr && matches_arr.any? matches_arr.each do |match| unless self.matches.where(uid: match.id).any? match_info = Dota.api.matches(match.id) new_match = self.matches.create({ uid: match.id, winner: match_info.winner.to_s.titleize, first_blood: parse_duration(match_info.first_blood), started_at: match_info.starts_at, mode: match_info.mode, cluster: match_info.cluster, duration: parse_duration(match_info.duration), match_type: match_info.type }) end end end end [...] 

load_players! Метод будет принимать два отдельных объекта с информацией о командах Radiant и Dire.

модель / match.rb

 [...] def load_players!(radiant, dire) roster = {radiant: radiant, dire: dire} roster.each_pair do |k, v| v.players.each do |player| self.players.create({ uid: player.id, items: player.items.delete_if { |item| item.name == "Empty" }.map { |item| {id: item.id, name: item.name, image: item.image_url} }, hero: {id: player.hero.id, name: player.hero.name, image: player.hero.image_url}, level: player.level, kills: player.kills, deaths: player.deaths, assists: player.assists, last_hits: player.last_hits, denies: player.denies, gold: player.gold, gpm: player.gpm, xpm: player.xpm, status: player.status.to_s.titleize, gold_spent: player.gold_spent, hero_damage: player.hero_damage, tower_damage: player.tower_damage, hero_healing: player.hero_healing, slot: player.slot, radiant: k == :radiant }) end end end [...] 

Каждый объект ( radiant и dire ) отвечает на метод players который на самом деле возвращает массив игроков. Большая часть этого метода довольно проста, поэтому я объясню, возможно, неясные моменты.

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

Здесь мы вызываем items на объекте player чтобы получить предметы, которые игрок имел к концу матча. items возвращает другой объект, который является экземпляром отдельного класса. Этот объект реагирует на три основных метода: id ( id элемента), name ( name элемента) и image_url (URL-адрес изображения элемента, хранящегося в Dota 2 CDN). У каждого игрока есть 6 слотов для хранения предметов. Если слот был пустым, указывается имя «Пусто». Нам не нужно сохранять информацию о пустых слотах, поэтому просто удалите все эти элементы. После этого сгенерируйте хеш, содержащий всю информацию, и сохраните ее внутри столбца. Благодаря сериализации, позже мы можем получить этот хеш и использовать его как обычно.

 hero: {id: player.hero.id, name: player.hero.name, image: player.hero.image_url}, 

Идея здесь та же. hero возвращает отдельный объект, который отвечает трем основным методам: id ( id героя), name ( name героя) и image_url (изображение героя).

Обновление 05/05/2015

Я обнаружил, что мы можем выбрать размер портрета героя, предоставив необязательный аргумент для метода image_url . Вот список всех возможных значений:

* :full — полный горизонтальный портрет (256x114px, PNG). Это используется по умолчанию.
* :lg — большой горизонтальный портрет (205x11px, PNG).
* :sb — маленький горизонтальный портрет (59x33px, PNG). Я рекомендую использовать этот, поскольку нам нужен наименьший возможный портрет.
* :vert — вертикальный портрет высокого качества (234x272px, JPEG). Как ни странно, это может быть только * .jpg *.

Загружайте игроков, группируя их по командам — true для Radiant, false для Dire. Кстати, это кажется псевдофилософским: лучистые — это хорошо, поэтому они «верны», а ужасные — плохи, хаотичны, значит, они «ложны» :).

matches_controller.rb

 [...] def show @match = Match.includes(:players).find_by(id: params[:id]) @players = @match.players.order('slot ASC').group_by(&:radiant) end [...] 

Обновите представление, соответственно:

просмотры / матчи / 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> <h3 class="radiant">Team Radiant</h3> <%= render 'players_table', players: @players[true] %> <h3 class="dire">Team Dire</h3> <%= render 'players_table', players: @players[false] %> 

Я использую частичный, чтобы избежать дублирования кода:

просмотры / матчи / _players_table.html.erb

 <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)</td> <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.titleize %>"><%= player.uid %></abbr> <% else %> <%= player.uid %> <% end %> </td> <td><%= render 'player_hero', hero: player.hero %></td> <td><%= player.level %></td> <td> <% player.items.each do |item| %> <%= image_tag item[:image], alt: item[:name], title: item[:name] %> <% end %> </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> 

Давайте двигаться сверху вниз. info-table — это CSS-класс для применения некоторых специальных стилей к изображениям внутри этой таблицы:

application.scss

 .info-table { img { width: 30px; } } 

Идея в том, что все картинки, возвращаемые API-интерфейсом Dota 2, довольно большие, поэтому мы просто уменьшаем их.

abandoned_or_not_connected? это метод, который делает в значительной степени то, что говорит — проверяет, покинули ли игроки игру или не подключились вообще:

модели / player.rb

 [...] def abandoned_or_not_connected? status != 'played' end [...] 

Если игрок не остался до конца матча, мы принимаем к сведению.

player_hero это еще один частичный:

просмотры / матчи / _player_hero.html.erb

 <%= image_tag hero[:image], alt: hero[:name], title: hero[:name] %> 

Большой! Теперь это выглядит гораздо более информативно. Однако у пользователя все еще нет простого способа проверить, выиграл ли он матч. Давайте это исправим!

Кто выиграл?

Чтобы это выяснить, просто нужно выяснить, за какую команду играл пользователь и какая команда выиграла матч. Я настрою вид так:

просмотры / матчи / index.html.erb

 <% if @matches && @matches.any? %> <table class="table table-striped table-hover info-table"> <% @matches.each do |match| %> <tr> <td> <%= link_to match.started_at, match_path(match) %> <% if current_user.won?(match) %> <span class="label label-success">won</span> <% else %> <span class="label label-danger">lost</span> <% end %> </td> </tr> <% end %> </table> <% end %> 

Ввести won? метод, который принимает единственный аргумент — объект match :

модели / user.rb

 [...] def won?(match) player = find_self_in(match) (player.radiant? && match.winner == 'Radiant') || (!player.radiant? && match.winner == 'Dire') end private def find_self_in(match) match.players.find_by(uid: uid) end [...] 

Ницца! Как насчет показа героя игрока? Ну вот:

просмотры / матчи / index.html.erb

 <table class="table table-striped table-hover info-table"> <% @matches.each do |match| %> <tr> <td> <%= render 'player_hero', hero: current_user.played_for_in(match) %> <%= link_to match.started_at, match_path(match) %> <% if current_user.won?(match) %> <span class="label label-success">won</span> <% else %> <span class="label label-danger">lost</span> <% end %> </td> </tr> <% end %> </table> 

модели / user.rb

 [...] def played_for_in(match) find_self_in(match).hero end [...] 

Для производства абсолютно необходимо использовать кеширование здесь. Например, вы можете использовать кеширование моделей следующим образом:

 def played_for_in(match) Rails.cache.fetch(self.uid + '_played_for_in_' + match.uid) { find_self_in(match).hero } end 

Только не забудьте установить подходящие условия кэширования.

Вывод

Мы подошли к концу этой статьи. Мы использовали базовые методы API Dota 2 и создали приложение, чтобы предоставить пользователю основную информацию о последних совпадениях. Не стесняйтесь расширять эту демонстрацию и делиться результатами!

Работать с Dota 2 API весело, и я надеюсь, вам тоже понравилось! Вы заинтересованы в просмотре второй части по этой теме? Если это так, в следующей статье я расскажу о получении данных для матчей Dota 2 в реальном времени.

Спасибо за чтение. Удачи и веселья (как говорят игроки Dota)!