Skip to content

Instantly share code, notes, and snippets.

@Youch
Last active December 18, 2015 09:49
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Youch/5764378 to your computer and use it in GitHub Desktop.
Save Youch/5764378 to your computer and use it in GitHub Desktop.
GW2 Match Info (Gicko2 Rating Projections, Current Match PPT)
require 'open-uri'
require 'nokogiri'
require 'httparty'
## Ruby Classe to Gather WVW Info and Calculate Glicko2 Ratings
##
## Snowreap.5174 did all the hard work
## Youch just created the ruby stuff
##
## Usage:
## ruby api.rb report eu
## ruby api.rb report na
## ruby api.rb report eu
## ruby api.rb match na 'Ehmry Bay'
## ruby api.rb match eu "Miller's Sound [DE]"
##
## Helper Module for JSON to Hash Maps by Name and Id. Used below
## for the ObjectiveNames and WorldNames API.
##
module NameIdMap
def self.included(base)
base.extend(ClassMethods)
end
module ClassMethods
def find_by_name(name)
create_maps unless @by_name
@by_name[name]
end
def find_by_id(id)
create_maps unless @by_id
@by_id[id.to_i]
end
def all
create_maps unless @by_id
@by_id
end
private
def create_maps
name_key, id_key, data = map_definition
@by_id = {}
@by_name = {}
data.each { |d|
@by_id[d[id_key].to_i] = d[name_key]
@by_name[d[name_key]] = d[id_key].to_i
}
end
end
end
##
## Objective Name Lookup
##
class ObjectiveNames
include HTTParty
include NameIdMap
# Provide the raw data for NameIdMap
def self.map_definition
['name', 'id', get('https://api.guildwars2.com/v1/wvw/objective_names.json')]
end
# Score Value of WVW Objectives
def self.value(objective)
case objective
when 'Castle'
35
when 'Keep'
25
when 'Tower'
10
else
5
end
end
end
##
## World Name Lookup
##
class WorldNames
include HTTParty
include NameIdMap
# Provide the raw data for NameIdMap
def self.map_definition
['name', 'id', get('https://api.guildwars2.com/v1/world_names.json')]
end
end
##
## Information about a WVW Match
##
class Match
include HTTParty
attr_accessor :match_id, :red_world_id, :blue_world_id, :green_world_id, :red_world_score, :blue_world_score, :green_world_score
attr_accessor :details
def self.all
@all ||= load_matches
end
def self.find_by_world_id(world_id)
all.find { |m| m.red_world_id == world_id || m.blue_world_id == world_id || m.green_world_id == world_id }
end
def world_ids
[red_world_id, blue_world_id, green_world_id]
end
def map_name(name)
case name
when 'RedHome'
"#{WorldNames.find_by_id(red_world_id)} Borderland"
when 'BlueHome'
"#{WorldNames.find_by_id(blue_world_id)} Borderland"
when 'GreenHome'
"#{WorldNames.find_by_id(green_world_id)} Borderland"
when 'Center'
'Eternal Battle Grounds'
else
name
end
end
def score_for(world_id)
case world_id
when red_world_id
red_world_score
when green_world_id
green_world_score
when blue_world_id
blue_world_score
else
raise "Illegal World Id for Match"
end
end
def color_for(world_id)
case world_id
when red_world_id
"Red"
when green_world_id
"Green"
else
"Blue"
end
end
def ppt_for(world_id)
calculate_ppt unless @green_ppt
case world_id
when red_world_id
@red_ppt
when green_world_id
@green_ppt
else
@blue_ppt
end
end
private
##
## Load Matches from the WVW API
##
def self.load_matches
rows = []
get('https://api.guildwars2.com/v1/wvw/matches.json')['wvw_matches'].each do |match|
m = Match.new
rows << m
m.match_id = match['wvw_match_id']
m.red_world_id = match['red_world_id']
m.blue_world_id = match['blue_world_id']
m.green_world_id = match['green_world_id']
m.details = get("https://api.guildwars2.com/v1/wvw/match_details.json?match_id=#{m.match_id}")
m.red_world_score = m.details['scores'][0]
m.blue_world_score = m.details['scores'][1]
m.green_world_score = m.details['scores'][2]
end
rows
end
##
## Calculate Current PPT
##
def calculate_ppt
@red_ppt = 0
@blue_ppt = 0
@green_ppt = 0
details["maps"].each do |map|
map['objectives'].each do |objective|
obj_name = ObjectiveNames.find_by_id(objective['id'])
owner = objective['owner']
value = ObjectiveNames.value(obj_name)
case owner
when "Red"
@red_ppt += value
when "Blue"
@blue_ppt += value
when "Green"
@green_ppt += value
end
end
end
end
end
##
## Combines info from the leaderboard and the API
##
class World
include HTTParty
attr_accessor :world_id, :name, :rank, :rating, :deviation, :volatility
attr_accessor :random_deviation, :mu, :phi, :sigma, :sigma1, :phi1, :mu1, :rating1, :deviation1, :rank1
##
## Class Methods
##
def self.all
@all ||= load_server_ratings
end
def self.find_by_world_id(world_id)
all.find { |w| w.world_id == world_id }
end
def self.region=(value)
@region=value
end
def self.region
@region || 'na'
end
##
## Helpers
##
def current_match
@current_match ||= Match.find_by_world_id(self.world_id)
end
def current_score
current_match.score_for(world_id)
end
def current_opponents
(current_match.world_ids - [world_id]).map { |world_id| World.find_by_world_id(world_id) }
end
def evolution
rating1 - rating
end
def debug message
puts "\n***** #{message} ****** #{self.inspect} @current_score=#{current_score}"
end
##
## Calculate Future Projections
##
def self.project
all.each { |w| w.prepare_deviation }
all.each { |w| w.calculate }
all.sort { |a,b| b.rating1 <=> a.rating1 }.each_with_index { |w,index| w.rank1 = index + 1 }
all
end
def prepare_deviation
# Random Devitation
self.random_deviation = 40.0 + deviation
# Glicko-2 rating
self.mu = (rating - 1500.0) / 173.7178
# Glicko-2 deviation
self.phi = deviation / 173.7178
# Glicko-2 volatility
self.sigma = volatility
end
##
## Main Glicko2 Calculations (Snowreap.5174 did all the hard work here)
##
def calculate
v = 0.0
delta = 0.0
current_opponents.each do |opponent|
g = fn_g(opponent.phi)
e = fn_e(mu, opponent.mu, opponent.phi)
s = current_score.to_f / (current_score + opponent.current_score)
s = (Math.sin((s - 0.5) * Math::PI) + 1.0) / 2.0
v += g * g * e * (1.0 - e)
delta += g * (s - e)
end
v = 1.0 / v
delta = v * delta
a = Math.log(sigma * sigma)
aa = a
if (delta * delta > phi * phi + v)
bb = Math.log(delta * delta - phi * phi - v)
else
k = 1.0
while (fn_f(a - k * Math.sqrt(0.6 * 0.6), delta, phi, v, sigma) < 0.0)
k += 1.0
end
bb = a - k * Math.sqrt(0.6 * 0.6)
end
fA = fn_f(aa, delta, phi, v, sigma)
fB = fn_f(bb, delta, phi, v, sigma)
while ((bb - aa).abs > 0.000001)
c = aa + (aa - bb) * fA / (fB - fA)
fC = fn_f(c, delta, phi, v, sigma)
if (fC * fB < 0.0)
aa = bb
fA = fB
else
fA = fA / 2.0
end
bb = c
fB = fC
end
self.sigma1 = Math.exp(aa / 2.0)
phi_star = Math.sqrt(phi * phi + sigma1 * sigma1)
self.phi1 = 1.0 / Math.sqrt(1.0 / (phi_star * phi_star) + 1.0 / v)
self.mu1 = mu + phi1 * phi1 * delta / v # Delta = v Sigma[g(){s - E()}], therefore Sigma[G(){s - E()}] = Delta / v
self.rating1 = 173.7178 * mu1 + 1500.0
self.deviation1 = 173.7178 * phi1
end
def fn_g(phi)
1.0 / Math.sqrt(1.0 + 3.0*(phi*phi)/(Math::PI*Math::PI))
end
def fn_e(mu, muj, phij)
1.0 / (1.0 + Math.exp(fn_g(phij) * (muj - mu)))
end
def fn_f(x, delta, phi, v, sigma)
n1 = Math.exp(x) * (delta * delta - phi * phi - v - Math.exp(x))
d1 = phi * phi + v + Math.exp(x)
d1 = 2.0 * d1 * d1
n2 = x - Math.log(sigma * sigma)
d2 = 0.6 * 0.6
return n1 / d1 - n2 / d2
end
##
## Scrape Ratings from Leaderboard
##
def self.load_server_ratings
rows = []
# Ignore SSL Certificate because the open call doesn't
# work with certificates on windows (jruby or regular ruby)
ssl_options = { :ssl_verify_mode => OpenSSL::SSL::VERIFY_NONE }
doc = Nokogiri::HTML(
open("https://leaderboards.guildwars2.com/en/#{region}/wvw", ssl_options)
).css("table tr").each do |tr|
unless tr.css('.rank')[0].content =~ /Rank/
world = World.new
rows << world
tr.css('.rank').first.content =~ /(\d+)/
world.rank = $1.to_f
world.name = tr.css('.name').first.content.strip
tr.css('.rating').first.content =~ /(\d+\.\d+)/
world.rating = $1.to_f
tr.css('.deviation').first.content =~ /(\d+\.\d+)/
world.deviation = $1.to_f
tr.css('.volatility').first.content =~ /(\d+\.\d+)/
world.volatility = $1.to_f
world.world_id = WorldNames.find_by_name(world.name)
end
end
rows
end
end
class MatchReport
def self.run(server_name = 'Ehmry Bay', region='na')
# Glicko2 Ratings
World.region = region
World.project
world_id = WorldNames.find_by_name(server_name)
match = Match.find_by_world_id(world_id)
match.world_ids.each do |world_id|
world = World.find_by_world_id(world_id)
name = '%-20s' % WorldNames.find_by_id(world_id)
score = '%6d' % match.score_for(world_id)
color = '%-5s' % match.color_for(world_id)
ppt = '+%3d' % match.ppt_for(world_id)
ev = '%+4.2f' % world.evolution
puts "#{name} #{color} #{score} #{ppt} PPT - Rating:#{ev}"
end
end
end
class RatingReport
def self.run(region='na')
World.region = region
World.project
puts "## ## World Old Rating New Rating Evolution Old Deviat New Deviat Travel Min Matchu Max Matchu"
puts "-- -- -------------------- ---------- ---------- ---------- ---------- ---------- --------- ---------- ----------"
World.all.each do |w|
# Old Rank and New Rank
r = ('%2i' % w.rank).rjust 2
r1 = ('%2i' % w.rank1).rjust 2
# World Name
name = w.name.ljust 20
# Old and New Ratings
old =('%4.4f' % w.rating).rjust 9
new = ('%4.4f' % w.rating1).rjust 9
# Old and New Deviations
dev0 = ('%4.4f' % w.deviation).rjust 9
dev = ('%4.4f' % w.deviation1).rjust 9
# Rating Evolution
ev = w.evolution >=0 ? ('+%3.2f' % w.evolution).rjust(9) : (' %3.2f' % w.evolution).rjust(9)
# Range of possibilities for random rating
travel = w.deviation1 + 40
trv = ('%4.4f' % travel)
# Min and Max Next Random Rating
min = ('%4.4f' % (w.rating1 - travel)).rjust 9
max = ('%4.4f' % (w.rating1 + travel)).rjust 9
puts "#{r} #{r1} #{name} #{old} #{new} #{ev} #{dev0} #{dev} #{trv} #{min} #{max}"
end
end
end
# Interactive Script Mode?
if __FILE__ == $0
cmd = ARGV[0] || 'usage'
region = ARGV[1] || 'na'
world = ARGV[2] || 'Ehmry Bay'
if cmd=='rank'
RatingReport.run region
elsif cmd=='match'
MatchReport.run world, region
else
puts "-------------------------------------------------"
puts "USAGE"
puts ""
puts "World Rank Reports"
puts " ruby api.rb report eu"
puts " ruby api.rb report na"
puts ""
puts "Match Reports"
puts " ruby api.rb match na 'Ehmry Bay'"
puts " ruby api.rb match eu \"Miller's Sound [DE]\""
puts
end
end
@Youch
Copy link
Author

Youch commented Jun 12, 2013

Added usage and command line arguments to run as a script.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment