Skip to content

Commit

Permalink
Add num_matches column (#545)
Browse files Browse the repository at this point in the history
  • Loading branch information
jauggy authored Dec 26, 2024
1 parent 5dcc6bd commit b8d22a8
Show file tree
Hide file tree
Showing 4 changed files with 232 additions and 31 deletions.
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

0 comments on commit b8d22a8

Please sign in to comment.