Skip to content

Commit

Permalink
Add function to calculate seasonal uncertainty reset - Allow inactive…
Browse files Browse the repository at this point in the history
… 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
jauggy authored Dec 27, 2024
1 parent 87ed95b commit 3528bed
Show file tree
Hide file tree
Showing 4 changed files with 235 additions and 62 deletions.
62 changes: 0 additions & 62 deletions lib/teiserver/battle/tasks/seasonal_uncertainty_reset_task.ex

This file was deleted.

101 changes: 101 additions & 0 deletions lib/teiserver/mix_tasks/seasonal_uncertainty_reset_task.ex
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
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
76 changes: 76 additions & 0 deletions test/teiserver/sql/season_uncertainty_reset_test.exs
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

0 comments on commit 3528bed

Please sign in to comment.