mirror of
				https://github.com/avitex/elixir-glicko
				synced 2025-10-31 13:53:28 +00:00 
			
		
		
		
	Run mix format (#1)
				
					
				
			Add formatter configuration and run `mix format`. Additionally, manually replace leading tabs in documentation examples with four spaces.
This commit is contained in:
		
							parent
							
								
									8612b2aa97
								
							
						
					
					
						commit
						e22c34f2ff
					
				
							
								
								
									
										3
									
								
								.formatter.exs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								.formatter.exs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,3 @@ | |||||||
|  | [ | ||||||
|  |   inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] | ||||||
|  | ] | ||||||
							
								
								
									
										459
									
								
								lib/glicko.ex
									
									
									
									
									
								
							
							
						
						
									
										459
									
								
								lib/glicko.ex
									
									
									
									
									
								
							| @ -1,246 +1,311 @@ | |||||||
| defmodule Glicko do | defmodule Glicko do | ||||||
| 	@moduledoc """ |   @moduledoc """ | ||||||
| 	Provides the implementation of the Glicko rating system. |   Provides the implementation of the Glicko rating system. | ||||||
| 
 | 
 | ||||||
| 	See the [specification](http://www.glicko.net/glicko/glicko2.pdf) for implementation details. |   See the [specification](http://www.glicko.net/glicko/glicko2.pdf) for implementation details. | ||||||
| 
 | 
 | ||||||
| 	## Usage |   ## Usage | ||||||
| 
 | 
 | ||||||
| 	Get a player's new rating after a series of matches in a rating period. |   Get a player's new rating after a series of matches in a rating period. | ||||||
| 
 | 
 | ||||||
| 		iex> results = [Result.new(Player.new_v1([rating: 1400, rating_deviation: 30]), :win), |       iex> results = [Result.new(Player.new_v1([rating: 1400, rating_deviation: 30]), :win), | ||||||
| 		...> Result.new(Player.new_v1([rating: 1550, rating_deviation: 100]), :loss), |       ...> Result.new(Player.new_v1([rating: 1550, rating_deviation: 100]), :loss), | ||||||
| 		...> Result.new(Player.new_v1([rating: 1700, rating_deviation: 300]), :loss)] |       ...> Result.new(Player.new_v1([rating: 1700, rating_deviation: 300]), :loss)] | ||||||
| 		iex> player = Player.new_v1([rating: 1500, rating_deviation: 200]) |       iex> player = Player.new_v1([rating: 1500, rating_deviation: 200]) | ||||||
| 		iex> Glicko.new_rating(player, results, [system_constant: 0.5]) |       iex> Glicko.new_rating(player, results, [system_constant: 0.5]) | ||||||
| 		{1464.0506705393013, 151.51652412385727} |       {1464.0506705393013, 151.51652412385727} | ||||||
| 
 | 
 | ||||||
| 	Get a player's new rating when they haven't played within a rating period. |   Get a player's new rating when they haven't played within a rating period. | ||||||
| 
 | 
 | ||||||
| 		iex> player = Player.new_v1([rating: 1500, rating_deviation: 200]) |       iex> player = Player.new_v1([rating: 1500, rating_deviation: 200]) | ||||||
| 		iex> Glicko.new_rating(player, [], [system_constant: 0.5]) |       iex> Glicko.new_rating(player, [], [system_constant: 0.5]) | ||||||
| 		{1.5e3, 200.27141669877065} |       {1.5e3, 200.27141669877065} | ||||||
| 
 | 
 | ||||||
| 	Calculate the probability of a player winning against an opponent. |   Calculate the probability of a player winning against an opponent. | ||||||
| 
 | 
 | ||||||
| 		iex> player = Player.new_v1 |       iex> player = Player.new_v1 | ||||||
| 		iex> opponent = Player.new_v1 |       iex> opponent = Player.new_v1 | ||||||
| 		iex> Glicko.win_probability(player, opponent) |       iex> Glicko.win_probability(player, opponent) | ||||||
| 		0.5 |       0.5 | ||||||
| 
 | 
 | ||||||
| 	Calculate the probability of a player drawing against an opponent. |   Calculate the probability of a player drawing against an opponent. | ||||||
| 
 | 
 | ||||||
| 		iex> player = Player.new_v1 |       iex> player = Player.new_v1 | ||||||
| 		iex> opponent = Player.new_v1 |       iex> opponent = Player.new_v1 | ||||||
| 		iex> Glicko.draw_probability(player, opponent) |       iex> Glicko.draw_probability(player, opponent) | ||||||
| 		1.0 |       1.0 | ||||||
| 
 | 
 | ||||||
| 	""" |   """ | ||||||
| 
 | 
 | ||||||
| 	alias __MODULE__.{ |   alias __MODULE__.{ | ||||||
| 		Player, |     Player, | ||||||
| 		Result, |     Result | ||||||
| 	} |   } | ||||||
| 
 | 
 | ||||||
| 	@default_system_constant 0.8 |   @default_system_constant 0.8 | ||||||
| 	@default_convergence_tolerance 1.0e-7 |   @default_convergence_tolerance 1.0e-7 | ||||||
| 
 | 
 | ||||||
| 	@type new_rating_opts :: [system_constant: float, convergence_tolerance: float] |   @type new_rating_opts :: [system_constant: float, convergence_tolerance: float] | ||||||
| 
 | 
 | ||||||
| 	@doc """ |   @doc """ | ||||||
| 	Calculates the probability of a player winning against an opponent. |   Calculates the probability of a player winning against an opponent. | ||||||
| 
 | 
 | ||||||
| 	Returns a value between `0.0` and `1.0`. |   Returns a value between `0.0` and `1.0`. | ||||||
| 	""" |   """ | ||||||
| 	@spec win_probability(player :: Player.t, opponent :: Player.t) :: float |   @spec win_probability(player :: Player.t(), opponent :: Player.t()) :: float | ||||||
| 	def win_probability(player, opponent) do |   def win_probability(player, opponent) do | ||||||
| 		win_probability(player |> Player.rating(:v2), opponent |> Player.rating(:v2), opponent |> Player.rating_deviation(:v2)) |     win_probability( | ||||||
| 	end |       player |> Player.rating(:v2), | ||||||
|  |       opponent |> Player.rating(:v2), | ||||||
|  |       opponent |> Player.rating_deviation(:v2) | ||||||
|  |     ) | ||||||
|  |   end | ||||||
| 
 | 
 | ||||||
| 	@doc """ |   @doc """ | ||||||
| 	Calculates the probability of a player winning against an opponent from a player rating, opponent rating and opponent rating deviation. |   Calculates the probability of a player winning against an opponent from a player rating, opponent rating and opponent rating deviation. | ||||||
| 
 | 
 | ||||||
| 	Values provided for the player rating, opponent rating and opponent rating deviation must be *v2* based. |   Values provided for the player rating, opponent rating and opponent rating deviation must be *v2* based. | ||||||
| 
 | 
 | ||||||
| 	Returns a value between `0.0` and `1.0`. |   Returns a value between `0.0` and `1.0`. | ||||||
| 	""" |   """ | ||||||
| 	@spec win_probability(player_rating :: Player.rating, opponent_rating :: Player.rating, opponent_rating_deviation :: Player.rating_deviation) :: float |   @spec win_probability( | ||||||
| 	def win_probability(player_rating, opponent_rating, opponent_rating_deviation) do |           player_rating :: Player.rating(), | ||||||
| 		calc_e(player_rating, opponent_rating, calc_g(opponent_rating_deviation)) |           opponent_rating :: Player.rating(), | ||||||
| 	end |           opponent_rating_deviation :: Player.rating_deviation() | ||||||
|  |         ) :: float | ||||||
|  |   def win_probability(player_rating, opponent_rating, opponent_rating_deviation) do | ||||||
|  |     calc_e(player_rating, opponent_rating, calc_g(opponent_rating_deviation)) | ||||||
|  |   end | ||||||
| 
 | 
 | ||||||
| 	@doc """ |   @doc """ | ||||||
| 	Calculates the probability of a player drawing against an opponent. |   Calculates the probability of a player drawing against an opponent. | ||||||
| 
 | 
 | ||||||
| 	Returns a value between `0.0` and `1.0`. |   Returns a value between `0.0` and `1.0`. | ||||||
| 	""" |   """ | ||||||
| 	@spec draw_probability(player :: Player.t, opponent :: Player.t) :: float |   @spec draw_probability(player :: Player.t(), opponent :: Player.t()) :: float | ||||||
| 	def draw_probability(player, opponent) do |   def draw_probability(player, opponent) do | ||||||
| 		draw_probability(player |> Player.rating(:v2), opponent |> Player.rating(:v2), opponent |> Player.rating_deviation(:v2)) |     draw_probability( | ||||||
| 	end |       player |> Player.rating(:v2), | ||||||
|  |       opponent |> Player.rating(:v2), | ||||||
|  |       opponent |> Player.rating_deviation(:v2) | ||||||
|  |     ) | ||||||
|  |   end | ||||||
| 
 | 
 | ||||||
| 	@doc """ |   @doc """ | ||||||
| 	Calculates the probability of a player drawing against an opponent from a player rating, opponent rating and opponent rating deviation. |   Calculates the probability of a player drawing against an opponent from a player rating, opponent rating and opponent rating deviation. | ||||||
| 
 | 
 | ||||||
| 	Values provided for the player rating, opponent rating and opponent rating deviation must be *v2* based. |   Values provided for the player rating, opponent rating and opponent rating deviation must be *v2* based. | ||||||
| 
 | 
 | ||||||
| 	Returns a value between `0.0` and `1.0`. |   Returns a value between `0.0` and `1.0`. | ||||||
| 	""" |   """ | ||||||
| 	@spec draw_probability(player_rating :: Player.rating, opponent_rating :: Player.rating, opponent_rating_deviation :: Player.rating_deviation) :: float |   @spec draw_probability( | ||||||
| 	def draw_probability(player_rating, opponent_rating, opponent_rating_deviation) do |           player_rating :: Player.rating(), | ||||||
| 		1 - abs(win_probability(player_rating, opponent_rating, opponent_rating_deviation) - 0.5) / 0.5 |           opponent_rating :: Player.rating(), | ||||||
| 	end |           opponent_rating_deviation :: Player.rating_deviation() | ||||||
|  |         ) :: float | ||||||
|  |   def draw_probability(player_rating, opponent_rating, opponent_rating_deviation) do | ||||||
|  |     1 - | ||||||
|  |       abs(win_probability(player_rating, opponent_rating, opponent_rating_deviation) - 0.5) / 0.5 | ||||||
|  |   end | ||||||
| 
 | 
 | ||||||
| 	@doc """ |   @doc """ | ||||||
| 	Generate a new rating from an existing rating and a series (or lack) of results. |   Generate a new rating from an existing rating and a series (or lack) of results. | ||||||
| 
 | 
 | ||||||
| 	Returns the updated player with the same version given to the function. |   Returns the updated player with the same version given to the function. | ||||||
| 	""" |   """ | ||||||
| 	@spec new_rating(player :: Player.t, results :: list(Result.t), opts :: new_rating_opts) :: Player.t |   @spec new_rating(player :: Player.t(), results :: list(Result.t()), opts :: new_rating_opts) :: | ||||||
| 	def new_rating(player, results, opts \\ []) |           Player.t() | ||||||
| 	def new_rating(player, results, opts) when tuple_size(player) == 3 do |   def new_rating(player, results, opts \\ []) | ||||||
| 		do_new_rating(player, results, opts) |  | ||||||
| 	end |  | ||||||
| 	def new_rating(player, results, opts) when tuple_size(player) == 2 do |  | ||||||
| 		player |  | ||||||
| 		|> Player.to_v2 |  | ||||||
| 		|> do_new_rating(results, opts) |  | ||||||
| 		|> Player.to_v1 |  | ||||||
| 	end |  | ||||||
| 
 | 
 | ||||||
| 	defp do_new_rating({player_r, player_pre_rd, player_v}, [], _) do |   def new_rating(player, results, opts) when tuple_size(player) == 3 do | ||||||
| 		player_post_rd = calc_player_post_base_rd(:math.pow(player_pre_rd, 2), player_v) |     do_new_rating(player, results, opts) | ||||||
|  |   end | ||||||
| 
 | 
 | ||||||
| 		{player_r, player_post_rd, player_v} |   def new_rating(player, results, opts) when tuple_size(player) == 2 do | ||||||
| 	end |     player | ||||||
| 	defp do_new_rating({player_pre_r, player_pre_rd, player_pre_v}, results, opts) do |     |> Player.to_v2() | ||||||
| 		sys_const = Keyword.get(opts, :system_constant, @default_system_constant) |     |> do_new_rating(results, opts) | ||||||
| 		conv_tol = Keyword.get(opts, :convergence_tolerance, @default_convergence_tolerance) |     |> Player.to_v1() | ||||||
|  |   end | ||||||
| 
 | 
 | ||||||
| 		# Initialization (skips steps 1, 2 and 3) |   defp do_new_rating({player_r, player_pre_rd, player_v}, [], _) do | ||||||
| 		player_pre_rd_sq = :math.pow(player_pre_rd, 2) |     player_post_rd = calc_player_post_base_rd(:math.pow(player_pre_rd, 2), player_v) | ||||||
| 		{variance_est, results_effect} = result_calculations(results, player_pre_r) |  | ||||||
| 		# Step 4 |  | ||||||
| 		delta = calc_delta(results_effect, variance_est) |  | ||||||
| 		# Step 5.1 |  | ||||||
| 		alpha = calc_alpha(player_pre_v) |  | ||||||
| 		# Step 5.2 |  | ||||||
| 		k = calc_k(alpha, delta, player_pre_rd_sq, variance_est, sys_const, 1) |  | ||||||
| 		{initial_a, initial_b} = iterative_algorithm_initial( |  | ||||||
| 			alpha, delta, player_pre_rd_sq, variance_est, sys_const, k |  | ||||||
| 		) |  | ||||||
| 		# Step 5.3 |  | ||||||
| 		initial_fa = calc_f(alpha, delta, player_pre_rd_sq, variance_est, sys_const, initial_a) |  | ||||||
| 		initial_fb = calc_f(alpha, delta, player_pre_rd_sq, variance_est, sys_const, initial_b) |  | ||||||
| 		# Step 5.4 |  | ||||||
| 		a = iterative_algorithm_body( |  | ||||||
| 			alpha, delta, player_pre_rd_sq, variance_est, sys_const, conv_tol, |  | ||||||
| 			initial_a, initial_b, initial_fa, initial_fb |  | ||||||
| 		) |  | ||||||
| 		# Step 5.5 |  | ||||||
| 		player_post_v = calc_new_player_volatility(a) |  | ||||||
| 		# Step 6 |  | ||||||
| 		player_post_base_rd = calc_player_post_base_rd(player_pre_rd_sq, player_post_v) |  | ||||||
| 		# Step 7 |  | ||||||
| 		player_post_rd = calc_new_player_rating_deviation(player_post_base_rd, variance_est) |  | ||||||
| 		player_post_r = calc_new_player_rating(results_effect, player_pre_r, player_post_rd) |  | ||||||
| 
 | 
 | ||||||
| 		{player_post_r, player_post_rd, player_post_v} |     {player_r, player_post_rd, player_v} | ||||||
| 	end |   end | ||||||
| 
 | 
 | ||||||
| 	defp result_calculations(results, player_pre_r) do |   defp do_new_rating({player_pre_r, player_pre_rd, player_pre_v}, results, opts) do | ||||||
| 		{variance_estimate_acc, result_effect_acc} = |     sys_const = Keyword.get(opts, :system_constant, @default_system_constant) | ||||||
| 			Enum.reduce(results, {0.0, 0.0}, fn result, {variance_estimate_acc, result_effect_acc} -> |     conv_tol = Keyword.get(opts, :convergence_tolerance, @default_convergence_tolerance) | ||||||
| 				opponent_rd_g = |  | ||||||
| 					result |  | ||||||
| 					|> Result.opponent_rating_deviation |  | ||||||
| 					|> calc_g |  | ||||||
| 
 | 
 | ||||||
| 				win_probability = calc_e(player_pre_r, Result.opponent_rating(result), opponent_rd_g) |     # Initialization (skips steps 1, 2 and 3) | ||||||
|  |     player_pre_rd_sq = :math.pow(player_pre_rd, 2) | ||||||
|  |     {variance_est, results_effect} = result_calculations(results, player_pre_r) | ||||||
|  |     # Step 4 | ||||||
|  |     delta = calc_delta(results_effect, variance_est) | ||||||
|  |     # Step 5.1 | ||||||
|  |     alpha = calc_alpha(player_pre_v) | ||||||
|  |     # Step 5.2 | ||||||
|  |     k = calc_k(alpha, delta, player_pre_rd_sq, variance_est, sys_const, 1) | ||||||
| 
 | 
 | ||||||
| 				{ |     {initial_a, initial_b} = | ||||||
| 					variance_estimate_acc + :math.pow(opponent_rd_g, 2) * win_probability * (1 - win_probability), |       iterative_algorithm_initial( | ||||||
| 					result_effect_acc + opponent_rd_g * (Result.score(result) - win_probability) |         alpha, | ||||||
| 				} |         delta, | ||||||
| 			end) |         player_pre_rd_sq, | ||||||
|  |         variance_est, | ||||||
|  |         sys_const, | ||||||
|  |         k | ||||||
|  |       ) | ||||||
| 
 | 
 | ||||||
| 		{:math.pow(variance_estimate_acc, -1), result_effect_acc} |     # Step 5.3 | ||||||
| 	end |     initial_fa = calc_f(alpha, delta, player_pre_rd_sq, variance_est, sys_const, initial_a) | ||||||
|  |     initial_fb = calc_f(alpha, delta, player_pre_rd_sq, variance_est, sys_const, initial_b) | ||||||
|  |     # Step 5.4 | ||||||
|  |     a = | ||||||
|  |       iterative_algorithm_body( | ||||||
|  |         alpha, | ||||||
|  |         delta, | ||||||
|  |         player_pre_rd_sq, | ||||||
|  |         variance_est, | ||||||
|  |         sys_const, | ||||||
|  |         conv_tol, | ||||||
|  |         initial_a, | ||||||
|  |         initial_b, | ||||||
|  |         initial_fa, | ||||||
|  |         initial_fb | ||||||
|  |       ) | ||||||
| 
 | 
 | ||||||
| 	defp calc_delta(results_effect, variance_est) do |     # Step 5.5 | ||||||
| 		results_effect * variance_est |     player_post_v = calc_new_player_volatility(a) | ||||||
| 	end |     # Step 6 | ||||||
|  |     player_post_base_rd = calc_player_post_base_rd(player_pre_rd_sq, player_post_v) | ||||||
|  |     # Step 7 | ||||||
|  |     player_post_rd = calc_new_player_rating_deviation(player_post_base_rd, variance_est) | ||||||
|  |     player_post_r = calc_new_player_rating(results_effect, player_pre_r, player_post_rd) | ||||||
| 
 | 
 | ||||||
| 	defp calc_f(alpha, delta, player_pre_rd_sq, variance_est, sys_const, x) do |     {player_post_r, player_post_rd, player_post_v} | ||||||
| 		:math.exp(x) * |   end | ||||||
| 		(:math.pow(delta, 2) - :math.exp(x) - player_pre_rd_sq - variance_est) / |  | ||||||
| 		(2 * :math.pow(player_pre_rd_sq + variance_est + :math.exp(x), 2)) - |  | ||||||
| 		(x - alpha) / :math.pow(sys_const, 2) |  | ||||||
| 	end |  | ||||||
| 
 | 
 | ||||||
| 	defp calc_alpha(player_pre_v) do |   defp result_calculations(results, player_pre_r) do | ||||||
| 		:math.log(:math.pow(player_pre_v, 2)) |     {variance_estimate_acc, result_effect_acc} = | ||||||
| 	end |       Enum.reduce(results, {0.0, 0.0}, fn result, {variance_estimate_acc, result_effect_acc} -> | ||||||
|  |         opponent_rd_g = | ||||||
|  |           result | ||||||
|  |           |> Result.opponent_rating_deviation() | ||||||
|  |           |> calc_g | ||||||
| 
 | 
 | ||||||
| 	defp calc_new_player_volatility(a) do |         win_probability = calc_e(player_pre_r, Result.opponent_rating(result), opponent_rd_g) | ||||||
| 		:math.exp(a / 2) |  | ||||||
| 	end |  | ||||||
| 
 | 
 | ||||||
| 	defp calc_new_player_rating(results_effect, player_pre_r, player_post_rd) do |         { | ||||||
| 		player_pre_r + :math.pow(player_post_rd, 2) * results_effect |           variance_estimate_acc + | ||||||
| 	end |             :math.pow(opponent_rd_g, 2) * win_probability * (1 - win_probability), | ||||||
|  |           result_effect_acc + opponent_rd_g * (Result.score(result) - win_probability) | ||||||
|  |         } | ||||||
|  |       end) | ||||||
| 
 | 
 | ||||||
| 	defp calc_new_player_rating_deviation(player_post_base_rd, variance_est) do |     {:math.pow(variance_estimate_acc, -1), result_effect_acc} | ||||||
| 		1 / :math.sqrt(1 / :math.pow(player_post_base_rd, 2) + 1 / variance_est) |   end | ||||||
| 	end |  | ||||||
| 
 | 
 | ||||||
| 	defp calc_player_post_base_rd(player_pre_rd_sq, player_pre_v) do |   defp calc_delta(results_effect, variance_est) do | ||||||
| 		:math.sqrt((:math.pow(player_pre_v, 2) + player_pre_rd_sq)) |     results_effect * variance_est | ||||||
| 	end |   end | ||||||
| 
 | 
 | ||||||
| 	defp iterative_algorithm_initial(alpha, delta, player_pre_rd_sq, variance_est, sys_const, k) do |   defp calc_f(alpha, delta, player_pre_rd_sq, variance_est, sys_const, x) do | ||||||
| 		initial_a = alpha |     :math.exp(x) * | ||||||
| 		initial_b = |       (:math.pow(delta, 2) - :math.exp(x) - player_pre_rd_sq - variance_est) / | ||||||
| 			if :math.pow(delta, 2) > player_pre_rd_sq + variance_est do |       (2 * :math.pow(player_pre_rd_sq + variance_est + :math.exp(x), 2)) - | ||||||
| 				:math.log(:math.pow(delta, 2) - player_pre_rd_sq - variance_est) |       (x - alpha) / :math.pow(sys_const, 2) | ||||||
| 			else |   end | ||||||
| 				alpha - k * sys_const |  | ||||||
| 			end |  | ||||||
| 
 | 
 | ||||||
| 			{initial_a, initial_b} |   defp calc_alpha(player_pre_v) do | ||||||
| 	end |     :math.log(:math.pow(player_pre_v, 2)) | ||||||
|  |   end | ||||||
| 
 | 
 | ||||||
| 	defp iterative_algorithm_body(alpha, delta, player_pre_rd_sq, variance_est, sys_const, conv_tol, a, b, fa, fb) do |   defp calc_new_player_volatility(a) do | ||||||
| 		if abs(b - a) > conv_tol do |     :math.exp(a / 2) | ||||||
| 			c = a + (a - b) * fa / (fb - fa) |   end | ||||||
| 			fc = calc_f(alpha, delta, player_pre_rd_sq, variance_est, sys_const, c) |  | ||||||
| 			{a, fa} = |  | ||||||
| 				if fc * fb < 0 do |  | ||||||
| 					{b, fb} |  | ||||||
| 				else |  | ||||||
| 					{a, fa / 2} |  | ||||||
| 				end |  | ||||||
| 			iterative_algorithm_body(alpha, delta, player_pre_rd_sq, variance_est, sys_const, conv_tol, a, c, fa, fc) |  | ||||||
| 		else |  | ||||||
| 			a |  | ||||||
| 		end |  | ||||||
| 	end |  | ||||||
| 
 | 
 | ||||||
| 	defp calc_k(alpha, delta, player_pre_rd_sq, variance_est, sys_const, k) do |   defp calc_new_player_rating(results_effect, player_pre_r, player_post_rd) do | ||||||
| 		if calc_f(alpha, delta, player_pre_rd_sq, variance_est, sys_const, alpha - k * sys_const) < 0 do |     player_pre_r + :math.pow(player_post_rd, 2) * results_effect | ||||||
| 			calc_k(alpha, delta, player_pre_rd_sq, variance_est, sys_const, k + 1) |   end | ||||||
| 		else |  | ||||||
| 			k |  | ||||||
| 		end |  | ||||||
| 	end |  | ||||||
| 
 | 
 | ||||||
| 	# g function |   defp calc_new_player_rating_deviation(player_post_base_rd, variance_est) do | ||||||
| 	defp calc_g(rd) do |     1 / :math.sqrt(1 / :math.pow(player_post_base_rd, 2) + 1 / variance_est) | ||||||
| 		1 / :math.sqrt(1 + 3 * :math.pow(rd, 2) / :math.pow(:math.pi, 2)) |   end | ||||||
| 	end |  | ||||||
| 
 | 
 | ||||||
| 	# E function |   defp calc_player_post_base_rd(player_pre_rd_sq, player_pre_v) do | ||||||
| 	defp calc_e(player_pre_r, opponent_r, opponent_rd_g) do |     :math.sqrt(:math.pow(player_pre_v, 2) + player_pre_rd_sq) | ||||||
| 		1 / (1 + :math.exp(-1 * opponent_rd_g * (player_pre_r - opponent_r))) |   end | ||||||
| 	end | 
 | ||||||
|  |   defp iterative_algorithm_initial(alpha, delta, player_pre_rd_sq, variance_est, sys_const, k) do | ||||||
|  |     initial_a = alpha | ||||||
|  | 
 | ||||||
|  |     initial_b = | ||||||
|  |       if :math.pow(delta, 2) > player_pre_rd_sq + variance_est do | ||||||
|  |         :math.log(:math.pow(delta, 2) - player_pre_rd_sq - variance_est) | ||||||
|  |       else | ||||||
|  |         alpha - k * sys_const | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |     {initial_a, initial_b} | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   defp iterative_algorithm_body( | ||||||
|  |          alpha, | ||||||
|  |          delta, | ||||||
|  |          player_pre_rd_sq, | ||||||
|  |          variance_est, | ||||||
|  |          sys_const, | ||||||
|  |          conv_tol, | ||||||
|  |          a, | ||||||
|  |          b, | ||||||
|  |          fa, | ||||||
|  |          fb | ||||||
|  |        ) do | ||||||
|  |     if abs(b - a) > conv_tol do | ||||||
|  |       c = a + (a - b) * fa / (fb - fa) | ||||||
|  |       fc = calc_f(alpha, delta, player_pre_rd_sq, variance_est, sys_const, c) | ||||||
|  | 
 | ||||||
|  |       {a, fa} = | ||||||
|  |         if fc * fb < 0 do | ||||||
|  |           {b, fb} | ||||||
|  |         else | ||||||
|  |           {a, fa / 2} | ||||||
|  |         end | ||||||
|  | 
 | ||||||
|  |       iterative_algorithm_body( | ||||||
|  |         alpha, | ||||||
|  |         delta, | ||||||
|  |         player_pre_rd_sq, | ||||||
|  |         variance_est, | ||||||
|  |         sys_const, | ||||||
|  |         conv_tol, | ||||||
|  |         a, | ||||||
|  |         c, | ||||||
|  |         fa, | ||||||
|  |         fc | ||||||
|  |       ) | ||||||
|  |     else | ||||||
|  |       a | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   defp calc_k(alpha, delta, player_pre_rd_sq, variance_est, sys_const, k) do | ||||||
|  |     if calc_f(alpha, delta, player_pre_rd_sq, variance_est, sys_const, alpha - k * sys_const) < 0 do | ||||||
|  |       calc_k(alpha, delta, player_pre_rd_sq, variance_est, sys_const, k + 1) | ||||||
|  |     else | ||||||
|  |       k | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   # g function | ||||||
|  |   defp calc_g(rd) do | ||||||
|  |     1 / :math.sqrt(1 + 3 * :math.pow(rd, 2) / :math.pow(:math.pi(), 2)) | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   # E function | ||||||
|  |   defp calc_e(player_pre_r, opponent_r, opponent_rd_g) do | ||||||
|  |     1 / (1 + :math.exp(-1 * opponent_rd_g * (player_pre_r - opponent_r))) | ||||||
|  |   end | ||||||
| end | end | ||||||
|  | |||||||
| @ -1,192 +1,214 @@ | |||||||
| defmodule Glicko.Player do | defmodule Glicko.Player do | ||||||
| 	@moduledoc """ |   @moduledoc """ | ||||||
| 	Provides convenience functions that handle conversions between Glicko versions one and two. |   Provides convenience functions that handle conversions between Glicko versions one and two. | ||||||
| 
 | 
 | ||||||
| 	## Usage |   ## Usage | ||||||
| 
 | 
 | ||||||
| 	Create a *v1* player with the default values for an unrated player. |   Create a *v1* player with the default values for an unrated player. | ||||||
| 
 | 
 | ||||||
| 		iex> Player.new_v1 |       iex> Player.new_v1 | ||||||
| 		{1.5e3, 350.0} |       {1.5e3, 350.0} | ||||||
| 
 | 
 | ||||||
| 	Create a *v2* player with the default values for an unrated player. |   Create a *v2* player with the default values for an unrated player. | ||||||
| 
 | 
 | ||||||
| 		iex> Player.new_v2 |       iex> Player.new_v2 | ||||||
| 		{0.0, 2.014761872416068, 0.06} |       {0.0, 2.014761872416068, 0.06} | ||||||
| 
 | 
 | ||||||
| 	Create a player with custom values. |   Create a player with custom values. | ||||||
| 
 | 
 | ||||||
| 		iex> Player.new_v2([rating: 3.0, rating_deviation: 2.0, volatility: 0.05]) |       iex> Player.new_v2([rating: 3.0, rating_deviation: 2.0, volatility: 0.05]) | ||||||
| 		{3.0, 2.0, 0.05} |       {3.0, 2.0, 0.05} | ||||||
| 
 | 
 | ||||||
| 	Convert a *v2* player to a *v1*. Note this drops the volatility. |   Convert a *v2* player to a *v1*. Note this drops the volatility. | ||||||
| 
 | 
 | ||||||
| 		iex> Player.new_v2 |> Player.to_v1 |       iex> Player.new_v2 |> Player.to_v1 | ||||||
| 		{1.5e3, 350.0} |       {1.5e3, 350.0} | ||||||
| 
 | 
 | ||||||
| 	Convert a *v1* player to a *v2*. |   Convert a *v1* player to a *v2*. | ||||||
| 
 | 
 | ||||||
| 		iex> Player.new_v1 |> Player.to_v2(0.06) |       iex> Player.new_v1 |> Player.to_v2(0.06) | ||||||
| 		{0.0, 2.014761872416068, 0.06} |       {0.0, 2.014761872416068, 0.06} | ||||||
| 
 | 
 | ||||||
| 	Note calling `to_v1` with a *v1* player or likewise with `to_v2` and a *v2* player |   Note calling `to_v1` with a *v1* player or likewise with `to_v2` and a *v2* player | ||||||
| 	will pass-through unchanged. The volatility arg in this case is ignored. |   will pass-through unchanged. The volatility arg in this case is ignored. | ||||||
| 
 | 
 | ||||||
| 		iex> player_v2 = Player.new_v2 |       iex> player_v2 = Player.new_v2 | ||||||
| 		iex> player_v2 == Player.to_v2(player_v2) |       iex> player_v2 == Player.to_v2(player_v2) | ||||||
| 		true |       true | ||||||
| 
 | 
 | ||||||
| 	""" |   """ | ||||||
| 
 | 
 | ||||||
| 	@magic_version_scale 173.7178 |   @magic_version_scale 173.7178 | ||||||
| 	@magic_version_scale_rating 1500.0 |   @magic_version_scale_rating 1500.0 | ||||||
| 
 | 
 | ||||||
| 	@type t :: v1 | v2 |   @type t :: v1 | v2 | ||||||
| 
 | 
 | ||||||
| 	@type v1 :: {rating, rating_deviation} |   @type v1 :: {rating, rating_deviation} | ||||||
| 	@type v2 :: {rating, rating_deviation, volatility} |   @type v2 :: {rating, rating_deviation, volatility} | ||||||
| 
 | 
 | ||||||
| 	@type version :: :v1 | :v2 |   @type version :: :v1 | :v2 | ||||||
| 	@type rating :: float |   @type rating :: float | ||||||
| 	@type rating_deviation :: float |   @type rating_deviation :: float | ||||||
| 	@type volatility :: float |   @type volatility :: float | ||||||
| 
 | 
 | ||||||
| 	@doc """ |   @doc """ | ||||||
| 	The recommended initial rating value for a new player. |   The recommended initial rating value for a new player. | ||||||
| 	""" |   """ | ||||||
| 	@spec initial_rating(version) :: rating |   @spec initial_rating(version) :: rating | ||||||
| 	def initial_rating(_version = :v1), do: 1500.0 |   def initial_rating(_version = :v1), do: 1500.0 | ||||||
| 	def initial_rating(_version = :v2), do: :v1 |> initial_rating |> scale_rating_to(:v2) |   def initial_rating(_version = :v2), do: :v1 |> initial_rating |> scale_rating_to(:v2) | ||||||
| 
 | 
 | ||||||
| 	@doc """ |   @doc """ | ||||||
| 	The recommended initial rating deviation value for a new player. |   The recommended initial rating deviation value for a new player. | ||||||
| 	""" |   """ | ||||||
| 	@spec initial_rating_deviation(version) :: rating_deviation |   @spec initial_rating_deviation(version) :: rating_deviation | ||||||
| 	def initial_rating_deviation(_version = :v1), do: 350.0 |   def initial_rating_deviation(_version = :v1), do: 350.0 | ||||||
| 	def initial_rating_deviation(_version = :v2), do: :v1 |> initial_rating_deviation |> scale_rating_deviation_to(:v2) |  | ||||||
| 
 | 
 | ||||||
| 	@doc """ |   def initial_rating_deviation(_version = :v2), | ||||||
| 	The recommended initial volatility value for a new player. |     do: :v1 |> initial_rating_deviation |> scale_rating_deviation_to(:v2) | ||||||
| 	""" |  | ||||||
| 	@spec initial_volatility :: volatility |  | ||||||
| 	def initial_volatility, do: 0.06 |  | ||||||
| 
 | 
 | ||||||
| 	@doc """ |   @doc """ | ||||||
| 	Creates a new v1 player. |   The recommended initial volatility value for a new player. | ||||||
|  |   """ | ||||||
|  |   @spec initial_volatility :: volatility | ||||||
|  |   def initial_volatility, do: 0.06 | ||||||
| 
 | 
 | ||||||
| 	If not overriden, will use the default values for an unrated player. |   @doc """ | ||||||
| 	""" |   Creates a new v1 player. | ||||||
| 	@spec new_v1([rating: rating, rating_deviation: rating_deviation]) :: v1 |  | ||||||
| 	def new_v1(opts \\ []) when is_list(opts), do: { |  | ||||||
| 		Keyword.get(opts, :rating, initial_rating(:v1)), |  | ||||||
| 		Keyword.get(opts, :rating_deviation, initial_rating_deviation(:v1)), |  | ||||||
| 	} |  | ||||||
| 
 | 
 | ||||||
| 	@doc """ |   If not overriden, will use the default values for an unrated player. | ||||||
| 	Creates a new v2 player. |   """ | ||||||
|  |   @spec new_v1(rating: rating, rating_deviation: rating_deviation) :: v1 | ||||||
|  |   def new_v1(opts \\ []) when is_list(opts), | ||||||
|  |     do: { | ||||||
|  |       Keyword.get(opts, :rating, initial_rating(:v1)), | ||||||
|  |       Keyword.get(opts, :rating_deviation, initial_rating_deviation(:v1)) | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
| 	If not overriden, will use default values for an unrated player. |   @doc """ | ||||||
| 	""" |   Creates a new v2 player. | ||||||
| 	@spec new_v2([rating: rating, rating_deviation: rating_deviation, volatility: volatility]) :: v2 |  | ||||||
| 	def new_v2(opts \\ []) when is_list(opts), do: { |  | ||||||
| 		Keyword.get(opts, :rating, initial_rating(:v2)), |  | ||||||
| 		Keyword.get(opts, :rating_deviation, initial_rating_deviation(:v2)), |  | ||||||
| 		Keyword.get(opts, :volatility, initial_volatility()), |  | ||||||
| 	} |  | ||||||
| 
 | 
 | ||||||
| 	@doc """ |   If not overriden, will use default values for an unrated player. | ||||||
| 	Converts a v2 player to a v1. |   """ | ||||||
|  |   @spec new_v2(rating: rating, rating_deviation: rating_deviation, volatility: volatility) :: v2 | ||||||
|  |   def new_v2(opts \\ []) when is_list(opts), | ||||||
|  |     do: { | ||||||
|  |       Keyword.get(opts, :rating, initial_rating(:v2)), | ||||||
|  |       Keyword.get(opts, :rating_deviation, initial_rating_deviation(:v2)), | ||||||
|  |       Keyword.get(opts, :volatility, initial_volatility()) | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
| 	A v1 player will pass-through unchanged. |   @doc """ | ||||||
|  |   Converts a v2 player to a v1. | ||||||
| 
 | 
 | ||||||
| 	Note the volatility field used in a v2 player will be lost in the conversion. |   A v1 player will pass-through unchanged. | ||||||
| 	""" |  | ||||||
| 	@spec to_v1(player :: t) :: v1 |  | ||||||
| 	def to_v1({rating, rating_deviation}), do: {rating, rating_deviation} |  | ||||||
| 	def to_v1({rating, rating_deviation, _}), do: { |  | ||||||
| 		rating |> scale_rating_to(:v1), |  | ||||||
| 		rating_deviation |> scale_rating_deviation_to(:v1), |  | ||||||
| 	} |  | ||||||
| 
 | 
 | ||||||
| 	@doc """ |   Note the volatility field used in a v2 player will be lost in the conversion. | ||||||
| 	Converts a v1 player to a v2. |   """ | ||||||
|  |   @spec to_v1(player :: t) :: v1 | ||||||
|  |   def to_v1({rating, rating_deviation}), do: {rating, rating_deviation} | ||||||
| 
 | 
 | ||||||
| 	A v2 player will pass-through unchanged with the volatility arg ignored. |   def to_v1({rating, rating_deviation, _}), | ||||||
| 	""" |     do: { | ||||||
| 	@spec to_v2(player :: t, volatility :: volatility) :: v2 |       rating |> scale_rating_to(:v1), | ||||||
| 	def to_v2(player, volatility \\ initial_volatility()) |       rating_deviation |> scale_rating_deviation_to(:v1) | ||||||
| 	def to_v2({rating, rating_deviation, volatility}, _volatility), do: {rating, rating_deviation, volatility} |     } | ||||||
| 	def to_v2({rating, rating_deviation}, volatility), do: { |  | ||||||
| 		rating |> scale_rating_to(:v2), |  | ||||||
| 		rating_deviation |> scale_rating_deviation_to(:v2), |  | ||||||
| 		volatility, |  | ||||||
| 	} |  | ||||||
| 
 | 
 | ||||||
| 	@doc """ |   @doc """ | ||||||
| 	A version agnostic method for getting a player's rating. |   Converts a v1 player to a v2. | ||||||
| 	""" |  | ||||||
| 	@spec rating(player :: t, as_version :: version) :: rating |  | ||||||
| 	def rating(player, as_version \\ nil) |  | ||||||
| 	def rating({rating, _}, nil), do: rating |  | ||||||
| 	def rating({rating, _, _}, nil), do: rating |  | ||||||
| 	def rating({rating, _}, :v1), do: rating |  | ||||||
| 	def rating({rating, _}, :v2), do: rating |> scale_rating_to(:v2) |  | ||||||
| 	def rating({rating, _, _}, :v1), do: rating |> scale_rating_to(:v1) |  | ||||||
| 	def rating({rating, _, _}, :v2), do: rating |  | ||||||
| 
 | 
 | ||||||
| 	@doc """ |   A v2 player will pass-through unchanged with the volatility arg ignored. | ||||||
| 	A version agnostic method for getting a player's rating deviation. |   """ | ||||||
| 	""" |   @spec to_v2(player :: t, volatility :: volatility) :: v2 | ||||||
| 	@spec rating_deviation(player :: t, as_version :: version) :: rating_deviation |   def to_v2(player, volatility \\ initial_volatility()) | ||||||
| 	def rating_deviation(player, as_version \\ nil) |  | ||||||
| 	def rating_deviation({_, rating_deviation}, nil), do: rating_deviation |  | ||||||
| 	def rating_deviation({_, rating_deviation, _}, nil), do: rating_deviation |  | ||||||
| 	def rating_deviation({_, rating_deviation}, :v1), do: rating_deviation |  | ||||||
| 	def rating_deviation({_, rating_deviation}, :v2), do: rating_deviation |> scale_rating_deviation_to(:v2) |  | ||||||
| 	def rating_deviation({_, rating_deviation, _}, :v1), do: rating_deviation |> scale_rating_deviation_to(:v1) |  | ||||||
| 	def rating_deviation({_, rating_deviation, _}, :v2), do: rating_deviation |  | ||||||
| 
 | 
 | ||||||
| 	@doc """ |   def to_v2({rating, rating_deviation, volatility}, _volatility), | ||||||
| 	A version agnostic method for getting a player's volatility. |     do: {rating, rating_deviation, volatility} | ||||||
| 	""" |  | ||||||
| 	@spec volatility(player :: t, default_volatility :: volatility) :: volatility |  | ||||||
| 	def volatility(player, default_volatility \\ initial_volatility()) |  | ||||||
| 	def volatility({_, _}, default_volatility), do: default_volatility |  | ||||||
| 	def volatility({_, _, volatility}, _), do: volatility |  | ||||||
| 
 | 
 | ||||||
| 	@doc """ |   def to_v2({rating, rating_deviation}, volatility), | ||||||
| 	A convenience function for summarizing a player's strength as a 95% |     do: { | ||||||
| 	confidence interval. |       rating |> scale_rating_to(:v2), | ||||||
|  |       rating_deviation |> scale_rating_deviation_to(:v2), | ||||||
|  |       volatility | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
| 	The lowest value in the interval is the player's rating minus twice the RD, |   @doc """ | ||||||
| 	and the highest value is the player's rating plus twice the RD. |   A version agnostic method for getting a player's rating. | ||||||
| 	The volatility measure does not appear in the calculation of this interval. |   """ | ||||||
|  |   @spec rating(player :: t, as_version :: version) :: rating | ||||||
|  |   def rating(player, as_version \\ nil) | ||||||
|  |   def rating({rating, _}, nil), do: rating | ||||||
|  |   def rating({rating, _, _}, nil), do: rating | ||||||
|  |   def rating({rating, _}, :v1), do: rating | ||||||
|  |   def rating({rating, _}, :v2), do: rating |> scale_rating_to(:v2) | ||||||
|  |   def rating({rating, _, _}, :v1), do: rating |> scale_rating_to(:v1) | ||||||
|  |   def rating({rating, _, _}, :v2), do: rating | ||||||
| 
 | 
 | ||||||
| 	An example would be if a player's rating is 1850 and the RD is 50, |   @doc """ | ||||||
| 	the interval would range from 1750 to 1950. We would then say that we're 95% |   A version agnostic method for getting a player's rating deviation. | ||||||
| 	confident that the player's actual strength is between 1750 and 1950. |   """ | ||||||
|  |   @spec rating_deviation(player :: t, as_version :: version) :: rating_deviation | ||||||
|  |   def rating_deviation(player, as_version \\ nil) | ||||||
|  |   def rating_deviation({_, rating_deviation}, nil), do: rating_deviation | ||||||
|  |   def rating_deviation({_, rating_deviation, _}, nil), do: rating_deviation | ||||||
|  |   def rating_deviation({_, rating_deviation}, :v1), do: rating_deviation | ||||||
| 
 | 
 | ||||||
| 	When a player has a low RD, the interval would be narrow, so that we would |   def rating_deviation({_, rating_deviation}, :v2), | ||||||
| 	be 95% confident about a player’s strength being in a small interval of values. |     do: rating_deviation |> scale_rating_deviation_to(:v2) | ||||||
| 	""" |  | ||||||
| 	@spec rating_interval(player :: t) :: {rating_low :: float, rating_high :: float} |  | ||||||
| 	def rating_interval(player, as_version \\ nil), do: { |  | ||||||
| 		rating(player, as_version) - rating_deviation(player, as_version) * 2, |  | ||||||
| 		rating(player, as_version) + rating_deviation(player, as_version) * 2, |  | ||||||
| 	} |  | ||||||
| 
 | 
 | ||||||
| 	@doc """ |   def rating_deviation({_, rating_deviation, _}, :v1), | ||||||
| 	Scales a player's rating. |     do: rating_deviation |> scale_rating_deviation_to(:v1) | ||||||
| 	""" |  | ||||||
| 	@spec scale_rating_to(rating :: rating, to_version :: version) :: rating |  | ||||||
| 	def scale_rating_to(rating, _version = :v1), do: (rating * @magic_version_scale) + @magic_version_scale_rating |  | ||||||
| 	def scale_rating_to(rating, _version = :v2), do: (rating - @magic_version_scale_rating) / @magic_version_scale |  | ||||||
| 
 | 
 | ||||||
| 	@doc """ |   def rating_deviation({_, rating_deviation, _}, :v2), do: rating_deviation | ||||||
| 	Scales a player's rating deviation. |  | ||||||
| 	""" |  | ||||||
| 	@spec scale_rating_deviation_to(rating_deviation :: rating_deviation, to_version :: version) :: rating_deviation |  | ||||||
| 	def scale_rating_deviation_to(rating_deviation, _version = :v1), do: rating_deviation * @magic_version_scale |  | ||||||
| 	def scale_rating_deviation_to(rating_deviation, _version = :v2), do: rating_deviation / @magic_version_scale |  | ||||||
| 
 | 
 | ||||||
|  |   @doc """ | ||||||
|  |   A version agnostic method for getting a player's volatility. | ||||||
|  |   """ | ||||||
|  |   @spec volatility(player :: t, default_volatility :: volatility) :: volatility | ||||||
|  |   def volatility(player, default_volatility \\ initial_volatility()) | ||||||
|  |   def volatility({_, _}, default_volatility), do: default_volatility | ||||||
|  |   def volatility({_, _, volatility}, _), do: volatility | ||||||
|  | 
 | ||||||
|  |   @doc """ | ||||||
|  |   A convenience function for summarizing a player's strength as a 95% | ||||||
|  |   confidence interval. | ||||||
|  | 
 | ||||||
|  |   The lowest value in the interval is the player's rating minus twice the RD, | ||||||
|  |   and the highest value is the player's rating plus twice the RD. | ||||||
|  |   The volatility measure does not appear in the calculation of this interval. | ||||||
|  | 
 | ||||||
|  |   An example would be if a player's rating is 1850 and the RD is 50, | ||||||
|  |   the interval would range from 1750 to 1950. We would then say that we're 95% | ||||||
|  |   confident that the player's actual strength is between 1750 and 1950. | ||||||
|  | 
 | ||||||
|  |   When a player has a low RD, the interval would be narrow, so that we would | ||||||
|  |   be 95% confident about a player’s strength being in a small interval of values. | ||||||
|  |   """ | ||||||
|  |   @spec rating_interval(player :: t) :: {rating_low :: float, rating_high :: float} | ||||||
|  |   def rating_interval(player, as_version \\ nil), | ||||||
|  |     do: { | ||||||
|  |       rating(player, as_version) - rating_deviation(player, as_version) * 2, | ||||||
|  |       rating(player, as_version) + rating_deviation(player, as_version) * 2 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |   @doc """ | ||||||
|  |   Scales a player's rating. | ||||||
|  |   """ | ||||||
|  |   @spec scale_rating_to(rating :: rating, to_version :: version) :: rating | ||||||
|  |   def scale_rating_to(rating, _version = :v1), | ||||||
|  |     do: rating * @magic_version_scale + @magic_version_scale_rating | ||||||
|  | 
 | ||||||
|  |   def scale_rating_to(rating, _version = :v2), | ||||||
|  |     do: (rating - @magic_version_scale_rating) / @magic_version_scale | ||||||
|  | 
 | ||||||
|  |   @doc """ | ||||||
|  |   Scales a player's rating deviation. | ||||||
|  |   """ | ||||||
|  |   @spec scale_rating_deviation_to(rating_deviation :: rating_deviation, to_version :: version) :: | ||||||
|  |           rating_deviation | ||||||
|  |   def scale_rating_deviation_to(rating_deviation, _version = :v1), | ||||||
|  |     do: rating_deviation * @magic_version_scale | ||||||
|  | 
 | ||||||
|  |   def scale_rating_deviation_to(rating_deviation, _version = :v2), | ||||||
|  |     do: rating_deviation / @magic_version_scale | ||||||
| end | end | ||||||
|  | |||||||
| @ -1,68 +1,69 @@ | |||||||
| defmodule Glicko.Result do | defmodule Glicko.Result do | ||||||
| 	@moduledoc """ |   @moduledoc """ | ||||||
| 	Provides convenience functions for handling a result against an opponent. |   Provides convenience functions for handling a result against an opponent. | ||||||
| 
 | 
 | ||||||
| 	## Usage |   ## Usage | ||||||
| 
 | 
 | ||||||
| 		iex> opponent = Player.new_v2 |       iex> opponent = Player.new_v2 | ||||||
| 		iex> Result.new(opponent, 1.0) |       iex> Result.new(opponent, 1.0) | ||||||
| 		{0.0, 2.014761872416068, 1.0} |       {0.0, 2.014761872416068, 1.0} | ||||||
| 		iex> Result.new(opponent, :draw) # With shortcut |       iex> Result.new(opponent, :draw) # With shortcut | ||||||
| 		{0.0, 2.014761872416068, 0.5} |       {0.0, 2.014761872416068, 0.5} | ||||||
| 
 | 
 | ||||||
| 	""" |   """ | ||||||
| 
 | 
 | ||||||
| 	alias Glicko.Player |   alias Glicko.Player | ||||||
| 
 | 
 | ||||||
| 	@type t :: {Player.rating, Player.rating_deviation, score} |   @type t :: {Player.rating(), Player.rating_deviation(), score} | ||||||
| 
 | 
 | ||||||
| 	@type score :: float |   @type score :: float | ||||||
| 	@type score_shortcut :: :loss | :draw | :win |   @type score_shortcut :: :loss | :draw | :win | ||||||
| 
 | 
 | ||||||
| 	@score_shortcut_map %{loss: 0.0, draw: 0.5, win: 1.0} |   @score_shortcut_map %{loss: 0.0, draw: 0.5, win: 1.0} | ||||||
| 	@score_shortcuts Map.keys(@score_shortcut_map) |   @score_shortcuts Map.keys(@score_shortcut_map) | ||||||
| 
 | 
 | ||||||
| 	@doc """ |   @doc """ | ||||||
| 	Creates a new result from an opponent rating, opponent rating deviation and score. |   Creates a new result from an opponent rating, opponent rating deviation and score. | ||||||
| 
 | 
 | ||||||
| 	Values provided for the opponent rating and opponent rating deviation must be *v2* based. |   Values provided for the opponent rating and opponent rating deviation must be *v2* based. | ||||||
| 
 | 
 | ||||||
| 	Supports passing either `:loss`, `:draw`, or `:win` as shortcuts. |   Supports passing either `:loss`, `:draw`, or `:win` as shortcuts. | ||||||
| 	""" |   """ | ||||||
| 	@spec new(Player.rating, Player.rating_deviation, score | score_shortcut) :: t |   @spec new(Player.rating(), Player.rating_deviation(), score | score_shortcut) :: t | ||||||
| 	def new(opponent_rating, opponent_rating_deviation, score) when is_number(score) do |   def new(opponent_rating, opponent_rating_deviation, score) when is_number(score) do | ||||||
| 		{opponent_rating, opponent_rating_deviation, score} |     {opponent_rating, opponent_rating_deviation, score} | ||||||
| 	end |   end | ||||||
| 	def new(opponent_rating, opponent_rating_deviation, score_type) when is_atom(score_type) and score_type in @score_shortcuts do |  | ||||||
| 		{opponent_rating, opponent_rating_deviation, Map.fetch!(@score_shortcut_map, score_type)} |  | ||||||
| 	end |  | ||||||
| 
 | 
 | ||||||
| 	@doc """ |   def new(opponent_rating, opponent_rating_deviation, score_type) | ||||||
| 	Creates a new result from an opponent and score. |       when is_atom(score_type) and score_type in @score_shortcuts do | ||||||
|  |     {opponent_rating, opponent_rating_deviation, Map.fetch!(@score_shortcut_map, score_type)} | ||||||
|  |   end | ||||||
| 
 | 
 | ||||||
| 	Supports passing either `:loss`, `:draw`, or `:win` as shortcuts. |   @doc """ | ||||||
| 	""" |   Creates a new result from an opponent and score. | ||||||
| 	@spec new(opponent :: Player.t, score :: score | score_shortcut) :: t |  | ||||||
| 	def new(opponent, score) do |  | ||||||
| 		new(Player.rating(opponent, :v2), Player.rating_deviation(opponent, :v2), score) |  | ||||||
| 	end |  | ||||||
| 
 | 
 | ||||||
| 	@doc """ |   Supports passing either `:loss`, `:draw`, or `:win` as shortcuts. | ||||||
| 	Convenience function for accessing an opponent's rating. |   """ | ||||||
| 	""" |   @spec new(opponent :: Player.t(), score :: score | score_shortcut) :: t | ||||||
| 	@spec opponent_rating(result :: Result.t) :: Player.rating |   def new(opponent, score) do | ||||||
| 	def opponent_rating(_result = {rating, _, _}), do: rating |     new(Player.rating(opponent, :v2), Player.rating_deviation(opponent, :v2), score) | ||||||
|  |   end | ||||||
| 
 | 
 | ||||||
| 	@doc """ |   @doc """ | ||||||
| 	Convenience function for accessing an opponent's rating deviation. |   Convenience function for accessing an opponent's rating. | ||||||
| 	""" |   """ | ||||||
| 	@spec opponent_rating_deviation(result :: Result.t) :: Player.rating_deviation |   @spec opponent_rating(result :: Result.t()) :: Player.rating() | ||||||
| 	def opponent_rating_deviation(_result = {_, rating_deviation, _}), do: rating_deviation |   def opponent_rating(_result = {rating, _, _}), do: rating | ||||||
| 
 | 
 | ||||||
| 	@doc """ |   @doc """ | ||||||
| 	Convenience function for accessing the score. |   Convenience function for accessing an opponent's rating deviation. | ||||||
| 	""" |   """ | ||||||
| 	@spec score(result :: Result.t) :: score |   @spec opponent_rating_deviation(result :: Result.t()) :: Player.rating_deviation() | ||||||
| 	def score(_result = {_, _, score}), do: score |   def opponent_rating_deviation(_result = {_, rating_deviation, _}), do: rating_deviation | ||||||
| 
 | 
 | ||||||
|  |   @doc """ | ||||||
|  |   Convenience function for accessing the score. | ||||||
|  |   """ | ||||||
|  |   @spec score(result :: Result.t()) :: score | ||||||
|  |   def score(_result = {_, _, score}), do: score | ||||||
| end | end | ||||||
|  | |||||||
							
								
								
									
										51
									
								
								mix.exs
									
									
									
									
									
								
							
							
						
						
									
										51
									
								
								mix.exs
									
									
									
									
									
								
							| @ -1,30 +1,33 @@ | |||||||
| defmodule Glicko.Mixfile do | defmodule Glicko.Mixfile do | ||||||
| 	use Mix.Project |   use Mix.Project | ||||||
| 
 | 
 | ||||||
| 	@description """ |   @description """ | ||||||
| 	Implementation of the Glicko rating system |   Implementation of the Glicko rating system | ||||||
| 	""" |   """ | ||||||
| 
 | 
 | ||||||
| 	def project, do: [ |   def project, | ||||||
| 		app: :glicko, |     do: [ | ||||||
| 		version: "0.6.0", |       app: :glicko, | ||||||
| 		elixir: "~> 1.5", |       version: "0.6.0", | ||||||
| 		start_permanent: Mix.env == :prod, |       elixir: "~> 1.5", | ||||||
| 		deps: deps(), |       start_permanent: Mix.env() == :prod, | ||||||
| 		package: package(), |       deps: deps(), | ||||||
| 		description: @description, |       package: package(), | ||||||
| 	] |       description: @description | ||||||
|  |     ] | ||||||
| 
 | 
 | ||||||
| 	defp deps, do: [ |   defp deps, | ||||||
| 		{:inch_ex, "~> 0.5", only: :docs}, |     do: [ | ||||||
| 		{:ex_doc, "~> 0.16", only: :dev, runtime: false}, |       {:inch_ex, "~> 0.5", only: :docs}, | ||||||
| 		{:credo, "~> 0.8", only: [:dev, :test], runtime: false}, |       {:ex_doc, "~> 0.16", only: :dev, runtime: false}, | ||||||
| 	] |       {:credo, "~> 0.8", only: [:dev, :test], runtime: false} | ||||||
|  |     ] | ||||||
| 
 | 
 | ||||||
| 	defp package, do: [ |   defp package, | ||||||
| 		name: :glicko, |     do: [ | ||||||
| 		maintainers: ["James Dyson"], |       name: :glicko, | ||||||
| 		licenses: ["MIT"], |       maintainers: ["James Dyson"], | ||||||
| 		links: %{"GitHub" => "https://github.com/avitex/elixir-glicko"}, |       licenses: ["MIT"], | ||||||
| 	] |       links: %{"GitHub" => "https://github.com/avitex/elixir-glicko"} | ||||||
|  |     ] | ||||||
| end | end | ||||||
|  | |||||||
| @ -1,68 +1,77 @@ | |||||||
| defmodule GlickoTest do | defmodule GlickoTest do | ||||||
| 	use ExUnit.Case |   use ExUnit.Case | ||||||
| 
 | 
 | ||||||
| 	alias Glicko.{ |   alias Glicko.{ | ||||||
| 		Player, |     Player, | ||||||
| 		Result, |     Result | ||||||
| 	} |   } | ||||||
| 
 | 
 | ||||||
| 	doctest Glicko |   doctest Glicko | ||||||
| 
 | 
 | ||||||
| 	@player [rating: 1500, rating_deviation: 200] |> Player.new_v1 |> Player.to_v2 |   @player [rating: 1500, rating_deviation: 200] |> Player.new_v1() |> Player.to_v2() | ||||||
| 
 | 
 | ||||||
| 	@results [ |   @results [ | ||||||
| 		Result.new(Player.new_v1([rating: 1400, rating_deviation: 30]), :win), |     Result.new(Player.new_v1(rating: 1400, rating_deviation: 30), :win), | ||||||
| 		Result.new(Player.new_v1([rating: 1550, rating_deviation: 100]), :loss), |     Result.new(Player.new_v1(rating: 1550, rating_deviation: 100), :loss), | ||||||
| 		Result.new(Player.new_v1([rating: 1700, rating_deviation: 300]), :loss), |     Result.new(Player.new_v1(rating: 1700, rating_deviation: 300), :loss) | ||||||
| 	] |   ] | ||||||
| 
 | 
 | ||||||
| 	@valid_player_rating_after_results 1464.06 |> Player.scale_rating_to(:v2) |   @valid_player_rating_after_results 1464.06 |> Player.scale_rating_to(:v2) | ||||||
| 	@valid_player_rating_deviation_after_results 151.52 |> Player.scale_rating_deviation_to(:v2) |   @valid_player_rating_deviation_after_results 151.52 |> Player.scale_rating_deviation_to(:v2) | ||||||
| 	@valid_player_volatility_after_results 0.05999 |   @valid_player_volatility_after_results 0.05999 | ||||||
| 
 | 
 | ||||||
| 	@valid_player_rating_deviation_after_no_results 200.2714 |> Player.scale_rating_deviation_to(:v2) |   @valid_player_rating_deviation_after_no_results 200.2714 | ||||||
|  |                                                   |> Player.scale_rating_deviation_to(:v2) | ||||||
| 
 | 
 | ||||||
| 	describe "new rating" do |   describe "new rating" do | ||||||
| 		test "with results" do |     test "with results" do | ||||||
| 			player = Glicko.new_rating(@player, @results, [system_constant: 0.5]) |       player = Glicko.new_rating(@player, @results, system_constant: 0.5) | ||||||
| 	 |  | ||||||
| 			assert_in_delta Player.rating(player), @valid_player_rating_after_results, 1.0e-4 |  | ||||||
| 			assert_in_delta Player.rating_deviation(player), @valid_player_rating_deviation_after_results, 1.0e-4 |  | ||||||
| 			assert_in_delta Player.volatility(player), @valid_player_volatility_after_results, 1.0e-5 |  | ||||||
| 		end |  | ||||||
| 	 |  | ||||||
| 		test "no results" do |  | ||||||
| 			player = Glicko.new_rating(@player, []) |  | ||||||
| 	 |  | ||||||
| 			assert_in_delta Player.rating_deviation(player), @valid_player_rating_deviation_after_no_results, 1.0e-4 |  | ||||||
| 		end |  | ||||||
| 	end |  | ||||||
| 
 | 
 | ||||||
| 	describe "win probability" do |       assert_in_delta Player.rating(player), @valid_player_rating_after_results, 1.0e-4 | ||||||
| 		test "with same ratings" do | 
 | ||||||
| 			assert Glicko.win_probability(Player.new_v1, Player.new_v1) == 0.5 |       assert_in_delta Player.rating_deviation(player), | ||||||
| 		end |                       @valid_player_rating_deviation_after_results, | ||||||
| 	 |                       1.0e-4 | ||||||
| 		test "with better opponent" do | 
 | ||||||
| 			assert Glicko.win_probability(Player.new_v1([rating: 1500]), Player.new_v1([rating: 1600])) < 0.5 |       assert_in_delta Player.volatility(player), @valid_player_volatility_after_results, 1.0e-5 | ||||||
| 		end |     end | ||||||
| 	 | 
 | ||||||
| 		test "with better player" do |     test "no results" do | ||||||
| 			assert Glicko.win_probability(Player.new_v1([rating: 1600]), Player.new_v1([rating: 1500])) > 0.5 |       player = Glicko.new_rating(@player, []) | ||||||
| 		end | 
 | ||||||
| 	end |       assert_in_delta Player.rating_deviation(player), | ||||||
| 	 |                       @valid_player_rating_deviation_after_no_results, | ||||||
| 	describe "draw probability" do |                       1.0e-4 | ||||||
| 		test "with same ratings" do |     end | ||||||
| 			assert Glicko.draw_probability(Player.new_v1, Player.new_v1) == 1 |   end | ||||||
| 		end | 
 | ||||||
| 	 |   describe "win probability" do | ||||||
| 		test "with better opponent" do |     test "with same ratings" do | ||||||
| 			assert Glicko.draw_probability(Player.new_v1([rating: 1500]), Player.new_v1([rating: 1600])) < 1 |       assert Glicko.win_probability(Player.new_v1(), Player.new_v1()) == 0.5 | ||||||
| 		end |     end | ||||||
| 	 | 
 | ||||||
| 		test "with better player" do |     test "with better opponent" do | ||||||
| 			assert Glicko.draw_probability(Player.new_v1([rating: 1600]), Player.new_v1([rating: 1500])) < 1 |       assert Glicko.win_probability(Player.new_v1(rating: 1500), Player.new_v1(rating: 1600)) < | ||||||
| 		end |                0.5 | ||||||
| 	end |     end | ||||||
|  | 
 | ||||||
|  |     test "with better player" do | ||||||
|  |       assert Glicko.win_probability(Player.new_v1(rating: 1600), Player.new_v1(rating: 1500)) > | ||||||
|  |                0.5 | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   describe "draw probability" do | ||||||
|  |     test "with same ratings" do | ||||||
|  |       assert Glicko.draw_probability(Player.new_v1(), Player.new_v1()) == 1 | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     test "with better opponent" do | ||||||
|  |       assert Glicko.draw_probability(Player.new_v1(rating: 1500), Player.new_v1(rating: 1600)) < 1 | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     test "with better player" do | ||||||
|  |       assert Glicko.draw_probability(Player.new_v1(rating: 1600), Player.new_v1(rating: 1500)) < 1 | ||||||
|  |     end | ||||||
|  |   end | ||||||
| end | end | ||||||
|  | |||||||
| @ -1,60 +1,62 @@ | |||||||
| defmodule Glicko.PlayerTest do | defmodule Glicko.PlayerTest do | ||||||
| 	use ExUnit.Case |   use ExUnit.Case | ||||||
| 
 | 
 | ||||||
| 	alias Glicko.Player |   alias Glicko.Player | ||||||
| 
 | 
 | ||||||
| 	doctest Player |   doctest Player | ||||||
| 
 | 
 | ||||||
| 	@valid_v1_base {1.0, 2.0} |   @valid_v1_base {1.0, 2.0} | ||||||
| 	@valid_v2_base {1.0, 2.0, 3.0} |   @valid_v2_base {1.0, 2.0, 3.0} | ||||||
| 
 | 
 | ||||||
| 	test "create v1" do |   test "create v1" do | ||||||
| 		assert @valid_v1_base == Player.new_v1([rating: 1.0, rating_deviation: 2.0]) |     assert @valid_v1_base == Player.new_v1(rating: 1.0, rating_deviation: 2.0) | ||||||
| 	end |   end | ||||||
| 
 | 
 | ||||||
| 	test "create v2" do |   test "create v2" do | ||||||
| 		assert @valid_v2_base == Player.new_v2([rating: 1.0, rating_deviation: 2.0, volatility: 3.0]) |     assert @valid_v2_base == Player.new_v2(rating: 1.0, rating_deviation: 2.0, volatility: 3.0) | ||||||
| 	end |   end | ||||||
| 
 | 
 | ||||||
| 	test "convert player v1 -> v2" do |   test "convert player v1 -> v2" do | ||||||
| 		assert {Player.scale_rating_to(1.0, :v2), Player.scale_rating_deviation_to(2.0, :v2), 3.0} == Player.to_v2(@valid_v1_base, 3.0) |     assert {Player.scale_rating_to(1.0, :v2), Player.scale_rating_deviation_to(2.0, :v2), 3.0} == | ||||||
| 	end |              Player.to_v2(@valid_v1_base, 3.0) | ||||||
|  |   end | ||||||
| 
 | 
 | ||||||
| 	test "convert player v2 -> v1" do |   test "convert player v2 -> v1" do | ||||||
| 		assert {Player.scale_rating_to(1.0, :v1), Player.scale_rating_deviation_to(2.0, :v1)} == Player.to_v1(@valid_v2_base) |     assert {Player.scale_rating_to(1.0, :v1), Player.scale_rating_deviation_to(2.0, :v1)} == | ||||||
| 	end |              Player.to_v1(@valid_v2_base) | ||||||
|  |   end | ||||||
| 
 | 
 | ||||||
| 	test "convert player v1 -> v1" do |   test "convert player v1 -> v1" do | ||||||
| 		assert @valid_v1_base == Player.to_v1(@valid_v1_base) |     assert @valid_v1_base == Player.to_v1(@valid_v1_base) | ||||||
| 	end |   end | ||||||
| 
 | 
 | ||||||
| 	test "convert player v2 -> v2" do |   test "convert player v2 -> v2" do | ||||||
| 		assert @valid_v2_base == Player.to_v2(@valid_v2_base) |     assert @valid_v2_base == Player.to_v2(@valid_v2_base) | ||||||
| 	end |   end | ||||||
| 
 | 
 | ||||||
| 	test "scale rating v1 -> v2" do |   test "scale rating v1 -> v2" do | ||||||
| 		assert_in_delta Player.scale_rating_to(1673.7178, :v2), 1.0, 0.1 |     assert_in_delta Player.scale_rating_to(1673.7178, :v2), 1.0, 0.1 | ||||||
| 	end |   end | ||||||
| 
 | 
 | ||||||
| 	test "scale rating v2 -> v1" do |   test "scale rating v2 -> v1" do | ||||||
| 		assert_in_delta Player.scale_rating_to(1.0, :v1), 1673.7178, 0.1 |     assert_in_delta Player.scale_rating_to(1.0, :v1), 1673.7178, 0.1 | ||||||
| 	end |   end | ||||||
| 
 | 
 | ||||||
| 	test "scale rating deviation v1 -> v2" do |   test "scale rating deviation v1 -> v2" do | ||||||
| 		assert_in_delta Player.scale_rating_deviation_to(173.7178, :v2), 1.0, 0.1 |     assert_in_delta Player.scale_rating_deviation_to(173.7178, :v2), 1.0, 0.1 | ||||||
| 	end |   end | ||||||
| 
 | 
 | ||||||
| 	test "scale rating deviation v2 -> v1" do |   test "scale rating deviation v2 -> v1" do | ||||||
| 		assert_in_delta Player.scale_rating_deviation_to(1.0, :v1), 173.7178, 0.1 |     assert_in_delta Player.scale_rating_deviation_to(1.0, :v1), 173.7178, 0.1 | ||||||
| 	end |   end | ||||||
| 
 | 
 | ||||||
| 	test "rating interval" do |   test "rating interval" do | ||||||
| 		assert {rating_low, rating_high} = |     assert {rating_low, rating_high} = | ||||||
| 			[rating: 1850, rating_deviation: 50] |              [rating: 1850, rating_deviation: 50] | ||||||
| 			|> Player.new_v2 |              |> Player.new_v2() | ||||||
| 			|> Player.rating_interval |              |> Player.rating_interval() | ||||||
| 
 | 
 | ||||||
| 		assert_in_delta rating_low, 1750, 0.1 |     assert_in_delta rating_low, 1750, 0.1 | ||||||
| 		assert_in_delta rating_high, 1950, 0.1 |     assert_in_delta rating_high, 1950, 0.1 | ||||||
| 	end |   end | ||||||
| end | end | ||||||
|  | |||||||
| @ -1,22 +1,22 @@ | |||||||
| defmodule Glicko.ResultTest do | defmodule Glicko.ResultTest do | ||||||
| 	use ExUnit.Case |   use ExUnit.Case | ||||||
| 
 | 
 | ||||||
| 	alias Glicko.{ |   alias Glicko.{ | ||||||
| 		Player, |     Player, | ||||||
| 		Result, |     Result | ||||||
| 	} |   } | ||||||
| 
 | 
 | ||||||
| 	doctest Result |   doctest Result | ||||||
| 
 | 
 | ||||||
| 	@opponent Player.new_v2 |   @opponent Player.new_v2() | ||||||
| 
 | 
 | ||||||
| 	@valid_game_result Result.new(@opponent, 0.0) |   @valid_game_result Result.new(@opponent, 0.0) | ||||||
| 
 | 
 | ||||||
| 	test "create game result" do |   test "create game result" do | ||||||
| 		assert @valid_game_result == Result.new(@opponent, 0.0) |     assert @valid_game_result == Result.new(@opponent, 0.0) | ||||||
| 	end |   end | ||||||
| 
 | 
 | ||||||
| 	test "create game result with shortcut" do |   test "create game result with shortcut" do | ||||||
| 		assert @valid_game_result == Result.new(@opponent, :loss) |     assert @valid_game_result == Result.new(@opponent, :loss) | ||||||
| 	end |   end | ||||||
| end | end | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user