1
0
mirror of https://github.com/avitex/elixir-glicko synced 2025-04-18 13:49:57 +00:00
glicko-elixir/lib/glicko.ex
Mikael Muszynski ce08ab5e24 Convert Player (v1/v2) and Result to structs
In an effort to give names to core concepts in the library, replace the
current way of passing around tuples (of varying length and content)
with appropriately named structs.

As a consequence of creating `Player.{V1, V2}` and `Result`, a variety
of functions that extracted values out of the tuples have either been
removed or changed to access struct fields instead.
2020-02-21 22:53:28 +01:00

322 lines
9.6 KiB
Elixir

defmodule Glicko do
@moduledoc """
Provides the implementation of the Glicko rating system.
See the [specification](http://www.glicko.net/glicko/glicko2.pdf) for implementation details.
## Usage
Get a player's new rating after a series of matches in a rating period.
iex> results = [Result.new(%Player.V1{rating: 1400, rating_deviation: 30}, :win),
...> Result.new(%Player.V1{rating: 1550, rating_deviation: 100}, :loss),
...> Result.new(%Player.V1{rating: 1700, rating_deviation: 300}, :loss)]
iex> player = %Player.V1{rating: 1500, rating_deviation: 200}
iex> Glicko.new_rating(player, results, [system_constant: 0.5])
%Player.V1{rating: 1464.0506705393013, rating_deviation: 151.51652412385727}
Get a player's new rating when they haven't played within a rating period.
iex> player = %Player.V1{rating: 1500, rating_deviation: 200}
iex> Glicko.new_rating(player, [], [system_constant: 0.5])
%Player.V1{rating: 1.5e3, rating_deviation: 200.27141669877065}
Calculate the probability of a player winning against an opponent.
iex> player = %Player.V1{}
iex> opponent = %Player.V1{}
iex> Glicko.win_probability(player, opponent)
0.5
Calculate the probability of a player drawing against an opponent.
iex> player = %Player.V1{}
iex> opponent = %Player.V1{}
iex> Glicko.draw_probability(player, opponent)
1.0
"""
alias __MODULE__.{
Player,
Result
}
@default_system_constant 0.8
@default_convergence_tolerance 1.0e-7
@type new_rating_opts :: [system_constant: float, convergence_tolerance: float]
@doc """
Calculates the probability of a player winning against an opponent.
Returns a value between `0.0` and `1.0`.
"""
@spec win_probability(player :: Player.t(), opponent :: Player.t()) :: float
def win_probability(player, opponent) do
win_probability(
player |> Player.rating(:v2),
opponent |> Player.rating(:v2),
opponent |> Player.rating_deviation(:v2)
)
end
@doc """
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.
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
def win_probability(player_rating, opponent_rating, opponent_rating_deviation) do
calc_e(player_rating, opponent_rating, calc_g(opponent_rating_deviation))
end
@doc """
Calculates the probability of a player drawing against an opponent.
Returns a value between `0.0` and `1.0`.
"""
@spec draw_probability(player :: Player.t(), opponent :: Player.t()) :: float
def draw_probability(player, opponent) do
draw_probability(
player |> Player.rating(:v2),
opponent |> Player.rating(:v2),
opponent |> Player.rating_deviation(:v2)
)
end
@doc """
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.
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
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 """
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.
"""
@spec new_rating(player :: Player.t(), results :: list(Result.t()), opts :: new_rating_opts) ::
Player.t()
def new_rating(player, results, opts \\ [])
def new_rating(%Player.V2{} = player, results, opts) do
do_new_rating(player, results, opts)
end
def new_rating(%Player.V1{} = player, results, opts) do
player
|> Player.to_v2()
|> do_new_rating(results, opts)
|> Player.to_v1()
end
defp do_new_rating(%Player.V2{} = player, [], _) do
player_post_rd =
calc_player_post_base_rd(:math.pow(player.rating_deviation, 2), player.volatility)
%{player | rating_deviation: player_post_rd}
end
defp do_new_rating(
%Player.V2{
rating: player_pre_r,
rating_deviation: player_pre_rd,
volatility: player_pre_v
},
results,
opts
) do
sys_const = Keyword.get(opts, :system_constant, @default_system_constant)
conv_tol = Keyword.get(opts, :convergence_tolerance, @default_convergence_tolerance)
# 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} =
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.V2{
rating: player_post_r,
rating_deviation: player_post_rd,
volatility: player_post_v
}
end
defp result_calculations(results, player_pre_r) do
{variance_estimate_acc, result_effect_acc} =
Enum.reduce(results, {0.0, 0.0}, fn result, {variance_estimate_acc, result_effect_acc} ->
opponent_rd_g = calc_g(result.rating_deviation)
win_probability = calc_e(player_pre_r, result.rating, opponent_rd_g)
{
variance_estimate_acc +
:math.pow(opponent_rd_g, 2) * win_probability * (1 - win_probability),
result_effect_acc + opponent_rd_g * (result.score - win_probability)
}
end)
{:math.pow(variance_estimate_acc, -1), result_effect_acc}
end
defp calc_delta(results_effect, variance_est) do
results_effect * variance_est
end
defp calc_f(alpha, delta, player_pre_rd_sq, variance_est, sys_const, x) do
:math.exp(x) *
(: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
:math.log(:math.pow(player_pre_v, 2))
end
defp calc_new_player_volatility(a) do
: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
end
defp calc_new_player_rating_deviation(player_post_base_rd, variance_est) do
1 / :math.sqrt(1 / :math.pow(player_post_base_rd, 2) + 1 / variance_est)
end
defp calc_player_post_base_rd(player_pre_rd_sq, player_pre_v) do
:math.sqrt(:math.pow(player_pre_v, 2) + player_pre_rd_sq)
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