mirror of
https://github.com/avitex/elixir-glicko
synced 2024-12-03 16:19:58 +00:00
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.
This commit is contained in:
parent
9cb997e2c1
commit
c1089996d3
@ -13,13 +13,13 @@ defmodule Glicko do
|
||||
...> 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])
|
||||
{1464.0506705393013, 151.51652412385727}
|
||||
%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])
|
||||
{1.5e3, 200.27141669877065}
|
||||
%Player.V1{rating: 1.5e3, rating_deviation: 200.27141669877065}
|
||||
|
||||
Calculate the probability of a player winning against an opponent.
|
||||
|
||||
@ -117,24 +117,33 @@ defmodule Glicko do
|
||||
Player.t()
|
||||
def new_rating(player, results, opts \\ [])
|
||||
|
||||
def new_rating(player, results, opts) when tuple_size(player) == 3 do
|
||||
def new_rating(%Player.V2{} = player, results, opts) do
|
||||
do_new_rating(player, results, opts)
|
||||
end
|
||||
|
||||
def new_rating(player, results, opts) when tuple_size(player) == 2 do
|
||||
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_r, player_pre_rd, player_v}, [], _) do
|
||||
player_post_rd = calc_player_post_base_rd(:math.pow(player_pre_rd, 2), player_v)
|
||||
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_r, player_post_rd, player_v}
|
||||
%{player | rating_deviation: player_post_rd}
|
||||
end
|
||||
|
||||
defp do_new_rating({player_pre_r, player_pre_rd, player_pre_v}, results, opts) do
|
||||
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)
|
||||
|
||||
@ -184,7 +193,11 @@ defmodule Glicko do
|
||||
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.V2{
|
||||
rating: player_post_r,
|
||||
rating_deviation: player_post_rd,
|
||||
volatility: player_post_v
|
||||
}
|
||||
end
|
||||
|
||||
defp result_calculations(results, player_pre_r) do
|
||||
|
@ -7,27 +7,27 @@ defmodule Glicko.Player do
|
||||
Create a *v1* player with the default values for an unrated player.
|
||||
|
||||
iex> Player.new_v1
|
||||
{1.5e3, 350.0}
|
||||
%Player.V1{rating: 1.5e3, rating_deviation: 350.0}
|
||||
|
||||
Create a *v2* player with the default values for an unrated player.
|
||||
|
||||
iex> Player.new_v2
|
||||
{0.0, 2.014761872416068, 0.06}
|
||||
%Player.V2{rating: 0.0, rating_deviation: 2.014761872416068, volatility: 0.06}
|
||||
|
||||
Create a player with custom values.
|
||||
|
||||
iex> Player.new_v2([rating: 3.0, rating_deviation: 2.0, volatility: 0.05])
|
||||
{3.0, 2.0, 0.05}
|
||||
iex> Player.new_v2(rating: 3.0, rating_deviation: 2.0, volatility: 0.05)
|
||||
%Player.V2{rating: 3.0, rating_deviation: 2.0, volatility: 0.05}
|
||||
|
||||
Convert a *v2* player to a *v1*. Note this drops the volatility.
|
||||
|
||||
iex> Player.new_v2 |> Player.to_v1
|
||||
{1.5e3, 350.0}
|
||||
%Player.V1{rating: 1.5e3, rating_deviation: 350.0}
|
||||
|
||||
Convert a *v1* player to a *v2*.
|
||||
|
||||
iex> Player.new_v1 |> Player.to_v2(0.06)
|
||||
{0.0, 2.014761872416068, 0.06}
|
||||
%Player.V2{rating: 0.0, rating_deviation: 2.014761872416068, volatility: 0.06}
|
||||
|
||||
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.
|
||||
@ -38,39 +38,54 @@ defmodule Glicko.Player do
|
||||
|
||||
"""
|
||||
|
||||
defmodule V1 do
|
||||
@initial_rating 1500.0
|
||||
@initial_rating_deviation 350.0
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
rating: float(),
|
||||
rating_deviation: float()
|
||||
}
|
||||
|
||||
defstruct rating: @initial_rating,
|
||||
rating_deviation: @initial_rating_deviation
|
||||
end
|
||||
|
||||
defmodule V2 do
|
||||
@magic_version_scale 173.7178
|
||||
@magic_version_scale_rating 1500.0
|
||||
|
||||
@v1_initial_rating 1500.0
|
||||
@v1_initial_rating_deviation 350.0
|
||||
|
||||
@initial_rating (@v1_initial_rating - @magic_version_scale_rating) / @magic_version_scale
|
||||
@initial_rating_deviation @v1_initial_rating_deviation / @magic_version_scale
|
||||
@initial_volatility 0.06
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
rating: float(),
|
||||
rating_deviation: float(),
|
||||
volatility: float()
|
||||
}
|
||||
|
||||
defstruct rating: @initial_rating,
|
||||
rating_deviation: @initial_rating_deviation,
|
||||
volatility: @initial_volatility
|
||||
end
|
||||
|
||||
@magic_version_scale 173.7178
|
||||
@magic_version_scale_rating 1500.0
|
||||
|
||||
@type t :: v1 | v2
|
||||
|
||||
@type v1 :: {rating, rating_deviation}
|
||||
@type v2 :: {rating, rating_deviation, volatility}
|
||||
@type v1 :: V1.t()
|
||||
@type v2 :: V2.t()
|
||||
|
||||
@type version :: :v1 | :v2
|
||||
@type rating :: float
|
||||
@type rating_deviation :: float
|
||||
@type volatility :: float
|
||||
|
||||
@doc """
|
||||
The recommended initial rating value for a new player.
|
||||
"""
|
||||
@spec initial_rating(version) :: rating
|
||||
def initial_rating(_version = :v1), do: 1500.0
|
||||
|
||||
def initial_rating(_version = :v2) do
|
||||
:v1 |> initial_rating |> scale_rating_to(:v2)
|
||||
end
|
||||
|
||||
@doc """
|
||||
The recommended initial rating deviation value for a new player.
|
||||
"""
|
||||
@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)
|
||||
end
|
||||
|
||||
@doc """
|
||||
The recommended initial volatility value for a new player.
|
||||
"""
|
||||
@ -84,10 +99,7 @@ defmodule Glicko.Player do
|
||||
"""
|
||||
@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))
|
||||
}
|
||||
struct(V1, opts)
|
||||
end
|
||||
|
||||
@doc """
|
||||
@ -97,11 +109,7 @@ defmodule Glicko.Player do
|
||||
"""
|
||||
@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())
|
||||
}
|
||||
struct(V2, opts)
|
||||
end
|
||||
|
||||
@doc """
|
||||
@ -112,12 +120,12 @@ defmodule Glicko.Player do
|
||||
Note the volatility field used in a v2 player will be lost in the conversion.
|
||||
"""
|
||||
@spec to_v1(player :: t) :: v1
|
||||
def to_v1({rating, rating_deviation}), do: {rating, rating_deviation}
|
||||
def to_v1(%V1{} = player), do: player
|
||||
|
||||
def to_v1({rating, rating_deviation, _}) do
|
||||
{
|
||||
rating |> scale_rating_to(:v1),
|
||||
rating_deviation |> scale_rating_deviation_to(:v1)
|
||||
def to_v1(%V2{rating: rating, rating_deviation: rating_deviation}) do
|
||||
%V1{
|
||||
rating: scale_rating_to(rating, :v1),
|
||||
rating_deviation: scale_rating_deviation_to(rating_deviation, :v1)
|
||||
}
|
||||
end
|
||||
|
||||
@ -129,53 +137,52 @@ defmodule Glicko.Player do
|
||||
@spec to_v2(player :: t, volatility :: volatility) :: v2
|
||||
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_v2(%V1{rating: rating, rating_deviation: rating_deviation}, volatility) do
|
||||
%V2{
|
||||
rating: scale_rating_to(rating, :v2),
|
||||
rating_deviation: scale_rating_deviation_to(rating_deviation, :v2),
|
||||
volatility: volatility
|
||||
}
|
||||
end
|
||||
|
||||
def to_v2(%V2{} = player, _volatility) do
|
||||
player
|
||||
end
|
||||
|
||||
@doc """
|
||||
A version agnostic method for getting a player's rating.
|
||||
"""
|
||||
@spec rating(player :: t, as_version :: version | nil) :: 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
|
||||
def rating(%_{rating: rating}, nil), do: rating
|
||||
def rating(%V1{rating: rating}, :v1), do: rating
|
||||
def rating(%V2{rating: rating}, :v2), do: rating
|
||||
def rating(%V1{rating: rating}, :v2), do: rating |> scale_rating_to(:v2)
|
||||
def rating(%V2{rating: rating}, :v1), do: rating |> scale_rating_to(:v1)
|
||||
|
||||
@doc """
|
||||
A version agnostic method for getting a player's rating deviation.
|
||||
"""
|
||||
@spec rating_deviation(player :: t, as_version :: version | nil) :: 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(%V1{rating_deviation: rating_deviation}, nil), do: rating_deviation
|
||||
def rating_deviation(%V2{rating_deviation: rating_deviation}, nil), do: rating_deviation
|
||||
def rating_deviation(%V1{rating_deviation: rating_deviation}, :v1), do: rating_deviation
|
||||
def rating_deviation(%V2{rating_deviation: rating_deviation}, :v2), do: rating_deviation
|
||||
|
||||
def rating_deviation({_, rating_deviation}, :v2),
|
||||
def rating_deviation(%V1{rating_deviation: rating_deviation}, :v2),
|
||||
do: rating_deviation |> scale_rating_deviation_to(:v2)
|
||||
|
||||
def rating_deviation({_, rating_deviation, _}, :v1),
|
||||
def rating_deviation(%V2{rating_deviation: rating_deviation}, :v1),
|
||||
do: rating_deviation |> scale_rating_deviation_to(:v1)
|
||||
|
||||
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
|
||||
def volatility(%V1{}, default_volatility), do: default_volatility
|
||||
def volatility(%V2{volatility: volatility}, _), do: volatility
|
||||
|
||||
@doc """
|
||||
A convenience function for summarizing a player's strength as a 95%
|
||||
@ -192,7 +199,8 @@ defmodule Glicko.Player do
|
||||
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, as_version :: version | nil) :: {rating_low :: float, rating_high :: float}
|
||||
@spec rating_interval(player :: t, as_version :: version | nil) ::
|
||||
{rating_low :: float, rating_high :: float}
|
||||
def rating_interval(player, as_version \\ nil) do
|
||||
{
|
||||
rating(player, as_version) - rating_deviation(player, as_version) * 2,
|
||||
|
@ -6,15 +6,19 @@ defmodule Glicko.Result do
|
||||
|
||||
iex> opponent = Player.new_v2
|
||||
iex> Result.new(opponent, 1.0)
|
||||
{0.0, 2.014761872416068, 1.0}
|
||||
%Result{opponent_rating: 0.0, opponent_rating_deviation: 2.014761872416068, score: 1.0}
|
||||
iex> Result.new(opponent, :draw) # With shortcut
|
||||
{0.0, 2.014761872416068, 0.5}
|
||||
%Result{opponent_rating: 0.0, opponent_rating_deviation: 2.014761872416068, score: 0.5}
|
||||
|
||||
"""
|
||||
|
||||
alias Glicko.Player
|
||||
|
||||
@type t :: {Player.rating(), Player.rating_deviation(), score}
|
||||
@type t :: %__MODULE__{
|
||||
opponent_rating: Player.rating(),
|
||||
opponent_rating_deviation: Player.rating_deviation(),
|
||||
score: score
|
||||
}
|
||||
|
||||
@type score :: float
|
||||
@type score_shortcut :: :loss | :draw | :win
|
||||
@ -22,6 +26,8 @@ defmodule Glicko.Result do
|
||||
@score_shortcut_map %{loss: 0.0, draw: 0.5, win: 1.0}
|
||||
@score_shortcuts Map.keys(@score_shortcut_map)
|
||||
|
||||
defstruct [:opponent_rating, :opponent_rating_deviation, :score]
|
||||
|
||||
@doc """
|
||||
Creates a new result from an opponent rating, opponent rating deviation and score.
|
||||
|
||||
@ -31,12 +37,17 @@ defmodule Glicko.Result do
|
||||
"""
|
||||
@spec new(Player.rating(), Player.rating_deviation(), score | score_shortcut) :: t
|
||||
def new(opponent_rating, opponent_rating_deviation, score) when is_number(score) do
|
||||
{opponent_rating, opponent_rating_deviation, score}
|
||||
%__MODULE__{
|
||||
opponent_rating: opponent_rating,
|
||||
opponent_rating_deviation: opponent_rating_deviation,
|
||||
score: score
|
||||
}
|
||||
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)}
|
||||
score = Map.fetch!(@score_shortcut_map, score_type)
|
||||
new(opponent_rating, opponent_rating_deviation, score)
|
||||
end
|
||||
|
||||
@doc """
|
||||
@ -53,17 +64,17 @@ defmodule Glicko.Result do
|
||||
Convenience function for accessing an opponent's rating.
|
||||
"""
|
||||
@spec opponent_rating(result :: t()) :: Player.rating()
|
||||
def opponent_rating(_result = {rating, _, _}), do: rating
|
||||
def opponent_rating(%__MODULE__{} = result), do: result.opponent_rating
|
||||
|
||||
@doc """
|
||||
Convenience function for accessing an opponent's rating deviation.
|
||||
"""
|
||||
@spec opponent_rating_deviation(result :: t()) :: Player.rating_deviation()
|
||||
def opponent_rating_deviation(_result = {_, rating_deviation, _}), do: rating_deviation
|
||||
def opponent_rating_deviation(%__MODULE__{} = result), do: result.opponent_rating_deviation
|
||||
|
||||
@doc """
|
||||
Convenience function for accessing the score.
|
||||
"""
|
||||
@spec score(result :: t()) :: score
|
||||
def score(_result = {_, _, score}), do: score
|
||||
def score(%__MODULE__{} = result), do: result.score
|
||||
end
|
||||
|
@ -5,8 +5,8 @@ defmodule Glicko.PlayerTest do
|
||||
|
||||
doctest Player
|
||||
|
||||
@valid_v1_base {1.0, 2.0}
|
||||
@valid_v2_base {1.0, 2.0, 3.0}
|
||||
@valid_v1_base %Player.V1{rating: 1.0, rating_deviation: 2.0}
|
||||
@valid_v2_base %Player.V2{rating: 1.0, rating_deviation: 2.0, volatility: 3.0}
|
||||
|
||||
test "create v1" do
|
||||
assert @valid_v1_base == Player.new_v1(rating: 1.0, rating_deviation: 2.0)
|
||||
@ -17,13 +17,18 @@ defmodule Glicko.PlayerTest do
|
||||
end
|
||||
|
||||
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.new_v2(
|
||||
rating: Player.scale_rating_to(1.0, :v2),
|
||||
rating_deviation: Player.scale_rating_deviation_to(2.0, :v2),
|
||||
volatility: 3.0
|
||||
) == Player.to_v2(@valid_v1_base, 3.0)
|
||||
end
|
||||
|
||||
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.new_v1(
|
||||
rating: Player.scale_rating_to(1.0, :v1),
|
||||
rating_deviation: Player.scale_rating_deviation_to(2.0, :v1)
|
||||
) == Player.to_v1(@valid_v2_base)
|
||||
end
|
||||
|
||||
test "convert player v1 -> v1" do
|
||||
|
Loading…
Reference in New Issue
Block a user