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 function to calculate seasonal uncertainty reset - Allow inactive users to have larger reset #540

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
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
62 changes: 0 additions & 62 deletions lib/teiserver/battle/tasks/seasonal_uncertainty_reset_task.ex

This file was deleted.

99 changes: 99 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,99 @@
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
FROM
teiserver_account_ratings tar
);
"""

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
)
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
Loading