Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update split_one_chevs balancemode v2 #328

Merged
merged 10 commits into from
Jul 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion lib/teiserver/battle/balance/balance_types.ex
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ defmodule Teiserver.Battle.Balance.BalanceTypes do
names: [String.t()],
ranks: [non_neg_integer()],
group_rating: rating_value(),
count: non_neg_integer()
count: non_neg_integer(),
uncertainties: [number()]
}

@type group() :: %{
Expand Down
9 changes: 5 additions & 4 deletions lib/teiserver/battle/balance/cheeky_switcher_smart.ex
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ defmodule Teiserver.Battle.Balance.CheekySwitcherSmart do
# Alias the types
alias Teiserver.Battle.BalanceLib
alias Teiserver.Battle.Balance.BalanceTypes, as: BT
import Teiserver.Helper.NumberHelper, only: [format: 1]

# @type algorithm_state :: %{
# teams: map,
Expand Down Expand Up @@ -158,7 +159,7 @@ defmodule Teiserver.Battle.Balance.CheekySwitcherSmart do
|> Enum.map(fn {group, _} ->
group.names
|> Enum.with_index()
|> Enum.map(fn {name, i} -> "#{name}[#{Enum.at(group.ratings, i)}]" end)
|> Enum.map(fn {name, i} -> "#{name}[#{format(Enum.at(group.ratings, i))}]" end)
end)
|> List.flatten()
|> Enum.join(",")
Expand All @@ -168,7 +169,7 @@ defmodule Teiserver.Battle.Balance.CheekySwitcherSmart do
|> Enum.map(fn {group, _} ->
group.names
|> Enum.with_index()
|> Enum.map(fn {name, i} -> "#{name}[#{Enum.at(group.ratings, i)}]" end)
|> Enum.map(fn {name, i} -> "#{name}[#{format(Enum.at(group.ratings, i))}]" end)
end)
|> List.flatten()
|> Enum.join(",")
Expand Down Expand Up @@ -429,11 +430,11 @@ defmodule Teiserver.Battle.Balance.CheekySwitcherSmart do

if next_group.count > 1 do
[
"Group picked #{names |> Enum.join(", ")} for team #{team_key}, adding #{group_rating} points for a new total of #{round(existing_team_rating + group_rating)}"
"Group picked #{names |> Enum.join(", ")} for team #{team_key}, adding #{format(group_rating)} points for a new total of #{round(existing_team_rating + group_rating)}"
]
else
[
"Picked #{Enum.at(names, 0)} for team #{team_key}, adding #{group_rating} points for a new total of #{round(existing_team_rating + group_rating)}"
"Picked #{Enum.at(names, 0)} for team #{team_key}, adding #{format(group_rating)} points for a new total of #{round(existing_team_rating + group_rating)}"
]
end
end
Expand Down
96 changes: 77 additions & 19 deletions lib/teiserver/battle/balance/split_one_chevs.ex
Original file line number Diff line number Diff line change
@@ -1,53 +1,104 @@
defmodule Teiserver.Battle.Balance.SplitOneChevs do
@moduledoc """
This balance algorithm first sorts the users by visible OS (match rating) descending. Then all rank=0 (one chevs) will be placed at the bottom of this sorted list.
Overview:
The goal of this algorithm is to mimic how a human would draft players given the visual information in a lobby.
Humans will generally avoid drafting overrated new players.

Next a team will be chosen to be the picking team. The picking team is the team with the least amount of players. If tied, then the team with the lowest total rating.
Details:
The team with the least amount of players will pick an unchosen player. If there are multiple teams tied for
the lowest player count, then the team with the lowest match rating picks.

Next the picking team will pick the player at the top of the sorted list.
Your team will prefer 3Chev+ players with high OS. If your team must pick a 1-2Chev player,
it will prefer lower uncertainty.

This is repeated until all players are chosen.
This is repeated until all players are chosen.

This algorithm completely ignores parties.
This algorithm completely ignores parties.

"""
alias Teiserver.Battle.Balance.SplitOneChevsTypes, as: ST
alias Teiserver.Battle.Balance.BalanceTypes, as: BT
import Teiserver.Helper.NumberHelper, only: [format: 1]

@splitter "---------------------------"

@doc """
Main entry point used by balance_lib
See split_one_chevs_internal_test.exs for sample input
"""
@spec perform([BT.expanded_group()], non_neg_integer(), list()) :: any()
def perform(expanded_group, team_count, _opts \\ []) do
members = flatten_members(expanded_group) |> sort_members()
%{teams: teams, logs: logs} = assign_teams(members, team_count)
standardise_result(teams, logs)
def perform(expanded_group, team_count, opts \\ []) do
if has_enough_noobs?(expanded_group) do
members = flatten_members(expanded_group) |> sort_members()
%{teams: teams, logs: logs} = assign_teams(members, team_count)
standardise_result(teams, logs)
else
# Not enough noobs; so call another balancer
result = Teiserver.Battle.Balance.LoserPicks.perform(expanded_group, team_count, opts)

new_logs =
["Not enough new players; will use another balance algorithm.", @splitter, result.logs]
|> List.flatten()

Map.put(result, :logs, new_logs)
end
end

@doc """
For now this simply checks there is at least a single 1chev or a single 2chev.
However we could modify this in the future to be more complicated e.g. at least a single 1chev
or at least two, 2chevs.
"""
@spec has_enough_noobs?([BT.expanded_group()]) :: bool()
def has_enough_noobs?(expanded_group) do
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

has_enough_noobs only checks if there are noobs, not how many?
Is this intended or not?

Copy link
Member Author

@jauggy jauggy Jun 28, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had some ideas like enough noobs be something like

  • At least a single 1chev or
  • At least two, 2chevs

But decided to keep it simple for now. So the function is more about allowing additional complexity in the future.

A single noob actually gets treated differently compared to Teifion's balancer. Teifion's balancer will pick the noob higher (since it's based on their OS). My algorithm will always pick the noob last irrespective of OS.

ranks =
Enum.map(expanded_group, fn x ->
Map.get(x, :ranks, [])
end)
|> List.flatten()

Enum.any?(ranks, fn x ->
x < 2
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} <- expanded_group,
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} <- Enum.zip([members, ratings, ranks, names]),
{id, rating, rank, name, uncertainty} <-
Enum.zip([members, ratings, ranks, names, uncertainties]),
# Create result value
do: %{member_id: id, rating: rating, rank: rank, name: name}
do: %{
member_id: id,
rating: rating,
rank: rank,
name: name,
uncertainty: uncertainty
}
end

@doc """
Sorts members by rating but puts one chevs at the bottom
See split_one_chevs_internal_test.exs for sample input
Experienced players will be on top followed by noobs.
Experienced players are 3+ Chevs. They will be sorted with higher OS on top.
Noobs are 1-2 Chevs. They will be sorted with lower uncertainty on top.
"""
def sort_members(members) do
non_noobs = Enum.filter(members, fn x -> x.rank != 0 end)
noobs = Enum.filter(members, fn x -> x.rank == 0 end)
non_noobs = Enum.filter(members, fn x -> x.rank >= 2 end)
noobs = Enum.filter(members, fn x -> x.rank < 2 end)

[
Enum.sort_by(non_noobs, fn x -> x.rating end, &>=/2),
Enum.sort_by(noobs, fn x -> x.rating end, &>=/2)
Enum.sort_by(noobs, fn x -> x.uncertainty end, &<=/2)
]
|> List.flatten()
end
Expand All @@ -59,14 +110,21 @@ defmodule Teiserver.Battle.Balance.SplitOneChevs do
def assign_teams(member_list, number_of_teams) do
default_acc = %{
teams: create_empty_teams(number_of_teams),
logs: ["Begin split_one_chevs balance"]
logs: [
"Algorithm: split_one_chevs",
@splitter,
"Your team will try and pick 3Chev+ players first, with preference for higher OS. If 1-2Chevs are the only remaining players, then lower uncertainty is preferred.",
@splitter
]
}

Enum.reduce(member_list, default_acc, fn x, acc ->
picking_team = get_picking_team(acc.teams)
update_picking_team = Map.merge(picking_team, %{members: [x | picking_team.members]})
username = x.name
new_log = "#{username} (Chev: #{x.rank + 1}) picked for Team #{picking_team.team_id}"

new_log =
"#{username} (#{format(x.rating)}, σ: #{format(x.uncertainty)}, Chev: #{x.rank + 1}) picked for Team #{picking_team.team_id}"

%{
teams: [update_picking_team | get_non_picking_teams(acc.teams, picking_team)],
Expand Down
Loading
Loading