From 2049f77734640882d15d3aceb925803c94b13bfe Mon Sep 17 00:00:00 2001 From: aolsen Date: Sun, 2 Oct 2011 01:32:52 -0500 Subject: [PATCH] Create Rating object to help deal with converting between rating scales Preparing to try out glicko2 --- rating/strategies/glicko.rb | 201 ++++++++++++++++++++---------------- rating/tests/glicko_test.rb | 151 +++++++++++++-------------- 2 files changed, 185 insertions(+), 167 deletions(-) diff --git a/rating/strategies/glicko.rb b/rating/strategies/glicko.rb index d4dda0a..c45a9a2 100644 --- a/rating/strategies/glicko.rb +++ b/rating/strategies/glicko.rb @@ -14,7 +14,7 @@ # kyudan: 1.0 for each stone. No gap around zero. # 0.0+epsilon = weakest 1d # 0.0-epsilon = strongest 1k -# Use this to do operations like determining correct handicap, +# Use this to do operations like determining correct handicap, # because it will work correctly across the dan/kyu boundary # # aga: 1.0 for each stone. Gap from (-1.0, 1.0) @@ -28,19 +28,10 @@ require 'delegate' require File.expand_path("../system", File.dirname(__FILE__)) -# Glicko requires :rd, :time_last_player, in addition to simply :rating. -# Add them in here. Intial value of nil is fine. -class Player - attr_accessor :rd, :time_last_played -end +# Abstract out what scale the ratings are on +class Rating + attr_accessor :elo, :rd, :time_last_played -module Glicko - INITIAL_RATING = 0.0 - DEBUG = false - MAX_RD = 240.0 # maximum rating deviation for new/inactive players - MIN_RD = 70.0 # minimum rating deviation for very active players - RD_DECAY = 2*365 # Number of days for RD to decay from MIN to MAX - C_SQUARED = (MAX_RD**2.0-MIN_RD**2.0)/RD_DECAY Q = Math.log(10)/400.0 # Convert from classic Elo to natural scale KGS_KYU_TRANSFORM = 0.85/Q # kgs 5k- KGS_DAN_TRANSFORM = 1.30/Q # kgs 2d+ @@ -48,63 +39,106 @@ module Glicko KD_TWO_DAN = 1.0 # Weakest 2d on the kyudan scale A = (KGS_DAN_TRANSFORM - KGS_KYU_TRANSFORM) / (KD_TWO_DAN - KD_FIVE_KYU) # ~ 17.4 Intermediate constant for conversions B = KGS_KYU_TRANSFORM - KD_FIVE_KYU*A # ~ 208.6 Intermediate constant for conversions - FIVE_KYU = (A/2.0)*((KD_FIVE_KYU)**2) + (B*KD_FIVE_KYU) # ~ 695.2 -- Glicko rating of the strongest 5k - TWO_DAN = (A/2.0)*((KD_TWO_DAN )**2) + (B*KD_TWO_DAN ) # ~ 217.3 -- Glicko rating of the weakest 2d - EVEN_KOMI = { "aga" => 7, "jpn" => 6 } # even komi, after doing floor() + FIVE_KYU = (A/2.0)*((KD_FIVE_KYU)**2) + (B*KD_FIVE_KYU) # ~ 695.2 -- Elo rating of the strongest 5k + TWO_DAN = (A/2.0)*((KD_TWO_DAN )**2) + (B*KD_TWO_DAN ) # ~ 217.3 -- Elo rating of the weakest 2d - class GlickoError < StandardError; end - - def self.g(player) - return 1.0/Math.sqrt(1.0 + 3.0*(Q**2.0)*(player.rd**2.0)/Math::PI**2.0) + def self.new_aga(aga_rating) + r = Rating.new() + r.aga = aga_rating + return r end - - def self.win_probability(player, opp, hka=0) - return 1.0 / (1.0 + 10.0**(-g(opp)**2.0*(player.rating+hka-opp.rating)/400.0)) + def self.new_kyudan(kyudan) + r = Rating.new() + r.kyudan = kyudan + return r end - - def self.d_squared(player, opp, hka) - e = win_probability(player, opp, hka) - return 1.0 / (Q**2.0 * g(opp)**2.0 * e * (1.0-e)) + def self.advantage_in_stones(handi, komi, even_komi) + raise WhrError, "Handi=1 is illegal" if handi == 1 + komi = komi.floor + even_komi = even_komi.floor + handi -= 1 if handi > 0 + return handi + (even_komi-komi)/(even_komi*2.0) end - - def self.initial_rd_update(player, curr_time) - if player.time_last_played - delta_days = (curr_time-player.time_last_played).to_f - player.rd = [Math.sqrt(player.rd**2.0+C_SQUARED*delta_days), MAX_RD].min + def initialize(elo=0) + @elo = elo + return self + end + def gamma=(gamma) + @elo = 400.0*Math::log10(gamma) + return self + end + def gamma() + return 10**(@elo/400.0) + end + def kyudan() + return KD_FIVE_KYU + (@elo-FIVE_KYU)/KGS_KYU_TRANSFORM if @elo < FIVE_KYU + return KD_TWO_DAN + (@elo- TWO_DAN)/KGS_DAN_TRANSFORM if @elo > TWO_DAN + return (Math.sqrt(2.0*A*@elo+B**2.0)-B)/A + end + def aga() + r = kyudan() + return r < 0.0 ? r - 1.0 : r + 1.0 # Add the (-1.0,1.0) gap + end + def kyudan=(kyudan) + if kyudan < KD_FIVE_KYU + @elo = (kyudan - KD_FIVE_KYU)*KGS_KYU_TRANSFORM + FIVE_KYU + elsif kyudan > KD_TWO_DAN + @elo = (kyudan - KD_TWO_DAN )*KGS_DAN_TRANSFORM + TWO_DAN else - player.rd = MAX_RD + @elo = ((A*kyudan+B)**2.0 - B**2.0)/(2.0*A) end - return player + return self + end + def aga=(aga_rating) + raise WhrError, "Illegal aga_rating #{aga_rating}" unless aga_rating.abs >= 1.0 # Ratings in (-1.0,1.0) are illegal + self.kyudan = aga_rating < 0.0 ? aga_rating + 1.0 : aga_rating - 1.0 # Close the (-1.0,1.0) gap + return self + end + def rank() + r = self.aga + return r < 0.0 ? "%dk" % -r.ceil : "%dd" % r.floor end + def aga_rank_str() + r = self.aga + return r < 0.0 ? + "%0.1fk" % [(r*10.0).ceil/10.0] : + "%0.1fd" % [(r*10.0).floor/10.0] + end +end + + +module Glicko + INITIAL_RATING = Rating.new() + DEBUG = false + MAX_RD = 240.0 # maximum rating deviation for new/inactive players + MIN_RD = 70.0 # minimum rating deviation for very active players + RD_DECAY = 2*365 # Number of days for RD to decay from MIN to MAX + C_SQUARED = (MAX_RD**2.0-MIN_RD**2.0)/RD_DECAY + EVEN_KOMI = { "aga" => 7, "jpn" => 6 } # even komi, after doing floor() + + class GlickoError < StandardError; end - def self.get_kyudan_rating(player) - return KD_FIVE_KYU + (player.rating-FIVE_KYU)/KGS_KYU_TRANSFORM if player.rating < FIVE_KYU - return KD_TWO_DAN + (player.rating- TWO_DAN)/KGS_DAN_TRANSFORM if player.rating > TWO_DAN - return (Math.sqrt(2.0*A*player.rating+B**2.0)-B)/A + def self.g(rating) + return 1.0/Math.sqrt(1.0 + 3.0*(Rating::Q**2.0)*(rating.rd**2.0)/Math::PI**2.0) end - def self.get_aga_rating(player) - r = get_kyudan_rating(player) - return r < 0.0 ? r - 1.0 : r + 1.0 # Add the (-1.0,1.0) gap + def self.win_probability(player_r, opp_r, hka=0) + return 1.0 / (1.0 + 10.0**(-g(opp_r)**2.0*(player_r.elo+hka-opp_r.elo)/400.0)) end - def self.set_kyudan_rating(player, kyudan_rating) - if kyudan_rating < KD_FIVE_KYU - r = (kyudan_rating - KD_FIVE_KYU)*KGS_KYU_TRANSFORM + FIVE_KYU - elsif kyudan_rating > KD_TWO_DAN - r = (kyudan_rating - KD_TWO_DAN )*KGS_DAN_TRANSFORM + TWO_DAN - else - r = ((A*kyudan_rating+B)**2.0 - B**2.0)/(2.0*A) - end - player.rating = r - return player + def self.d_squared(player_r, opp_r, hka) + e = win_probability(player_r, opp_r, hka) + return 1.0 / (Rating::Q**2.0 * g(opp_r)**2.0 * e * (1.0-e)) end - def self.set_aga_rating(player, aga_rating) - raise GlickoError, "Illegal aga_rating #{aga_rating}" unless aga_rating.abs >= 1.0 # Ratings in (-1.0,1.0) are illegal - kyudan_rating = aga_rating < 0.0 ? aga_rating + 1.0 : aga_rating - 1.0 # Close the (-1.0,1.0) gap - set_kyudan_rating(player, kyudan_rating) - return player + def self.initial_rd_update(player_r, curr_time) + if player_r.time_last_played + delta_days = (curr_time-player_r.time_last_played).to_f + player_r.rd = [Math.sqrt(player_r.rd**2.0+C_SQUARED*delta_days), MAX_RD].min + else + player_r.rd = MAX_RD + end + return player_r end def self.advantage_in_stones(handi, komi, even_komi) @@ -115,19 +149,18 @@ def self.advantage_in_stones(handi, komi, even_komi) return handi + (even_komi-komi)/(even_komi*2.0) end - def self.handi_komi_advantage(white, black, rules, handi, komi) + def self.advantage_in_elo(white, black, rules, handi, komi) advantage_in_stones = advantage_in_stones(handi, komi, EVEN_KOMI[rules]) - avg_kyudan_rating = (get_kyudan_rating(white) + get_kyudan_rating(black)) / 2.0 - # Creating tmp player objects is weird, would be nicer if there was a Rating object - r1 = set_kyudan_rating(Player.new("", nil), avg_kyudan_rating + advantage_in_stones*0.5) - r2 = set_kyudan_rating(Player.new("", nil), avg_kyudan_rating - advantage_in_stones*0.5) - return r1.rating-r2.rating + avg_kyudan_rating = (white.rating.kyudan + black.rating.kyudan) / 2.0 + r1 = Rating.new_kyudan(avg_kyudan_rating + advantage_in_stones*0.5) + r2 = Rating.new_kyudan(avg_kyudan_rating - advantage_in_stones*0.5) + return r1.elo-r2.elo end def self.rating_to_s(player) - p_min = Player.new("", player.rating - player.rd*2) - p_max = Player.new("", player.rating + player.rd*2) - return "%5.0f [+-%3.0f] %6.2f [+-%5.2f]" % [player.rating, (p_max.rating-p_min.rating)/2.0, get_aga_rating(player), (get_aga_rating(p_max)-get_aga_rating(p_min))/2.0] + r_min = Rating.new(player.rating.elo - player.rating.rd*2) + r_max = Rating.new(player.rating.elo + player.rating.rd*2) + return "%5.0f [+-%3.0f] %6.2f [+-%5.2f]" % [player.rating.elo, (r_max.elo-r_min.elo)/2.0, player.rating.aga, (r_max.aga-r_min.aga)/2.0] end def self.add_result(input, players) @@ -136,49 +169,37 @@ def self.add_result(input, players) black = players[input[:black_player]] handi = input[:handicap] komi = (input[:komi]).floor - hka = handi_komi_advantage(white, black, input[:rules], handi, komi) + hka = advantage_in_elo(white, black, input[:rules], handi, komi) white_won = input[:winner] == 'W' print "%sw=%s %sb=%s h=%d k=%d hka=%0.0f " % [white_won ? "+":" ", white.id, white_won ? " ":"+", black.id, handi, komi, hka] if DEBUG # Initial update on RD based on how long it has been since the player's last game for player in [white, black] do - initial_rd_update(player, input[:datetime]) + initial_rd_update(player.rating, input[:datetime]) end new_r = {} # Updates must be calculated first, then applied. Temp store updates here. new_rd = {} for player, opp, player_won, hka in [[white, black, white_won, -hka], [black, white, !white_won, hka]] do score = player_won ? 1.0 : 0.0 - d_squared = d_squared(player, opp, hka) - e = win_probability(player, opp, hka) - q_term = Q / ((1.0/player.rd**2.0)+1.0/d_squared) - g_term = g(opp) + d_squared = d_squared(player.rating, opp.rating, hka) + e = win_probability(player.rating, opp.rating, hka) + q_term = Rating::Q / ((1.0/player.rating.rd**2.0)+1.0/d_squared) + g_term = g(opp.rating) s_term = score - e - new_r[player] = player.rating + q_term*g_term*s_term - new_rd[player] = [MIN_RD, Math.sqrt(1.0/((1.0/player.rd**2.0)+1.0/d_squared))].max + new_r[player] = player.rating.elo + q_term*g_term*s_term + new_rd[player] = [MIN_RD, Math.sqrt(1.0/((1.0/player.rating.rd**2.0)+1.0/d_squared))].max end # Apply updates for player in [white, black] - player.rating = new_r[player] - player.rd = new_rd[player] - player.time_last_played = input[:datetime] - print "id=%s rating=%7.2f rd=%6.2f " % [player.id, player.rating, player.rd] if DEBUG + player.rating.elo = new_r[player] + player.rating.rd = new_rd[player] + player.rating.time_last_played = input[:datetime] + print "id=%s rating=%7.2f rd=%6.2f " % [player.id, player.rating.elo, player.rating.rd] if DEBUG end print "\n" if DEBUG end - def self.rank(player) - r = get_aga_rating(player) - return r < 0.0 ? "%dk" % -r.ceil : "%dd" % r.floor - end - - def self.aga_rank_str(player) - r = get_aga_rating(player) - return r < 0.0 ? - "%0.1fk" % [(r*10.0).ceil/10.0] : - "%0.1fd" % [(r*10.0).floor/10.0] - end - def self.validate(player) - aga_rating = get_aga_rating(player) + aga_rating = player.rating.aga raise GlickoError, "Rating less than 35k" if aga_rating <= -36.0 raise GlickoError, "Rating more than 12d" if aga_rating >= 13.0 end diff --git a/rating/tests/glicko_test.rb b/rating/tests/glicko_test.rb index 7b42ad3..f5b3001 100644 --- a/rating/tests/glicko_test.rb +++ b/rating/tests/glicko_test.rb @@ -2,44 +2,42 @@ require File.expand_path("../strategies/glicko", File.dirname(__FILE__)) require File.expand_path("../system", File.dirname(__FILE__)) -player_a = Player.new("a", 0) -player_b = Player.new("b", 0) - test "Validate should raise error if rating is too small" do - Glicko::set_aga_rating(player_a, -36.01) - assert_raise(Glicko::GlickoError) do + player_a = Player.new("a", Glicko::INITIAL_RATING) + player_a.rating.aga = -36.01 + assert_raise(Glicko::GlickoError) do Glicko::validate(player_a) end - Glicko::set_aga_rating(player_a, -35.99) # no problem here + player_a.rating.aga = -35.99 # no problem here Glicko::validate(player_a) end test "Validate should raise error if rating is too large" do - player_a = Player.new("a", 0) - Glicko::set_aga_rating(player_a, 13.01) - assert_raise(Glicko::GlickoError) do + player_a = Player.new("a", Glicko::INITIAL_RATING) + player_a.rating.aga = 13.01 + assert_raise(Glicko::GlickoError) do Glicko::validate(player_a) end - Glicko::set_aga_rating(player_a, 12.99) # no problem here + player_a.rating.aga = 12.99 # no problem here Glicko::validate(player_a) end test "Check rating conversion at key boundaries" do # rank() returns traditional ranks -- no extra accuracy, no negative signs for kyus - assert(Glicko::rank(Glicko::set_aga_rating(Player.new("a", nil), -4.999)) == "4k") # Weakest 4k - assert(Glicko::rank(Glicko::set_aga_rating(Player.new("a", nil), -5.001)) == "5k") # Strongest 5k - assert(Glicko::rank(Glicko::set_aga_rating(Player.new("a", nil), 2.001)) == "2d") # Weakest 2d - assert(Glicko::rank(Glicko::set_aga_rating(Player.new("a", nil), 1.999)) == "1d") # Strongest 1d - assert(Glicko::rank(Glicko::set_aga_rating(Player.new("a", nil), 1.001)) == "1d") # Weakest 1d - assert(Glicko::rank(Glicko::set_aga_rating(Player.new("a", nil), -1.001)) == "1k") # Strongest 1k + assert(Rating.new_aga(-4.999).rank == "4k") # Weakest 4k + assert(Rating.new_aga(-5.001).rank == "5k") # Strongest 5k + assert(Rating.new_aga( 2.001).rank == "2d") # Weakest 2d + assert(Rating.new_aga( 1.999).rank == "1d") # Strongest 1d + assert(Rating.new_aga( 1.001).rank == "1d") # Weakest 1d + assert(Rating.new_aga(-1.001).rank == "1k") # Strongest 1k # aga_rank_str() returns a tenths digit, and negative signs for kyus - assert(Glicko::aga_rank_str(Glicko::set_aga_rating(Player.new("a", nil), -4.999)) == "-4.9k") # Weakest 4k - assert(Glicko::aga_rank_str(Glicko::set_aga_rating(Player.new("a", nil), -5.001)) == "-5.0k") # Strongest 5k - assert(Glicko::aga_rank_str(Glicko::set_aga_rating(Player.new("a", nil), 2.001)) == "2.0d") # Weakest 2d - assert(Glicko::aga_rank_str(Glicko::set_aga_rating(Player.new("a", nil), 1.999)) == "1.9d") # Strongest 1d - assert(Glicko::aga_rank_str(Glicko::set_aga_rating(Player.new("a", nil), 1.001)) == "1.0d") # Weakest 1d - assert(Glicko::aga_rank_str(Glicko::set_aga_rating(Player.new("a", nil), -1.001)) == "-1.0k") # Strongest 1k + assert(Rating.new_aga(-4.999).aga_rank_str == "-4.9k") # Weakest 4k + assert(Rating.new_aga(-5.001).aga_rank_str == "-5.0k") # Strongest 5k + assert(Rating.new_aga( 2.001).aga_rank_str == "2.0d") # Weakest 2d + assert(Rating.new_aga( 1.999).aga_rank_str == "1.9d") # Strongest 1d + assert(Rating.new_aga( 1.001).aga_rank_str == "1.0d") # Weakest 1d + assert(Rating.new_aga(-1.001).aga_rank_str == "-1.0k") # Strongest 1k end test "Handicap/Komi advantage" do @@ -49,7 +47,7 @@ print Glicko::advantage_in_stones(2, 0.5, 7.5) assert(Glicko::advantage_in_stones(2, 0.5, 7.5) == 1.5) assert(Glicko::advantage_in_stones(6, 0.5, 7.5) == 5.5) - assert_raise(Glicko::GlickoError) do + assert_raise(Glicko::GlickoError) do assert(Glicko::advantage_in_stones(1, 0.5, 7.5)) # handi=1 is illegal -- instead just set komi=0.5 end end @@ -61,25 +59,25 @@ puts "Ratings table" for aga_rating in ((-30.999..-1.99).step(1.0).to_a+(1.001..10.001).step(1.0).to_a).reverse next if aga_rating > -1.0 && aga_rating < 1.0 - p_low = Glicko::set_aga_rating(Player.new("a", nil), aga_rating ) - p_high = Glicko::set_aga_rating(Player.new("a", nil), aga_rating+0.998) - puts "%7.3f %7.3f %3s %5.0f %5.0f" % [aga_rating, aga_rating+0.998, Glicko::rank(p_low), p_low.rating, p_high.rating] + r_low = Rating.new_aga(aga_rating ) + r_high = Rating.new_aga(aga_rating+0.998) + puts "%7.3f %7.3f %3s %5.0f %5.0f" % [aga_rating, aga_rating+0.998, r_low.rank, r_low.elo, r_high.elo] end end test "Calculate win probability between 2 players" do - player_a.rating = 0.0 - player_b.rating = 0.0 - player_a.rd = 0.0 - player_b.rd = 0.0 - assert_equal Glicko::win_probability(player_a, player_b), 0.500 # Equal ratings = 50% winrate - player_a.rating = -Math.log(2.0)*400.0/Math.log(10.0) - assert_equal Glicko::win_probability(player_a, player_b), 1.0/3.0 # A slightly weaker than B = 33% - player_a.rating = Math.log(3.0)*400.0/Math.log(10.0) - assert (Glicko::win_probability(player_a, player_b) - 0.75).abs < 0.0001 # A stronger than B = 75% - player_a.rd = Glicko::MAX_RD - player_b.rd = Glicko::MAX_RD - assert Glicko::win_probability(player_a, player_b) < 0.75-0.01 # Higher RD means less confidence, so ratings tend toward 0.5 + rating_a = Rating.new(0) + rating_b = Rating.new(0) + rating_a.rd = 0.0 + rating_b.rd = 0.0 + assert_equal Glicko::win_probability(rating_a, rating_b), 0.500 # Equal ratings = 50% winrate + rating_a.elo = -Math.log(2.0)*400.0/Math.log(10.0) + assert_equal Glicko::win_probability(rating_a, rating_b), 1.0/3.0 # A slightly weaker than B = 33% + rating_a.elo = Math.log(3.0)*400.0/Math.log(10.0) + assert (Glicko::win_probability(rating_a, rating_b) - 0.75).abs < 0.0001 # A stronger than B = 75% + rating_a.rd = Glicko::MAX_RD + rating_b.rd = Glicko::MAX_RD + assert Glicko::win_probability(rating_a, rating_b) < 0.75-0.01 # Higher RD means less confidence, so ratings tend toward 0.5 end test "Equal wins" do @@ -88,17 +86,17 @@ system = System.new(Glicko) for init_aga_rating in [-25, -1, 5] for (handi, komi) in [[0, 7.5], [0, 0.5], [0, -6.5], [2, 0.5], [6, 0.5]] - plr_w = system.players["w"] = Glicko::set_aga_rating(Player.new("w", nil), init_aga_rating) - plr_b = system.players["b"] = Glicko::set_aga_rating(Player.new("b", nil), init_aga_rating) + plr_w = system.players["w"] = Player.new("w", Rating.new_aga(init_aga_rating)) + plr_b = system.players["b"] = Player.new("b", Rating.new_aga(init_aga_rating)) 80.times do system.add_result({:white_player => "w", :black_player => "b", :rules => "aga", :handicap => handi, :komi => komi, :winner => "W", :datetime => DateTime.parse("2011-09-24")}) system.add_result({:white_player => "w", :black_player => "b", :rules => "aga", :handicap => handi, :komi => komi, :winner => "B", :datetime => DateTime.parse("2011-09-24")}) end - diff = Glicko::get_kyudan_rating(plr_w) - Glicko::get_kyudan_rating(plr_b) - Glicko::advantage_in_stones(handi, komi, 7.5) + diff = plr_w.rating.kyudan - plr_b.rating.kyudan - Glicko::advantage_in_stones(handi, komi, 7.5) #puts "diff=%0.2f %s %s" % [diff, Glicko::rating_to_s(plr_w), Glicko::rating_to_s(plr_b)] assert (diff.abs < 0.2) # Ratings should almost match the handicap advantage - assert (plr_w.rd == Glicko::MIN_RD) # rd should be smallest value with so many games - assert (plr_b.rd == Glicko::MIN_RD) + assert (plr_w.rating.rd == Glicko::MIN_RD) # rd should be smallest value with so many games + assert (plr_b.rating.rd == Glicko::MIN_RD) end end #puts @@ -111,20 +109,20 @@ for init_aga_rating in [-25, -1, 5] (handi, komi) = [0, 7.5] for win_ratio in 2..9 - plr_w = system.players["w"] = Glicko::set_aga_rating(Player.new("w", nil), init_aga_rating) - plr_b = system.players["b"] = Glicko::set_aga_rating(Player.new("b", nil), init_aga_rating) + plr_w = system.players["w"] = Player.new("w", Rating.new_aga(init_aga_rating)) + plr_b = system.players["b"] = Player.new("b", Rating.new_aga(init_aga_rating)) 80.times do win_ratio.times do # White wins win_ratio times system.add_result({:white_player => "w", :black_player => "b", :rules => "aga", :handicap => handi, :komi => komi, :winner => "W", :datetime => DateTime.parse("2011-09-24")}) end system.add_result({:white_player => "w", :black_player => "b", :rules => "aga", :handicap => handi, :komi => komi, :winner => "B", :datetime => DateTime.parse("2011-09-24")}) end - diff = plr_w.rating - plr_b.rating - exp_diff = Math::log(win_ratio) / Glicko::Q + diff = plr_w.rating.elo - plr_b.rating.elo + exp_diff = Math::log(win_ratio) / Rating::Q #puts "diff=%0.2f exp=%0.2f %s %s" % [diff, exp_diff, Glicko::rating_to_s(plr_w), Glicko::rating_to_s(plr_b)] - assert ((diff - exp_diff).abs < (0.2/Glicko::Q)) # Diff should be close to expected diff - assert (plr_w.rd == Glicko::MIN_RD) # rd should be smallest value with so many games - assert (plr_b.rd == Glicko::MIN_RD) + assert ((diff - exp_diff).abs < (0.2/Rating::Q)) # Diff should be close to expected diff + assert (plr_w.rating.rd == Glicko::MIN_RD) # rd should be smallest value with so many games + assert (plr_b.rating.rd == Glicko::MIN_RD) end end #puts @@ -145,33 +143,31 @@ def self.recursive system = System.new(Glicko) puts "days rd" for days_rest in (0..1200).step(30) - plr_b = system.players["b"] = Player.new("b", nil) - plr_b.rd = Glicko::MIN_RD - plr_b.time_last_played = datetime + rat_b = Rating.new(0) + rat_b.rd = Glicko::MIN_RD + rat_b.time_last_played = datetime datetime += days_rest - #Glicko::initial_rd_update(plr_b, datetime + days_rest) - Glicko::initial_rd_update(plr_b, datetime) - puts "%3d %3d" % [days_rest, plr_b.rd] + Glicko::initial_rd_update(rat_b, datetime) + puts "%3d %3d" % [days_rest, rat_b.rd] end for init_aga_rating in [8.0, -8.0] puts " rd rd*2 newR 95% newAGA 95% dR dKD (1/dKD)" for rd in (Glicko::MIN_RD..Glicko::MAX_RD).step(10) - plr_anchor = system.players["anchor"] = Glicko::set_aga_rating(Player.new("anchor", nil), init_aga_rating) - plr_b = system.players["b"] = Glicko::set_aga_rating(Player.new("b" , nil), init_aga_rating) - plr_anchor.rd = Glicko::MIN_RD # Assume anchor plays a lot and has low RD - plr_b.rd = rd - plr_b.time_last_played = plr_anchor.time_last_played = datetime # Avoid RD update logic - prev_plr_b = plr_b.dup - prev_plr_anchor = plr_anchor.dup - # To avoid going across the weird 5k-2d transition area, + plr_anchor = system.players["anchor"] = Player.new("anchor", Rating.new_aga(init_aga_rating)) + plr_b = system.players["b"] = Player.new("b" , Rating.new_aga(init_aga_rating)) + plr_anchor.rating.rd = Glicko::MIN_RD # Assume anchor plays a lot and has low RD + plr_b.rating.rd = rd + plr_b.rating.time_last_played = plr_anchor.rating.time_last_played = datetime # Avoid RD update logic + prev_rat_b = plr_b.rating.dup + # To avoid going across the weird 5k-2d transition area, # do win streak for dans but loss streak for kyus if init_aga_rating >= 0 system.add_result({:white_player => "anchor", :black_player => "b", :rules => "aga", :handicap => 0, :komi => 7.5, :winner => "B", :datetime => datetime}) else system.add_result({:white_player => "anchor", :black_player => "b", :rules => "aga", :handicap => 0, :komi => 7.5, :winner => "W", :datetime => datetime}) end - dKD = (Glicko::get_kyudan_rating(plr_b)-Glicko::get_kyudan_rating(prev_plr_b)).abs - puts "%3d %3d %s %3.0f %4.2f (%4.1f)" % [rd, rd*2, Glicko::rating_to_s(plr_b), (plr_b.rating-prev_plr_b.rating).abs, dKD, 1/dKD] + dKD = (plr_b.rating.kyudan-prev_rat_b.kyudan).abs + puts "%3d %3d %s %3.0f %4.2f (%4.1f)" % [rd, rd*2, Glicko::rating_to_s(plr_b), (plr_b.rating.elo-prev_rat_b.elo).abs, dKD, 1/dKD] end end key_results = Hash.recursive @@ -181,25 +177,26 @@ def self.recursive datetime = DateTime.parse("2011-09-24") puts "init_aga_rating=#{init_aga_rating} days_rest=#{days_rest}" puts " # newR 95% newAGA 95% dR dKD (1/dKD)" - plr_anchor = system.players["anchor"] = Glicko::set_aga_rating(Player.new("anchor", nil), init_aga_rating) - plr_b = system.players["b"] = Glicko::set_aga_rating(Player.new("b" , nil), init_aga_rating) + plr_anchor = system.players["anchor"] = Player.new("anchor", Rating.new_aga(init_aga_rating)) + plr_b = system.players["b"] = Player.new("b" , Rating.new_aga(init_aga_rating)) for i in 1..MAX_GAMES - prev_plr_b = plr_b.dup - prev_plr_anchor = plr_anchor.dup - plr_anchor.rating = system.players["b"].rating # Keep reseting the anchor to be same rating as the player - plr_anchor.rd = Glicko::MIN_RD # Also assume the anchor plays a lot and has low RD - plr_anchor.time_last_played = datetime # Avoid RD update logic - # To avoid going across the weird 5k-2d transition area, + prev_rat_b = plr_b.rating.dup + plr_anchor.rating = system.players["b"].rating.dup # Keep reseting the anchor to be same rating as the player + plr_anchor.rating.rd = Glicko::MIN_RD # Also assume the anchor plays a lot and has low RD + plr_anchor.rating.time_last_played = datetime # Avoid RD update logic + # To avoid going across the weird 5k-2d transition area, # do win streak for dans but loss streak for kyus if init_aga_rating >= 0 system.add_result({:white_player => "anchor", :black_player => "b", :rules => "aga", :handicap => 0, :komi => 7.5, :winner => "B", :datetime => datetime}) else system.add_result({:white_player => "anchor", :black_player => "b", :rules => "aga", :handicap => 0, :komi => 7.5, :winner => "W", :datetime => datetime}) end - dKD = (Glicko::get_kyudan_rating(plr_b)-Glicko::get_kyudan_rating(prev_plr_b)).abs - puts "%3d %s %3.0f %4.2f (%4.1f)" % [i, Glicko::rating_to_s(plr_b), (plr_b.rating-prev_plr_b.rating).abs, dKD, 1/dKD] + dKD = (plr_b.rating.kyudan-prev_rat_b.kyudan).abs + puts "%3d %s %3.0f %4.2f (%4.1f)" % [i, Glicko::rating_to_s(plr_b), (plr_b.rating.elo-prev_rat_b.elo).abs, dKD, 1/dKD] key_results[init_aga_rating][:dKD_init ][days_rest] = dKD if i==1 - key_results[init_aga_rating][:numgame_minrd][days_rest] = i if plr_b.rd==Glicko::MIN_RD and key_results[init_aga_rating][:numgame_minrd][days_rest] == {} + if plr_b.rating.rd==Glicko::MIN_RD and key_results[init_aga_rating][:numgame_minrd][days_rest] == {} + key_results[init_aga_rating][:numgame_minrd][days_rest] = i + end key_results[init_aga_rating][:dKD_final ][days_rest] = dKD if i==MAX_GAMES key_results[init_aga_rating][:dKD_inv_final][days_rest] = 1/dKD if i==MAX_GAMES datetime += days_rest # new person waits this many days before playing again