-
Notifications
You must be signed in to change notification settings - Fork 54
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add function to calculate seasonal uncertainty reset - Allow inactive…
… users to have larger reset (#540) * Add function to calculate seasonal uncertainty * Add seasonal_uncertainty_reset_task * Add num_matches to logs
- Loading branch information
Showing
4 changed files
with
235 additions
and
62 deletions.
There are no files selected for viewing
62 changes: 0 additions & 62 deletions
62
lib/teiserver/battle/tasks/seasonal_uncertainty_reset_task.ex
This file was deleted.
Oops, something went wrong.
101 changes: 101 additions & 0 deletions
101
lib/teiserver/mix_tasks/seasonal_uncertainty_reset_task.ex
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
defmodule Mix.Tasks.Teiserver.SeasonalUncertaintyResetTask do | ||
@moduledoc """ | ||
Run with | ||
mix teiserver.seasonal_uncertainty_reset_task | ||
If you want to specify the uncertainty target use | ||
mix teiserver.seasonal_uncertainty_reset_task 5 | ||
where 5 is the uncertainty target | ||
""" | ||
|
||
use Mix.Task | ||
|
||
require Logger | ||
|
||
@spec run(list()) :: :ok | ||
def run(args) do | ||
Logger.info("Args: #{args}") | ||
default_uncertainty_target = "5" | ||
{uncertainty_target, _} = Enum.at(args, 0, default_uncertainty_target) |> Float.parse() | ||
|
||
Application.ensure_all_started(:teiserver) | ||
|
||
start_time = System.system_time(:millisecond) | ||
|
||
sql_transaction_result = | ||
Ecto.Multi.new() | ||
|> Ecto.Multi.run(:create_temp_table, fn repo, _ -> | ||
query = """ | ||
CREATE temp table temp_table as | ||
SELECT | ||
*, | ||
greatest(0, skill - new_uncertainty) as new_rating, | ||
new_uncertainty - uncertainty as uncertainty_change, | ||
greatest(0, skill - new_uncertainty)- rating_value as rating_value_change, | ||
skill - 3 * new_uncertainty as new_leaderboard_rating | ||
FROM | ||
( | ||
SELECT | ||
user_id, | ||
rating_type_id, | ||
rating_value, | ||
uncertainty, | ||
calculate_season_uncertainty(uncertainty, last_updated, $1) as new_uncertainty, | ||
skill, | ||
num_matches | ||
FROM | ||
teiserver_account_ratings tar | ||
) as a; | ||
""" | ||
|
||
Ecto.Adapters.SQL.query(repo, query, [uncertainty_target]) | ||
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', 'Uncertainty minimum override', | ||
'uncertainty', new_uncertainty, | ||
'rating_value', new_rating, | ||
'skill_change', 0.0, | ||
'uncertainty_change', uncertainty_change, | ||
'rating_value_change', rating_value_change, | ||
'num_matches', num_matches | ||
) | ||
FROM temp_table; | ||
""" | ||
|
||
Ecto.Adapters.SQL.query(repo, query, []) | ||
end) | ||
|> Ecto.Multi.run(:update_ratings, fn repo, _ -> | ||
query = """ | ||
UPDATE teiserver_account_ratings tar | ||
SET | ||
uncertainty = t.new_uncertainty, | ||
rating_value = t.new_rating, | ||
leaderboard_rating = t.new_leaderboard_rating | ||
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 | ||
time_taken = System.system_time(:millisecond) - start_time | ||
|
||
Logger.info( | ||
"SeasonalUncertaintyResetTask complete, took #{time_taken}ms. Updated #{result.update_ratings.num_rows} ratings." | ||
) | ||
else | ||
_ -> | ||
Logger.error("SeasonalUncertaintyResetTask failed.") | ||
end | ||
end | ||
end |
58 changes: 58 additions & 0 deletions
58
priv/repo/migrations/20241209014803_season_uncertainty_function.exs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
defmodule Teiserver.Repo.Migrations.SeasonUncertaintyFunction do | ||
use Ecto.Migration | ||
|
||
def up do | ||
query = """ | ||
create or replace function calculate_season_uncertainty(current_uncertainty float, last_updated timestamp, min_uncertainty float default 5) | ||
returns float | ||
language plpgsql | ||
as | ||
$$ | ||
declare | ||
-- variable declaration | ||
default_uncertainty float; | ||
days_not_played float; | ||
one_month float; | ||
one_year float; | ||
interpolated_uncertainty float; | ||
min_days float; | ||
max_days float; | ||
max_uncertainty float; | ||
begin | ||
-- Your new uncertainty will be: greatest(target_uncertainty, current_uncertainty) | ||
-- Where target_uncertainty will be default if you have not played for over a year | ||
-- 5 (min_uncertainty) if you have played within one month | ||
-- And use linear interpolation for values in between | ||
one_year = 365.0; | ||
default_uncertainty = 25.0/3; | ||
one_month = one_year / 12; | ||
days_not_played = abs(DATE_PART('day', (now()- last_updated ))); | ||
min_days = one_month; | ||
max_days = one_year; | ||
max_uncertainty = default_uncertainty; | ||
if(days_not_played >= max_days) then | ||
return default_uncertainty; | ||
elsif days_not_played <= min_days then | ||
return GREATEST(current_uncertainty, min_uncertainty); | ||
else | ||
-- Use linear interpolation | ||
interpolated_uncertainty = min_uncertainty +(days_not_played - min_days) * (max_uncertainty - min_uncertainty) /(max_days - min_days); | ||
return GREATEST(current_uncertainty, interpolated_uncertainty); | ||
end if; | ||
end; | ||
$$; | ||
""" | ||
|
||
execute(query) | ||
end | ||
|
||
def down do | ||
query = "drop function calculate_season_uncertainty" | ||
execute(query) | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
defmodule Teiserver.Sql.SeasonUncertaintyResetTest do | ||
@moduledoc false | ||
use Teiserver.DataCase | ||
|
||
test "it can calculate seasonal uncertainty reset target" do | ||
# Start by removing all anon properties | ||
{:ok, now} = DateTime.now("Etc/UTC") | ||
|
||
result = calculate_seasonal_uncertainty(now) | ||
assert result == 5 | ||
|
||
# Now 30 days ago | ||
month = 365 / 12 | ||
last_updated = DateTime.add(now, -month |> trunc(), :day) | ||
result = calculate_seasonal_uncertainty(last_updated) | ||
|
||
# assert_in_delta checks that the result is close to expected. Helps deal with rounding issues | ||
assert_in_delta(result, 5, 0.1) | ||
|
||
# Now 2 months ago | ||
last_updated = DateTime.add(now, (-2 * month) |> trunc(), :day) | ||
result = calculate_seasonal_uncertainty(last_updated) | ||
assert_in_delta(result, 5.294728102947281, 0.1) | ||
|
||
# Now 3 months ago | ||
last_updated = DateTime.add(now, (-3 * month) |> trunc(), :day) | ||
result = calculate_seasonal_uncertainty(last_updated) | ||
assert_in_delta(result, 5.6035699460357, 0.1) | ||
|
||
# Now 6 months ago | ||
last_updated = DateTime.add(now, (-6 * month) |> trunc(), :day) | ||
result = calculate_seasonal_uncertainty(last_updated) | ||
assert_in_delta(result, 6.510170195101702, 0.1) | ||
|
||
# Now 9 months ago | ||
last_updated = DateTime.add(now, (-9 * month) |> trunc(), :day) | ||
result = calculate_seasonal_uncertainty(last_updated) | ||
assert_in_delta(result, 7.416770444167705, 0.1) | ||
|
||
# Now 11 months ago | ||
last_updated = DateTime.add(now, (-11 * month) |> trunc(), :day) | ||
result = calculate_seasonal_uncertainty(last_updated) | ||
assert_in_delta(result, 8.024491490244916, 0.1) | ||
|
||
# Now 12 months ago | ||
last_updated = DateTime.add(now, (-12 * month) |> trunc(), :day) | ||
result = calculate_seasonal_uncertainty(last_updated) | ||
assert_in_delta(result, 8.333333333333334, 0.1) | ||
|
||
# Now 13 months ago | ||
last_updated = DateTime.add(now, (-13 * month) |> trunc(), :day) | ||
|
||
result = calculate_seasonal_uncertainty(last_updated) | ||
assert_in_delta(result, 8.333333333333334, 0.1) | ||
end | ||
|
||
# This will calculate the new uncertainty during a season reset | ||
# The longer your last_updated is from today, the more your uncertainty will be reset | ||
# The number should range from min_uncertainty and default_uncertainty (8.333) | ||
# Your new_uncertainty can grow from current_uncertainty but never reduce | ||
# Full details in comments of the sql function calculate_season_uncertainty | ||
defp calculate_seasonal_uncertainty(last_updated) do | ||
current_uncertainty = 1 | ||
min_uncertainty = 5 | ||
query = "SELECT calculate_season_uncertainty($1, $2, $3);" | ||
|
||
results = | ||
Ecto.Adapters.SQL.query!(Repo, query, [current_uncertainty, last_updated, min_uncertainty]) | ||
|
||
[new_uncertainty] = | ||
results.rows | ||
|> Enum.at(0) | ||
|
||
new_uncertainty | ||
end | ||
end |