From e261714440afb51cac9565c734e568792d3d3863 Mon Sep 17 00:00:00 2001 From: Joshua Augustinus Date: Sat, 8 Jun 2024 21:32:21 +1000 Subject: [PATCH] Fix create balance when incomplete data --- lib/teiserver/battle/libs/balance_lib.ex | 39 +++++++++++- mix.exs | 2 + mix.lock | 2 + .../battle/balance_lib_internal_test.exs | 59 +++++++++++++++++++ .../battle/split_one_chevs_internal_test.exs | 52 ++++++++++------ .../teiserver/battle/split_one_chevs_test.exs | 3 +- 6 files changed, 135 insertions(+), 22 deletions(-) create mode 100644 test/teiserver/battle/balance_lib_internal_test.exs diff --git a/lib/teiserver/battle/libs/balance_lib.ex b/lib/teiserver/battle/libs/balance_lib.ex index ca95a5ecc..07c560c2c 100644 --- a/lib/teiserver/battle/libs/balance_lib.ex +++ b/lib/teiserver/battle/libs/balance_lib.ex @@ -99,6 +99,7 @@ defmodule Teiserver.Battle.BalanceLib do def create_balance(groups, team_count, opts) do start_time = System.system_time(:microsecond) + groups = standardise_groups(groups) # We perform all our group calculations here and assign each group # an ID that's used purely for this run of balance @@ -154,6 +155,30 @@ defmodule Teiserver.Battle.BalanceLib do |> Map.put(:time_taken, System.system_time(:microsecond) - start_time) end + @doc """ + Sometimes groups have missing data so we need to refetch it. + If we go through balancer_server then all the required data should be there + """ + def standardise_groups(groups) do + groups + |> Enum.map(fn group -> + # Iterate over our map + {_ignored, better_map} = + Enum.map_reduce(group, %{}, fn {user_id, value}, acc -> + fixed_value = + cond do + # We're missing data so need to fetch it + is_number(value) -> get_user_rating_rank_old(user_id, value) + true -> value + end + + {nil, Map.put(acc, user_id, fixed_value)} + end) + + better_map + end) + end + # Removes various keys we don't care about defp cleanup_result(result) do Map.take( @@ -164,7 +189,8 @@ defmodule Teiserver.Battle.BalanceLib do # Only take keys we need defp clean_groups(groups) do - groups |> Enum.map(fn x-> + groups + |> Enum.map(fn x -> Map.take(x, ~w(members count group_rating ratings)a) end) end @@ -179,7 +205,7 @@ defmodule Teiserver.Battle.BalanceLib do true -> balance_result.teams |> Map.new(fn {team_id, groups} -> - {team_id, Enum.reverse(clean_groups((groups)))} + {team_id, Enum.reverse(clean_groups(groups))} end) end @@ -541,7 +567,6 @@ defmodule Teiserver.Battle.BalanceLib do get_user_rating_value(userid, rating_type_id) end - # Used to get the rating value of the user for internal balance purposes which might be # different from public/reporting @spec get_user_balance_rating_value(T.userid(), String.t() | non_neg_integer()) :: @@ -582,6 +607,14 @@ defmodule Teiserver.Battle.BalanceLib do %{rating: rating, rank: rank, name: name} end + @doc """ + This is used by some screens to calculate a theoretical balance based on old ratings + """ + def get_user_rating_rank_old(userid, rating_value) do + %{rank: rank, name: name} = Account.get_user_by_id(userid) + %{rating: rating_value, rank: rank, name: name} + end + defp fuzz_rating(rating, multiplier) do # Generate something between -1 and 1 modifier = 1 - :rand.uniform() * 2 diff --git a/mix.exs b/mix.exs index df79da0fa..d37a47be1 100644 --- a/mix.exs +++ b/mix.exs @@ -33,6 +33,7 @@ defmodule Teiserver.MixProject do def application do # get that with mix app.tree nostrum nostrum_extras = [:certifi, :gun, :inets, :jason, :kcl, :mime] + [ mod: {Teiserver.Application, []}, included_applications: [:nostrum], @@ -90,6 +91,7 @@ defmodule Teiserver.MixProject do {:libcluster, "~> 3.3"}, {:tzdata, "~> 1.1"}, {:ex_ulid, "~> 0.1.0"}, + {:mock, "~> 0.3.0", only: :test}, # Teiserver libs {:openskill, git: "https://github.com/StanczakDominik/openskill.ex.git", branch: "master"}, diff --git a/mix.lock b/mix.lock index d0ea79fd1..090d704e1 100644 --- a/mix.lock +++ b/mix.lock @@ -59,10 +59,12 @@ "libring": {:hex, :libring, "1.6.0", "d5dca4bcb1765f862ab59f175b403e356dec493f565670e0bacc4b35e109ce0d", [:mix], [], "hexpm", "5e91ece396af4bce99953d49ee0b02f698cd38326d93cd068361038167484319"}, "logger_file_backend": {:hex, :logger_file_backend, "0.0.13", "df07b14970e9ac1f57362985d76e6f24e3e1ab05c248055b7d223976881977c2", [:mix], [], "hexpm", "71a453a7e6e899ae4549fb147b1c6621f4233f8f48f58ca10a64ec67b6c50018"}, "math": {:hex, :math, "0.6.0", "69325af99e600123f6d994833502f6d063a2fa8f2786a3c0461fe6c6123a5166", [:mix], [], "hexpm", "2c73a64d64f719ee1f2821d382f3ed63e8e9564e5176d1c8aa777aac49b41bf7"}, + "meck": {:hex, :meck, "0.9.2", "85ccbab053f1db86c7ca240e9fc718170ee5bda03810a6292b5306bf31bae5f5", [:rebar3], [], "hexpm", "81344f561357dc40a8344afa53767c32669153355b626ea9fcbc8da6b3045826"}, "merkle_map": {:hex, :merkle_map, "0.2.1", "01a88c87a6b9fb594c67c17ebaf047ee55ffa34e74297aa583ed87148006c4c8", [:mix], [], "hexpm", "fed4d143a5c8166eee4fa2b49564f3c4eace9cb252f0a82c1613bba905b2d04d"}, "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, "mime": {:hex, :mime, "1.6.0", "dabde576a497cef4bbdd60aceee8160e02a6c89250d6c0b29e56c0dfb00db3d2", [:mix], [], "hexpm", "31a1a8613f8321143dde1dafc36006a17d28d02bdfecb9e95a880fa7aabd19a7"}, "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, + "mock": {:hex, :mock, "0.3.8", "7046a306b71db2488ef54395eeb74df0a7f335a7caca4a3d3875d1fc81c884dd", [:mix], [{:meck, "~> 0.9.2", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm", "7fa82364c97617d79bb7d15571193fc0c4fe5afd0c932cef09426b3ee6fe2022"}, "nostrum": {:hex, :nostrum, "0.8.0", "36f5a08e99c3df3020523be9e1c650ad926a63becc5318562abfe782d586e078", [:mix], [{:certifi, "~> 2.8", [hex: :certifi, repo: "hexpm", optional: false]}, {:gun, "~> 2.0", [hex: :gun, repo: "hexpm", optional: false]}, {:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: false]}, {:kcl, "~> 1.4", [hex: :kcl, repo: "hexpm", optional: false]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}], "hexpm", "ce6861391ff346089d32a243fa71c0cb8bff79ab86ad53e8bf72808267899aee"}, "oban": {:hex, :oban, "2.15.2", "8f934a49db39163633965139c8846d8e24c2beb4180f34a005c2c7c3f69a6aa2", [:mix], [{:ecto_sql, "~> 3.6", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:ecto_sqlite3, "~> 0.9", [hex: :ecto_sqlite3, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "0f4a579ea48fc7489e0d84facf8b01566e142bdc6542d7dabce32c10e664f1e9"}, "openskill": {:git, "https://github.com/StanczakDominik/openskill.ex.git", "163a72f7423b8aa964909ea6aa3e9943739d87a6", [branch: "master"]}, diff --git a/test/teiserver/battle/balance_lib_internal_test.exs b/test/teiserver/battle/balance_lib_internal_test.exs new file mode 100644 index 000000000..c0bfbffa4 --- /dev/null +++ b/test/teiserver/battle/balance_lib_internal_test.exs @@ -0,0 +1,59 @@ +defmodule Teiserver.Battle.BalanceLibInternalTest do + @moduledoc """ + Can run all balance tests via + mix test --only balance_test + """ + use ExUnit.Case + import Mock + @moduletag :balance_test + alias Teiserver.Battle.BalanceLib + + setup_with_mocks([ + {Teiserver.Account, [:passthrough], + [get_user_by_id: fn member_id -> get_user_by_id_mock(member_id) end]} + ]) do + :ok + end + + test "Able to standardise groups with incomplete data" do + groups = [ + %{1 => 19, 2 => 20}, + %{3 => 18}, + %{4 => 15}, + %{5 => 11} + ] + + fixed_groups = BalanceLib.standardise_groups(groups) + + assert fixed_groups == [ + %{ + 1 => %{name: "User_1", rank: 0, rating: 19}, + 2 => %{name: "User_2", rank: 0, rating: 20} + }, + %{3 => %{name: "User_3", rank: 0, rating: 18}}, + %{4 => %{name: "User_4", rank: 0, rating: 15}}, + %{5 => %{name: "User_5", rank: 0, rating: 11}} + ] + + # loser_picks algo will hit the databases so let's just test with split_one_chevs + result = BalanceLib.create_balance(fixed_groups, 2, algorithm: "split_one_chevs") + assert result != nil + end + + test "Handle groups with incomplete data in create_balance" do + groups = [ + %{1 => 19, 2 => 20}, + %{3 => 18}, + %{4 => 15}, + %{5 => 11} + ] + + # loser_picks algo will hit the databases so let's just test with split_one_chevs + result = BalanceLib.create_balance(groups, 2, algorithm: "split_one_chevs") + assert result != nil + end + + defp get_user_by_id_mock(user_id) do + %{rank: 0, name: "User_#{user_id}"} + end +end diff --git a/test/teiserver/battle/split_one_chevs_internal_test.exs b/test/teiserver/battle/split_one_chevs_internal_test.exs index 0231185e1..8ecbccd40 100644 --- a/test/teiserver/battle/split_one_chevs_internal_test.exs +++ b/test/teiserver/battle/split_one_chevs_internal_test.exs @@ -4,30 +4,50 @@ defmodule Teiserver.Battle.SplitOneChevsInternalTest do Can run tests in this file only by mix test test/teiserver/battle/split_one_chevs_internal_test.exs """ - use Teiserver.DataCase, async: true + use ExUnit.Case @moduletag :balance_test alias Teiserver.Battle.Balance.SplitOneChevs - test "perform" do expanded_group = [ - %{count: 2, members: ["Pro1", "Noob1"], group_rating: 13, ratings: [8, 5], ranks: [1,0], names: ["Pro1", "Noob1"],}, - %{count: 1, members: ["Noob2"], group_rating: 6, ratings: [6], ranks: [0], names: ["Noob2"]}, - %{count: 1, members: ["Noob3"], group_rating: 7, ratings: [17], ranks: [0], names: ["Noob3"]} + %{ + count: 2, + members: ["Pro1", "Noob1"], + group_rating: 13, + ratings: [8, 5], + ranks: [1, 0], + names: ["Pro1", "Noob1"] + }, + %{ + count: 1, + members: ["Noob2"], + group_rating: 6, + ratings: [6], + ranks: [0], + names: ["Noob2"] + }, + %{ + count: 1, + members: ["Noob3"], + group_rating: 7, + ratings: [17], + ranks: [0], + names: ["Noob3"] + } ] result = SplitOneChevs.perform(expanded_group, 2) - assert result.team_groups == %{ - 1 => [ - %{count: 1, group_rating: 6, members: ["Noob2"], ratings: [6]}, - %{count: 1, group_rating: 8, members: ["Pro1"], ratings: [8]} - ], - 2 => [ - %{count: 1, group_rating: 5, members: ["Noob1"], ratings: [5]}, - %{count: 1, group_rating: 17, members: ["Noob3"], ratings: [17]} - ] - } + assert result.team_groups == %{ + 1 => [ + %{count: 1, group_rating: 6, members: ["Noob2"], ratings: [6]}, + %{count: 1, group_rating: 8, members: ["Pro1"], ratings: [8]} + ], + 2 => [ + %{count: 1, group_rating: 5, members: ["Noob1"], ratings: [5]}, + %{count: 1, group_rating: 17, members: ["Noob3"], ratings: [17]} + ] + } end test "sort members" do @@ -85,6 +105,4 @@ defmodule Teiserver.Battle.SplitOneChevsInternalTest do %{members: [], team_id: 3} ] end - - end diff --git a/test/teiserver/battle/split_one_chevs_test.exs b/test/teiserver/battle/split_one_chevs_test.exs index d2531e251..37412718a 100644 --- a/test/teiserver/battle/split_one_chevs_test.exs +++ b/test/teiserver/battle/split_one_chevs_test.exs @@ -3,7 +3,7 @@ defmodule Teiserver.Battle.SplitOneChevsTest do Can run tests in this file only by mix test test/teiserver/battle/split_one_chevs_test.exs """ - use Teiserver.DataCase, async: true + use ExUnit.Case @moduletag :balance_test alias Teiserver.Battle.BalanceLib @@ -85,7 +85,6 @@ defmodule Teiserver.Battle.SplitOneChevsTest do assert result.team_players == %{1 => [4, 1], 2 => [2, 3]} end - test "logs FFA" do result = BalanceLib.create_balance(