defmodule Glicko.Player do @moduledoc """ A convenience wrapper that handles conversions between glicko versions one and two. ## Usage Create a player with the default values for an unrated player. iex> Player.new_v2 %Player{version: :v2, rating: 0.0, rating_deviation: 2.014761872416068, volatility: 0.06} Create a player with custom values. iex> Player.new_v2([rating: 1500, rating_deviation: 50, volatility: 0.05]) %Player{version: :v2, rating: 1500, rating_deviation: 50, volatility: 0.05} Convert a *v2* player to a *v1*. Note this drops the volatility. iex> Player.new_v2 |> Player.to_v1 %Player{version: :v1, rating: 1.5e3, rating_deviation: 350.0, volatility: nil} Convert a *v1* player to a *v2*. iex> Player.new_v1 |> Player.to_v2(0.06) %Player{version: :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. iex> player_v2 = Player.new_v2 iex> player_v2 == Player.to_v2(player_v2) true """ @magic_version_scale 173.7178 @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 v1_t :: %__MODULE__{version: :v1, rating: float, rating_deviation: float, volatility: nil} @type v2_t :: %__MODULE__{version: :v2, rating: float, rating_deviation: float, volatility: float} defstruct [ :version, :rating, :rating_deviation, :volatility, ] @doc """ Creates a new v1 player. If not overriden, will use default values for an unrated player. """ @spec new_v1([rating: float, rating_deviation: float]) :: v1_t def new_v1(opts \\ []), do: %__MODULE__{ version: :v1, rating: Keyword.get(opts, :rating, @default_v1_rating), rating_deviation: Keyword.get(opts, :rating_deviation, @default_v1_rating_deviation), volatility: nil, } @doc """ Creates a new v2 player. If not overriden, will use default values for an unrated player. """ @spec new_v2([rating: float, rating_deviation: float, volatility: float]) :: v2_t def new_v2(opts \\ []), do: %__MODULE__{ version: :v2, rating: Keyword.get(opts, :rating, @default_v1_rating |> scale_rating_to(:v2)), rating_deviation: Keyword.get(opts, :rating_deviation, @default_v1_rating_deviation |> scale_rating_deviation_to(:v2)), volatility: Keyword.get(opts, :volatility, @default_v2_volatility), } @doc """ Converts a v2 player to a v1. A v1 player will pass-through unchanged. Note the volatility field used in a v2 player will be lost in the conversion. """ @spec to_v1(player :: t) :: v1_t def to_v1(player = %__MODULE__{version: :v1}), do: player def to_v1(player = %__MODULE__{version: :v2}), do: new_v1([ rating: player.rating |> scale_rating_to(:v1), rating_deviation: player.rating_deviation |> scale_rating_deviation_to(:v1), ]) @doc """ Converts a v1 player to a v2. A v2 player will pass-through unchanged with the volatility arg ignored. """ @spec to_v2(player :: t, volatility :: float) :: v2_t def to_v2(player, volatility \\ @default_v2_volatility) def to_v2(player = %__MODULE__{version: :v2}, _volatility), do: player def to_v2(player = %__MODULE__{version: :v1}, volatility), do: new_v2([ rating: player.rating |> scale_rating_to(:v2), rating_deviation: player.rating_deviation |> scale_rating_deviation_to(:v2), volatility: volatility, ]) @doc """ Scales a players rating. """ @spec scale_rating_to(rating :: float, to_version :: :v1 | :v2) :: float 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 @doc """ Scales a players rating deviation. """ @spec scale_rating_deviation_to(rating_deviation :: float, to_version :: :v1 | :v2) :: float 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 end