mirror of
https://github.com/avitex/elixir-glicko
synced 2024-11-21 18:59:57 +00:00
Support using tuples
This commit is contained in:
parent
168ec234dc
commit
b6dd4c2dee
141
lib/glicko.ex
141
lib/glicko.ex
@ -6,11 +6,14 @@ defmodule Glicko do
|
|||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
|
Players can be represented by either the convenience `Glicko.Player` module or a tuple (see `player_t`).
|
||||||
|
Results can be represented by either the convenience `Glicko.Result` module or a tuple (see `result_t`).
|
||||||
|
|
||||||
Get a players new rating after a series of matches in a rating period.
|
Get a players new rating after a series of matches in a rating period.
|
||||||
|
|
||||||
iex> results = [GameResult.new(Player.new_v1([rating: 1400, rating_deviation: 30]), :win),
|
iex> results = [Result.new(Player.new_v1([rating: 1400, rating_deviation: 30]), :win),
|
||||||
...> GameResult.new(Player.new_v1([rating: 1550, rating_deviation: 100]), :loss),
|
...> Result.new(Player.new_v1([rating: 1550, rating_deviation: 100]), :loss),
|
||||||
...> GameResult.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])
|
||||||
%Glicko.Player{version: :v1, rating: 1464.0506705393013, rating_deviation: 151.51652412385727, volatility: nil}
|
%Glicko.Player{version: :v1, rating: 1464.0506705393013, rating_deviation: 151.51652412385727, volatility: nil}
|
||||||
@ -25,12 +28,24 @@ defmodule Glicko do
|
|||||||
|
|
||||||
alias __MODULE__.{
|
alias __MODULE__.{
|
||||||
Player,
|
Player,
|
||||||
GameResult,
|
Result,
|
||||||
}
|
}
|
||||||
|
|
||||||
@default_system_constant 0.8
|
@default_system_constant 0.8
|
||||||
@default_convergence_tolerance 1.0e-7
|
@default_convergence_tolerance 1.0e-7
|
||||||
|
|
||||||
|
@type version_t :: :v1 | :v2
|
||||||
|
@type rating_t :: float
|
||||||
|
@type rating_deviation_t :: float
|
||||||
|
@type volatility_t :: float
|
||||||
|
@type score_t :: float
|
||||||
|
|
||||||
|
@type player_t :: player_v1_t | player_v2_t
|
||||||
|
@type player_v1_t :: {rating :: rating_t, rating_deviation :: rating_deviation_t}
|
||||||
|
@type player_v2_t :: {rating :: rating_t, rating_deviation :: rating_deviation_t, volatility :: volatility_t}
|
||||||
|
|
||||||
|
@type result_t :: {opponent :: player_t, score :: score_t}
|
||||||
|
|
||||||
@type new_rating_opts_t :: [system_constant: float, convergence_tolerance: float]
|
@type new_rating_opts_t :: [system_constant: float, convergence_tolerance: float]
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
@ -38,50 +53,34 @@ defmodule Glicko do
|
|||||||
|
|
||||||
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(GameResult.t), opts :: new_rating_opts_t) :: Player.t
|
@spec new_rating(player :: player_t | Player.t, results :: list(result_t | Result.t), opts :: new_rating_opts_t) :: player_t | Player.t
|
||||||
def new_rating(player, results, opts \\ [])
|
def new_rating(player, results, opts \\ []) do
|
||||||
def new_rating(player = %Player{version: :v1}, results, opts) do
|
{cast_from, internal_player} = cast_player_to_internal(player)
|
||||||
player
|
internal_player = do_new_rating(internal_player, results, opts)
|
||||||
|> Player.to_v2
|
cast_internal_to_player({cast_from, internal_player})
|
||||||
|> do_new_rating(results, opts)
|
|
||||||
|> Player.to_v1
|
|
||||||
end
|
|
||||||
def new_rating(player = %Player{version: :v2}, results, opts) do
|
|
||||||
do_new_rating(player, results, opts)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
defp do_new_rating(player, [], _) do
|
defp do_new_rating({player_rating, player_rating_deviation, player_volatility}, [], _) do
|
||||||
player_post_rating_deviation =
|
player_post_rating_deviation = calc_player_pre_rating_deviation(
|
||||||
Map.new
|
:math.pow(player_rating_deviation, 2),
|
||||||
|> Map.put(:player_rating_deviation_squared, :math.pow(player.rating_deviation, 2))
|
player_volatility
|
||||||
|> calc_player_pre_rating_deviation(player.volatility)
|
)
|
||||||
|
|
||||||
%{player | rating_deviation: player_post_rating_deviation}
|
{player_rating, player_post_rating_deviation, player_volatility}
|
||||||
end
|
end
|
||||||
defp do_new_rating(player, results, opts) do
|
defp do_new_rating({player_rating, player_rating_deviation, player_volatility}, results, opts) do
|
||||||
results = Enum.map(results, fn result ->
|
|
||||||
opponent = Player.to_v2(result.opponent)
|
|
||||||
|
|
||||||
result =
|
|
||||||
Map.new
|
|
||||||
|> Map.put(:score, result.score)
|
|
||||||
|> Map.put(:opponent_rating, opponent.rating)
|
|
||||||
|> Map.put(:opponent_rating_deviation, opponent.rating_deviation)
|
|
||||||
|> Map.put(:opponent_rating_deviation_g, calc_g(opponent.rating_deviation))
|
|
||||||
|
|
||||||
Map.put(result, :e, calc_e(player.rating, result))
|
|
||||||
end)
|
|
||||||
|
|
||||||
ctx =
|
ctx =
|
||||||
Map.new
|
Map.new
|
||||||
|> Map.put(:system_constant, Keyword.get(opts, :system_constant, @default_system_constant))
|
|> Map.put(:system_constant, Keyword.get(opts, :system_constant, @default_system_constant))
|
||||||
|> Map.put(:convergence_tolerance, Keyword.get(opts, :convergence_tolerance, @default_convergence_tolerance))
|
|> Map.put(:convergence_tolerance, Keyword.get(opts, :convergence_tolerance, @default_convergence_tolerance))
|
||||||
|> Map.put(:results, results)
|
|> Map.put(:player_rating, player_rating)
|
||||||
|> Map.put(:player_rating, player.rating)
|
|> Map.put(:player_volatility, player_volatility)
|
||||||
|> Map.put(:player_volatility, player.volatility)
|
|> Map.put(:player_rating_deviation, player_rating_deviation)
|
||||||
|> Map.put(:player_rating_deviation, player.rating_deviation)
|
|> Map.put(:player_rating_deviation_squared, :math.pow(player_rating_deviation, 2))
|
||||||
|> Map.put(:player_rating_deviation_squared, :math.pow(player.rating_deviation, 2))
|
|
||||||
|
|
||||||
|
# Init
|
||||||
|
ctx = Map.put(ctx, :results, Enum.map(results, &build_internal_result(ctx, &1)))
|
||||||
|
ctx = Map.put(ctx, :results_effect, calc_results_effect(ctx))
|
||||||
# Step 3
|
# Step 3
|
||||||
ctx = Map.put(ctx, :variance_estimate, calc_variance_estimate(ctx))
|
ctx = Map.put(ctx, :variance_estimate, calc_variance_estimate(ctx))
|
||||||
# Step 4
|
# Step 4
|
||||||
@ -102,21 +101,35 @@ defmodule Glicko do
|
|||||||
# Step 5.5
|
# Step 5.5
|
||||||
ctx = Map.put(ctx, :new_player_volatility, calc_new_player_volatility(ctx))
|
ctx = Map.put(ctx, :new_player_volatility, calc_new_player_volatility(ctx))
|
||||||
# Step 6
|
# Step 6
|
||||||
ctx = Map.put(ctx, :player_pre_rating_deviation, calc_player_pre_rating_deviation(ctx, ctx.new_player_volatility))
|
ctx = Map.put(ctx, :player_pre_rating_deviation, calc_player_pre_rating_deviation(
|
||||||
|
ctx.player_rating_deviation_squared, ctx.new_player_volatility
|
||||||
|
))
|
||||||
# Step 7
|
# Step 7
|
||||||
ctx = Map.put(ctx, :new_player_rating_deviation, calc_new_player_rating_deviation(ctx))
|
ctx = Map.put(ctx, :new_player_rating_deviation, calc_new_player_rating_deviation(ctx))
|
||||||
ctx = Map.put(ctx, :new_player_rating, calc_new_player_rating(ctx))
|
ctx = Map.put(ctx, :new_player_rating, calc_new_player_rating(ctx))
|
||||||
|
|
||||||
Player.new_v2([
|
{ctx.new_player_rating, ctx.new_player_rating_deviation, ctx.new_player_volatility}
|
||||||
rating: ctx.new_player_rating,
|
end
|
||||||
rating_deviation: ctx.new_player_rating_deviation,
|
|
||||||
volatility: ctx.new_player_volatility,
|
defp build_internal_result(ctx, {opponent, score}) do
|
||||||
])
|
{_, {opponent_rating, opponent_rating_deviation, _}} = cast_player_to_internal(opponent)
|
||||||
|
|
||||||
|
result =
|
||||||
|
Map.new
|
||||||
|
|> Map.put(:score, score)
|
||||||
|
|> Map.put(:opponent_rating, opponent_rating)
|
||||||
|
|> Map.put(:opponent_rating_deviation, opponent_rating_deviation)
|
||||||
|
|> Map.put(:opponent_rating_deviation_g, calc_g(opponent_rating_deviation))
|
||||||
|
|
||||||
|
Map.put(result, :e, calc_e(ctx.player_rating, result))
|
||||||
|
end
|
||||||
|
defp build_internal_result(ctx, %Result{score: score, opponent: opponent}) do
|
||||||
|
build_internal_result(ctx, {opponent, score})
|
||||||
end
|
end
|
||||||
|
|
||||||
# Calculation of the estimated variance of the player's rating based on game outcomes
|
# Calculation of the estimated variance of the player's rating based on game outcomes
|
||||||
defp calc_variance_estimate(%{results: results}) do
|
defp calc_variance_estimate(ctx) do
|
||||||
results
|
ctx.results
|
||||||
|> Enum.reduce(0.0, fn result, acc ->
|
|> Enum.reduce(0.0, fn result, acc ->
|
||||||
acc + :math.pow(result.opponent_rating_deviation_g, 2) * result.e * (1 - result.e)
|
acc + :math.pow(result.opponent_rating_deviation_g, 2) * result.e * (1 - result.e)
|
||||||
end)
|
end)
|
||||||
@ -124,7 +137,7 @@ defmodule Glicko do
|
|||||||
end
|
end
|
||||||
|
|
||||||
defp calc_delta(ctx) do
|
defp calc_delta(ctx) do
|
||||||
calc_results_effect(ctx) * ctx.variance_estimate
|
ctx.results_effect * ctx.variance_estimate
|
||||||
end
|
end
|
||||||
|
|
||||||
defp calc_f(ctx, x) do
|
defp calc_f(ctx, x) do
|
||||||
@ -142,22 +155,22 @@ defmodule Glicko do
|
|||||||
:math.exp(a / 2)
|
:math.exp(a / 2)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp calc_results_effect(%{results: results}) do
|
defp calc_results_effect(ctx) do
|
||||||
Enum.reduce(results, 0.0, fn result, acc ->
|
Enum.reduce(ctx.results, 0.0, fn result, acc ->
|
||||||
acc + result.opponent_rating_deviation_g * (result.score - result.e)
|
acc + result.opponent_rating_deviation_g * (result.score - result.e)
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp calc_new_player_rating(ctx) do
|
defp calc_new_player_rating(ctx) do
|
||||||
ctx.player_rating + :math.pow(ctx.new_player_rating_deviation, 2) * calc_results_effect(ctx)
|
ctx.player_rating + :math.pow(ctx.new_player_rating_deviation, 2) * ctx.results_effect
|
||||||
end
|
end
|
||||||
|
|
||||||
defp calc_new_player_rating_deviation(ctx) do
|
defp calc_new_player_rating_deviation(ctx) do
|
||||||
1 / :math.sqrt(1 / :math.pow(ctx.player_pre_rating_deviation, 2) + 1 / ctx.variance_estimate)
|
1 / :math.sqrt(1 / :math.pow(ctx.player_pre_rating_deviation, 2) + 1 / ctx.variance_estimate)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp calc_player_pre_rating_deviation(ctx, player_volatility) do
|
defp calc_player_pre_rating_deviation(player_rating_deviation_squared, player_volatility) do
|
||||||
:math.sqrt((:math.pow(player_volatility, 2) + ctx.player_rating_deviation_squared))
|
:math.sqrt((:math.pow(player_volatility, 2) + player_rating_deviation_squared))
|
||||||
end
|
end
|
||||||
|
|
||||||
defp iterative_algorithm_initial(ctx) do
|
defp iterative_algorithm_initial(ctx) do
|
||||||
@ -205,4 +218,26 @@ defmodule Glicko do
|
|||||||
defp calc_e(player_rating, result) do
|
defp calc_e(player_rating, result) do
|
||||||
1 / (1 + :math.exp(-1 * result.opponent_rating_deviation_g * (player_rating - result.opponent_rating)))
|
1 / (1 + :math.exp(-1 * result.opponent_rating_deviation_g * (player_rating - result.opponent_rating)))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp cast_player_to_internal(player) when is_tuple(player) and tuple_size(player) == 2 do
|
||||||
|
{:v1, Player.new_v1(player) |> Player.to_v2 |> cast_player_to_internal |> elem(1)}
|
||||||
|
end
|
||||||
|
defp cast_player_to_internal(player) when is_tuple(player) and tuple_size(player) == 3 do
|
||||||
|
{:v2, player}
|
||||||
|
end
|
||||||
|
defp cast_player_to_internal(player = %Player{version: :v1}) do
|
||||||
|
{:player_v1, player |> Player.to_v2 |> cast_player_to_internal |> elem(1)}
|
||||||
|
end
|
||||||
|
defp cast_player_to_internal(player = %Player{version: :v2}) do
|
||||||
|
{:player_v2, {player.rating, player.rating_deviation, player.volatility}}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp cast_internal_to_player({:v1, {rating, rating_deviation, _}}), do: {
|
||||||
|
rating |> Player.scale_rating_to(:v1),
|
||||||
|
rating_deviation |> Player.scale_rating_deviation_to(:v1),
|
||||||
|
}
|
||||||
|
defp cast_internal_to_player({:v2, player}), do: player
|
||||||
|
defp cast_internal_to_player({:player_v1, player}), do: Player.new_v2(player) |> Player.to_v1
|
||||||
|
defp cast_internal_to_player({:player_v2, player}), do: Player.new_v2(player)
|
||||||
|
|
||||||
end
|
end
|
||||||
|
@ -1,41 +0,0 @@
|
|||||||
defmodule Glicko.GameResult do
|
|
||||||
@moduledoc """
|
|
||||||
Provides a representation of a game result against an opponent.
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
iex> opponent = Player.new_v2
|
|
||||||
iex> GameResult.new(opponent, 0.0)
|
|
||||||
%GameResult{score: 0.0, opponent: %Player{version: :v2, rating: 0.0, rating_deviation: 2.014761872416068, volatility: 0.06}}
|
|
||||||
iex> GameResult.new(opponent, :win) # With shortcut
|
|
||||||
%GameResult{score: 1.0, opponent: %Player{version: :v2, rating: 0.0, rating_deviation: 2.014761872416068, volatility: 0.06}}
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
alias Glicko.Player
|
|
||||||
|
|
||||||
defstruct [
|
|
||||||
:score,
|
|
||||||
:opponent,
|
|
||||||
]
|
|
||||||
|
|
||||||
@type t :: %__MODULE__{score: float, opponent: Player.t}
|
|
||||||
|
|
||||||
@type result_type_t :: :loss | :draw | :win
|
|
||||||
|
|
||||||
@result_type_map %{loss: 0.0, draw: 0.5, win: 1.0}
|
|
||||||
|
|
||||||
@doc """
|
|
||||||
Creates a new GameResult against an opponent.
|
|
||||||
|
|
||||||
Supports passing either `:loss`, `:draw`, or `:win` as shortcuts.
|
|
||||||
"""
|
|
||||||
@spec new(opponent :: Player.t, result_type_t | float) :: t
|
|
||||||
def new(opponent, result_type) when is_atom(result_type) and result_type in [:loss, :draw, :win] do
|
|
||||||
new(opponent, Map.fetch!(@result_type_map, result_type))
|
|
||||||
end
|
|
||||||
def new(opponent, score) when is_number(score), do: %__MODULE__{
|
|
||||||
score: score,
|
|
||||||
opponent: opponent,
|
|
||||||
}
|
|
||||||
end
|
|
@ -36,15 +36,21 @@ defmodule Glicko.Player do
|
|||||||
@magic_version_scale 173.7178
|
@magic_version_scale 173.7178
|
||||||
@magic_version_scale_rating 1500.0
|
@magic_version_scale_rating 1500.0
|
||||||
|
|
||||||
@default_v1_rating 1500.0
|
|
||||||
@default_v1_rating_deviation 350.0
|
|
||||||
|
|
||||||
@default_v2_volatility 0.06
|
|
||||||
|
|
||||||
@type t :: v1_t | v2_t
|
@type t :: v1_t | v2_t
|
||||||
|
|
||||||
@type v1_t :: %__MODULE__{version: :v1, rating: float, rating_deviation: float, volatility: nil}
|
@type v1_t :: %__MODULE__{
|
||||||
@type v2_t :: %__MODULE__{version: :v2, rating: float, rating_deviation: float, volatility: float}
|
version: :v1,
|
||||||
|
rating: Glicko.rating_t,
|
||||||
|
rating_deviation: Glicko.rating_deviation_t,
|
||||||
|
volatility: nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
@type v2_t :: %__MODULE__{
|
||||||
|
version: :v2,
|
||||||
|
rating: Glicko.rating_t,
|
||||||
|
rating_deviation: Glicko.rating_deviation_t,
|
||||||
|
volatility: Glicko.volatility_t,
|
||||||
|
}
|
||||||
|
|
||||||
defstruct [
|
defstruct [
|
||||||
:version,
|
:version,
|
||||||
@ -53,31 +59,68 @@ defmodule Glicko.Player do
|
|||||||
:volatility,
|
:volatility,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
The recommended initial rating value for a new player.
|
||||||
|
"""
|
||||||
|
@spec initial_rating(Glicko.version_t) :: Glicko.rating_t
|
||||||
|
def initial_rating(:v1), do: 1500.0
|
||||||
|
def initial_rating(:v2), do: initial_rating(:v1) |> scale_rating_to(:v2)
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
The recommended initial rating deviation value for a new player.
|
||||||
|
"""
|
||||||
|
@spec initial_rating_deviation(Glicko.version_t) :: Glicko.rating_deviation_t
|
||||||
|
def initial_rating_deviation(:v1), do: 350.0
|
||||||
|
def initial_rating_deviation(:v2), do: initial_rating_deviation(:v1) |> scale_rating_deviation_to(:v2)
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
The recommended initial volatility value for a new player.
|
||||||
|
"""
|
||||||
|
@spec initial_v2_volatility :: Glicko.volatility_t
|
||||||
|
def initial_v2_volatility, do: 0.06
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Creates a new v1 player.
|
Creates a new v1 player.
|
||||||
|
|
||||||
If not overriden, will use default values for an unrated player.
|
If not overriden, will use the default values for an unrated player.
|
||||||
"""
|
"""
|
||||||
@spec new_v1([rating: float, rating_deviation: float]) :: v1_t
|
@spec new_v1(
|
||||||
def new_v1(opts \\ []), do: %__MODULE__{
|
{Glicko.rating_t, Glicko.rating_deviation_t} |
|
||||||
|
[rating: Glicko.rating_t, rating_deviation: Glicko.rating_deviation_t]
|
||||||
|
) :: v1_t
|
||||||
|
def new_v1(opts \\ [])
|
||||||
|
def new_v1({rating, rating_deviation}), do: %__MODULE__{
|
||||||
version: :v1,
|
version: :v1,
|
||||||
rating: Keyword.get(opts, :rating, @default_v1_rating),
|
rating: rating,
|
||||||
rating_deviation: Keyword.get(opts, :rating_deviation, @default_v1_rating_deviation),
|
rating_deviation: rating_deviation,
|
||||||
volatility: nil,
|
volatility: nil,
|
||||||
}
|
}
|
||||||
|
def new_v1(opts), do: new_v1({
|
||||||
|
Keyword.get(opts, :rating, initial_rating(:v1)),
|
||||||
|
Keyword.get(opts, :rating_deviation, initial_rating_deviation(:v1)),
|
||||||
|
})
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Creates a new v2 player.
|
Creates a new v2 player.
|
||||||
|
|
||||||
If not overriden, will use default values for an unrated player.
|
If not overriden, will use default values for an unrated player.
|
||||||
"""
|
"""
|
||||||
@spec new_v2([rating: float, rating_deviation: float, volatility: float]) :: v2_t
|
@spec new_v2(
|
||||||
def new_v2(opts \\ []), do: %__MODULE__{
|
{Glicko.rating_t, Glicko.rating_deviation_t, Glicko.volatility_t} |
|
||||||
|
[rating: Glicko.rating_t, rating_deviation: Glicko.rating_deviation_t, volatility: Glicko.volatility_t]
|
||||||
|
) :: v2_t
|
||||||
|
def new_v2(opts \\ [])
|
||||||
|
def new_v2({rating, rating_deviation, volatility}), do: %__MODULE__{
|
||||||
version: :v2,
|
version: :v2,
|
||||||
rating: Keyword.get(opts, :rating, @default_v1_rating |> scale_rating_to(:v2)),
|
rating: rating,
|
||||||
rating_deviation: Keyword.get(opts, :rating_deviation, @default_v1_rating_deviation |> scale_rating_deviation_to(:v2)),
|
rating_deviation: rating_deviation,
|
||||||
volatility: Keyword.get(opts, :volatility, @default_v2_volatility),
|
volatility: volatility,
|
||||||
}
|
}
|
||||||
|
def new_v2(opts), do: new_v2({
|
||||||
|
Keyword.get(opts, :rating, initial_rating(:v2)),
|
||||||
|
Keyword.get(opts, :rating_deviation, initial_rating_deviation(:v2)),
|
||||||
|
Keyword.get(opts, :volatility, initial_v2_volatility()),
|
||||||
|
})
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Converts a v2 player to a v1.
|
Converts a v2 player to a v1.
|
||||||
@ -99,7 +142,7 @@ defmodule Glicko.Player do
|
|||||||
A v2 player will pass-through unchanged with the volatility arg ignored.
|
A v2 player will pass-through unchanged with the volatility arg ignored.
|
||||||
"""
|
"""
|
||||||
@spec to_v2(player :: t, volatility :: float) :: v2_t
|
@spec to_v2(player :: t, volatility :: float) :: v2_t
|
||||||
def to_v2(player, volatility \\ @default_v2_volatility)
|
def to_v2(player, volatility \\ initial_v2_volatility())
|
||||||
def to_v2(player = %__MODULE__{version: :v2}, _volatility), do: player
|
def to_v2(player = %__MODULE__{version: :v2}, _volatility), do: player
|
||||||
def to_v2(player = %__MODULE__{version: :v1}, volatility), do: new_v2([
|
def to_v2(player = %__MODULE__{version: :v1}, volatility), do: new_v2([
|
||||||
rating: player.rating |> scale_rating_to(:v2),
|
rating: player.rating |> scale_rating_to(:v2),
|
||||||
@ -131,14 +174,15 @@ defmodule Glicko.Player do
|
|||||||
@doc """
|
@doc """
|
||||||
Scales a players rating.
|
Scales a players rating.
|
||||||
"""
|
"""
|
||||||
@spec scale_rating_to(rating :: float, to_version :: :v1 | :v2) :: float
|
@spec scale_rating_to(rating :: Glicko.rating_t, to_version :: Glicko.version_t) :: Glicko.rating_t
|
||||||
def scale_rating_to(rating, :v1), do: (rating * @magic_version_scale) + @magic_version_scale_rating
|
def scale_rating_to(rating, :v1), do: (rating * @magic_version_scale) + @magic_version_scale_rating
|
||||||
def scale_rating_to(rating, :v2), do: (rating - @magic_version_scale_rating) / @magic_version_scale
|
def scale_rating_to(rating, :v2), do: (rating - @magic_version_scale_rating) / @magic_version_scale
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Scales a players rating deviation.
|
Scales a players rating deviation.
|
||||||
"""
|
"""
|
||||||
@spec scale_rating_deviation_to(rating_deviation :: float, to_version :: :v1 | :v2) :: float
|
@spec scale_rating_deviation_to(rating_deviation :: Glicko.rating_deviation_t, to_version :: Glicko.version_t) :: Glicko.rating_deviation_t
|
||||||
def scale_rating_deviation_to(rating_deviation, :v1), do: rating_deviation * @magic_version_scale
|
def scale_rating_deviation_to(rating_deviation, :v1), do: rating_deviation * @magic_version_scale
|
||||||
def scale_rating_deviation_to(rating_deviation, :v2), do: rating_deviation / @magic_version_scale
|
def scale_rating_deviation_to(rating_deviation, :v2), do: rating_deviation / @magic_version_scale
|
||||||
|
|
||||||
end
|
end
|
||||||
|
41
lib/glicko/result.ex
Normal file
41
lib/glicko/result.ex
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
defmodule Glicko.Result do
|
||||||
|
@moduledoc """
|
||||||
|
A convenience wrapper representing a result against an opponent.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
iex> opponent = Player.new_v2
|
||||||
|
iex> Result.new(opponent, 0.0)
|
||||||
|
%Result{score: 0.0, opponent: %Player{version: :v2, rating: 0.0, rating_deviation: 2.014761872416068, volatility: 0.06}}
|
||||||
|
iex> Result.new(opponent, :win) # With shortcut
|
||||||
|
%Result{score: 1.0, opponent: %Player{version: :v2, rating: 0.0, rating_deviation: 2.014761872416068, volatility: 0.06}}
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
alias Glicko.Player
|
||||||
|
|
||||||
|
defstruct [
|
||||||
|
:score,
|
||||||
|
:opponent,
|
||||||
|
]
|
||||||
|
|
||||||
|
@type t :: %__MODULE__{score: Glicko.score_t, opponent: Glicko.player_t | Player.t}
|
||||||
|
|
||||||
|
@type result_type_t :: :loss | :draw | :win
|
||||||
|
|
||||||
|
@result_type_map %{loss: 0.0, draw: 0.5, win: 1.0}
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Creates a new Result against an opponent.
|
||||||
|
|
||||||
|
Supports passing either `:loss`, `:draw`, or `:win` as shortcuts.
|
||||||
|
"""
|
||||||
|
@spec new(opponent :: Glicko.player_t | Player.t, result_type_t | float) :: t
|
||||||
|
def new(opponent, result_type) when is_atom(result_type) and result_type in [:loss, :draw, :win] do
|
||||||
|
new(opponent, Map.fetch!(@result_type_map, result_type))
|
||||||
|
end
|
||||||
|
def new(opponent, score) when is_number(score), do: %__MODULE__{
|
||||||
|
score: score,
|
||||||
|
opponent: opponent,
|
||||||
|
}
|
||||||
|
end
|
@ -1,22 +0,0 @@
|
|||||||
defmodule Glicko.GameResultTest do
|
|
||||||
use ExUnit.Case
|
|
||||||
|
|
||||||
alias Glicko.{
|
|
||||||
Player,
|
|
||||||
GameResult,
|
|
||||||
}
|
|
||||||
|
|
||||||
doctest GameResult
|
|
||||||
|
|
||||||
@opponent Player.new_v2
|
|
||||||
|
|
||||||
@valid_game_result %GameResult{opponent: @opponent, score: 0.0}
|
|
||||||
|
|
||||||
test "create game result" do
|
|
||||||
assert @valid_game_result == GameResult.new(@opponent, 0.0)
|
|
||||||
end
|
|
||||||
|
|
||||||
test "create game result with shortcut" do
|
|
||||||
assert @valid_game_result == GameResult.new(@opponent, :loss)
|
|
||||||
end
|
|
||||||
end
|
|
@ -3,7 +3,7 @@ defmodule GlickoTest do
|
|||||||
|
|
||||||
alias Glicko.{
|
alias Glicko.{
|
||||||
Player,
|
Player,
|
||||||
GameResult,
|
Result,
|
||||||
}
|
}
|
||||||
|
|
||||||
doctest Glicko
|
doctest Glicko
|
||||||
@ -11,9 +11,9 @@ defmodule GlickoTest do
|
|||||||
@player Player.new_v1([rating: 1500, rating_deviation: 200]) |> Player.to_v2
|
@player Player.new_v1([rating: 1500, rating_deviation: 200]) |> Player.to_v2
|
||||||
|
|
||||||
@results [
|
@results [
|
||||||
GameResult.new(Player.new_v1([rating: 1400, rating_deviation: 30]), :win),
|
{{1400, 30}, 1.0},
|
||||||
GameResult.new(Player.new_v1([rating: 1550, rating_deviation: 100]), :loss),
|
Result.new({1550, 100}, :loss),
|
||||||
GameResult.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)
|
||||||
|
22
test/result_test.exs
Normal file
22
test/result_test.exs
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
defmodule Glicko.ResultTest do
|
||||||
|
use ExUnit.Case
|
||||||
|
|
||||||
|
alias Glicko.{
|
||||||
|
Player,
|
||||||
|
Result,
|
||||||
|
}
|
||||||
|
|
||||||
|
doctest Result
|
||||||
|
|
||||||
|
@opponent Player.new_v2
|
||||||
|
|
||||||
|
@valid_game_result %Result{opponent: @opponent, score: 0.0}
|
||||||
|
|
||||||
|
test "create game result" do
|
||||||
|
assert @valid_game_result == Result.new(@opponent, 0.0)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "create game result with shortcut" do
|
||||||
|
assert @valid_game_result == Result.new(@opponent, :loss)
|
||||||
|
end
|
||||||
|
end
|
Loading…
Reference in New Issue
Block a user