From f24fb5d1ff0313da350f8e20f9b290af7f002677 Mon Sep 17 00:00:00 2001 From: Joshua Augustinus Date: Mon, 21 Oct 2024 09:43:30 +1100 Subject: [PATCH] Add stdev diff penalty to brute_force and split_noobs Fix failing test --- lib/teiserver/battle/balance/brute_force.ex | 59 +++++++++++-------- .../battle/balance/brute_force_types.ex | 1 + lib/teiserver/battle/balance/split_noobs.ex | 1 + .../battle/brute_force_internal_test.exs | 52 +++++++++++----- test/teiserver/battle/brute_force_test.exs | 29 ++++----- test/teiserver/battle/respect_avoids_test.exs | 2 +- .../battle/split_noobs_internal_test.exs | 24 ++++---- test/teiserver/battle/split_noobs_test.exs | 45 +++++++------- 8 files changed, 126 insertions(+), 87 deletions(-) diff --git a/lib/teiserver/battle/balance/brute_force.ex b/lib/teiserver/battle/balance/brute_force.ex index eadd880d7..fc8a8d1b9 100644 --- a/lib/teiserver/battle/balance/brute_force.ex +++ b/lib/teiserver/battle/balance/brute_force.ex @@ -16,6 +16,7 @@ defmodule Teiserver.Battle.Balance.BruteForce do import Teiserver.Helper.NumberHelper, only: [format: 1] require Integer + @stdev_diff_importance 4 @party_importance 7 @splitter "------------------------------------------------------" @@ -110,45 +111,55 @@ defmodule Teiserver.Battle.Balance.BruteForce do players_with_index = Enum.with_index(players) # Go through every possibility and get the combination with the lowest score - result = - Enum.map(combos, fn x -> - get_players_from_indexes(x, players_with_index) - end) - |> Enum.map(fn team -> - result = score_combo(team, players, parties) - Map.put(result, :first_team, team) - end) - |> Enum.min_by(fn z -> - z.score - end) - - first_team = result.first_team + Enum.map(combos, fn x -> + get_players_from_indexes(x, players_with_index) + end) + |> Enum.map(fn team -> + score_combo(team, players, parties) + end) + |> Enum.min_by(fn z -> + z.score + end) + end - second_team = - players - |> Enum.filter(fn x -> - !Enum.any?(first_team, fn y -> - y.id == x.id - end) - end) + @spec get_second_team([BF.player()], [BF.player()]) :: [BF.player()] + def get_second_team(first_team, all_players) do + all_players + |> Enum.filter(fn player -> !Enum.any?(first_team, fn x -> x.id == player.id end) end) + end - Map.put(result, :second_team, second_team) + @spec get_st_dev([BF.player()]) :: any() + def get_st_dev(team) do + if(length(team) > 0) do + ratings = Enum.map(team, fn player -> player.rating end) + Statistics.stdev(ratings) + else + 0 + end end - @spec score_combo([BF.player()], [BF.player()], [String.t()]) :: any() + @spec score_combo([BF.player()], [BF.player()], [String.t()]) :: BF.combo_result() def score_combo(first_team, all_players, parties) do + second_team = get_second_team(first_team, all_players) first_team_rating = get_team_rating(first_team) both_team_rating = get_team_rating(all_players) rating_diff_penalty = abs(both_team_rating - first_team_rating * 2) broken_party_penalty = count_broken_parties(first_team, parties) * @party_importance - score = rating_diff_penalty + broken_party_penalty + stdev_diff_penalty = + abs(get_st_dev(first_team) - get_st_dev(second_team)) * + @stdev_diff_importance + + score = rating_diff_penalty + broken_party_penalty + stdev_diff_penalty %{ score: score, rating_diff_penalty: rating_diff_penalty, - broken_party_penalty: broken_party_penalty + broken_party_penalty: broken_party_penalty, + stdev_diff_penalty: stdev_diff_penalty, + first_team: first_team, + second_team: second_team } end diff --git a/lib/teiserver/battle/balance/brute_force_types.ex b/lib/teiserver/battle/balance/brute_force_types.ex index 1d3c41422..703d3a79e 100644 --- a/lib/teiserver/battle/balance/brute_force_types.ex +++ b/lib/teiserver/battle/balance/brute_force_types.ex @@ -18,6 +18,7 @@ defmodule Teiserver.Battle.Balance.BruteForceTypes do @type combo_result :: %{ broken_party_penalty: number(), rating_diff_penalty: number(), + stdev_diff_penalty: number(), score: number(), first_team: [player()], second_team: [player()] diff --git a/lib/teiserver/battle/balance/split_noobs.ex b/lib/teiserver/battle/balance/split_noobs.ex index dc7d0733e..6198ed963 100644 --- a/lib/teiserver/battle/balance/split_noobs.ex +++ b/lib/teiserver/battle/balance/split_noobs.ex @@ -246,6 +246,7 @@ defmodule Teiserver.Battle.Balance.SplitNoobs do "Brute force result:", "Team rating diff penalty: #{format(combo_result.rating_diff_penalty)}", "Broken party penalty: #{combo_result.broken_party_penalty}", + "Stdev diff penalty: #{format(combo_result.stdev_diff_penalty)}", "Score: #{format(combo_result.score)} (lower is better)", @splitter, "Draft remaining players (ordered from best to worst).", diff --git a/test/teiserver/battle/brute_force_internal_test.exs b/test/teiserver/battle/brute_force_internal_test.exs index fa467b214..d79ffcea5 100644 --- a/test/teiserver/battle/brute_force_internal_test.exs +++ b/test/teiserver/battle/brute_force_internal_test.exs @@ -118,34 +118,54 @@ defmodule Teiserver.Battle.BruteForceInternalTest do assert result == %{ broken_party_penalty: 0, + first_team: [ + %{id: 1, name: "kyutoryu", rating: 12.25}, + %{id: 2, name: "fbots1998", rating: 13.98}, + %{id: 3, name: "Dixinormus", rating: 18.28}, + %{id: 4, name: "HungDaddy", rating: 2.8}, + %{id: 5, name: "SLOPPYGAGGER", rating: 8.89}, + %{id: 6, name: "jauggy", rating: 20.49} + ], rating_diff_penalty: 11.670000000000044, - score: 11.670000000000044 + score: 13.987048974705417, + second_team: [ + %{id: 7, name: "reddragon2010", rating: 18.4}, + %{id: 8, name: "Aposis", rating: 20.42}, + %{id: 9, name: "MaTThiuS_82", rating: 8.26}, + %{id: 10, name: "Noody", rating: 17.64}, + %{id: 11, name: "[DTG]BamBin0", rating: 20.06}, + %{id: 12, name: "barmalev", rating: 3.58} + ], + stdev_diff_penalty: 2.317048974705372 } best_combo = BruteForce.get_best_combo(combos, input.players, input.parties) assert best_combo == %{ broken_party_penalty: 0, - rating_diff_penalty: 0.5100000000000477, - score: 0.5100000000000477, first_team: [ %{id: 1, name: "kyutoryu", rating: 12.25}, %{id: 2, name: "fbots1998", rating: 13.98}, %{id: 5, name: "SLOPPYGAGGER", rating: 8.89}, %{id: 6, name: "jauggy", rating: 20.49}, - %{id: 7, name: "reddragon2010", rating: 18.4}, - %{id: 9, name: "MaTThiuS_82", rating: 8.26} + %{id: 8, name: "Aposis", rating: 20.42}, + %{id: 12, name: "barmalev", rating: 3.58} ], + rating_diff_penalty: 5.830000000000041, + score: 7.322550984245979, second_team: [ %{id: 3, name: "Dixinormus", rating: 18.28}, %{id: 4, name: "HungDaddy", rating: 2.8}, - %{id: 8, name: "Aposis", rating: 20.42}, + %{id: 7, name: "reddragon2010", rating: 18.4}, + %{id: 9, name: "MaTThiuS_82", rating: 8.26}, %{id: 10, name: "Noody", rating: 17.64}, - %{id: 11, name: "[DTG]BamBin0", rating: 20.06}, - %{id: 12, name: "barmalev", rating: 3.58} - ] + %{id: 11, name: "[DTG]BamBin0", rating: 20.06} + ], + stdev_diff_penalty: 1.4925509842459377 } + # The stdev team 1 = 5.47, and team 2 = 6.91 + result = BruteForce.standardise_result(best_combo, input.parties) |> Map.drop([:logs]) assert result == %{ @@ -155,19 +175,19 @@ defmodule Teiserver.Battle.BruteForceInternalTest do %{count: 1, group_rating: 13.98, members: [2], ratings: [13.98]}, %{count: 1, group_rating: 8.89, members: [5], ratings: [8.89]}, %{count: 1, group_rating: 20.49, members: [6], ratings: [20.49]}, - %{count: 1, group_rating: 18.4, members: [7], ratings: [18.4]}, - %{count: 1, group_rating: 8.26, members: [9], ratings: [8.26]} + %{count: 1, group_rating: 20.42, members: ~c"\b", ratings: [20.42]}, + %{count: 1, group_rating: 3.58, members: ~c"\f", ratings: [3.58]} ], 2 => [ %{count: 1, group_rating: 18.28, members: [3], ratings: [18.28]}, %{count: 1, group_rating: 2.8, members: [4], ratings: [2.8]}, - %{count: 1, group_rating: 20.42, members: [8], ratings: [20.42]}, - %{count: 1, group_rating: 17.64, members: [10], ratings: [17.64]}, - %{count: 1, group_rating: 20.06, members: [11], ratings: [20.06]}, - %{count: 1, group_rating: 3.58, members: [12], ratings: [3.58]} + %{count: 1, group_rating: 18.4, members: ~c"\a", ratings: [18.4]}, + %{count: 1, group_rating: 8.26, members: ~c"\t", ratings: [8.26]}, + %{count: 1, members: ~c"\n", ratings: [17.64], group_rating: 17.64}, + %{count: 1, group_rating: 20.06, members: ~c"\v", ratings: [20.06]} ] }, - team_players: %{1 => [1, 2, 5, 6, 7, 9], 2 => [3, 4, 8, 10, 11, 12]} + team_players: %{1 => [1, 2, 5, 6, 8, 12], 2 => [3, 4, 7, 9, 10, 11]} } end diff --git a/test/teiserver/battle/brute_force_test.exs b/test/teiserver/battle/brute_force_test.exs index 673d29a16..6f835a233 100644 --- a/test/teiserver/battle/brute_force_test.exs +++ b/test/teiserver/battle/brute_force_test.exs @@ -102,6 +102,7 @@ defmodule Teiserver.Battle.BruteForceTest do result = BruteForce.perform(expanded_group, 2) |> Map.drop([:logs]) + # If we us a stdev penalty of less than 4, then all the 20+ players end up on the same team assert result == %{ team_groups: %{ 1 => [ @@ -109,28 +110,28 @@ defmodule Teiserver.Battle.BruteForceTest do %{count: 1, group_rating: 13.98, members: ["fbots1998"], ratings: [13.98]}, %{count: 1, group_rating: 8.89, members: ["SLOPPYGAGGER"], ratings: [8.89]}, %{count: 1, group_rating: 20.49, members: ["jauggy"], ratings: [20.49]}, - %{count: 1, group_rating: 18.4, members: ["reddragon2010"], ratings: [18.4]}, - %{count: 1, group_rating: 8.26, members: ["MaTThiuS_82"], ratings: [8.26]} + %{count: 1, group_rating: 20.42, members: ["Aposis"], ratings: [20.42]}, + %{count: 1, group_rating: 3.58, members: ["barmalev"], ratings: [3.58]} ], 2 => [ %{count: 1, group_rating: 18.28, members: ["Dixinormus"], ratings: [18.28]}, %{count: 1, group_rating: 2.8, members: ["HungDaddy"], ratings: [2.8]}, - %{count: 1, group_rating: 20.42, members: ["Aposis"], ratings: [20.42]}, - %{count: 1, group_rating: 17.64, members: ["Noody"], ratings: [17.64]}, - %{count: 1, group_rating: 20.06, members: ["[DTG]BamBin0"], ratings: [20.06]}, - %{count: 1, group_rating: 3.58, members: ["barmalev"], ratings: [3.58]} + %{count: 1, group_rating: 18.4, members: ["reddragon2010"], ratings: [18.4]}, + %{count: 1, group_rating: 8.26, members: ["MaTThiuS_82"], ratings: [8.26]}, + %{count: 1, members: ["Noody"], ratings: [17.64], group_rating: 17.64}, + %{count: 1, group_rating: 20.06, members: ["[DTG]BamBin0"], ratings: [20.06]} ] }, team_players: %{ - 1 => [ - "kyutoryu", - "fbots1998", - "SLOPPYGAGGER", - "jauggy", + 1 => ["kyutoryu", "fbots1998", "SLOPPYGAGGER", "jauggy", "Aposis", "barmalev"], + 2 => [ + "Dixinormus", + "HungDaddy", "reddragon2010", - "MaTThiuS_82" - ], - 2 => ["Dixinormus", "HungDaddy", "Aposis", "Noody", "[DTG]BamBin0", "barmalev"] + "MaTThiuS_82", + "Noody", + "[DTG]BamBin0" + ] } } end diff --git a/test/teiserver/battle/respect_avoids_test.exs b/test/teiserver/battle/respect_avoids_test.exs index 1dada1240..e65a145e9 100644 --- a/test/teiserver/battle/respect_avoids_test.exs +++ b/test/teiserver/battle/respect_avoids_test.exs @@ -425,7 +425,7 @@ defmodule Teiserver.Battle.RespectAvoidsTest do "Avoid min time required: 2 h", "Avoids considered: 0 (Max: 6)", "------------------------------------------------------", - "New players: None", + "Solo new players: None", "------------------------------------------------------", "Perform brute force with the following players to get the best score.", "Players: fraqzilla, Spaceh, [DmE], Jeff, Jarial, AcowAdonis, AbyssWatcher, Gmans, MeowCat, mighty, Threekey, Zippo9, Kaa, Faeton", diff --git a/test/teiserver/battle/split_noobs_internal_test.exs b/test/teiserver/battle/split_noobs_internal_test.exs index 1d0ff9fef..f204dc937 100644 --- a/test/teiserver/battle/split_noobs_internal_test.exs +++ b/test/teiserver/battle/split_noobs_internal_test.exs @@ -596,12 +596,12 @@ defmodule Teiserver.Battle.SplitNoobsInternalTest do }, %{ id: "reddragon2010", + in_party?: false, index: 6, name: "reddragon2010", - uncertainty: 3, - rating: 18.4, rank: 2, - in_party?: false + rating: 18.4, + uncertainty: 3 }, %{ id: "Noody", @@ -614,7 +614,7 @@ defmodule Teiserver.Battle.SplitNoobsInternalTest do } ], rating_diff_penalty: 6.203564356435635, - score: 6.203564356435635, + score: 7.221262567387239, second_team: [ %{ id: "Dixinormus", @@ -653,12 +653,12 @@ defmodule Teiserver.Battle.SplitNoobsInternalTest do }, %{ id: "MaTThiuS_82", + in_party?: false, index: 9, name: "MaTThiuS_82", - uncertainty: 3, - rating: 8.26, rank: 2, - in_party?: false + rating: 8.26, + uncertainty: 3 }, %{ id: "barmalev", @@ -669,7 +669,8 @@ defmodule Teiserver.Battle.SplitNoobsInternalTest do rating: 3.58, uncertainty: 3 } - ] + ], + stdev_diff_penalty: 1.017698210951604 } standard_result = SplitNoobs.standardise_result(result, initial_state) @@ -692,7 +693,8 @@ defmodule Teiserver.Battle.SplitNoobsInternalTest do "Brute force result:", "Team rating diff penalty: 6.2", "Broken party penalty: 0", - "Score: 6.2 (lower is better)", + "Stdev diff penalty: 1.0", + "Score: 7.2 (lower is better)", "------------------------------------------------------", "Draft remaining players (ordered from best to worst).", "Remaining: Dixinormus, HungDaddy", @@ -722,7 +724,7 @@ defmodule Teiserver.Battle.SplitNoobsInternalTest do ratings: [2.768316831683173] }, %{count: 1, group_rating: 20.06, members: ["[DTG]BamBin0"], ratings: [20.06]}, - %{count: 1, members: ["reddragon2010"], ratings: [18.4], group_rating: 18.4}, + %{count: 1, group_rating: 18.4, members: ["reddragon2010"], ratings: [18.4]}, %{count: 1, group_rating: 17.64, members: ["Noody"], ratings: [17.64]} ], 2 => [ @@ -735,7 +737,7 @@ defmodule Teiserver.Battle.SplitNoobsInternalTest do %{count: 1, group_rating: 20.49, members: ["jauggy"], ratings: [20.49]}, %{count: 1, group_rating: 20.42, members: ["Aposis"], ratings: [20.42]}, %{count: 1, group_rating: 8.89, members: ["SLOPPYGAGGER"], ratings: [8.89]}, - %{count: 1, members: ["MaTThiuS_82"], ratings: [8.26], group_rating: 8.26}, + %{count: 1, group_rating: 8.26, members: ["MaTThiuS_82"], ratings: [8.26]}, %{count: 1, group_rating: 3.58, members: ["barmalev"], ratings: [3.58]} ] }, diff --git a/test/teiserver/battle/split_noobs_test.exs b/test/teiserver/battle/split_noobs_test.exs index f82d9e400..3d32b7526 100644 --- a/test/teiserver/battle/split_noobs_test.exs +++ b/test/teiserver/battle/split_noobs_test.exs @@ -102,35 +102,36 @@ defmodule Teiserver.Battle.SplitNoobsTest do result = SplitNoobs.perform(expanded_group, 2) |> Map.drop([:logs]) + # If we use stdev importance less than 4, all 20+ players end up on same team assert result == %{ team_groups: %{ 1 => [ %{count: 1, group_rating: 13.98, members: ["fbots1998"], ratings: [13.98]}, %{count: 1, group_rating: 12.25, members: ["kyutoryu"], ratings: [12.25]}, %{count: 1, group_rating: 20.49, members: ["jauggy"], ratings: [20.49]}, - %{count: 1, group_rating: 18.4, members: ["reddragon2010"], ratings: [18.4]}, + %{count: 1, group_rating: 20.42, members: ["Aposis"], ratings: [20.42]}, %{count: 1, group_rating: 8.89, members: ["SLOPPYGAGGER"], ratings: [8.89]}, - %{count: 1, group_rating: 8.26, members: ["MaTThiuS_82"], ratings: [8.26]} + %{count: 1, group_rating: 3.58, members: ["barmalev"], ratings: [3.58]} ], 2 => [ - %{count: 1, group_rating: 20.42, members: ["Aposis"], ratings: [20.42]}, %{count: 1, group_rating: 20.06, members: ["[DTG]BamBin0"], ratings: [20.06]}, - %{count: 1, group_rating: 18.28, members: ["Dixinormus"], ratings: [18.28]}, - %{count: 1, group_rating: 17.64, members: ["Noody"], ratings: [17.64]}, - %{count: 1, group_rating: 3.58, members: ["barmalev"], ratings: [3.58]}, + %{count: 1, group_rating: 18.4, members: ["reddragon2010"], ratings: [18.4]}, + %{count: 1, members: ["Dixinormus"], ratings: [18.28], group_rating: 18.28}, + %{count: 1, members: ["Noody"], ratings: [17.64], group_rating: 17.64}, + %{count: 1, group_rating: 8.26, members: ["MaTThiuS_82"], ratings: [8.26]}, %{count: 1, group_rating: 2.8, members: ["HungDaddy"], ratings: [2.8]} ] }, team_players: %{ - 1 => [ - "fbots1998", - "kyutoryu", - "jauggy", + 1 => ["fbots1998", "kyutoryu", "jauggy", "Aposis", "SLOPPYGAGGER", "barmalev"], + 2 => [ + "[DTG]BamBin0", "reddragon2010", - "SLOPPYGAGGER", - "MaTThiuS_82" - ], - 2 => ["Aposis", "[DTG]BamBin0", "Dixinormus", "Noody", "barmalev", "HungDaddy"] + "Dixinormus", + "Noody", + "MaTThiuS_82", + "HungDaddy" + ] } } end @@ -413,14 +414,15 @@ defmodule Teiserver.Battle.SplitNoobsTest do "Brute force result:", "Team rating diff penalty: 1", "Broken party penalty: 0", - "Score: 1 (lower is better)", + "Stdev diff penalty: 0.2", + "Score: 1.2 (lower is better)", "------------------------------------------------------", "Draft remaining players (ordered from best to worst).", "Remaining: StinkBee, HoldButyLeg", "------------------------------------------------------", "Final result:", - "Team 1: CowOfWar, Akio, Regithros, Orii, DUFFY, LuBaee, TimeContainer, HoldButyLeg", - "Team 2: nubl, Darth, 976, onse, Theo45, PotatoesHead, colossus, StinkBee" + "Team 1: Akio, Darth, Regithros, 976, DUFFY, LuBaee, TimeContainer, HoldButyLeg", + "Team 2: CowOfWar, nubl, onse, Theo45, PotatoesHead, colossus, Orii, StinkBee" ] # Note DUFFY (Strongest captain) is on same team with noobiest noob HoldButyLeg @@ -576,16 +578,17 @@ defmodule Teiserver.Battle.SplitNoobsTest do "Players: Raigeki, Engolianth, Demodred, FRODODOR, shoeofobama, Larch, Artifical_Banana, Cobaltstore, quest, SHAAARKBATE, illusiveman2024, UnreasonableIkko, ColorlesScum, Renkei", "------------------------------------------------------", "Brute force result:", - "Team rating diff penalty: 0.0", + "Team rating diff penalty: 1.5", "Broken party penalty: 0", - "Score: 0.0 (lower is better)", + "Stdev diff penalty: 27.4", + "Score: 28.9 (lower is better)", "------------------------------------------------------", "Draft remaining players (ordered from best to worst).", "Remaining: MrKicks, BIL", "------------------------------------------------------", "Final result:", - "Team 1: Renkei, ColorlesScum, UnreasonableIkko, SHAAARKBATE, shoeofobama, FRODODOR, Raigeki, BIL", - "Team 2: illusiveman2024, quest, Cobaltstore, Artifical_Banana, Larch, Demodred, Engolianth, MrKicks" + "Team 1: UnreasonableIkko, illusiveman2024, SHAAARKBATE, quest, Cobaltstore, Artifical_Banana, Raigeki, BIL", + "Team 2: Renkei, ColorlesScum, Larch, shoeofobama, FRODODOR, Demodred, Engolianth, MrKicks" ] end end