1
0
mirror of https://github.com/avitex/elixir-glicko synced 2024-09-21 15:19:57 +00:00
glicko-elixir/lib/glicko.ex
Mikael Muszynski c1089996d3 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.

In the interest of keeping the patch focused: primarily replace internal
implementation of the added struct modules, and keep the interfaces for
creation, conversion (player v1 to v2, and vice versa), and field access
as they currently are.
2024-06-10 13:04:45 +02:00

325 lines
9.7 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.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: 1700, rating_deviation: 300]), :loss)]
iex> player = Player.new_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.new_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.new_v1
iex> opponent = Player.new_v1
iex> Glicko.win_probability(player, opponent)
0.5
Calculate the probability of a player drawing against an opponent.
iex> player = Player.new_v1
iex> opponent = Player.new_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 =
result
|> Result.opponent_rating_deviation()
|> calc_g
win_probability = calc_e(player_pre_r, Result.opponent_rating(result), 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(result) - 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