1
0
mirror of https://github.com/avitex/elixir-glicko synced 2025-01-25 23:39:57 +00:00

Compare commits

..

No commits in common. "6e22fcf0b0a945eb86b4841c8e7a92715f05825a" and "8612b2aa97e15933bfbfdff6ceb5b243b648be7d" have entirely different histories.

12 changed files with 612 additions and 727 deletions

View File

@ -1,66 +1,68 @@
%{ %{
configs: [ configs: [%{
%{ name: "default",
name: "default", files: %{
files: %{ included: ["lib/", "test/"],
included: ["lib/", "test/"], excluded: [~r"/_build/", ~r"/deps/"]
excluded: [~r"/_build/", ~r"/deps/"] },
}, checks: [
checks: [ {Credo.Check.Consistency.ExceptionNames},
{Credo.Check.Consistency.ExceptionNames}, {Credo.Check.Consistency.LineEndings},
{Credo.Check.Consistency.LineEndings}, {Credo.Check.Consistency.ParameterPatternMatching},
{Credo.Check.Consistency.ParameterPatternMatching}, {Credo.Check.Consistency.SpaceAroundOperators},
{Credo.Check.Consistency.SpaceAroundOperators}, {Credo.Check.Consistency.SpaceInParentheses},
{Credo.Check.Consistency.SpaceInParentheses}, {Credo.Check.Consistency.TabsOrSpaces},
{Credo.Check.Consistency.TabsOrSpaces},
{Credo.Check.Design.AliasUsage, priority: :low}, {Credo.Check.Design.AliasUsage, priority: :low},
{Credo.Check.Design.DuplicatedCode, excluded_macros: []}, {Credo.Check.Design.DuplicatedCode, excluded_macros: []},
{Credo.Check.Design.TagTODO, exit_status: 2}, {Credo.Check.Design.TagTODO, exit_status: 2},
{Credo.Check.Design.TagFIXME}, {Credo.Check.Design.TagFIXME},
{Credo.Check.Readability.FunctionNames},
{Credo.Check.Readability.LargeNumbers}, {Credo.Check.Readability.FunctionNames},
{Credo.Check.Readability.MaxLineLength, priority: :low, max_length: 80}, {Credo.Check.Readability.LargeNumbers},
{Credo.Check.Readability.ModuleAttributeNames}, {Credo.Check.Readability.MaxLineLength, priority: :low, max_length: 80},
{Credo.Check.Readability.ModuleDoc}, {Credo.Check.Readability.ModuleAttributeNames},
{Credo.Check.Readability.ModuleNames}, {Credo.Check.Readability.ModuleDoc},
{Credo.Check.Readability.ParenthesesOnZeroArityDefs}, {Credo.Check.Readability.ModuleNames},
{Credo.Check.Readability.ParenthesesInCondition}, {Credo.Check.Readability.ParenthesesOnZeroArityDefs},
{Credo.Check.Readability.PredicateFunctionNames}, {Credo.Check.Readability.ParenthesesInCondition},
{Credo.Check.Readability.PreferImplicitTry}, {Credo.Check.Readability.PredicateFunctionNames},
{Credo.Check.Readability.RedundantBlankLines}, {Credo.Check.Readability.PreferImplicitTry},
{Credo.Check.Readability.StringSigils}, {Credo.Check.Readability.RedundantBlankLines},
{Credo.Check.Readability.TrailingBlankLine}, {Credo.Check.Readability.StringSigils},
{Credo.Check.Readability.TrailingWhiteSpace}, {Credo.Check.Readability.TrailingBlankLine},
{Credo.Check.Readability.VariableNames}, {Credo.Check.Readability.TrailingWhiteSpace},
{Credo.Check.Readability.Semicolons}, {Credo.Check.Readability.VariableNames},
{Credo.Check.Readability.SpaceAfterCommas}, {Credo.Check.Readability.Semicolons},
{Credo.Check.Refactor.DoubleBooleanNegation}, {Credo.Check.Readability.SpaceAfterCommas},
{Credo.Check.Refactor.CondStatements},
{Credo.Check.Refactor.CyclomaticComplexity}, {Credo.Check.Refactor.DoubleBooleanNegation},
{Credo.Check.Refactor.FunctionArity, [ignore_defp: true]}, {Credo.Check.Refactor.CondStatements},
{Credo.Check.Refactor.LongQuoteBlocks}, {Credo.Check.Refactor.CyclomaticComplexity},
{Credo.Check.Refactor.MatchInCondition}, {Credo.Check.Refactor.FunctionArity, [ignore_defp: true]},
{Credo.Check.Refactor.NegatedConditionsInUnless}, {Credo.Check.Refactor.LongQuoteBlocks},
{Credo.Check.Refactor.NegatedConditionsWithElse}, {Credo.Check.Refactor.MatchInCondition},
{Credo.Check.Refactor.Nesting}, {Credo.Check.Refactor.NegatedConditionsInUnless},
{Credo.Check.Refactor.PipeChainStart}, {Credo.Check.Refactor.NegatedConditionsWithElse},
{Credo.Check.Refactor.UnlessWithElse}, {Credo.Check.Refactor.Nesting},
{Credo.Check.Warning.BoolOperationOnSameValues}, {Credo.Check.Refactor.PipeChainStart},
{Credo.Check.Warning.IExPry}, {Credo.Check.Refactor.UnlessWithElse},
{Credo.Check.Warning.IoInspect},
{Credo.Check.Warning.LazyLogging}, {Credo.Check.Warning.BoolOperationOnSameValues},
{Credo.Check.Warning.OperationOnSameValues}, {Credo.Check.Warning.IExPry},
{Credo.Check.Warning.OperationWithConstantResult}, {Credo.Check.Warning.IoInspect},
{Credo.Check.Warning.UnusedEnumOperation}, {Credo.Check.Warning.LazyLogging},
{Credo.Check.Warning.UnusedFileOperation}, {Credo.Check.Warning.OperationOnSameValues},
{Credo.Check.Warning.UnusedKeywordOperation}, {Credo.Check.Warning.OperationWithConstantResult},
{Credo.Check.Warning.UnusedListOperation}, {Credo.Check.Warning.UnusedEnumOperation},
{Credo.Check.Warning.UnusedPathOperation}, {Credo.Check.Warning.UnusedFileOperation},
{Credo.Check.Warning.UnusedRegexOperation}, {Credo.Check.Warning.UnusedKeywordOperation},
{Credo.Check.Warning.UnusedStringOperation}, {Credo.Check.Warning.UnusedListOperation},
{Credo.Check.Warning.UnusedTupleOperation}, {Credo.Check.Warning.UnusedPathOperation},
{Credo.Check.Warning.RaiseInsideRescue} {Credo.Check.Warning.UnusedRegexOperation},
] {Credo.Check.Warning.UnusedStringOperation},
} {Credo.Check.Warning.UnusedTupleOperation},
] {Credo.Check.Warning.RaiseInsideRescue},
]
}]
} }

View File

@ -1,3 +0,0 @@
[
inputs: ["{mix,.formatter,.credo}.exs", "{config,lib,test}/**/*.{ex,exs}"]
]

View File

@ -1,11 +1,11 @@
language: elixir language: elixir
elixir: elixir:
- 1.9.4 - 1.5.0
notifications: notifications:
recipients: recipients:
- theavitex@gmail.com - theavitex@gmail.com
otp_release: otp_release:
- 22.1 - 18.2
env: env:
- MIX_ENV=test - MIX_ENV=test
script: script:

View File

@ -1,6 +1,6 @@
MIT License MIT License
Copyright (c) 2017-2020 James Dyson <theavitex@gmail.com> Copyright (c) 2017 James Dyson
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View File

@ -5,15 +5,15 @@
# Glicko # Glicko
**Implementation of the [Glicko rating system](http://www.glicko.net/glicko.html).** **Implementation of the [Glicko rating system](http://www.glicko.net/glicko.html).**
Documentation hosted on [hexdocs](https://hexdocs.pm/glicko). Documentation hosted on [hexdocs](https://hexdocs.pm/glicko).
## Installation ## Installation
Add `glicko` to your list of dependencies in `mix.exs`: Add `glicko` to your list of dependencies in `mix.exs`:
```elixir ```elixir
def deps do def deps do
[{:glicko, "~> 0.6.0"}] [{:glicko, "~> 0.6.0"}]
end end
``` ```

View File

@ -1,311 +1,246 @@
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( win_probability(player |> Player.rating(:v2), opponent |> Player.rating(:v2), opponent |> Player.rating_deviation(:v2))
player |> Player.rating(:v2), end
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( @spec win_probability(player_rating :: Player.rating, opponent_rating :: Player.rating, opponent_rating_deviation :: Player.rating_deviation) :: float
player_rating :: Player.rating(), def win_probability(player_rating, opponent_rating, opponent_rating_deviation) do
opponent_rating :: Player.rating(), calc_e(player_rating, opponent_rating, calc_g(opponent_rating_deviation))
opponent_rating_deviation :: Player.rating_deviation() end
) :: 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( draw_probability(player |> Player.rating(:v2), opponent |> Player.rating(:v2), opponent |> Player.rating_deviation(:v2))
player |> Player.rating(:v2), end
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( @spec draw_probability(player_rating :: Player.rating, opponent_rating :: Player.rating, opponent_rating_deviation :: Player.rating_deviation) :: float
player_rating :: Player.rating(), def draw_probability(player_rating, opponent_rating, opponent_rating_deviation) do
opponent_rating :: Player.rating(), 1 - abs(win_probability(player_rating, opponent_rating, opponent_rating_deviation) - 0.5) / 0.5
opponent_rating_deviation :: Player.rating_deviation() end
) :: 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) :: @spec new_rating(player :: Player.t, results :: list(Result.t), opts :: new_rating_opts) :: Player.t
Player.t() def new_rating(player, results, opts \\ [])
def new_rating(player, results, opts \\ []) def new_rating(player, results, opts) when tuple_size(player) == 3 do
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
def new_rating(player, results, opts) when tuple_size(player) == 3 do defp do_new_rating({player_r, player_pre_rd, player_v}, [], _) do
do_new_rating(player, results, opts) player_post_rd = calc_player_post_base_rd(:math.pow(player_pre_rd, 2), player_v)
end
def new_rating(player, results, opts) when tuple_size(player) == 2 do {player_r, player_post_rd, player_v}
player end
|> Player.to_v2() defp do_new_rating({player_pre_r, player_pre_rd, player_pre_v}, results, opts) do
|> do_new_rating(results, opts) sys_const = Keyword.get(opts, :system_constant, @default_system_constant)
|> Player.to_v1() conv_tol = Keyword.get(opts, :convergence_tolerance, @default_convergence_tolerance)
end
defp do_new_rating({player_r, player_pre_rd, player_v}, [], _) do # Initialization (skips steps 1, 2 and 3)
player_post_rd = calc_player_post_base_rd(:math.pow(player_pre_rd, 2), player_v) 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_r, player_post_rd, player_v} {player_post_r, player_post_rd, player_post_v}
end end
defp do_new_rating({player_pre_r, player_pre_rd, player_pre_v}, results, opts) do defp result_calculations(results, player_pre_r) do
sys_const = Keyword.get(opts, :system_constant, @default_system_constant) {variance_estimate_acc, result_effect_acc} =
conv_tol = Keyword.get(opts, :convergence_tolerance, @default_convergence_tolerance) 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
# Initialization (skips steps 1, 2 and 3) win_probability = calc_e(player_pre_r, Result.opponent_rating(result), opponent_rd_g)
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( variance_estimate_acc + :math.pow(opponent_rd_g, 2) * win_probability * (1 - win_probability),
alpha, result_effect_acc + opponent_rd_g * (Result.score(result) - win_probability)
delta, }
player_pre_rd_sq, end)
variance_est,
sys_const,
k
)
# Step 5.3 {:math.pow(variance_estimate_acc, -1), result_effect_acc}
initial_fa = calc_f(alpha, delta, player_pre_rd_sq, variance_est, sys_const, initial_a) end
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 defp calc_delta(results_effect, variance_est) do
player_post_v = calc_new_player_volatility(a) results_effect * variance_est
# Step 6 end
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} defp calc_f(alpha, delta, player_pre_rd_sq, variance_est, sys_const, x) do
end :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 result_calculations(results, player_pre_r) do defp calc_alpha(player_pre_v) do
{variance_estimate_acc, result_effect_acc} = :math.log(:math.pow(player_pre_v, 2))
Enum.reduce(results, {0.0, 0.0}, fn result, {variance_estimate_acc, result_effect_acc} -> end
opponent_rd_g =
result
|> Result.opponent_rating_deviation()
|> calc_g
win_probability = calc_e(player_pre_r, Result.opponent_rating(result), opponent_rd_g) 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
variance_estimate_acc + player_pre_r + :math.pow(player_post_rd, 2) * results_effect
:math.pow(opponent_rd_g, 2) * win_probability * (1 - win_probability), end
result_effect_acc + opponent_rd_g * (Result.score(result) - win_probability)
}
end)
{:math.pow(variance_estimate_acc, -1), result_effect_acc} defp calc_new_player_rating_deviation(player_post_base_rd, variance_est) do
end 1 / :math.sqrt(1 / :math.pow(player_post_base_rd, 2) + 1 / variance_est)
end
defp calc_delta(results_effect, variance_est) do defp calc_player_post_base_rd(player_pre_rd_sq, player_pre_v) do
results_effect * variance_est :math.sqrt((:math.pow(player_pre_v, 2) + player_pre_rd_sq))
end end
defp calc_f(alpha, delta, player_pre_rd_sq, variance_est, sys_const, x) do defp iterative_algorithm_initial(alpha, delta, player_pre_rd_sq, variance_est, sys_const, k) do
:math.exp(x) * initial_a = alpha
(:math.pow(delta, 2) - :math.exp(x) - player_pre_rd_sq - variance_est) / initial_b =
(2 * :math.pow(player_pre_rd_sq + variance_est + :math.exp(x), 2)) - if :math.pow(delta, 2) > player_pre_rd_sq + variance_est do
(x - alpha) / :math.pow(sys_const, 2) :math.log(:math.pow(delta, 2) - player_pre_rd_sq - variance_est)
end else
alpha - k * sys_const
end
defp calc_alpha(player_pre_v) do {initial_a, initial_b}
:math.log(:math.pow(player_pre_v, 2)) end
end
defp calc_new_player_volatility(a) do defp iterative_algorithm_body(alpha, delta, player_pre_rd_sq, variance_est, sys_const, conv_tol, a, b, fa, fb) do
:math.exp(a / 2) if abs(b - a) > conv_tol do
end 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_new_player_rating(results_effect, player_pre_r, player_post_rd) do defp calc_k(alpha, delta, player_pre_rd_sq, variance_est, sys_const, k) do
player_pre_r + :math.pow(player_post_rd, 2) * results_effect if calc_f(alpha, delta, player_pre_rd_sq, variance_est, sys_const, alpha - k * sys_const) < 0 do
end calc_k(alpha, delta, player_pre_rd_sq, variance_est, sys_const, k + 1)
else
k
end
end
defp calc_new_player_rating_deviation(player_post_base_rd, variance_est) do # g function
1 / :math.sqrt(1 / :math.pow(player_post_base_rd, 2) + 1 / variance_est) defp calc_g(rd) do
end 1 / :math.sqrt(1 + 3 * :math.pow(rd, 2) / :math.pow(:math.pi, 2))
end
defp calc_player_post_base_rd(player_pre_rd_sq, player_pre_v) do # E function
:math.sqrt(:math.pow(player_pre_v, 2) + player_pre_rd_sq) defp calc_e(player_pre_r, opponent_r, opponent_rd_g) do
end 1 / (1 + :math.exp(-1 * opponent_rd_g * (player_pre_r - opponent_r)))
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

View File

@ -1,223 +1,192 @@
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 @doc """
:v1 |> initial_rating |> scale_rating_to(:v2) The recommended initial rating deviation value for a new player.
end """
@spec initial_rating_deviation(version) :: rating_deviation
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 """ @doc """
The recommended initial rating deviation value for a new player. The recommended initial volatility value for a new player.
""" """
@spec initial_rating_deviation(version) :: rating_deviation @spec initial_volatility :: volatility
def initial_rating_deviation(_version = :v1), do: 350.0 def initial_volatility, do: 0.06
def initial_rating_deviation(_version = :v2) do @doc """
:v1 |> initial_rating_deviation |> scale_rating_deviation_to(:v2) Creates a new v1 player.
end
@doc """ If not overriden, will use the default values for an unrated player.
The recommended initial volatility value for a new player. """
""" @spec new_v1([rating: rating, rating_deviation: rating_deviation]) :: v1
@spec initial_volatility :: volatility def new_v1(opts \\ []) when is_list(opts), do: {
def initial_volatility, do: 0.06 Keyword.get(opts, :rating, initial_rating(:v1)),
Keyword.get(opts, :rating_deviation, initial_rating_deviation(:v1)),
}
@doc """ @doc """
Creates a new v1 player. Creates a new v2 player.
If not overriden, will use the default values for an unrated player. If not overriden, will use default values for an unrated player.
""" """
@spec new_v1(rating: rating, rating_deviation: rating_deviation) :: v1 @spec new_v2([rating: rating, rating_deviation: rating_deviation, volatility: volatility]) :: v2
def new_v1(opts \\ []) when is_list(opts) do def new_v2(opts \\ []) when is_list(opts), do: {
{ Keyword.get(opts, :rating, initial_rating(:v2)),
Keyword.get(opts, :rating, initial_rating(:v1)), Keyword.get(opts, :rating_deviation, initial_rating_deviation(:v2)),
Keyword.get(opts, :rating_deviation, initial_rating_deviation(:v1)) Keyword.get(opts, :volatility, initial_volatility()),
} }
end
@doc """ @doc """
Creates a new v2 player. Converts a v2 player to a v1.
If not overriden, will use default values for an unrated player. A v1 player will pass-through unchanged.
"""
@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())
}
end
@doc """ Note the volatility field used in a v2 player will be lost in the conversion.
Converts a v2 player to a v1. """
@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),
}
A v1 player will pass-through unchanged. @doc """
Converts a v1 player to a v2.
Note the volatility field used in a v2 player will be lost in the conversion. A v2 player will pass-through unchanged with the volatility arg ignored.
""" """
@spec to_v1(player :: t) :: v1 @spec to_v2(player :: t, volatility :: volatility) :: v2
def to_v1({rating, rating_deviation}), do: {rating, rating_deviation} def to_v2(player, volatility \\ initial_volatility())
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,
}
def to_v1({rating, rating_deviation, _}) do @doc """
{ A version agnostic method for getting a player's rating.
rating |> scale_rating_to(:v1), """
rating_deviation |> scale_rating_deviation_to(:v1) @spec rating(player :: t, as_version :: version) :: rating
} def rating(player, as_version \\ nil)
end 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 """ @doc """
Converts a v1 player to a v2. A version agnostic method for getting a player's rating deviation.
"""
@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
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
A v2 player will pass-through unchanged with the volatility arg ignored. @doc """
""" A version agnostic method for getting a player's volatility.
@spec to_v2(player :: t, volatility :: volatility) :: v2 """
def to_v2(player, volatility \\ initial_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
def to_v2({rating, rating_deviation, volatility}, _volatility), @doc """
do: {rating, rating_deviation, volatility} A convenience function for summarizing a player's strength as a 95%
confidence interval.
def to_v2({rating, rating_deviation}, volatility) do 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.
rating |> scale_rating_to(:v2), The volatility measure does not appear in the calculation of this interval.
rating_deviation |> scale_rating_deviation_to(:v2),
volatility
}
end
@doc """ An example would be if a player's rating is 1850 and the RD is 50,
A version agnostic method for getting a player's rating. 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.
@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 """ When a player has a low RD, the interval would be narrow, so that we would
A version agnostic method for getting a player's rating deviation. be 95% confident about a players strength being in a small interval of values.
""" """
@spec rating_deviation(player :: t, as_version :: version) :: rating_deviation @spec rating_interval(player :: t) :: {rating_low :: float, rating_high :: float}
def rating_deviation(player, as_version \\ nil) def rating_interval(player, as_version \\ nil), do: {
def rating_deviation({_, rating_deviation}, nil), do: rating_deviation rating(player, as_version) - rating_deviation(player, as_version) * 2,
def rating_deviation({_, rating_deviation, _}, nil), do: rating_deviation rating(player, as_version) + rating_deviation(player, as_version) * 2,
def rating_deviation({_, rating_deviation}, :v1), do: rating_deviation }
def rating_deviation({_, rating_deviation}, :v2), @doc """
do: rating_deviation |> scale_rating_deviation_to(:v2) 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
def rating_deviation({_, rating_deviation, _}, :v1), @doc """
do: rating_deviation |> scale_rating_deviation_to(:v1) 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
def rating_deviation({_, rating_deviation, _}, :v2), do: rating_deviation
@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 players 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
}
end
@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

View File

@ -1,69 +1,68 @@
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
def new(opponent_rating, opponent_rating_deviation, score_type) @doc """
when is_atom(score_type) and score_type in @score_shortcuts do Creates a new result from an opponent and score.
{opponent_rating, opponent_rating_deviation, Map.fetch!(@score_shortcut_map, score_type)}
end
@doc """ Supports passing either `:loss`, `:draw`, or `:win` as shortcuts.
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
Supports passing either `:loss`, `:draw`, or `:win` as shortcuts. @doc """
""" Convenience function for accessing an opponent's rating.
@spec new(opponent :: Player.t(), score :: score | score_shortcut) :: t """
def new(opponent, score) do @spec opponent_rating(result :: Result.t) :: Player.rating
new(Player.rating(opponent, :v2), Player.rating_deviation(opponent, :v2), score) def opponent_rating(_result = {rating, _, _}), do: rating
end
@doc """ @doc """
Convenience function for accessing an opponent's rating. Convenience function for accessing an opponent's rating deviation.
""" """
@spec opponent_rating(result :: Result.t()) :: Player.rating() @spec opponent_rating_deviation(result :: Result.t) :: Player.rating_deviation
def opponent_rating(_result = {rating, _, _}), do: rating def opponent_rating_deviation(_result = {_, rating_deviation, _}), do: rating_deviation
@doc """ @doc """
Convenience function for accessing an opponent's rating deviation. Convenience function for accessing the score.
""" """
@spec opponent_rating_deviation(result :: Result.t()) :: Player.rating_deviation() @spec score(result :: Result.t) :: score
def opponent_rating_deviation(_result = {_, rating_deviation, _}), do: rating_deviation def score(_result = {_, _, score}), do: score
@doc """
Convenience function for accessing the score.
"""
@spec score(result :: Result.t()) :: score
def score(_result = {_, _, score}), do: score
end end

54
mix.exs
View File

@ -1,36 +1,30 @@
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, do: [
[ app: :glicko,
app: :glicko, version: "0.6.0",
version: "0.6.0", elixir: "~> 1.5",
elixir: "~> 1.9", start_permanent: Mix.env == :prod,
start_permanent: Mix.env() == :prod, deps: deps(),
deps: deps(), package: package(),
package: package(), description: @description,
description: @description ]
]
end
defp deps do defp deps, do: [
[ {:inch_ex, "~> 0.5", only: :docs},
{:inch_ex, "~> 0.5", only: :docs}, {:ex_doc, "~> 0.16", only: :dev, runtime: false},
{:ex_doc, "~> 0.16", only: :dev, runtime: false}, {:credo, "~> 0.8", only: [:dev, :test], runtime: false},
{:credo, "~> 0.8", only: [:dev, :test], runtime: false} ]
]
end
defp package do defp package, do: [
[ name: :glicko,
name: :glicko, maintainers: ["James Dyson"],
maintainers: ["James Dyson"], licenses: ["MIT"],
licenses: ["MIT"], links: %{"GitHub" => "https://github.com/avitex/elixir-glicko"},
links: %{"GitHub" => "https://github.com/avitex/elixir-glicko"} ]
]
end
end end

View File

@ -1,77 +1,68 @@
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 @valid_player_rating_deviation_after_no_results 200.2714 |> Player.scale_rating_deviation_to(:v2)
|> 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
assert_in_delta Player.rating(player), @valid_player_rating_after_results, 1.0e-4 describe "win probability" do
test "with same ratings" do
assert_in_delta Player.rating_deviation(player), assert Glicko.win_probability(Player.new_v1, Player.new_v1) == 0.5
@valid_player_rating_deviation_after_results, end
1.0e-4
test "with better opponent" do
assert_in_delta Player.volatility(player), @valid_player_volatility_after_results, 1.0e-5 assert Glicko.win_probability(Player.new_v1([rating: 1500]), Player.new_v1([rating: 1600])) < 0.5
end end
test "no results" do test "with better player" do
player = Glicko.new_rating(@player, []) assert Glicko.win_probability(Player.new_v1([rating: 1600]), Player.new_v1([rating: 1500])) > 0.5
end
assert_in_delta Player.rating_deviation(player), end
@valid_player_rating_deviation_after_no_results,
1.0e-4 describe "draw probability" do
end test "with same ratings" do
end assert Glicko.draw_probability(Player.new_v1, Player.new_v1) == 1
end
describe "win probability" do
test "with same ratings" do test "with better opponent" do
assert Glicko.win_probability(Player.new_v1(), Player.new_v1()) == 0.5 assert Glicko.draw_probability(Player.new_v1([rating: 1500]), Player.new_v1([rating: 1600])) < 1
end end
test "with better opponent" do test "with better player" do
assert Glicko.win_probability(Player.new_v1(rating: 1500), Player.new_v1(rating: 1600)) < assert Glicko.draw_probability(Player.new_v1([rating: 1600]), Player.new_v1([rating: 1500])) < 1
0.5 end
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

View File

@ -1,62 +1,60 @@
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} == 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)
Player.to_v2(@valid_v1_base, 3.0) end
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)} == assert {Player.scale_rating_to(1.0, :v1), Player.scale_rating_deviation_to(2.0, :v1)} == Player.to_v1(@valid_v2_base)
Player.to_v1(@valid_v2_base) end
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

View File

@ -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