mirror of
https://github.com/avitex/elixir-glicko
synced 2024-12-21 16:09:57 +00:00
Initial commit
This commit is contained in:
commit
94e637f9bd
22
.gitignore
vendored
Normal file
22
.gitignore
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
# The directory Mix will write compiled artifacts to.
|
||||
/_build/
|
||||
|
||||
# If you run "mix test --cover", coverage assets end up here.
|
||||
/cover/
|
||||
|
||||
# The directory Mix downloads your dependencies sources to.
|
||||
/deps/
|
||||
|
||||
# Where 3rd-party dependencies like ExDoc output generated docs.
|
||||
/doc/
|
||||
|
||||
# Ignore .fetch files in case you like to edit your project deps locally.
|
||||
/.fetch
|
||||
|
||||
# If the VM crashes, it generates a dump, let's ignore it too.
|
||||
erl_crash.dump
|
||||
|
||||
# Also ignore archive artifacts (built via "mix archive.build").
|
||||
*.ez
|
||||
|
||||
.editorconfig
|
41
lib/glicko/game_result.ex
Normal file
41
lib/glicko/game_result.ex
Normal file
@ -0,0 +1,41 @@
|
||||
defmodule Glicko.GameResult do
|
||||
@moduledoc """
|
||||
This module 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
|
123
lib/glicko/player.ex
Normal file
123
lib/glicko/player.ex
Normal file
@ -0,0 +1,123 @@
|
||||
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
|
20
mix.exs
Normal file
20
mix.exs
Normal file
@ -0,0 +1,20 @@
|
||||
defmodule Glicko.Mixfile do
|
||||
use Mix.Project
|
||||
|
||||
def project, do: [
|
||||
app: :glicko,
|
||||
version: "0.1.0",
|
||||
elixir: "~> 1.5",
|
||||
start_permanent: Mix.env == :prod,
|
||||
deps: deps(),
|
||||
]
|
||||
|
||||
def application, do: [
|
||||
extra_applications: [:logger],
|
||||
]
|
||||
|
||||
defp deps, do: [
|
||||
{:ex_doc, "~> 0.16", only: :dev, runtime: false},
|
||||
{:credo, "~> 0.8", only: [:dev, :test], runtime: false},
|
||||
]
|
||||
end
|
4
mix.lock
Normal file
4
mix.lock
Normal file
@ -0,0 +1,4 @@
|
||||
%{"bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [], [], "hexpm"},
|
||||
"credo": {:hex, :credo, "0.8.8", "990e7844a8d06ebacd88744a55853a83b74270b8a8461c55a4d0334b8e1736c9", [], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}], "hexpm"},
|
||||
"earmark": {:hex, :earmark, "1.2.3", "206eb2e2ac1a794aa5256f3982de7a76bf4579ff91cb28d0e17ea2c9491e46a4", [], [], "hexpm"},
|
||||
"ex_doc": {:hex, :ex_doc, "0.18.1", "37c69d2ef62f24928c1f4fdc7c724ea04aecfdf500c4329185f8e3649c915baf", [], [{:earmark, "~> 1.1", [hex: :earmark, repo: "hexpm", optional: false]}], "hexpm"}}
|
22
test/game_result_test.exs
Normal file
22
test/game_result_test.exs
Normal file
@ -0,0 +1,22 @@
|
||||
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
|
60
test/player_test.exs
Normal file
60
test/player_test.exs
Normal file
@ -0,0 +1,60 @@
|
||||
defmodule Glicko.PlayerTest do
|
||||
use ExUnit.Case
|
||||
|
||||
alias Glicko.Player
|
||||
|
||||
doctest Player
|
||||
|
||||
@valid_v1_base %Player{version: :v1, rating: 1.0, rating_deviation: 2.0, volatility: nil}
|
||||
@valid_v2_base %Player{version: :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])
|
||||
end
|
||||
|
||||
test "create v2" do
|
||||
assert @valid_v2_base == Player.new_v2([rating: 1.0, rating_deviation: 2.0, volatility: 3.0])
|
||||
end
|
||||
|
||||
test "convert player v1 -> v2" do
|
||||
assert %Player{
|
||||
version: :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{
|
||||
version: :v1,
|
||||
rating: Player.scale_rating_to(1.0, :v1),
|
||||
rating_deviation: Player.scale_rating_deviation_to(2.0, :v1),
|
||||
volatility: nil,
|
||||
} == Player.to_v1(@valid_v2_base)
|
||||
end
|
||||
|
||||
test "convert player v1 -> v1" do
|
||||
assert @valid_v1_base == Player.to_v1(@valid_v1_base)
|
||||
end
|
||||
|
||||
test "convert player v2 -> v2" do
|
||||
assert @valid_v2_base == Player.to_v2(@valid_v2_base)
|
||||
end
|
||||
|
||||
test "scale rating v1 -> v2" do
|
||||
assert_in_delta Player.scale_rating_to(1673.7178, :v2), 1.0, 0.1
|
||||
end
|
||||
|
||||
test "scale rating v2 -> v1" do
|
||||
assert_in_delta Player.scale_rating_to(1.0, :v1), 1673.7178, 0.1
|
||||
end
|
||||
|
||||
test "scale rating deviation v1 -> v2" do
|
||||
assert_in_delta Player.scale_rating_deviation_to(173.7178, :v2), 1.0, 0.1
|
||||
end
|
||||
|
||||
test "scale rating deviation v2 -> v1" do
|
||||
assert_in_delta Player.scale_rating_deviation_to(1.0, :v1), 173.7178, 0.1
|
||||
end
|
||||
end
|
1
test/test_helper.exs
Normal file
1
test/test_helper.exs
Normal file
@ -0,0 +1 @@
|
||||
ExUnit.start()
|
Loading…
Reference in New Issue
Block a user