Skip to content

Commit

Permalink
Add new rating system setup task
Browse files Browse the repository at this point in the history
  • Loading branch information
jauggy committed Dec 31, 2024
1 parent f4e1556 commit 92a9a63
Show file tree
Hide file tree
Showing 7 changed files with 277 additions and 7 deletions.
26 changes: 22 additions & 4 deletions lib/teiserver/battle/libs/balance_lib.ex
Original file line number Diff line number Diff line change
Expand Up @@ -583,8 +583,8 @@ defmodule Teiserver.Battle.BalanceLib do
captain.member_id
end

@spec default_rating :: List.t()
@spec default_rating(non_neg_integer()) :: List.t()
@spec default_rating :: any()
@spec default_rating(non_neg_integer()) :: any()
def default_rating(rating_type_id \\ nil) do
{skill, uncertainty} = Openskill.rating()
rating_value = calculate_rating_value(skill, uncertainty)
Expand All @@ -595,7 +595,8 @@ defmodule Teiserver.Battle.BalanceLib do
skill: skill,
uncertainty: uncertainty,
rating_value: rating_value,
leaderboard_rating: leaderboard_rating
leaderboard_rating: leaderboard_rating,
num_matches: 0
}
end

Expand Down Expand Up @@ -704,7 +705,24 @@ defmodule Teiserver.Battle.BalanceLib do

@spec calculate_rating_value(float(), float()) :: float()
def calculate_rating_value(skill, uncertainty) do
max(skill - uncertainty, 0)
calculate_rating_value(skill, uncertainty, 0)
end

@spec calculate_rating_value(float(), float(), integer()) :: float()
def calculate_rating_value(skill, uncertainty, num_matches) do
config = Config.get_site_config_cache("hidden.Rating method")

if config == "start at zero; converge to skill" do
num_matches_target = get_num_matches_for_rating_to_equal_skill()

min(1, num_matches / num_matches_target) * skill
else
max(skill - uncertainty, 0)
end
end

def get_num_matches_for_rating_to_equal_skill() do
Config.get_site_config_cache("profile.Num matches for rating to equal skill")
end

@doc """
Expand Down
5 changes: 4 additions & 1 deletion lib/teiserver/game/libs/match_rating_lib.ex
Original file line number Diff line number Diff line change
Expand Up @@ -467,7 +467,10 @@ defmodule Teiserver.Game.MatchRatingLib do

rating_type_id = user_rating.rating_type_id
{new_skill, new_uncertainty} = rating_update
new_rating_value = BalanceLib.calculate_rating_value(new_skill, new_uncertainty)

new_rating_value =
BalanceLib.calculate_rating_value(new_skill, new_uncertainty, new_num_matches)

new_leaderboard_rating = BalanceLib.calculate_leaderboard_rating(new_skill, new_uncertainty)

Account.update_rating(user_rating, %{
Expand Down
24 changes: 24 additions & 0 deletions lib/teiserver/libs/teiserver_configs.ex
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ defmodule Teiserver.TeiserverConfigs do
lobby_configs()
debugging_configs()
profile_configs()
hidden_configs()
end

@spec site_configs :: any
Expand Down Expand Up @@ -578,6 +579,15 @@ defmodule Teiserver.TeiserverConfigs do
opts: [choices: ["Leaderboard rating", "Rating value", "Playtime", "Role"]]
})

add_site_config_type(%{
key: "profile.Num matches for rating to equal skill",
section: "Profiles",
type: "integer",
default: 30,
permissions: ["Admin"],
description: "The minimum number of matches required for match rating to equal skill"
})

add_site_config_type(%{
key: "user.Enable one time links",
section: "User permissions",
Expand Down Expand Up @@ -633,4 +643,18 @@ defmodule Teiserver.TeiserverConfigs do
value_label: "Light mode as default"
})
end

# These configs can be saved to the database but will not be editable on the Admin page
@spec hidden_configs() :: :ok
defp hidden_configs() do
add_site_config_type(%{
key: "hidden.Rating method",
section: "Hidden",
type: "select",
default: "skill minus uncertainty",
permissions: ["Admin"],
description: "The rating system. Use Mix.Tasks.Teiserver.ProvisionalRatingSetup to change.",
opts: [choices: ["skill minus uncertainty", "start at zero; converge to skill"]]
})
end
end
112 changes: 112 additions & 0 deletions lib/teiserver/mix_tasks/provisional_rating_setup.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
defmodule Mix.Tasks.Teiserver.ProvisionalRatingSetup do
@moduledoc """
Running this task will change the rating system to one where new players start at zero,
then converge towards their skill over time.
The rating formula:
least(1, num_matches/30) * skill
30 is the target number of matches for your rating to just equal to skill.
This number is adjustable on the admin site config page.
To use this new rating system run:
mix teiserver.provisional_rating_setup
To rollback use:
mix teiserver.provisional_rating_setup -rollback
"""

use Mix.Task
require Logger
alias Teiserver.Config
alias Teiserver.Battle.BalanceLib

def run(args) do
Application.ensure_all_started(:teiserver)

rollback? = Enum.member?(args, "-rollback")
target_matches = BalanceLib.get_num_matches_for_rating_to_equal_skill()

sql_transaction_result =
Ecto.Multi.new()
|> Ecto.Multi.run(:create_temp_table, fn repo, _ ->
{query, params} =
case rollback? do
false ->
{"""
CREATE temp table temp_table as
SELECT
*,
least(1, cast(num_matches as decimal) / $1) * skill as new_rating
FROM
teiserver_account_ratings tar;
""", [target_matches]}

true ->
{"""
CREATE temp table temp_table as
SELECT
*,
greatest(0,skill-uncertainty ) as new_rating
FROM
teiserver_account_ratings tar;
""", []}
end

Ecto.Adapters.SQL.query(repo, query, params)
end)
|> Ecto.Multi.run(:add_logs, fn repo, _ ->
query = """
INSERT INTO teiserver_game_rating_logs (inserted_at, rating_type_id, user_id, value)
SELECT
now(),
rating_type_id,
user_id,
JSON_BUILD_OBJECT(
'skill', skill,
'reason', 'New rating system',
'uncertainty', uncertainty,
'rating_value', new_rating,
'skill_change', 0.0,
'uncertainty_change', 0.0,
'rating_value_change', t.new_rating - t.rating_value,
'num_matches', num_matches
)
FROM temp_table t;
"""

Ecto.Adapters.SQL.query(repo, query, [])
end)
|> Ecto.Multi.run(:update_ratings, fn repo, _ ->
query = """
UPDATE teiserver_account_ratings tar
SET
rating_value = t.new_rating,
last_updated = now()
FROM temp_table t
WHERE t.user_id = tar.user_id
and t.rating_type_id = tar.rating_type_id;
"""

Ecto.Adapters.SQL.query(repo, query, [])
end)
|> Teiserver.Repo.transaction()

with {:ok, _result} <- sql_transaction_result do
case rollback? do
true ->
Logger.info("Rollback to old rating system complete")

# This config is not viewable in the admin page as we don't want someone to manually change it.
# To change it use this mix task
Config.update_site_config("hidden.Rating method", "skill minus uncertainty")

false ->
Logger.info("New rating system change complete")
Config.update_site_config("hidden.Rating method", "start at zero; converge to skill")
end
else
_ ->
Logger.error("Task failed.")
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,14 @@ defmodule TeiserverWeb.Admin.SiteConfigController do

@spec index(Plug.Conn.t(), any) :: Plug.Conn.t()
def index(conn, _params) do
site_configs = Config.get_grouped_site_configs()
site_configs =
Config.get_grouped_site_configs()
|> Enum.filter(fn x ->
case x do
{"Hidden", _} -> false
_ -> true
end
end)

conn
|> assign(:site_configs, site_configs)
Expand Down
8 changes: 7 additions & 1 deletion lib/teiserver_web/controllers/admin/user_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -514,7 +514,13 @@ defmodule TeiserverWeb.Admin.UserController do
user_rating = existing_rating || BalanceLib.default_rating()
new_skill = changes["skill"] |> float_parse
new_uncertainty = changes["uncertainty"] |> float_parse
new_rating_value = BalanceLib.calculate_rating_value(new_skill, new_uncertainty)

new_rating_value =
BalanceLib.calculate_rating_value(
new_skill,
new_uncertainty,
user_rating.num_matches
)

new_leaderboard_rating =
BalanceLib.calculate_leaderboard_rating(new_skill, new_uncertainty)
Expand Down
100 changes: 100 additions & 0 deletions test/teiserver/game/libs/match_rating_lib_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ defmodule Teiserver.Game.MatchRatingLibTest do
alias Teiserver.Account
alias Teiserver.Battle
alias Teiserver.Game
alias Teiserver.Config

test "num_matches is updated after rating a match" do
# Create two user
Expand Down Expand Up @@ -65,6 +66,105 @@ defmodule Teiserver.Game.MatchRatingLibTest do
assert ratings[user1.id].num_matches == 2
end

describe "Test rating system where new players start at zero" do
test "rating after one match" do
# Set config to use provisional ratings
Config.update_site_config("hidden.Rating method", "start at zero; converge to skill")

# Create two user
user1 = AccountTestLib.user_fixture()
user2 = AccountTestLib.user_fixture()

match = create_fake_match(user1.id, user2.id)

MatchRatingLib.rate_match(match.id)
rating_type_id = Game.get_or_add_rating_type(match.game_type)

# Check ratings of users after match
ratings = get_ratings([user1.id, user2.id], rating_type_id)

assert ratings[user1.id].skill == 27.637760127073694
assert ratings[user2.id].skill == 22.362239872926306

# New players start at zero then converge to skill over time
assert ratings[user1.id].rating_value == 0.9212586709024565
assert ratings[user2.id].rating_value == 0.7454079957642102
end

test "rating after many matches" do
# Set config to use provisional ratings
Config.update_site_config("hidden.Rating method", "start at zero; converge to skill")

# Create two user
user1 = AccountTestLib.user_fixture()
user2 = AccountTestLib.user_fixture()

matches_target =
Config.get_site_config_cache("profile.Num matches for rating to equal skill")

match_ids = 1..matches_target

matches =
Enum.map(match_ids, fn x ->
match = create_fake_match(user1.id, user2.id)

MatchRatingLib.rate_match(match.id)

match
end)

rating_type_id = Game.get_or_add_rating_type(Enum.at(matches, 0).game_type)

# Check ratings of users after match
ratings = get_ratings([user1.id, user2.id], rating_type_id)

assert ratings[user1.id].skill == 41.851075350620384
assert ratings[user2.id].skill == 8.148924649379609

# Rating should equal skill
assert ratings[user1.id].rating_value == ratings[user1.id].skill
assert ratings[user2.id].rating_value == ratings[user2.id].skill

# Rate one more match and rating should still equal skill since we've hit the matches_target
match = create_fake_match(user1.id, user2.id)
MatchRatingLib.rate_match(match.id)
# Check ratings of users after match
ratings = get_ratings([user1.id, user2.id], rating_type_id)

# Rating should equal skill
assert ratings[user1.id].rating_value == ratings[user1.id].skill
assert ratings[user2.id].rating_value == ratings[user2.id].skill
end
end

describe "Test rating system where rating = skill minus uncertainty" do
test "rating after one match" do
# Set config to use provisional ratings
Config.update_site_config("hidden.Rating method", "skill minus uncertainty")

# Create two user
user1 = AccountTestLib.user_fixture()
user2 = AccountTestLib.user_fixture()

match = create_fake_match(user1.id, user2.id)

MatchRatingLib.rate_match(match.id)
rating_type_id = Game.get_or_add_rating_type(match.game_type)

# Check ratings of users after match
ratings = get_ratings([user1.id, user2.id], rating_type_id)

assert ratings[user1.id].skill == 27.637760127073694
assert ratings[user2.id].skill == 22.362239872926306

assert ratings[user1.id].rating_value ==
ratings[user1.id].skill - ratings[user1.id].uncertainty

assert ratings[user2.id].rating_value ==
ratings[user2.id].skill - ratings[user2.id].uncertainty
end
end

defp get_ratings(userids, rating_type_id) do
Account.list_ratings(
search: [
Expand Down

0 comments on commit 92a9a63

Please sign in to comment.