Skip to content

Commit

Permalink
Add num_matches column
Browse files Browse the repository at this point in the history
Add num_matches column
  • Loading branch information
jauggy committed Dec 21, 2024
1 parent d7df6d3 commit 38ceb96
Show file tree
Hide file tree
Showing 4 changed files with 219 additions and 25 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
79 changes: 55 additions & 24 deletions lib/teiserver/game/libs/match_rating_lib.ex
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,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}
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
override? = Keyword.get(opts, :override?, 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
override? -> 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 override? then delete existing logs of that match before we insert
defp save_rating_logs(match_id, win_ratings, loss_ratings, opts) do
override? = Keyword.get(opts, :override?, false)

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
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
137 changes: 137 additions & 0 deletions test/teiserver/game/libs/match_rating_lib_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
defmodule Teiserver.Game.MatchRatingLibTest do
@moduledoc false
use Teiserver.DataCase, async: true
alias Teiserver.Game.MatchRatingLib
alias Teiserver.Account.AccountTestLib
alias Teiserver.Battle.MatchLib
alias Teiserver.Account
alias Teiserver.Battle
alias Teiserver.Game

test "num_matches is updated after rating a match" do
# Create two user
user1 = AccountTestLib.user_fixture()
user2 = AccountTestLib.user_fixture()

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

# Check ratings of users before we rate the match
rating_type_id = Game.get_or_add_rating_type(match.game_type)

ratings =
Account.list_ratings(
search: [
rating_type_id: rating_type_id,
user_id_in: [user1.id, user2.id]
]
)
|> Map.new(fn rating ->
{rating.user_id, rating}
end)

assert ratings[user1.id] == nil
assert ratings[user2.id] == nil

MatchRatingLib.rate_match(match.id)

# 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].num_matches == 1
assert ratings[user1.id].num_matches == 1

# Create another match
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)

assert ratings[user1.id].skill == 29.662576313923775
assert ratings[user2.id].skill == 20.337423686076225

# Check num_matches has increased
assert ratings[user1.id].num_matches == 2
assert ratings[user1.id].num_matches == 2
end

defp get_ratings(userids, rating_type_id) do
Account.list_ratings(
search: [
rating_type_id: rating_type_id,
user_id_in: userids
]
)
|> Map.new(fn rating ->
{rating.user_id, rating}
end)
end

defp create_fake_match(user1_id, user2_id) do
team_count = 2
team_size = 1
game_type = MatchLib.game_type(team_size, team_count)
server_uuid = UUID.uuid1()
end_time = Timex.now()

start_time = DateTime.add(end_time, 50, :minute)

# Create a match
{:ok, match} =
Battle.create_match(%{
server_uuid: server_uuid,
uuid: UUID.uuid1(),
map: "Koom valley",
data: %{},
tags: %{},
winning_team: 0,
team_count: team_count,
team_size: team_size,
passworded: false,
processed: true,
game_type: game_type,
# All rooms are hosted by the same user for now
founder_id: 1,
bots: %{},
queue_id: nil,
started: start_time,
finished: end_time
})

# Create match memberships
memberships1 = [
%{
team_id: 0,
win: match.winning_team == 0,
stats: %{},
party_id: nil,
user_id: user1_id,
match_id: match.id
}
]

memberships2 = [
%{
team_id: 1,
win: match.winning_team == 1,
stats: %{},
party_id: nil,
user_id: user2_id,
match_id: match.id
}
]

Ecto.Multi.new()
|> Ecto.Multi.insert_all(
:insert_all,
Battle.MatchMembership,
memberships1 ++ memberships2
)
|> Teiserver.Repo.transaction()

match
end
end

0 comments on commit 38ceb96

Please sign in to comment.