From 4ed4f5c976241a84a87f055c1d2327aeca0a87b5 Mon Sep 17 00:00:00 2001 From: Joshua Augustinus Date: Mon, 30 Dec 2024 19:29:22 +1100 Subject: [PATCH] Add new rating system setup task --- lib/teiserver/battle/libs/balance_lib.ex | 23 +++- lib/teiserver/game/libs/match_rating_lib.ex | 5 +- lib/teiserver/libs/teiserver_configs.ex | 23 ++++ .../mix_tasks/provisional_rating_setup.ex | 105 ++++++++++++++++++ .../admin/site_config_controller.ex | 9 +- .../controllers/admin/user_controller.ex | 8 +- 6 files changed, 166 insertions(+), 7 deletions(-) create mode 100644 lib/teiserver/mix_tasks/provisional_rating_setup.ex diff --git a/lib/teiserver/battle/libs/balance_lib.ex b/lib/teiserver/battle/libs/balance_lib.ex index 8fa89c499..a2ee57b50 100644 --- a/lib/teiserver/battle/libs/balance_lib.ex +++ b/lib/teiserver/battle/libs/balance_lib.ex @@ -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) @@ -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 @@ -704,7 +705,21 @@ 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 + if Config.get_site_config("profile.Rating method") == "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("profile.Num matches for rating to equal skill") end @doc """ diff --git a/lib/teiserver/game/libs/match_rating_lib.ex b/lib/teiserver/game/libs/match_rating_lib.ex index b980fe23f..3cbfda1e0 100644 --- a/lib/teiserver/game/libs/match_rating_lib.ex +++ b/lib/teiserver/game/libs/match_rating_lib.ex @@ -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, %{ diff --git a/lib/teiserver/libs/teiserver_configs.ex b/lib/teiserver/libs/teiserver_configs.ex index a443be199..e941b5cf7 100644 --- a/lib/teiserver/libs/teiserver_configs.ex +++ b/lib/teiserver/libs/teiserver_configs.ex @@ -13,6 +13,7 @@ defmodule Teiserver.TeiserverConfigs do lobby_configs() debugging_configs() profile_configs() + hidden_configs() end @spec site_configs :: any @@ -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", @@ -633,4 +643,17 @@ defmodule Teiserver.TeiserverConfigs do value_label: "Light mode as default" }) end + + @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 diff --git a/lib/teiserver/mix_tasks/provisional_rating_setup.ex b/lib/teiserver/mix_tasks/provisional_rating_setup.ex new file mode 100644 index 000000000..9bd397291 --- /dev/null +++ b/lib/teiserver/mix_tasks/provisional_rating_setup.ex @@ -0,0 +1,105 @@ +defmodule Mix.Tasks.Teiserver.ProvisionalRatingSetup do + @moduledoc """ + + 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 diff --git a/lib/teiserver_web/controllers/admin/site_config_controller.ex b/lib/teiserver_web/controllers/admin/site_config_controller.ex index e3d1b0f6d..7a20e1fb4 100644 --- a/lib/teiserver_web/controllers/admin/site_config_controller.ex +++ b/lib/teiserver_web/controllers/admin/site_config_controller.ex @@ -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) diff --git a/lib/teiserver_web/controllers/admin/user_controller.ex b/lib/teiserver_web/controllers/admin/user_controller.ex index 5e84ad77a..43c5c66cc 100644 --- a/lib/teiserver_web/controllers/admin/user_controller.ex +++ b/lib/teiserver_web/controllers/admin/user_controller.ex @@ -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)