From b38382ab1798dc6712458b373ded2a55071503b7 Mon Sep 17 00:00:00 2001 From: jauggy Date: Fri, 18 Oct 2024 09:01:42 +1100 Subject: [PATCH] Add captain diff penalty to respect avoids Fix new player in parties --- .../battle/balance/brute_force_avoid.ex | 62 +++--- .../battle/balance/brute_force_avoid_types.ex | 1 + .../battle/balance/respect_avoids.ex | 9 +- .../brute_force_avoid_internal_test.exs | 83 ++++++++ test/teiserver/battle/respect_avoids_test.exs | 178 +++++++++++++++++- 5 files changed, 303 insertions(+), 30 deletions(-) create mode 100644 test/teiserver/battle/brute_force_avoid_internal_test.exs diff --git a/lib/teiserver/battle/balance/brute_force_avoid.ex b/lib/teiserver/battle/balance/brute_force_avoid.ex index 8aa94d5ca..e5075baaa 100644 --- a/lib/teiserver/battle/balance/brute_force_avoid.ex +++ b/lib/teiserver/battle/balance/brute_force_avoid.ex @@ -22,6 +22,7 @@ defmodule Teiserver.Battle.Balance.BruteForceAvoid do @max_team_diff_importance 10000 @party_importance 1000 @avoid_importance 10 + @captain_diff_importance 1 def get_best_combo(players, avoids, parties) do potential_teams = potential_teams(length(players)) @@ -34,27 +35,13 @@ defmodule Teiserver.Battle.Balance.BruteForceAvoid 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 -> - team = get_players_from_indexes(x, players_with_index) - result = score_combo(team, players, avoids, parties) - Map.put(result, :first_team, team) - end) - |> Enum.min_by(fn z -> - z.score - end) - - first_team = result.first_team - - second_team = - players - |> Enum.filter(fn x -> - !Enum.any?(first_team, fn y -> - y.id == x.id - end) - end) - - Map.put(result, :second_team, second_team) + Enum.map(combos, fn x -> + team = get_players_from_indexes(x, players_with_index) + score_combo(team, players, avoids, parties) + end) + |> Enum.min_by(fn z -> + z.score + end) end @spec potential_teams(integer()) :: any() @@ -71,11 +58,34 @@ defmodule Teiserver.Battle.Balance.BruteForceAvoid do max(total_lobby_rating / num_teams * percentage_of_team, @max_team_diff_abs) end - @spec score_combo([BF.player()], [BF.player()], [[number()]], [[number()]]) :: any() + @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 + + @spec get_captain_rating([BF.player()]) :: any() + def get_captain_rating(team) do + if(length(team) > 0) do + captain = Enum.max_by(team, fn player -> player.rating end, &>=/2) + captain.rating + else + 0 + end + end + + @spec score_combo([BF.player()], [BF.player()], [[number()]], [[number()]]) :: BF.combo_result() def score_combo(first_team, all_players, avoids, 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) + + captain_diff_penalty = + abs(get_captain_rating(first_team) - get_captain_rating(second_team)) * + @captain_diff_importance + num_teams = 2 max_team_diff_penalty = @@ -100,13 +110,17 @@ defmodule Teiserver.Battle.Balance.BruteForceAvoid do end score = - rating_diff_penalty + broken_avoid_penalty + broken_party_penalty + max_team_diff_penalty + rating_diff_penalty + broken_avoid_penalty + broken_party_penalty + max_team_diff_penalty + + captain_diff_penalty %{ score: score, rating_diff_penalty: rating_diff_penalty, broken_avoid_penalty: broken_avoid_penalty, - broken_party_penalty: broken_party_penalty + broken_party_penalty: broken_party_penalty, + captain_diff_penalty: captain_diff_penalty, + second_team: second_team, + first_team: first_team } end diff --git a/lib/teiserver/battle/balance/brute_force_avoid_types.ex b/lib/teiserver/battle/balance/brute_force_avoid_types.ex index cd11c1bee..bf1635636 100644 --- a/lib/teiserver/battle/balance/brute_force_avoid_types.ex +++ b/lib/teiserver/battle/balance/brute_force_avoid_types.ex @@ -19,6 +19,7 @@ defmodule Teiserver.Battle.Balance.BruteForceAvoidTypes do broken_avoid_penalty: number(), broken_party_penalty: number(), rating_diff_penalty: number(), + captain_diff_penalty: number(), score: number(), first_team: [player()], second_team: [player()] diff --git a/lib/teiserver/battle/balance/respect_avoids.ex b/lib/teiserver/battle/balance/respect_avoids.ex index 73371ddcd..7f9ef0d42 100644 --- a/lib/teiserver/battle/balance/respect_avoids.ex +++ b/lib/teiserver/battle/balance/respect_avoids.ex @@ -53,7 +53,7 @@ defmodule Teiserver.Battle.Balance.RespectAvoids do players = flatten_members(expanded_group) parties = get_parties(expanded_group) - noobs = get_noobs(players) |> sort_noobs() + noobs = get_solo_noobs(players) |> sort_noobs() experienced_players = get_experienced_players(players, noobs) experienced_player_ids = experienced_players |> Enum.map(fn x -> x.id end) players_in_parties_count = parties |> List.flatten() |> Enum.count() @@ -297,6 +297,7 @@ defmodule Teiserver.Battle.Balance.RespectAvoids do "Team rating diff penalty: #{format(combo_result.rating_diff_penalty)}", "Broken party penalty: #{combo_result.broken_party_penalty}", "Broken avoid penalty: #{combo_result.broken_avoid_penalty}", + "Captain rating diff penalty: #{format(combo_result.captain_diff_penalty)}", "Score: #{format(combo_result.score)} (lower is better)", @splitter, "Draft remaining players (ordered from best to worst).", @@ -514,10 +515,10 @@ defmodule Teiserver.Battle.Balance.RespectAvoids do end # Noobs have high uncertainty and chev 1,2,3 - @spec get_noobs([RA.player()]) :: any() - def get_noobs(players) do + @spec get_solo_noobs([RA.player()]) :: any() + def get_solo_noobs(players) do Enum.filter(players, fn player -> - is_newish_player?(player.rank, player.uncertainty) + is_newish_player?(player.rank, player.uncertainty) && !player.in_party? end) end diff --git a/test/teiserver/battle/brute_force_avoid_internal_test.exs b/test/teiserver/battle/brute_force_avoid_internal_test.exs new file mode 100644 index 000000000..d02521747 --- /dev/null +++ b/test/teiserver/battle/brute_force_avoid_internal_test.exs @@ -0,0 +1,83 @@ +defmodule Teiserver.Battle.BruteForceAvoidInternalTest do + @moduledoc """ + Can run all balance tests via + mix test --only balance_test + """ + use ExUnit.Case + @moduletag :balance_test + alias Teiserver.Battle.Balance.BruteForceAvoid + + test "can get second team" do + players = [ + %{name: "kyutoryu", rating: 12.25, id: 1}, + %{name: "fbots1998", rating: 13.98, id: 2}, + %{name: "Dixinormus", rating: 18.28, id: 3}, + %{name: "HungDaddy", rating: 2.8, id: 4}, + %{name: "SLOPPYGAGGER", rating: 8.89, id: 5}, + %{name: "jauggy", rating: 20.49, id: 6}, + %{name: "reddragon2010", rating: 18.4, id: 7}, + %{name: "Aposis", rating: 20.42, id: 8}, + %{name: "MaTThiuS_82", rating: 8.26, id: 9}, + %{name: "Noody", rating: 17.64, id: 10}, + %{name: "[DTG]BamBin0", rating: 20.06, id: 11}, + %{name: "barmalev", rating: 3.58, id: 12} + ] + + first_team = [ + %{name: "kyutoryu", rating: 12.25, id: 1}, + %{name: "fbots1998", rating: 13.98, id: 2}, + %{name: "Dixinormus", rating: 18.28, id: 3}, + %{name: "HungDaddy", rating: 2.8, id: 4}, + %{name: "SLOPPYGAGGER", rating: 8.89, id: 5}, + %{name: "jauggy", rating: 20.49, id: 6} + ] + + result = BruteForceAvoid.get_second_team(first_team, players) + + assert result == [ + %{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} + ] + end + + test "can get second team - side cases" do + players = [] + + first_team = [] + + result = BruteForceAvoid.get_second_team(first_team, players) + + assert result == [] + + players = [%{id: 7, name: "reddragon2010", rating: 18.4}] + + first_team = [%{id: 7, name: "reddragon2010", rating: 18.4}] + + result = BruteForceAvoid.get_second_team(first_team, players) + + assert result == [] + end + + test "can get captain rating" do + first_team = [ + %{name: "kyutoryu", rating: 12.25, id: 1}, + %{name: "fbots1998", rating: 13.98, id: 2}, + %{name: "Dixinormus", rating: 18.28, id: 3}, + %{name: "HungDaddy", rating: 2.8, id: 4}, + %{name: "SLOPPYGAGGER", rating: 8.89, id: 5}, + %{name: "jauggy", rating: 20.49, id: 6} + ] + + result = BruteForceAvoid.get_captain_rating(first_team) + + assert result == 20.49 + + result = BruteForceAvoid.get_captain_rating([]) + + assert result == 0 + end +end diff --git a/test/teiserver/battle/respect_avoids_test.exs b/test/teiserver/battle/respect_avoids_test.exs index 635c8ef59..5d796f130 100644 --- a/test/teiserver/battle/respect_avoids_test.exs +++ b/test/teiserver/battle/respect_avoids_test.exs @@ -3,12 +3,19 @@ defmodule Teiserver.Battle.RespectAvoidsTest do Can run all balance tests via mix test --only balance_test """ - use ExUnit.Case, async: false + use ExUnit.Case import Mock @moduletag :balance_test alias Teiserver.Battle.Balance.RespectAvoids alias Teiserver.Account.RelationshipLib + alias Teiserver.Config + + setup_with_mocks([ + {Config, [:passthrough], [get_site_config_cache: fn key -> 2 end]} + ]) do + :ok + end test "can process expanded_group" do # Setup mocks with no avoids (insteading of calling db) @@ -123,7 +130,8 @@ defmodule Teiserver.Battle.RespectAvoidsTest do "Team rating diff penalty: 0.5", "Broken party penalty: 0", "Broken avoid penalty: 0", - "Score: 0.5 (lower is better)", + "Captain rating diff penalty: 0.1", + "Score: 0.6 (lower is better)", "------------------------------------------------------", "Draft remaining players (ordered from best to worst).", "Remaining: ", @@ -259,6 +267,7 @@ defmodule Teiserver.Battle.RespectAvoidsTest do "Team rating diff penalty: 0.7", "Broken party penalty: 0", "Broken avoid penalty: 0", + "Captain rating diff penalty: 0.1", "Score: 0.7 (lower is better)", "------------------------------------------------------", "Draft remaining players (ordered from best to worst).", @@ -272,4 +281,169 @@ defmodule Teiserver.Battle.RespectAvoidsTest do # Notice in result jauggy no longer on same team as reddragon2010 due to avoidance end end + + test "gives each team the best captain" do + # Setup mocks with no avoids (insteading of calling db) + with_mock(RelationshipLib, + get_lobby_avoids: fn _player_ids, _limit, _player_limit, _minimum_time_hours -> [] end, + get_lobby_avoids: fn _player_ids, _limit, _player_limit -> [] end + ) do + expanded_group = [ + %{ + count: 2, + members: ["fraqzilla", "Spaceh"], + ratings: [4, 0], + names: ["fraqzilla", "Spaceh"], + uncertainties: [0, 1], + ranks: [1, 1] + }, + %{ + count: 1, + members: ["[DmE]"], + ratings: [34], + names: ["[DmE]"], + uncertainties: [2], + ranks: [0] + }, + %{ + count: 1, + members: ["Jeff"], + ratings: [31], + names: ["Jeff"], + uncertainties: [3], + ranks: [2] + }, + %{ + count: 1, + members: ["Jarial"], + ratings: [26], + names: ["Jarial"], + uncertainties: [0], + ranks: [2] + }, + %{ + count: 1, + members: ["Gmans"], + ratings: [21], + names: ["Gmans"], + uncertainties: [3], + ranks: [2] + }, + %{ + count: 1, + members: ["Threekey"], + ratings: [18], + names: ["Threekey"], + uncertainties: [3], + ranks: [2] + }, + %{ + count: 1, + members: ["Zippo9"], + ratings: [17], + names: ["Zippo9"], + uncertainties: [3], + ranks: [2] + }, + %{ + count: 1, + members: ["AcowAdonis"], + ratings: [25], + names: ["AcowAdonis"], + uncertainties: [3], + ranks: [2] + }, + %{ + count: 1, + members: ["AbyssWatcher"], + ratings: [23], + names: ["AbyssWatcher"], + uncertainties: [3], + ranks: [2] + }, + %{ + count: 1, + members: ["MeowCat"], + ratings: [21], + names: ["MeowCat"], + uncertainties: [3], + ranks: [2] + }, + %{ + count: 1, + members: ["mighty"], + ratings: [21], + names: ["mighty"], + uncertainties: [3], + ranks: [2] + }, + %{ + count: 1, + members: ["Kaa"], + ratings: [16], + names: ["Kaa"], + uncertainties: [3], + ranks: [2] + }, + %{ + count: 1, + members: ["Faeton"], + ratings: [11], + names: ["Faeton"], + uncertainties: [3], + ranks: [2] + }, + %{ + count: 1, + members: ["Saffir"], + ratings: [10], + names: ["Saffir"], + uncertainties: [3], + ranks: [2] + }, + %{ + count: 1, + members: ["Pantsu_Ripper"], + ratings: [10], + names: ["Pantsu_Ripper"], + uncertainties: [3], + ranks: [2] + } + ] + + result = RespectAvoids.perform(expanded_group, 2) + + assert result.logs == [ + "------------------------------------------------------", + "Algorithm: respect_avoids", + "------------------------------------------------------", + "This algorithm will try and respect parties and avoids of players so long as it can keep team rating difference within certain bounds. Parties have higher importance than avoids.", + "Recent avoids will be ignored. New players will be spread evenly across teams and cannot be avoided.", + "------------------------------------------------------", + "Lobby details:", + "Parties: (fraqzilla, Spaceh)", + "Avoid min time required: 2 h", + "Avoids considered: 0 (Max: 6)", + "------------------------------------------------------", + "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", + "------------------------------------------------------", + "Brute force result:", + "Team rating diff penalty: 2", + "Broken party penalty: 0", + "Broken avoid penalty: 0", + "Captain rating diff penalty: 3", + "Score: 5 (lower is better)", + "------------------------------------------------------", + "Draft remaining players (ordered from best to worst).", + "Remaining: Saffir, Pantsu_Ripper", + "------------------------------------------------------", + "Final result:", + "Team 1: Gmans, AbyssWatcher, AcowAdonis, Jarial, [DmE], Spaceh, fraqzilla, Pantsu_Ripper", + "Team 2: Faeton, Kaa, Zippo9, Threekey, mighty, MeowCat, Jeff, Saffir" + ] + end + end end