From 62748774b2c6cbaa9a9c15e2ca1805189833da78 Mon Sep 17 00:00:00 2001 From: Joshua Augustinus Date: Sun, 21 Jul 2024 11:21:16 +1000 Subject: [PATCH 1/2] Better rating logs --- lib/teiserver/battle/libs/balance_lib.ex | 25 ++++++-- lib/teiserver/game/libs/match_rating_lib.ex | 67 +++++++++++++++----- lib/teiserver/mix_tasks/rerate_my_matches.ex | 47 ++++++++++++++ 3 files changed, 119 insertions(+), 20 deletions(-) create mode 100644 lib/teiserver/mix_tasks/rerate_my_matches.ex diff --git a/lib/teiserver/battle/libs/balance_lib.ex b/lib/teiserver/battle/libs/balance_lib.ex index b3d11f512..6fbeca886 100644 --- a/lib/teiserver/battle/libs/balance_lib.ex +++ b/lib/teiserver/battle/libs/balance_lib.ex @@ -195,9 +195,13 @@ defmodule Teiserver.Battle.BalanceLib do x when is_number(x) -> {user_id, get_user_rating_rank_old(user_id, x)} - # match_controller will use this condition when balancing using old data + # TeiserverWeb.Battle.MatchLive.Show will use this condition when balancing using old data + # It is the data taken from the teiserver_gaming_rating_logs table %{"rating_value" => rating_value, "uncertainty" => uncertainty} -> - {user_id, get_user_rating_rank_old(user_id, rating_value, uncertainty)} + # We might also have rank saved but it's not guaranteed + rank = Map.get(value, "rank", 0) + opts = [uncertainty: uncertainty, rank: rank] + {user_id, get_user_rating_rank_old(user_id, rating_value, opts)} _ -> {user_id, value} @@ -641,9 +645,20 @@ defmodule Teiserver.Battle.BalanceLib do @doc """ This is used by some screens to calculate a theoretical balance based on old ratings """ - def get_user_rating_rank_old(userid, rating_value, uncertainty \\ 0) do - stats_data = Account.get_user_stat_data(userid) - rank = Map.get(stats_data, "rank", 0) + + def get_user_rating_rank_old(userid, rating_value, opts \\ []) do + uncertainty = Keyword.get(opts, :uncertainty, 0) + rank = Keyword.get(opts, :rank, nil) + + rank = + case rank do + nil -> + stats_data = Account.get_user_stat_data(userid) + Map.get(stats_data, "rank", 0) + + _ -> + rank + end %{name: name} = Account.get_user_by_id(userid) %{rating: rating_value, rank: rank, name: name, uncertainty: uncertainty} diff --git a/lib/teiserver/game/libs/match_rating_lib.ex b/lib/teiserver/game/libs/match_rating_lib.ex index cec760546..8530acf18 100644 --- a/lib/teiserver/game/libs/match_rating_lib.ex +++ b/lib/teiserver/game/libs/match_rating_lib.ex @@ -69,7 +69,7 @@ defmodule Teiserver.Game.MatchRatingLib do not Enum.member?(@rated_match_types, match.game_type) -> {:error, :invalid_game_type} - match.processed == false -> + match.processed == false and !override -> {:error, :not_processed} match.winning_team == nil -> @@ -90,7 +90,7 @@ defmodule Teiserver.Game.MatchRatingLib do # If override is set to true we skip the next few checks override -> - do_rate_match(match) + do_rate_match(match, override?: true) not Enum.empty?(logs) -> {:error, :already_rated} @@ -104,9 +104,19 @@ defmodule Teiserver.Game.MatchRatingLib do end @spec do_rate_match(Teiserver.Battle.Match.t()) :: :ok + defp do_rate_match(match) do + if(Map.has_key?(match, :team_count)) do + do_rate_match(match, []) + else + :ok + end + end + + @spec do_rate_match(Teiserver.Battle.Match.t(), any()) :: :ok # The algorithm has not been implemented for FFA correctly so we have a clause for # 2 teams (correctly implemented) and a special for 3+ teams - defp do_rate_match(%{team_count: 2} = match) do + defp do_rate_match(%{team_count: 2} = match, opts) do + override? = Keyword.get(opts, :override?, false) rating_type_id = Game.get_or_add_rating_type(match.game_type) partied_rating_type_id = Game.get_or_add_rating_type("Partied Team") @@ -239,9 +249,7 @@ defmodule Teiserver.Game.MatchRatingLib do do_update_rating(user_id, match, user_rating, rating_update) end) - Ecto.Multi.new() - |> Ecto.Multi.insert_all(:insert_all, Teiserver.Game.RatingLog, win_ratings ++ loss_ratings) - |> Teiserver.Repo.transaction() + save_rating_logs(match.id, win_ratings, loss_ratings, override?) # Update the match to track rating type {:ok, _} = Battle.update_match(match, %{rating_type_id: rating_type_id}) @@ -256,11 +264,11 @@ defmodule Teiserver.Game.MatchRatingLib do :ok end - defp do_rate_match(%{team_count: team_count} = match) do + defp do_rate_match(%{team_count: team_count} = match, opts) do # When there are more than 2 teams we update the rating as if it was a 2 team game # where if you won, the opponent was the best losing team # and if you lost the opponent was whoever won - + override? = Keyword.get(opts, :override?, false) rating_type_id = Game.get_or_add_rating_type(match.game_type) partied_rating_type_id = Game.get_or_add_rating_type("Partied Team") @@ -452,9 +460,8 @@ defmodule Teiserver.Game.MatchRatingLib do # end) # |> List.flatten - Ecto.Multi.new() - |> Ecto.Multi.insert_all(:insert_all, Teiserver.Game.RatingLog, win_ratings ++ loss_ratings) - |> Teiserver.Repo.transaction() + + save_rating_logs(match.id, win_ratings, loss_ratings, override?) # Update the match to track rating type {:ok, _} = Battle.update_match(match, %{rating_type_id: rating_type_id}) @@ -469,7 +476,27 @@ defmodule Teiserver.Game.MatchRatingLib do :ok end - defp do_rate_match(_), do: :ok + # Saves ratings logs to database + # If override? then delete existing logs of that match before we insert + defp save_rating_logs(match_id, win_ratings, loss_ratings, override?) do + if(override?) do + Ecto.Multi.new() + |> Ecto.Multi.run(:delete_existing, fn repo, _ -> + query = """ + delete from teiserver_game_rating_logs l where + l.match_id = $1 + """ + + Ecto.Adapters.SQL.query(repo, query, [match_id]) + end) + |> Ecto.Multi.insert_all(:insert_all, Teiserver.Game.RatingLog, win_ratings ++ loss_ratings) + |> Teiserver.Repo.transaction() + else + Ecto.Multi.new() + |> Ecto.Multi.insert_all(:insert_all, Teiserver.Game.RatingLog, win_ratings ++ loss_ratings) + |> Teiserver.Repo.transaction() + end + end # Used to ratio the skill lost when there are more than 2 teams @spec apply_change_ratio(map(), {number(), number()}, number()) :: {number(), number()} @@ -585,6 +612,13 @@ defmodule Teiserver.Game.MatchRatingLib do last_updated: match.finished }) + # Get stats + stats = Account.get_user_stat_data(user_id) + play_hours = (Map.get(stats, "player_minutes", 0) / 60) |> trunc() + spectator_hours = (Map.get(stats, "spectator_minutes", 0) / 60) |> trunc() + + rank = Map.get(stats, "rank", 0) + %{ user_id: user_id, rating_type_id: rating_type_id, @@ -596,7 +630,10 @@ defmodule Teiserver.Game.MatchRatingLib do uncertainty: new_uncertainty, rating_value_change: new_rating_value - user_rating.rating_value, skill_change: new_skill - user_rating.skill, - uncertainty_change: new_uncertainty - user_rating.uncertainty + uncertainty_change: new_uncertainty - user_rating.uncertainty, + play_hours: play_hours, + spectator_hours: spectator_hours, + rank: rank } } end @@ -724,7 +761,7 @@ defmodule Teiserver.Game.MatchRatingLib do end end - defp re_rate_specific_matches(ids) do + def re_rate_specific_matches(ids) do Battle.list_matches( search: [ id_in: ids @@ -732,7 +769,7 @@ defmodule Teiserver.Game.MatchRatingLib do limit: :infinity, preload: [:members] ) - |> Enum.map(fn match -> rate_match(match) end) + |> Enum.map(fn match -> rate_match(match, true) end) end @spec predict_winning_team([map()], non_neg_integer()) :: map() diff --git a/lib/teiserver/mix_tasks/rerate_my_matches.ex b/lib/teiserver/mix_tasks/rerate_my_matches.ex new file mode 100644 index 000000000..e53572be1 --- /dev/null +++ b/lib/teiserver/mix_tasks/rerate_my_matches.ex @@ -0,0 +1,47 @@ +defmodule Mix.Tasks.Teiserver.RerateMyMatches do + @moduledoc """ + Re rates matches belonging to one user + + If you want to run this task invidually, use: + mix teiserver.rerate_my_matches + """ + use Mix.Task + alias Teiserver.Repo + require Logger + + def run(args) do + Application.ensure_all_started(:teiserver) + + username = Enum.at(args, 0) + + case username do + nil -> + Logger.info("Username parameter cannot be empty") + + _ -> + match_ids = get_match_ids(username) + Teiserver.Game.MatchRatingLib.re_rate_specific_matches(match_ids) + Logger.info("Finished rerating matches of #{username}") + end + end + + defp get_match_ids(username) do + query = """ + SELECT tbmm.match_id + FROM teiserver_battle_match_memberships tbmm + INNER JOIN teiserver_battle_matches tbm ON tbm.id = tbmm.match_id + WHERE user_id = ( + SELECT id FROM account_users au WHERE name = $1 + ) + """ + + case Ecto.Adapters.SQL.query(Repo, query, [username]) do + {:ok, results} -> + results.rows + |> List.flatten() + + {a, b} -> + raise "ERR: #{a}, #{b}" + end + end +end From 4599a55016f6aeffc1becfd271a66dceaaf8bb8d Mon Sep 17 00:00:00 2001 From: Joshua Augustinus Date: Sun, 21 Jul 2024 13:03:58 +1000 Subject: [PATCH 2/2] Fix dialyzer --- lib/teiserver/game/libs/match_rating_lib.ex | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/lib/teiserver/game/libs/match_rating_lib.ex b/lib/teiserver/game/libs/match_rating_lib.ex index 8530acf18..fc3576753 100644 --- a/lib/teiserver/game/libs/match_rating_lib.ex +++ b/lib/teiserver/game/libs/match_rating_lib.ex @@ -103,16 +103,9 @@ defmodule Teiserver.Game.MatchRatingLib do end end - @spec do_rate_match(Teiserver.Battle.Match.t()) :: :ok - defp do_rate_match(match) do - if(Map.has_key?(match, :team_count)) do - do_rate_match(match, []) - else - :ok - end - end - @spec do_rate_match(Teiserver.Battle.Match.t(), any()) :: :ok + defp do_rate_match(match, opts \\ []) + # The algorithm has not been implemented for FFA correctly so we have a clause for # 2 teams (correctly implemented) and a special for 3+ teams defp do_rate_match(%{team_count: 2} = match, opts) do @@ -460,7 +453,6 @@ defmodule Teiserver.Game.MatchRatingLib do # end) # |> List.flatten - save_rating_logs(match.id, win_ratings, loss_ratings, override?) # Update the match to track rating type