-
Notifications
You must be signed in to change notification settings - Fork 54
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
9 changed files
with
706 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,242 @@ | ||
defmodule Teiserver.Battle.Balance.BruteForce do | ||
@moduledoc """ | ||
Algorithm idea by Suuwassea | ||
Adjusted by jauggy | ||
Overview: | ||
Go through every possible combination and pick the best one. Ideal for keeping parties together. | ||
The best combination will have the lowest score. | ||
Score = difference in team rating + broken party penalty | ||
broken party penalty = num broken parties * broken party multiplier | ||
Only use for games with two teams and <=16 players | ||
""" | ||
alias Teiserver.Battle.Balance.BalanceTypes, as: BT | ||
alias Teiserver.Battle.Balance.BruteForceTypes, as: BF | ||
import Teiserver.Helper.NumberHelper, only: [format: 1] | ||
require Integer | ||
|
||
@broken_party_multiplier 3 | ||
@splitter "------------------------------------------------------" | ||
|
||
@doc """ | ||
Main entry point used by balance_lib | ||
""" | ||
@spec perform([BT.expanded_group()], non_neg_integer(), list()) :: any() | ||
def perform(expanded_group, team_count, opts \\ []) do | ||
input_data = %{ | ||
players: flatten_members(expanded_group), | ||
parties: get_parties(expanded_group) | ||
} | ||
|
||
case should_use_algo?(input_data, team_count) do | ||
:ok -> | ||
potential_teams = potential_teams(length(input_data.players)) | ||
best_combo = get_best_combo(potential_teams, input_data.players, input_data.parties) | ||
standardise_result(best_combo, input_data.players) | ||
|
||
{:error, message} -> | ||
# Call another balancer | ||
result = Teiserver.Battle.Balance.LoserPicks.perform(expanded_group, team_count, opts) | ||
|
||
new_logs = | ||
["#{message} Will use another balance algorithm instead.", @splitter, result.logs] | ||
|> List.flatten() | ||
|
||
Map.put(result, :logs, new_logs) | ||
end | ||
end | ||
|
||
@doc """ | ||
Use this algo if two teams and <=16 players | ||
""" | ||
@spec should_use_algo?(BR.input_data(), integer()) :: :ok | {:error, String.t()} | ||
def should_use_algo?(input_data, team_count) do | ||
num_players = length(input_data[:players]) | ||
|
||
cond do | ||
team_count != 2 -> {:error, "Team count doesn't equal two."} | ||
num_players > 16 -> {:error, "Number of players greater than 16."} | ||
Integer.is_odd(num_players) -> {:error, "Odd number of players."} | ||
true -> :ok | ||
end | ||
end | ||
|
||
@doc """ | ||
Remove all groups/parties and treats everyone as solo players. This algorithm doesn't support parties. | ||
See split_one_chevs_internal_test.exs for sample input | ||
""" | ||
def flatten_members(expanded_group) do | ||
for %{ | ||
members: members, | ||
ratings: ratings, | ||
ranks: ranks, | ||
names: names, | ||
uncertainties: uncertainties | ||
} <- expanded_group, | ||
# Zipping will create binary tuples from 2 lists | ||
{id, rating, _rank, name, _uncertainty} <- | ||
Enum.zip([members, ratings, ranks, names, uncertainties]), | ||
# Create result value | ||
do: %{ | ||
rating: rating, | ||
name: name, | ||
id: id | ||
} | ||
end | ||
|
||
@spec get_parties([BT.expanded_group()]) :: [String.t()] | ||
def get_parties(expanded_group) do | ||
Enum.filter(expanded_group, fn x -> | ||
x[:count] >= 2 | ||
end) | ||
|> Enum.map(fn y -> | ||
y[:names] | ||
end) | ||
end | ||
|
||
@spec potential_teams(integer()) :: any() | ||
def potential_teams(num_players) do | ||
Teiserver.Helper.CombinationsHelper.get_combinations(num_players) | ||
end | ||
|
||
@spec get_best_combo([integer()], [BF.player()], [String.t()]) :: BF.combo_result() | ||
def get_best_combo(combos, players, parties) do | ||
Enum.map(combos, fn x -> | ||
get_players_from_indexes(x, players) | ||
end) | ||
|> Enum.map(fn team -> | ||
result = score_combo(team, players, parties) | ||
Map.put(result, :team, team) | ||
end) | ||
|> Enum.min_by(fn z -> | ||
z.score | ||
end) | ||
end | ||
|
||
@spec score_combo([BF.player()], [BF.player()], [String.t()]) :: any() | ||
def score_combo(first_team, all_players, parties) do | ||
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) * @broken_party_multiplier | ||
|
||
score = rating_diff_penalty + broken_party_penalty | ||
|
||
%{ | ||
score: score, | ||
rating_diff_penalty: rating_diff_penalty, | ||
broken_party_penalty: broken_party_penalty | ||
} | ||
end | ||
|
||
def get_players_from_indexes(player_indexes, players) do | ||
players = Enum.with_index(players) | ||
|
||
Enum.filter(players, fn {_player, index} -> | ||
Enum.member?(player_indexes, index) | ||
end) | ||
|> Enum.map(fn {player, _index} -> | ||
player | ||
end) | ||
end | ||
|
||
def count_broken_parties(first_team, parties) do | ||
Enum.count(parties, fn party -> | ||
is_party_broken?(first_team, party) | ||
end) | ||
end | ||
|
||
@spec is_party_broken?([BF.player()], [String.t()]) :: any() | ||
def is_party_broken?(team, party) do | ||
count = | ||
Enum.count(party, fn x -> | ||
Enum.any?(team, fn y -> | ||
y.name == x | ||
end) | ||
end) | ||
|
||
cond do | ||
# Nobody from this party is on this team. Therefore unbroken. | ||
count == 0 -> false | ||
# Everyone from this party is on this team. Therefore unbroken. | ||
count == length(party) -> false | ||
# Otherwise, this party is broken. | ||
true -> true | ||
end | ||
end | ||
|
||
defp get_team_rating(players) do | ||
Enum.reduce(players, 0, fn x, acc -> | ||
acc + x.rating | ||
end) | ||
end | ||
|
||
@spec standardise_result(BF.combo_result(), [BF.player()]) :: any() | ||
def standardise_result(best_combo, all_players) do | ||
first_team = best_combo.team | ||
|
||
second_team = | ||
all_players | ||
|> Enum.filter(fn x -> | ||
!Enum.any?(first_team, fn y -> | ||
y.id == x.id | ||
end) | ||
end) | ||
|
||
team_groups = %{ | ||
1 => standardise_team_groups(first_team), | ||
2 => standardise_team_groups(second_team) | ||
} | ||
|
||
team_players = %{ | ||
1 => standardise_team_players(first_team), | ||
2 => standardise_team_players(second_team) | ||
} | ||
|
||
logs = [ | ||
"Algorithm: brute_force", | ||
@splitter, | ||
"Team rating diff penalty: #{format(best_combo.rating_diff_penalty)}", | ||
"Broken party penalty: #{best_combo.broken_party_penalty}", | ||
"Score: #{format(best_combo.score)} (lower is better)", | ||
"Team 1: #{log_team(first_team)}", | ||
"Team 2: #{log_team(second_team)}" | ||
] | ||
|
||
%{ | ||
team_groups: team_groups, | ||
team_players: team_players, | ||
logs: logs | ||
} | ||
end | ||
|
||
@spec log_team([BF.player()]) :: String.t() | ||
defp log_team(team) do | ||
Enum.map(team, fn x -> | ||
x.name | ||
end) | ||
|> Enum.join(", ") | ||
end | ||
|
||
@spec standardise_team_groups([BF.player()]) :: any() | ||
defp standardise_team_groups(team) do | ||
team | ||
|> Enum.map(fn x -> | ||
%{ | ||
members: [x.id], | ||
count: 1, | ||
group_rating: x.rating, | ||
ratings: [x.rating] | ||
} | ||
end) | ||
end | ||
|
||
defp standardise_team_players(team) do | ||
team | ||
|> Enum.map(fn x -> | ||
x.id | ||
end) | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
defmodule Teiserver.Battle.Balance.BruteForceTypes do | ||
@moduledoc false | ||
|
||
@type player :: %{ | ||
rating: float(), | ||
id: any(), | ||
name: String.t() | ||
} | ||
@type team :: %{ | ||
players: [player], | ||
id: integer() | ||
} | ||
@type input_data :: %{ | ||
players: [player], | ||
parties: [String.t()] | ||
} | ||
|
||
@type combo_result :: %{ | ||
broken_party_penalty: number(), | ||
rating_diff_penalty: number(), | ||
score: number(), | ||
team: [player()] | ||
} | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
defmodule Teiserver.Helper.CombinationsHelper do | ||
# This module to help get combinations taken from Elixir Forums: | ||
# https://elixirforum.com/t/create-all-possible-teams-from-a-list-of-players/64892/5?u=joshua.aug | ||
require Integer | ||
|
||
defp n_comb(0, _list), do: [[]] | ||
defp n_comb(_, []), do: [] | ||
|
||
defp n_comb(n, [h | t]) do | ||
list = for l <- n_comb(n - 1, t), do: [h | l] | ||
list ++ n_comb(n, t) | ||
end | ||
|
||
def n_comb(list) when is_list(list) do | ||
n = trunc(length(list) / 2) | ||
|
||
for i <- n_comb(n - 1, tl(list)), | ||
do: [hd(list) | i] | ||
end | ||
|
||
# Returns a list of possible combinations of a single team using indexes starting at zero | ||
# but doesn't include duplicates. Assumes we need exactly two teams of equal size. | ||
# E.g. for a 4 player lobby, Team1: [0,1] is a duplicate of Team1: [2,3] | ||
# since Team1 and Team2 are just swapped | ||
def get_combinations(num_players) when is_integer(num_players) do | ||
last = trunc(num_players) - 1 | ||
|
||
0..last |> Enum.to_list() |> n_comb() | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.