12/08/2018, 13:00

Dota on Rails

I. Giới thiệu về Dota 2 và hệ thống API 1. Dota 2 Dota 2 là một trò chơi hành động chiến thuật thời gian thực (ARTS) được Valve Corporation phát triển, dựa theo một mod game nổi tiếng, Defense of the Ancients, từ trò chơi Warcraft III: Reign of Chaos và bản mở rộng của nó The Frozen Throne. ...

dota2_icon.jpg

I. Giới thiệu về Dota 2 và hệ thống API

1. Dota 2

Dota 2 là một trò chơi hành động chiến thuật thời gian thực (ARTS) được Valve Corporation phát triển, dựa theo một mod game nổi tiếng, Defense of the Ancients, từ trò chơi Warcraft III: Reign of Chaos và bản mở rộng của nó The Frozen Throne. Valve phát hành Dota 2 qua hệ thống điều phối Steam của họ mà qua đó trò chơi được cập nhật song song với hệ thống phiên bản DotA.

The International là giải đấu lớn nhất của trò DotA 2, được tổ chức bởi Valve Corporation, nhà sang lập của trò chơi. Uy danh và giá trị của giải đấu thật không thể đo được: tiền thưởng cho cả giải đấu lên tới 16 triệu USD và vẫn tiếp tục tăng lên theo từng mùa. Team chiến thắng giải đấu có thể trở về nhà với 6 triệu USD, cùng với việc danh tiếng của họ được đi cùng trò chơi trong vòng 1 năm. (quaylen)

2. Steam API

Với nền tảng Engine mạnh mẽ do tự tay mình phát triển, cùng với gameplay tuyệt vời, Valve còn cung cấp API miễn phí cho các web developer.

Ngoài thông tin về account, với từng game API có thể lấy dữ liệu chi tiết cho từng trận đấu, dựa vào đó, người chơi có thế thống kê, phân tích tới từng game đấu. Điển hình như một số trang web: dotabuff, dotamax, ...
dotabuff.png

p=. _Hệ thống thống kê tuyệt vời_

Steam Web APIs

ISteamNews: Cung cấp methods lấy news feeds cho từng game Steam.

ISteamUserStats: Cung cấp methods lấy tất cả các chỉ số, info.

ISteamUser: Cung cấp thông tin về User Account.

Output Formats

Tất cả các API sẽ được request theo form:

http://api.steampowered.com/<interface name>/<method name>/v<version>/?key=<api key>&format=<format>.

Format có thể trả về dưới các dạng: json, xml, vdf

Steam OpenID Provider

Steam cung cấp OpenID, giúp ứng dụng của bạn có thể authenticate thông qua SteamID mà không cần phải tạo riêng User trên website của bạn.

II. Demo

Mục tiêu

Xây dựng ứng dụng thống kê các game đấu dota 2 thông qua dữ liệu lấy từ SteamAPI.

Yêu cầu

  • Rails 4.
  • Có account Steam và đặt chế độ public.

Các công việc thực hiện

  • Login thông qua tài khoản Steam, save thông tin User vào DB.
  • Lấy thông tin và show ra lịch sử các game đấu của User đang login hiện tại.

Good luck have fun! (honho)

1. Khởi tạo (len)

Tạo rails application mới:

rails new Doto

Tạo khung layout bằng Bootstrap

<nav class="nav navbar-inverse">
    <div class="container" >
      <div class="navbar-header">
        <%= link_to "Doto", root_path, class: "navbar-brand" %>
      </div>
    </div>
  </nav>
  <div class="container">
    <% flash.each do |message_type, message| %>
      <div class="alert alert-<%= message_type %>">
        <%= message %>
      </div>
    <% end %>
    <%= yield %>
  </div>

Create controller mới tên là MatchesController

rails g controller matches index show

Chỉnh root đến index của controller vừa tạo

# config/routes.rb
root "matches#index"

2. Login thông qua tài khoản Steam

Steam web API có hỗ trợ OAuth protocol, ta có thể thông qua nó để lấy về các thông tin như ID, nickname, avatar, ... của tài khoản đó trên Steam. Để giản lược công việc ta sử dụng gem omniauth-steam.

Nguồn: https://github.com/reu/omniauth-steam

Add gem:

gem "omniauth-steam"

Sau khi bundle install ta tạo thêm file để config:

# config/initializers/omniauth.rb
Rails.application.config.middleware.use OmniAuth::Builder do
  provider :steam, ENV['STEAM_KEY']
end

ENV["STEAM_KEY"] là key mà steam cung cấp, thông qua đó có thể kết nối tới steam API. Truy cập trang để đăng ký (free): https://steamcommunity.com/dev/apikey

Nhưng thông tin ta sẽ lấy về:

  • uid: user id của account.
  • nickname: nickname của user.
  • avatar_url: đường dẫn avatar.
  • profile_url: url profile.

Tạo model User và migration:

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

rake db:migrate

Thêm một vài routes.

# config/routes.rb
	post '/auth/:provider/callback' => 'sessions#create'
	delete '/logout' => 'sessions#destroy'

Gem chỉ hỗ trợ lấy về account detail, để xử lý login, ta tạo controller tên SessionsController

class SessionsController < ApplicationController
  def create
    begin
      @user = User.omniauth_to_db 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

Trong đó có hàm omniauth_to_db là class method được ta định nghĩa trong Model, với mục đích save user nếu nó chưa tồn tại trong DB.

# models/user.rb
  def self.omniauth_to_db auth
    info = auth['info']
    user ||= find_by uid: auth['uid']
    user.nickname = info['nickname']
    user.avatar_url = info['image']
    user.profile_url = info['urls']['Profile']
    user.save!
    user
  end

Tuy nhiên, lúc này login ta sẽ gặp lỗi

token_error.png
Nguyên nhân là khi create sessions, ta gửi tới Steam thông qua phương thức POST.

Vì vậy Rails sẽ tự động tìm tới CSRF token (không tồn tại). Để login hoạt động, cần add thêm callback để skip token.

skip_before_filter :verify_authenticity_token, only: :create

Thông tin user được lấy bằng API, trả về theo dạng hash, được đặt trong request.env['omniauth.auth'].

{
  :provider => "steam",
  :uid => "76561198010202071",
  :info => {
    :nickname => "Reu",
    :name => "Rodrigo Navarro",
    :location => "BR",
    :image => "http://media.steampowered.com/steamcommunity/public/images/avatars/3c/3c91a935dca0c1e243f3a67a198b0abea9cf6d48_medium.jpg",
    :urls => {
      :Profile => "http://steamcommunity.com/id/rnavarro1/"
    }
  },
  :credentials => {},
  :extra => {
    :raw_info => {
      :steamid => "76561198010202071",
      :communityvisibilitystate => 3,
      :profilestate => 1,
      :personaname => "Reu",
      :lastlogoff => 1325637158,
      :profileurl => "http://steamcommunity.com/id/rnavarro1/",
      :avatar => "http://media.steampowered.com/steamcommunity/public/images/avatars/3c/3c91a935dca0c1e243f3a67a198b0abea9cf6d48.jpg",
      :avatarmedium => "http://media.steampowered.com/steamcommunity/public/images/avatars/3c/3c91a935dca0c1e243f3a67a198b0abea9cf6d48_medium.jpg",
      :avatarfull => "http://media.steampowered.com/steamcommunity/public/images/avatars/3c/3c91a935dca0c1e243f3a67a198b0abea9cf6d48_full.jpg",
      :personastate => 1,
      :realname => "Rodrigo Navarro",
      :primaryclanid => "103582791432706194",
      :timecreated => 1243031082,
      :loccountrycode => "BR"
    }
  }
}

Xử lý Logout

Tạo method destroy đơn giản, xóa đi session khi trước được thiết lập dựa trên user id lấy về từ steam.

# sessions_controller.rb
def destroy
  if current_user
    session.delete :user_id
    flash[:success] = "GGWP"
  end
  redirect_to root_path
end

Xử lý login & logout bên phía backend đã xong, giờ đến lượt layout

 

  <nav class="nav navbar-inverse">
    <div class="container" >
      <div class="navbar-header">
        <%= link_to "Doto", 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', '/auth/steam' %></li>
          </ul>
        <% end %>
      </div>
    </div>
  </nav>

Kết quả hoàn thành bước 1

Khi click vào link Log In
steam_login.png
Sau khi ấn nút Sign In lập tức được khởi tạo session và redirect về trang chủ
steam_logged.png

2. Lấy thông tin từ Dota 2 API (lịch sử trận đấu)

Tương tự như khi login, ta cũng sử dụng thêm gem dota Nguồn: https://github.com/vinnicc/dota

Add gem

gem "dota", github: "vinnicc/dota", branch: "master"

Sau khi chạy `bundle install`, tạo thêm file để config:
# config/initializers/dota.rb

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

ENV['STEAM_KEY'] cũng chính là Steam Api key giống bước 1.

Dota 2 API cung cấp cho ta ty tỷ thứ liên quan đến trận đấu, gem dota đã giản lược chúng, cung cấp các method giúp ta dễ dàng lấy ra các thông tin. List các method bạn có thể nghiên cứu ở đây

https://github.com/vinnicc/dota#matches

Đối với app của chúng ta, dưới đây sẽ là các thông tin mà tôi sẽ get về:

  • uid: id của trận đấu.
  • winner: team chiến thắng (1 trong 2 phe - Radiant hoặc Dire).
  • starts_at: thời gian trận đấu bắt đầu.
  • mode: chế độ find (thường hoặc rank)
  • match_type: chế độ chơi (Tournament, Co-op with Bots, ...)
  • duration: thời lượng của trận đấu.
  • user_id: id của user.

Tạo model để lưu trữ:

rails g model Match uid:string winner:string starts_at:datetime mode:string match_type:string duration:string user:references

rake db:migrate

Thêm quan hệ trong model `User`
# models/user.rb

has_many :matches

Tương tự như method `omniatuth_to_db` ta cũng tạo mới 1 method nhằm lưu trữ lịch sử trận đấu được lấy về thông qua API vào DB.
def load_matches count
  matches_arr = Dota.api.matches(player_id: self.uid, limit: count)
  if matches_arr.present?
    matches_arr.each do |match|
      unless self.matches.where(uid: match.id).present?
        match_info = Dota.api.matches match.id
        new_match = self.matches.create({
            uid: match.id,
            winner: match_info.winner.to_s,
            starts_at: match_info.starts_at,
            first_blood: match_info.first_blood,
            mode: match_info.mode,
            duration: parse_duration(match_info.duration),
            match_type: match_info.type
          })
      end
    end
  end
end

Trong đó agrument count truyền vào là số trận đấu lấy về (tính từ thời điểm gần nhất).

Ngoài ra các bạn có thể còn thấy có hàm parse_duration, để chuyển đổi format thời lượng trận đấu - vốn được tính bằng tổng số giây => hh:mm:ss

# models/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

Gọi hàm load_matches tại controller

# sessions_controller.rb

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

Dưới model đã xử lý lưu trữ xong, giờ trên controller cần load nó ra:

# matches_controller.rb
def index
	@matches = current_user.matches if current_user
end

Config lại routes

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

Hiển thị trên view:

 
<% if @matches.present? %>
  <table class="table table-striped table-hover">
    <% @matches.each do |match| %>
      <tr>
        <td>
          <%= link_to match.starts_at, match_path(match) %>
        </td>
      </tr>
    <% end %>
  </table>
<% end %>

 
<h2><%= @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>

Kết quả thực hiện bước 2

Trang index hiện ra list các trận đấu

list.png

Show ra kết quả trận đấu

show.png

Good game well play!

To be continue. Phần tiếp theo sẽ ra chi tiết của từng trận đấu (hero, skill build, GPM, XPM, ...)

Github: https://github.com/NguyenTanDuc/doto
Heroku: Coming soon


**Nguồn tham khảo**:

http://dev.dota2.com/showthread.php?t=47115

http://www.sitepoint.com/steam-powered-dota-on-rails/

0