Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add num_matches column #545

Merged
merged 2 commits into from
Dec 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion lib/teiserver/account/schemas/rating.ex
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,17 @@ defmodule Teiserver.Account.Rating do
field :leaderboard_rating, :float

field :last_updated, :utc_datetime
field :num_matches, :integer
end

@doc false
def changeset(stats, attrs \\ %{}) do
stats
|> cast(
attrs,
~w(user_id rating_type_id rating_value skill uncertainty last_updated leaderboard_rating)a
~w(user_id rating_type_id rating_value skill uncertainty last_updated leaderboard_rating num_matches)a
)
# fields below are required; num_matches is not required
|> validate_required(
~w(user_id rating_type_id rating_value skill uncertainty last_updated leaderboard_rating)a
)
Expand Down
91 changes: 61 additions & 30 deletions lib/teiserver/game/libs/match_rating_lib.ex
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ defmodule Teiserver.Game.MatchRatingLib do
alias Teiserver.{Account, Coordinator, Config, Game, Battle}
alias Teiserver.Data.Types, as: T
alias Teiserver.Repo
alias Teiserver.Battle.{BalanceLib, MatchLib}
alias Teiserver.Battle.BalanceLib
require Logger

@rated_match_types [
Expand Down Expand Up @@ -44,14 +44,14 @@ defmodule Teiserver.Game.MatchRatingLib do

@spec rate_match(non_neg_integer() | Teiserver.Battle.Match.t(), boolean()) ::
:ok | {:error, atom}
def rate_match(match_id, override) when is_integer(match_id) do
def rate_match(match_id, rerate?) when is_integer(match_id) do
Battle.get_match(match_id, preload: [:members])
|> rate_match(override)
|> rate_match(rerate?)
end

def rate_match(nil, _), do: {:error, :no_match}

def rate_match(match, override) do
def rate_match(match, rerate?) do
logs = Game.list_rating_logs(search: [match_id: match.id], limit: 1, select: [:id])

sizes =
Expand Down Expand Up @@ -85,9 +85,9 @@ defmodule Teiserver.Game.MatchRatingLib do
Map.get(match.tags, "game/modoptions/ranked_game", "1") == "0" ->
{:error, :unranked_tag}

# If override is set to true we skip the next few checks
override ->
do_rate_match(match)
# If rerate? is set to true we skip the next few checks
rerate? ->
do_rate_match(match, rerate?: true)

not Enum.empty?(logs) ->
{:error, :already_rated}
Expand All @@ -100,13 +100,14 @@ defmodule Teiserver.Game.MatchRatingLib do
end
end

@spec do_rate_match(Teiserver.Battle.Match.t()) :: :ok
@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) do
defp do_rate_match(%{team_count: 2} = match, opts) do
rating_type_id = Game.get_or_add_rating_type(match.game_type)
partied_rating_type_id = Game.get_or_add_rating_type("Partied Team")

# This allows us to handle partied players slightly differently
# we looked at doing this but there was not enough data. I've
# left the code commented out because it was such a pain to get
Expand Down Expand Up @@ -205,20 +206,18 @@ defmodule Teiserver.Game.MatchRatingLib do
rating_update = rate_result[user_id]
user_rating = rating_lookup[user_id] || BalanceLib.default_rating(rating_type_id)

do_update_rating(user_id, match, user_rating, rating_update)
do_update_rating(user_id, match, user_rating, rating_update, opts)
end)

loss_ratings =
losers
|> Enum.map(fn %{user_id: user_id} ->
rating_update = rate_result[user_id]
user_rating = rating_lookup[user_id] || BalanceLib.default_rating(rating_type_id)
do_update_rating(user_id, match, user_rating, rating_update)
do_update_rating(user_id, match, user_rating, rating_update, opts)
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, opts)

# Update the match to track rating type
{:ok, _} = Battle.update_match(match, %{rating_type_id: rating_type_id})
Expand All @@ -233,11 +232,10 @@ 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

rating_type_id = Game.get_or_add_rating_type(match.game_type)
partied_rating_type_id = Game.get_or_add_rating_type("Partied Team")

Expand Down Expand Up @@ -351,7 +349,7 @@ defmodule Teiserver.Game.MatchRatingLib do
rating_update = win_result[user_id]
user_rating = rating_lookup[user_id] || BalanceLib.default_rating(rating_type_id)

do_update_rating(user_id, match, user_rating, rating_update)
do_update_rating(user_id, match, user_rating, rating_update, opts)
end)

# If you lose you just count as losing against the winner
Expand All @@ -366,7 +364,7 @@ defmodule Teiserver.Game.MatchRatingLib do

user_rating = rating_lookup[user_id] || BalanceLib.default_rating(rating_type_id)
ratiod_rating_update = apply_change_ratio(user_rating, rating_update, opponent_ratio)
do_update_rating(user_id, match, user_rating, ratiod_rating_update)
do_update_rating(user_id, match, user_rating, ratiod_rating_update, opts)
end)
end)
|> List.flatten()
Expand Down Expand Up @@ -409,9 +407,7 @@ 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, opts)

# Update the match to track rating type
{:ok, _} = Battle.update_match(match, %{rating_type_id: rating_type_id})
Expand All @@ -426,8 +422,6 @@ defmodule Teiserver.Game.MatchRatingLib do
:ok
end

defp do_rate_match(_), do: :ok

# Used to ratio the skill lost when there are more than 2 teams
@spec apply_change_ratio(map(), {number(), number()}, number()) :: {number(), number()}
defp apply_change_ratio(_user_rating, rating_update, 1.0), do: rating_update
Expand All @@ -441,8 +435,9 @@ defmodule Teiserver.Game.MatchRatingLib do
{new_skill, u}
end

@spec do_update_rating(T.userid(), map(), map(), {number(), number()}) :: any
defp do_update_rating(user_id, match, user_rating, rating_update) do
@spec do_update_rating(T.userid(), map(), map(), {number(), number()}, any()) :: any
defp do_update_rating(user_id, match, user_rating, rating_update, opts) do
rerate? = Keyword.get(opts, :rerate?, false)
# It's possible they don't yet have a rating
user_rating =
if Map.get(user_rating, :user_id) do
Expand All @@ -452,13 +447,24 @@ defmodule Teiserver.Game.MatchRatingLib do
Account.create_rating(
Map.merge(user_rating, %{
user_id: user_id,
last_updated: match.finished
last_updated: match.finished,
num_matches: 0
})
)

rating
end

new_num_matches =
cond do
# This is the player's first match
user_rating.num_matches == nil -> 1
# We are re-rating a previously rated match, so num_matches unchanged
rerate? -> user_rating.num_matches
# Otherwise increment by one
true -> user_rating.num_matches + 1
end

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)
Expand All @@ -469,7 +475,8 @@ defmodule Teiserver.Game.MatchRatingLib do
skill: new_skill,
uncertainty: new_uncertainty,
leaderboard_rating: new_leaderboard_rating,
last_updated: match.finished
last_updated: match.finished,
num_matches: new_num_matches
})

%{
Expand Down Expand Up @@ -611,15 +618,15 @@ 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
],
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()
Expand Down Expand Up @@ -723,4 +730,28 @@ defmodule Teiserver.Game.MatchRatingLib do
result
end
end

# Saves ratings logs to database
# If rerate? then delete existing logs of that match before we insert
defp save_rating_logs(match_id, win_ratings, loss_ratings, opts) do
rerate? = Keyword.get(opts, :rerate?, false)

if(rerate?) 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
end
24 changes: 24 additions & 0 deletions priv/repo/migrations/20241212224758_add_num_matches_column.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
defmodule Teiserver.Repo.Migrations.AddNumMatchesColumn do
use Ecto.Migration

def change do
alter table("teiserver_account_ratings") do
add :num_matches, :integer
end

# Populate num_matches column
up_query = """
UPDATE teiserver_account_ratings SET num_matches = temp_table.count
FROM (SELECT user_id, rating_type_id , count(*) from teiserver_game_rating_logs tgrl
where match_id is not null
group by user_id , rating_type_id ) AS temp_table
WHERE teiserver_account_ratings.user_id = temp_table.user_id
and teiserver_account_ratings.rating_type_id = temp_table.rating_type_id
"""

# If we rollback we don't have to do anything
rollback_query = ""

execute(up_query, rollback_query)
end
end
Loading
Loading