From 75a8729baa0c56f4e781cff3460263c6550e8c7d Mon Sep 17 00:00:00 2001 From: Joshua Augustinus Date: Sun, 8 Dec 2024 16:06:23 +1100 Subject: [PATCH] Improve season uncertainty reset Improve season uncertainty reset by taking into account players who have not played in a long time --- .../tasks/seasonal_uncertainty_reset_task.ex | 48 +++++++++++++++++-- .../seasonal_uncertainty_reset_task_test.exs | 48 +++++++++++++++++++ 2 files changed, 92 insertions(+), 4 deletions(-) create mode 100644 test/teiserver/battle/tasks/seasonal_uncertainty_reset_task_test.exs diff --git a/lib/teiserver/battle/tasks/seasonal_uncertainty_reset_task.ex b/lib/teiserver/battle/tasks/seasonal_uncertainty_reset_task.ex index 00ccb0add..77f5deef4 100644 --- a/lib/teiserver/battle/tasks/seasonal_uncertainty_reset_task.ex +++ b/lib/teiserver/battle/tasks/seasonal_uncertainty_reset_task.ex @@ -8,12 +8,11 @@ defmodule Teiserver.Battle.SeasonalUncertaintyResetTask do start_time = System.system_time(:millisecond) new_last_updated = Timex.now() - {_skill, new_uncertainty} = Openskill.rating() ratings_count = Account.list_ratings(limit: :infinity) |> Enum.map(fn rating -> - reset_rating(rating, new_uncertainty, new_last_updated) + reset_rating(rating, new_last_updated) 1 end) |> Enum.count() @@ -25,9 +24,13 @@ defmodule Teiserver.Battle.SeasonalUncertaintyResetTask do ) end - defp reset_rating(existing, _new_uncertainty, new_last_updated) do + defp reset_rating(existing, new_last_updated) do # Use the greater of the existing uncertainty or the minimum value (5.0) - new_uncertainty = max(existing.uncertainty, 5.0) + current_uncertainty = existing.uncertainty + # datetime + last_updated = existing.last_updated + + new_uncertainty = calculate_new_uncertainty(current_uncertainty, last_updated) new_rating_value = BalanceLib.calculate_rating_value(existing.skill, new_uncertainty) @@ -59,4 +62,41 @@ defmodule Teiserver.Battle.SeasonalUncertaintyResetTask do {:ok, _} = Game.create_rating_log(log_params) end + + def calculate_new_uncertainty(current_uncertainty, last_update_datetime) do + days_not_played = DateTime.diff(last_update_datetime, Timex.now(), :day) + target_uncertainty = abs(calculate_target_uncertainty(days_not_played)) + # The new uncertainty can increase but never decrease + max(target_uncertainty, current_uncertainty) + end + + # This is the player's new target uncertainty + # If the player hasn't played for a while, their target uncertainty will be higher + def calculate_target_uncertainty(days_not_played) do + # If you haven't played for more than a year reset uncertainty to default + # If you have played within one month, then the target uncertainty equals min_uncertainty + # If it's something in between one month and a year, use linear interpolation + # Linear interpolation formula: https://www.cuemath.com/linear-interpolation-formula/ + one_year = 365 + one_month = one_year / 12 + min_uncertainty = 5 + {_skill, max_uncertainty} = Openskill.rating() + + cond do + days_not_played >= one_year -> + max_uncertainty + + days_not_played <= one_month -> + min_uncertainty + + true -> + max_days = one_year + min_days = one_month + + # linear interpolation will give a value between min_uncertainty and max_uncertainty + min_uncertainty + + (days_not_played - min_days) * (max_uncertainty - min_uncertainty) / + (max_days - min_days) + end + end end diff --git a/test/teiserver/battle/tasks/seasonal_uncertainty_reset_task_test.exs b/test/teiserver/battle/tasks/seasonal_uncertainty_reset_task_test.exs new file mode 100644 index 000000000..91169ab1a --- /dev/null +++ b/test/teiserver/battle/tasks/seasonal_uncertainty_reset_task_test.exs @@ -0,0 +1,48 @@ +defmodule Teiserver.Battle.SeasonalUncertaintyResetTaskTest do + @moduledoc """ + Can run all balance tests via + mix test --only balance_test + """ + use ExUnit.Case + @moduletag :balance_test + alias Teiserver.Battle.SeasonalUncertaintyResetTask + + test "can calculate target uncertainty" do + result = SeasonalUncertaintyResetTask.calculate_target_uncertainty(400) + assert result == 8.333333333333334 + + one_year = 365 + + one_month = one_year / 12 + + # If you played yesterday then it should pick the min uncertainty of 5 + result = SeasonalUncertaintyResetTask.calculate_target_uncertainty(1) + assert result == 5 + + result = SeasonalUncertaintyResetTask.calculate_target_uncertainty(one_month) + assert result == 5 + + result = SeasonalUncertaintyResetTask.calculate_target_uncertainty(one_month * 2) + assert result == 5.303030303030303 + + result = SeasonalUncertaintyResetTask.calculate_target_uncertainty(one_month * 3) + assert result == 5.6060606060606063 + + result = SeasonalUncertaintyResetTask.calculate_target_uncertainty(one_month * 6) + assert result == 6.515151515151516 + + result = SeasonalUncertaintyResetTask.calculate_target_uncertainty(one_month * 9) + assert result == 7.424242424242426 + + result = SeasonalUncertaintyResetTask.calculate_target_uncertainty(one_month * 11) + assert result == 8.030303030303031 + + result = SeasonalUncertaintyResetTask.calculate_target_uncertainty(one_year) + assert result == 8.333333333333334 + + {_, end_date} = DateTime.new(~D[2016-05-24], ~T[13:26:08.003], "Etc/UTC") + {_, start_date} = DateTime.new(~D[2016-04-24], ~T[13:26:08.003], "Etc/UTC") + days = abs(DateTime.diff(start_date, end_date, :day)) + assert days == 30 + end +end