Initial commit

This commit is contained in:
avitex 2017-11-15 20:32:39 +11:00
commit 94e637f9bd
8 changed files with 293 additions and 0 deletions

22
.gitignore vendored Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View File

@ -0,0 +1 @@
ExUnit.start()