diff --git a/lib/teiserver/battle/balance/balance_types.ex b/lib/teiserver/battle/balance/balance_types.ex
index 47a1690ca..bafe94a91 100644
--- a/lib/teiserver/battle/balance/balance_types.ex
+++ b/lib/teiserver/battle/balance/balance_types.ex
@@ -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() :: %{
diff --git a/lib/teiserver/battle/balance/cheeky_switcher_smart.ex b/lib/teiserver/battle/balance/cheeky_switcher_smart.ex
index 0f91475b3..8964645ec 100644
--- a/lib/teiserver/battle/balance/cheeky_switcher_smart.ex
+++ b/lib/teiserver/battle/balance/cheeky_switcher_smart.ex
@@ -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,
@@ -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(",")
@@ -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(",")
@@ -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
diff --git a/lib/teiserver/battle/balance/split_one_chevs.ex b/lib/teiserver/battle/balance/split_one_chevs.ex
index 21c0a0342..ecedd8ad7 100644
--- a/lib/teiserver/battle/balance/split_one_chevs.ex
+++ b/lib/teiserver/battle/balance/split_one_chevs.ex
@@ -1,28 +1,65 @@
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
+ ranks =
+ Enum.map(expanded_group, fn x ->
+ Map.get(x, :ranks, [])
+ end)
+ |> List.flatten()
+
+ Enum.any?(ranks, fn x ->
+ x < 2
+ end)
end
@doc """
@@ -30,24 +67,38 @@ defmodule Teiserver.Battle.Balance.SplitOneChevs do
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
@@ -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)],
diff --git a/lib/teiserver/battle/libs/balance_lib.ex b/lib/teiserver/battle/libs/balance_lib.ex
index 3a7b760d7..b3d11f512 100644
--- a/lib/teiserver/battle/libs/balance_lib.ex
+++ b/lib/teiserver/battle/libs/balance_lib.ex
@@ -27,6 +27,8 @@ defmodule Teiserver.Battle.BalanceLib do
# which one will get to pick first
@shuffle_first_pick true
+ @default_balance_algorithm "loser_picks"
+
@spec defaults() :: map()
def defaults() do
%{
@@ -40,6 +42,11 @@ defmodule Teiserver.Battle.BalanceLib do
}
end
+ def get_default_algorithm() do
+ # For now it's a constant but this could be moved to a configurable value
+ @default_balance_algorithm
+ end
+
@spec algorithm_modules() :: %{String.t() => module}
def algorithm_modules() do
%{
@@ -53,6 +60,7 @@ defmodule Teiserver.Battle.BalanceLib do
@doc """
Teifion only allowed force_party to be used by mods because it led to noob-stomping unbalanced teams
"""
+ @spec get_allowed_algorithms(boolean()) :: [String.t()]
def get_allowed_algorithms(is_moderator) do
if(is_moderator) do
Teiserver.Battle.BalanceLib.algorithm_modules() |> Map.keys()
@@ -81,6 +89,12 @@ defmodule Teiserver.Battle.BalanceLib do
mean_diff_max: the maximum difference in mean between the party and paired parties
stddev_diff_max: the maximum difference in stddev between the party and paired parties
"""
+ @spec create_balance([BT.player_group()], non_neg_integer) :: map
+ def create_balance(groups, team_count) do
+ # This method sets default opts (and makes some warnings go away)
+ create_balance(groups, team_count, [])
+ end
+
@spec create_balance([BT.player_group()], non_neg_integer, list) :: map
def create_balance([], _team_count, _opts) do
%{
@@ -93,7 +107,8 @@ defmodule Teiserver.Battle.BalanceLib do
team_players: %{},
team_sizes: %{},
means: %{},
- stdevs: %{}
+ stdevs: %{},
+ has_parties?: false
}
end
@@ -118,6 +133,15 @@ defmodule Teiserver.Battle.BalanceLib do
end
end)
+ uncertainties =
+ Map.values(members)
+ |> Enum.map(fn x ->
+ cond do
+ Map.has_key?(x, :uncertainty) -> x.uncertainty
+ true -> 0
+ end
+ end)
+
names =
members
|> Enum.map(fn {id, details} ->
@@ -133,15 +157,18 @@ defmodule Teiserver.Battle.BalanceLib do
ranks: ranks,
names: names,
group_rating: Enum.sum(ratings),
- count: Enum.count(ratings)
+ count: Enum.count(ratings),
+ uncertainties: uncertainties
}
end)
+ algo_name = opts[:algorithm] || get_default_algorithm()
+
# Now we pass this to the algorithm and it does the rest!
balance_result =
- case algorithm_modules()[opts[:algorithm] || "loser_picks"] do
+ case algorithm_modules()[algo_name] do
nil ->
- raise "No balance module by the name of '#{opts[:algorithm] || "loser_picks"}'"
+ raise "No balance module by the name of '#{algo_name}'"
m ->
m.perform(expanded_groups, team_count, opts)
@@ -164,9 +191,16 @@ defmodule Teiserver.Battle.BalanceLib do
|> Enum.map(fn group ->
# Iterate over our map
Map.new(group, fn {user_id, value} ->
- cond do
- is_number(value) -> {user_id, get_user_rating_rank_old(user_id, value)}
- true -> {user_id, value}
+ case value do
+ x when is_number(x) ->
+ {user_id, get_user_rating_rank_old(user_id, x)}
+
+ # match_controller will use this condition when balancing using old data
+ %{"rating_value" => rating_value, "uncertainty" => uncertainty} ->
+ {user_id, get_user_rating_rank_old(user_id, rating_value, uncertainty)}
+
+ _ ->
+ {user_id, value}
end
end)
end)
@@ -176,7 +210,7 @@ defmodule Teiserver.Battle.BalanceLib do
defp cleanup_result(result) do
Map.take(
result,
- ~w(team_groups team_players ratings captains team_sizes deviation means stdevs logs)a
+ ~w(team_groups team_players ratings captains team_sizes deviation means stdevs logs has_parties?)a
)
end
@@ -221,7 +255,8 @@ defmodule Teiserver.Battle.BalanceLib do
Map.merge(balance_result, %{
team_groups: team_groups,
- team_players: team_players
+ team_players: team_players,
+ has_parties?: balanced_teams_has_parties?(team_groups)
})
end
@@ -560,17 +595,10 @@ 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()) ::
BT.rating_value()
defp get_user_balance_rating_value(userid, rating_type_id) when is_integer(rating_type_id) do
- real_rating = get_user_rating_value(userid, rating_type_id)
-
- stats = Account.get_user_stat_data(userid)
- adjustment = int_parse(stats["os_global_adjust"])
-
- real_rating + adjustment
+ get_user_rating_value(userid, rating_type_id)
end
defp get_user_balance_rating_value(_userid, nil), do: nil
@@ -586,26 +614,39 @@ defmodule Teiserver.Battle.BalanceLib do
def get_user_rating_rank(userid, rating_type, fuzz_multiplier) do
# This call will go to db or cache
- # The cache for ratings is :teiserver_user_stat_cache
+ # The cache for ratings is :teiserver_user_ratings
# which has an expiry of 60s
# See application.ex for cache settings
rating_type_id = MatchRatingLib.rating_type_name_lookup()[rating_type]
- rating = get_user_balance_rating_value(userid, rating_type_id)
+ {skill, uncertainty} = get_user_rating_value_uncertainty_pair(userid, rating_type_id)
+ rating = calculate_rating_value(skill, uncertainty)
rating = fuzz_rating(rating, fuzz_multiplier)
+
+ # Get stats data
+ # Potentially adjust ratings based on os_global_adjust
+ stats_data = Account.get_user_stat_data(userid)
+ adjustment = int_parse(stats_data["os_global_adjust"])
+ rating = rating + adjustment
+ rank = Map.get(stats_data, "rank", 0)
+
# This call will go to db or cache
# The cache for users is :users
# which is permanent (and would be instantiated on login)
# See application.ex for cache settings
- %{rank: rank, name: name} = Account.get_user_by_id(userid)
- %{rating: rating, rank: rank, name: name}
+
+ %{name: name} = Account.get_user_by_id(userid)
+ %{rating: rating, rank: rank, name: name, uncertainty: uncertainty}
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}
+ def get_user_rating_rank_old(userid, rating_value, uncertainty \\ 0) do
+ stats_data = Account.get_user_stat_data(userid)
+ rank = Map.get(stats_data, "rank", 0)
+
+ %{name: name} = Account.get_user_by_id(userid)
+ %{rating: rating_value, rank: rank, name: name, uncertainty: uncertainty}
end
defp fuzz_rating(rating, multiplier) do
@@ -837,4 +878,30 @@ defmodule Teiserver.Battle.BalanceLib do
for(y <- make_combinations(n - x.count, xs), do: [x | y]) ++ make_combinations(n, xs)
end
end
+
+ @doc """
+ Can be called to detect if a balance result has parties
+ If the result has no parties we do not need to check team deviation
+ """
+ def balanced_teams_has_parties?(team_groups) do
+ Enum.reduce_while(team_groups, false, fn {_key, team}, _acc ->
+ case team_has_parties?(team) do
+ true -> {:halt, true}
+ false -> {:cont, false}
+ end
+ end)
+ end
+
+ @spec team_has_parties?([BT.group()]) :: boolean()
+ def team_has_parties?(team) do
+ Enum.reduce_while(team, false, fn x, _acc ->
+ group_count = x[:count]
+
+ if group_count > 1 do
+ {:halt, true}
+ else
+ {:cont, false}
+ end
+ end)
+ end
end
diff --git a/lib/teiserver/data/cache_user.ex b/lib/teiserver/data/cache_user.ex
index 6bdd16fd1..47f9cef4f 100644
--- a/lib/teiserver/data/cache_user.ex
+++ b/lib/teiserver/data/cache_user.ex
@@ -1277,7 +1277,9 @@ defmodule Teiserver.CacheUser do
ingame_minutes =
(stats.data["player_minutes"] || 0) + (stats.data["spectator_minutes"] || 0) * 0.5
- round(ingame_minutes / 60)
+ # Hours are rounded down which helps to determine if a user has hit a
+ # chevron hours threshold. So a user with 4.9 hours is still chevron 1 or rank 0
+ trunc(ingame_minutes / 60)
end
# Based on actual ingame time
@@ -1310,14 +1312,16 @@ defmodule Teiserver.CacheUser do
def calculate_rank(userid, "Role") do
ingame_hours = rank_time(userid)
+ # Thresholds should match what is on the website:
+ # https://www.beyondallreason.info/guide/rating-and-lobby-balance#rank-icons
cond do
has_any_role?(userid, ["Tournament winner"]) -> 7
has_any_role?(userid, ~w(Core Contributor)) -> 6
- ingame_hours > 1000 -> 5
- ingame_hours > 250 -> 4
- ingame_hours > 100 -> 3
- ingame_hours > 15 -> 2
- ingame_hours > 5 -> 1
+ ingame_hours >= 1000 -> 5
+ ingame_hours >= 250 -> 4
+ ingame_hours >= 100 -> 3
+ ingame_hours >= 15 -> 2
+ ingame_hours >= 5 -> 1
true -> 0
end
end
diff --git a/lib/teiserver/game/servers/balancer_server.ex b/lib/teiserver/game/servers/balancer_server.ex
index c35913e15..4b408e8d9 100644
--- a/lib/teiserver/game/servers/balancer_server.ex
+++ b/lib/teiserver/game/servers/balancer_server.ex
@@ -7,6 +7,8 @@ defmodule Teiserver.Game.BalancerServer do
alias Teiserver.Battle.MatchLib
@tick_interval 2_000
+ # Balance algos that allow fuzz; randomness will be added to match rating before processing
+ @algos_allowing_fuzz ~w(loser_picks force_party)
@spec start_link(List.t()) :: :ignore | {:error, any} | {:ok, pid}
def start_link(opts) do
@@ -175,8 +177,9 @@ defmodule Teiserver.Game.BalancerServer do
if opts[:allow_groups] do
party_result = make_grouped_balance(team_count, players, game_type, opts)
+ has_parties? = Map.get(party_result, :has_parties?, true)
- if party_result.deviation > opts[:max_deviation] do
+ if has_parties? && party_result.deviation > opts[:max_deviation] do
make_solo_balance(
team_count,
players,
@@ -210,14 +213,16 @@ defmodule Teiserver.Game.BalancerServer do
player_id_list
|> Enum.map(fn userid ->
%{
- userid => BalanceLib.get_user_rating_rank(userid, game_type, opts[:fuzz_multiplier])
+ userid =>
+ BalanceLib.get_user_rating_rank(userid, game_type, get_fuzz_multiplier(opts))
}
end)
{_party_id, player_id_list} ->
player_id_list
|> Map.new(fn userid ->
- {userid, BalanceLib.get_user_rating_rank(userid, game_type, opts[:fuzz_multiplier])}
+ {userid,
+ BalanceLib.get_user_rating_rank(userid, game_type, get_fuzz_multiplier(opts))}
end)
end)
|> List.flatten()
@@ -233,7 +238,7 @@ defmodule Teiserver.Game.BalancerServer do
players
|> Enum.map(fn %{userid: userid} ->
%{
- userid => BalanceLib.get_user_rating_rank(userid, game_type, opts[:fuzz_multiplier])
+ userid => BalanceLib.get_user_rating_rank(userid, game_type, get_fuzz_multiplier(opts))
}
end)
@@ -246,6 +251,15 @@ defmodule Teiserver.Game.BalancerServer do
})
end
+ def get_fuzz_multiplier(opts) do
+ algo = opts[:algorithm]
+
+ case Enum.member?(@algos_allowing_fuzz, algo) do
+ true -> opts[:fuzz_multiplier]
+ false -> 0
+ end
+ end
+
@spec empty_state(T.lobby_id()) :: T.balance_server_state()
defp empty_state(lobby_id) do
# it's possible the lobby is nil before we even get to start this up (tests in particular)
diff --git a/lib/teiserver/helpers/number_helper.ex b/lib/teiserver/helpers/number_helper.ex
index 800eb194a..0c6bfd023 100644
--- a/lib/teiserver/helpers/number_helper.ex
+++ b/lib/teiserver/helpers/number_helper.ex
@@ -120,4 +120,20 @@ defmodule Teiserver.Helper.NumberHelper do
def percent(v, dp) do
round(v * 100, dp)
end
+
+ @doc """
+ Use this function for printing floats with only one decimal place.
+ Integers will be returned without modification.
+ If you need to always print the same amount of decimals no matter if float or integer,
+ then use the round function also defined in this module.
+ """
+ def format(rating_value) when is_float(rating_value) do
+ rating_value
+ |> Decimal.from_float()
+ |> Decimal.round(1)
+ end
+
+ def format(rating_value) when is_integer(rating_value) do
+ rating_value
+ end
end
diff --git a/lib/teiserver/mix_tasks/fake_data.ex b/lib/teiserver/mix_tasks/fake_data.ex
index 1b322b40b..a6f2a4d97 100644
--- a/lib/teiserver/mix_tasks/fake_data.ex
+++ b/lib/teiserver/mix_tasks/fake_data.ex
@@ -34,6 +34,9 @@ defmodule Mix.Tasks.Teiserver.Fakedata do
make_moderation()
make_one_time_code()
+ # Add fake playtime data to all our non-bot users
+ Mix.Task.run("teiserver.fake_playtime")
+
:timer.sleep(50)
IO.puts(
@@ -96,7 +99,7 @@ defmodule Mix.Tasks.Teiserver.Fakedata do
name: generate_throwaway_name() |> String.replace(" ", ""),
email: UUID.uuid1(),
password: root_user.password,
- permissions: ["admin.dev.developer"],
+ permissions: [],
icon: "fa-solid #{StylingHelper.random_icon()}",
colour: StylingHelper.random_colour(),
trust_score: 10_000,
diff --git a/lib/teiserver/mix_tasks/fake_playtime.ex b/lib/teiserver/mix_tasks/fake_playtime.ex
new file mode 100644
index 000000000..f07cbf66a
--- /dev/null
+++ b/lib/teiserver/mix_tasks/fake_playtime.ex
@@ -0,0 +1,68 @@
+defmodule Mix.Tasks.Teiserver.FakePlaytime do
+ @moduledoc """
+ Adds fake play time stats to all non bot users
+ This will also be called by the teiserver.fakedata task
+
+ If you want to run this task invidually, use:
+ mix teiserver.fake_playtime
+ """
+
+ use Mix.Task
+ require Logger
+ alias Teiserver.{Account, CacheUser}
+
+ def run(_args) do
+ Application.ensure_all_started(:teiserver)
+
+ if Application.get_env(:teiserver, Teiserver)[:enable_hailstorm] do
+ Account.list_users(
+ search: [
+ not_has_role: "Bot"
+ ],
+ select: [:id, :name]
+ )
+ |> Enum.map(fn user ->
+ update_stats(user.id, random_playtime())
+ end)
+
+ Logger.info("Finished applying fake playtime data")
+ end
+ end
+
+ def update_stats(user_id, player_minutes) do
+ Account.update_user_stat(user_id, %{
+ player_minutes: player_minutes,
+ total_minutes: player_minutes
+ })
+
+ # Now recalculate ranks
+ # This calc would usually be done in do_login
+ rank = CacheUser.calculate_rank(user_id)
+
+ Account.update_user_stat(user_id, %{
+ rank: rank
+ })
+ end
+
+ defp random_playtime() do
+ hours =
+ case get_player_experience() do
+ :just_installed -> Enum.random(0..4)
+ :beginner -> Enum.random(5..99)
+ :average -> Enum.random(100..249)
+ :pro -> Enum.random(250..1750)
+ end
+
+ hours * 60
+ end
+
+ @spec get_player_experience() :: :just_installed | :beginner | :average | :pro
+ defp get_player_experience do
+ case Enum.random(0..3) do
+ 0 -> :just_installed
+ 1 -> :beginner
+ 2 -> :average
+ 3 -> :pro
+ end
+ end
+end
diff --git a/lib/teiserver_web/live/battles/match/ratings.html.heex b/lib/teiserver_web/live/battles/match/ratings.html.heex
index ca93328ec..4c750f485 100644
--- a/lib/teiserver_web/live/battles/match/ratings.html.heex
+++ b/lib/teiserver_web/live/battles/match/ratings.html.heex
@@ -89,7 +89,9 @@
end %>
<%= if log.match do %>
- <%= log.match.map %> |
+
+ <%= live_redirect("#{log.match.map}", to: ~p"/battle/#{log.match_id}") %>
+ |
<%= log.match.team_size * log.match.team_count %> |
<% else %>
<%= log.value["reason"] || "No match" %> |
diff --git a/lib/teiserver_web/live/battles/match/show.ex b/lib/teiserver_web/live/battles/match/show.ex
index bd02fcf6b..09ff0660c 100644
--- a/lib/teiserver_web/live/battles/match/show.ex
+++ b/lib/teiserver_web/live/battles/match/show.ex
@@ -1,7 +1,7 @@
defmodule TeiserverWeb.Battle.MatchLive.Show do
@moduledoc false
use TeiserverWeb, :live_view
- alias Teiserver.{Battle, Game, Account, Telemetry}
+ alias Teiserver.{Battle, Game, Telemetry}
alias Teiserver.Battle.{MatchLib, BalanceLib}
@impl true
@@ -11,6 +11,11 @@ defmodule TeiserverWeb.Battle.MatchLive.Show do
|> assign(:site_menu_active, "match")
|> assign(:view_colour, Teiserver.Battle.MatchLib.colours())
|> assign(:tab, "details")
+ |> assign(
+ :algorithm_options,
+ BalanceLib.get_allowed_algorithms(true)
+ )
+ |> assign(:algorithm, BalanceLib.get_default_algorithm())
{:ok, socket}
end
@@ -49,8 +54,10 @@ defmodule TeiserverWeb.Battle.MatchLive.Show do
end
defp apply_action(%{assigns: %{match_name: match_name}} = socket, :balance, _params) do
+ # Restrict the balance tab to certain roles.
+ # Note that Staff roles like "Tester" also contain Contributor role.
socket
- |> mount_require_any(["Reviewer"])
+ |> mount_require_any(["Reviewer", "Contributor"])
|> assign(:page_title, "#{match_name} - Balance")
end
@@ -59,7 +66,9 @@ defmodule TeiserverWeb.Battle.MatchLive.Show do
# {:noreply, assign(socket, :tab, tab)}
# end
- defp get_match(%{assigns: %{id: id, current_user: _current_user}} = socket) do
+ defp get_match(
+ %{assigns: %{id: id, algorithm: algorithm, current_user: _current_user}} = socket
+ ) do
if connected?(socket) do
match =
Battle.get_match!(id,
@@ -115,24 +124,24 @@ defmodule TeiserverWeb.Battle.MatchLive.Show do
player_id_list
|> Enum.filter(fn userid -> rating_logs[userid] != nil end)
|> Enum.map(fn userid ->
- %{userid => rating_logs[userid].value["rating_value"]}
+ %{userid => rating_logs[userid].value}
end)
{_party_id, player_id_list} ->
player_id_list
|> Enum.filter(fn userid -> rating_logs[userid] != nil end)
|> Map.new(fn userid ->
- {userid, rating_logs[userid].value["rating_value"]}
+ {userid, rating_logs[userid].value}
end)
end)
|> List.flatten()
past_balance =
- BalanceLib.create_balance(groups, match.team_count, mode: :loser_picks)
+ BalanceLib.create_balance(groups, match.team_count, algorithm: algorithm)
|> Map.put(:balance_mode, :grouped)
# What about new balance?
- new_balance = generate_new_balance_data(match)
+ new_balance = generate_new_balance_data(match, algorithm)
raw_events =
Telemetry.list_simple_match_events(where: [match_id: match.id], preload: [:event_types])
@@ -197,7 +206,7 @@ defmodule TeiserverWeb.Battle.MatchLive.Show do
end
end
- defp generate_new_balance_data(match) do
+ defp generate_new_balance_data(match, algorithm) do
rating_type = MatchLib.game_type(match.team_size, match.team_count)
partied_players =
@@ -223,7 +232,21 @@ defmodule TeiserverWeb.Battle.MatchLive.Show do
end)
|> List.flatten()
- BalanceLib.create_balance(groups, match.team_count, mode: :loser_picks)
+ BalanceLib.create_balance(groups, match.team_count, algorithm: algorithm)
|> Map.put(:balance_mode, :grouped)
end
+
+ @doc """
+ Handles the dropdown for algorithm changing
+ """
+ @impl true
+ def handle_event("update-algorithm", event, socket) do
+ [key] = event["_target"]
+ value = event[key]
+
+ {:noreply,
+ socket
+ |> assign(:algorithm, value)
+ |> get_match()}
+ end
end
diff --git a/lib/teiserver_web/live/battles/match/show.html.heex b/lib/teiserver_web/live/battles/match/show.html.heex
index a38abbee4..6cf6094b0 100644
--- a/lib/teiserver_web/live/battles/match/show.html.heex
+++ b/lib/teiserver_web/live/battles/match/show.html.heex
@@ -41,7 +41,7 @@
<% end %>
- <%= if @rating_logs != %{} and allow?(@current_user, "Overwatch") do %>
+ <%= if @rating_logs != %{} and allow_any?(@current_user, ["Overwatch", "Contributor"]) do %>
<.tab_nav url={~p"/battle/#{@match.id}/balance"} selected={@tab == :balance}>
Balance
@@ -282,9 +282,22 @@
<% end %>
- <%= if allow?(@current_user, "Overwatch") do %>
+ <%= if allow_any?(@current_user, ["Overwatch", "Contributor"]) do %>
+
+
+
Based on data at the time
+
diff --git a/mix.exs b/mix.exs
index a8da2ba68..2253c9279 100644
--- a/mix.exs
+++ b/mix.exs
@@ -132,7 +132,7 @@ defmodule Teiserver.MixProject do
[
plt_core_path: "priv/plts",
plt_file: {:no_warn, "priv/plts/dialyzer.plt"},
- plt_add_apps: [:ex_unit]
+ plt_add_apps: [:ex_unit, :mix]
]
end
diff --git a/test/teiserver/battle/balance_lib_internal_test.exs b/test/teiserver/battle/balance_lib_internal_test.exs
index 9a6343640..ea0c1be4a 100644
--- a/test/teiserver/battle/balance_lib_internal_test.exs
+++ b/test/teiserver/battle/balance_lib_internal_test.exs
@@ -21,12 +21,12 @@ defmodule Teiserver.Battle.BalanceLibInternalTest do
assert fixed_groups == [
%{
- user1.id => %{name: user1.name, rank: 0, rating: 19},
- user2.id => %{name: user2.name, rank: 0, rating: 20}
+ user1.id => %{name: user1.name, rank: 0, rating: 19, uncertainty: 0},
+ user2.id => %{name: user2.name, rank: 0, rating: 20, uncertainty: 0}
},
- %{user3.id => %{name: user3.name, rank: 0, rating: 18}},
- %{user4.id => %{name: user4.name, rank: 0, rating: 15}},
- %{user5.id => %{name: user5.name, rank: 0, rating: 11}}
+ %{user3.id => %{name: user3.name, rank: 0, rating: 18, uncertainty: 0}},
+ %{user4.id => %{name: user4.name, rank: 0, rating: 15, uncertainty: 0}},
+ %{user5.id => %{name: user5.name, rank: 0, rating: 11, uncertainty: 0}}
]
# loser_picks algo will hit the databases so let's just test with split_one_chevs
@@ -64,6 +64,59 @@ defmodule Teiserver.Battle.BalanceLibInternalTest do
assert result != nil
end
+ test "does team have parties" do
+ team = [
+ %{count: 2, group_rating: 13, members: [1, 4], ratings: [8, 5]}
+ ]
+
+ assert BalanceLib.team_has_parties?(team)
+
+ team = [
+ %{count: 1, group_rating: 8, members: [2], ratings: [8]}
+ ]
+
+ refute BalanceLib.team_has_parties?(team)
+ end
+
+ test "does team_groups in balance result have parties" do
+ team_groups = %{
+ 1 => [
+ %{count: 2, group_rating: 13, members: [1, 4], ratings: [8, 5]}
+ ],
+ 2 => [
+ %{count: 1, group_rating: 6, members: [2], ratings: [6]}
+ ]
+ }
+
+ assert BalanceLib.balanced_teams_has_parties?(team_groups)
+
+ team_groups = %{
+ 1 => [
+ %{count: 1, group_rating: 8, members: [1], ratings: [8]},
+ %{count: 1, group_rating: 8, members: [2], ratings: [8]}
+ ],
+ 2 => [
+ %{count: 1, group_rating: 6, members: [3], ratings: [6]},
+ %{count: 1, group_rating: 8, members: [4], ratings: [8]}
+ ]
+ }
+
+ refute BalanceLib.balanced_teams_has_parties?(team_groups)
+
+ team_groups = %{
+ 1 => [
+ %{count: 1, group_rating: 8, members: [1], ratings: [8]},
+ %{count: 1, group_rating: 8, members: [2], ratings: [8]}
+ ],
+ 2 => [
+ %{count: 1, group_rating: 13, members: [3], ratings: [6]},
+ %{count: 2, group_rating: 8, members: [4, 5], ratings: [8, 0]}
+ ]
+ }
+
+ assert BalanceLib.balanced_teams_has_parties?(team_groups)
+ end
+
defp create_test_users do
Enum.map(1..5, fn k ->
Teiserver.TeiserverTestLib.new_user("User_#{k}")
diff --git a/test/teiserver/battle/balance_lib_test.exs b/test/teiserver/battle/balance_lib_test.exs
index e7782f7d3..a60e54eeb 100644
--- a/test/teiserver/battle/balance_lib_test.exs
+++ b/test/teiserver/battle/balance_lib_test.exs
@@ -1,7 +1,7 @@
defmodule Teiserver.Battle.BalanceLibTest do
@moduledoc """
- Can run tests in this file only by
- mix test test/teiserver/battle/balance_lib_test.exs
+ Can run all balance tests via
+ mix test --only balance_test
"""
use Teiserver.DataCase, async: true
@moduletag :balance_test
@@ -20,6 +20,7 @@ defmodule Teiserver.Battle.BalanceLibTest do
)
assert result != nil
+ refute Map.get(result, :has_parties?, true)
end)
end
@@ -38,6 +39,7 @@ defmodule Teiserver.Battle.BalanceLibTest do
)
assert result != nil
+ refute Map.get(result, :has_parties?, true)
end)
end
@@ -59,6 +61,7 @@ defmodule Teiserver.Battle.BalanceLibTest do
)
assert result != nil
+ refute Map.get(result, :has_parties?, true)
end)
end
@@ -80,6 +83,7 @@ defmodule Teiserver.Battle.BalanceLibTest do
)
assert result != nil
+ refute Map.get(result, :has_parties?, true)
end)
end
@@ -103,6 +107,7 @@ defmodule Teiserver.Battle.BalanceLibTest do
)
assert result != nil
+ refute Map.get(result, :has_parties?, true)
end)
end
@@ -139,6 +144,9 @@ defmodule Teiserver.Battle.BalanceLibTest do
)
assert result != nil
+ # has_parties? might be true/false depending on the algorithm.
+ # Just check that the key exists
+ assert Map.has_key?(result, :has_parties?)
end)
end
end
diff --git a/test/teiserver/battle/cheeky_switcher_smart_balance_test.exs b/test/teiserver/battle/cheeky_switcher_smart_balance_test.exs
index ef2069520..75cbabff4 100644
--- a/test/teiserver/battle/cheeky_switcher_smart_balance_test.exs
+++ b/test/teiserver/battle/cheeky_switcher_smart_balance_test.exs
@@ -1,7 +1,7 @@
defmodule Teiserver.Battle.CheekySwitcherSmartBalanceTest do
@moduledoc """
- Can run tests in this file only by
- mix test test/teiserver/battle/cheeky_switcher_smart_balance_test.exs
+ Can run all balance tests via
+ mix test --only balance_test
"""
use Teiserver.DataCase, async: true
@moduletag :balance_test
@@ -53,7 +53,8 @@ defmodule Teiserver.Battle.CheekySwitcherSmartBalanceTest do
},
deviation: 0,
means: %{1 => 6.5, 2 => 6.5},
- stdevs: %{1 => 0.5, 2 => 1.5}
+ stdevs: %{1 => 0.5, 2 => 1.5},
+ has_parties?: false
}
end
diff --git a/test/teiserver/battle/loser_picks_balance_test.exs b/test/teiserver/battle/loser_picks_balance_test.exs
index 52150e059..95ca2f5ef 100644
--- a/test/teiserver/battle/loser_picks_balance_test.exs
+++ b/test/teiserver/battle/loser_picks_balance_test.exs
@@ -1,7 +1,7 @@
defmodule Teiserver.Battle.LoserPicksBalanceTest do
@moduledoc """
- Can run tests in this file only by
- mix test test/teiserver/battle/loser_picks_balance_test.exs
+ Can run all balance tests via
+ mix test --only balance_test
"""
use Teiserver.DataCase, async: true
@moduletag :balance_test
@@ -52,7 +52,8 @@ defmodule Teiserver.Battle.LoserPicksBalanceTest do
},
deviation: 0,
means: %{1 => 6.5, 2 => 6.5},
- stdevs: %{1 => 1.5, 2 => 0.5}
+ stdevs: %{1 => 1.5, 2 => 0.5},
+ has_parties?: false
}
end
@@ -103,7 +104,8 @@ defmodule Teiserver.Battle.LoserPicksBalanceTest do
},
deviation: 13,
means: %{1 => 8.0, 2 => 7.0, 3 => 6.0, 4 => 5.0},
- stdevs: %{1 => 0.0, 2 => 0.0, 3 => 0.0, 4 => 0.0}
+ stdevs: %{1 => 0.0, 2 => 0.0, 3 => 0.0, 4 => 0.0},
+ has_parties?: false
}
end
@@ -160,7 +162,8 @@ defmodule Teiserver.Battle.LoserPicksBalanceTest do
},
deviation: 0,
means: %{1 => 7.5, 2 => 7.0, 3 => 7.5},
- stdevs: %{1 => 1.5, 2 => 2.0, 3 => 0.5}
+ stdevs: %{1 => 1.5, 2 => 2.0, 3 => 0.5},
+ has_parties?: false
}
end
@@ -209,7 +212,8 @@ defmodule Teiserver.Battle.LoserPicksBalanceTest do
},
deviation: 0,
means: %{1 => 6.5, 2 => 6.5},
- stdevs: %{1 => 1.5, 2 => 0.5}
+ stdevs: %{1 => 1.5, 2 => 0.5},
+ has_parties?: true
}
end
@@ -276,7 +280,8 @@ defmodule Teiserver.Battle.LoserPicksBalanceTest do
},
team_sizes: %{1 => 8, 2 => 8},
means: %{1 => 20.125, 2 => 20.5},
- stdevs: %{1 => 9.29297449689818, 2 => 8.671072598012312}
+ stdevs: %{1 => 9.29297449689818, 2 => 8.671072598012312},
+ has_parties?: true
}
end
@@ -335,7 +340,8 @@ defmodule Teiserver.Battle.LoserPicksBalanceTest do
team_players: %{1 => 'ejlnprft', 2 => 'hikmoqsg'},
team_sizes: %{1 => 8, 2 => 8},
means: %{1 => 23.0, 2 => 23.0},
- stdevs: %{1 => 12.816005617976296, 2 => 8.674675786448736}
+ stdevs: %{1 => 12.816005617976296, 2 => 8.674675786448736},
+ has_parties?: false
}
end
@@ -394,7 +400,8 @@ defmodule Teiserver.Battle.LoserPicksBalanceTest do
},
team_sizes: %{1 => 8, 2 => 8},
means: %{1 => 31.0, 2 => 31.625},
- stdevs: %{1 => 16.015617378046965, 2 => 15.090870584562046}
+ stdevs: %{1 => 16.015617378046965, 2 => 15.090870584562046},
+ has_parties?: true
}
result2 =
@@ -453,7 +460,8 @@ defmodule Teiserver.Battle.LoserPicksBalanceTest do
},
team_sizes: %{1 => 8, 2 => 8},
means: %{1 => 31.0, 2 => 31.625},
- stdevs: %{1 => 16.0312195418814, 2 => 15.074295174236173}
+ stdevs: %{1 => 16.0312195418814, 2 => 15.074295174236173},
+ has_parties?: true
}
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 dc5e9fc28..9ff7f975b 100644
--- a/test/teiserver/battle/split_one_chevs_internal_test.exs
+++ b/test/teiserver/battle/split_one_chevs_internal_test.exs
@@ -1,8 +1,8 @@
defmodule Teiserver.Battle.SplitOneChevsInternalTest do
@moduledoc """
- This tests the internal functions of SplitOneChevs
- Can run tests in this file only by
- mix test test/teiserver/battle/split_one_chevs_internal_test.exs
+ This tests the internal functions of SplitOneChevs
+ Can run all balance tests via
+ mix test --only balance_test
"""
use ExUnit.Case
@moduletag :balance_test
@@ -16,7 +16,8 @@ defmodule Teiserver.Battle.SplitOneChevsInternalTest do
group_rating: 13,
ratings: [8, 5],
ranks: [1, 0],
- names: ["Pro1", "Noob1"]
+ names: ["Pro1", "Noob1"],
+ uncertainties: [0, 1]
},
%{
count: 1,
@@ -24,7 +25,8 @@ defmodule Teiserver.Battle.SplitOneChevsInternalTest do
group_rating: 6,
ratings: [6],
ranks: [0],
- names: ["Noob2"]
+ names: ["Noob2"],
+ uncertainties: [2]
},
%{
count: 1,
@@ -32,7 +34,8 @@ defmodule Teiserver.Battle.SplitOneChevsInternalTest do
group_rating: 7,
ratings: [17],
ranks: [0],
- names: ["Noob3"]
+ names: ["Noob3"],
+ uncertainties: [3]
}
]
@@ -40,40 +43,40 @@ defmodule Teiserver.Battle.SplitOneChevsInternalTest do
assert result.team_groups == %{
1 => [
- %{count: 1, group_rating: 6, members: ["Noob2"], ratings: [6]},
+ %{count: 1, group_rating: 17, members: ["Noob3"], ratings: [17]},
%{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]}
+ %{count: 1, group_rating: 6, members: ["Noob2"], ratings: [6]},
+ %{count: 1, group_rating: 5, members: ["Noob1"], ratings: [5]}
]
}
end
test "sort members" do
members = [
- %{rating: 8, rank: 4, member_id: 100},
- %{rating: 5, rank: 0, member_id: 4},
- %{rating: 6, rank: 0, member_id: 2},
- %{rating: 17, rank: 0, member_id: 3}
+ %{rating: 8, rank: 4, member_id: 100, uncertainty: 8},
+ %{rating: 5, rank: 1, member_id: 4, uncertainty: 1},
+ %{rating: 6, rank: 0, member_id: 2, uncertainty: 2},
+ %{rating: 17, rank: 1, member_id: 3, uncertainty: 3}
]
result = SplitOneChevs.sort_members(members)
assert result == [
- %{rating: 8, rank: 4, member_id: 100},
- %{rating: 17, rank: 0, member_id: 3},
- %{rating: 6, rank: 0, member_id: 2},
- %{rating: 5, rank: 0, member_id: 4}
+ %{member_id: 100, rank: 4, rating: 8, uncertainty: 8},
+ %{member_id: 4, rank: 1, rating: 5, uncertainty: 1},
+ %{member_id: 2, rank: 0, rating: 6, uncertainty: 2},
+ %{member_id: 3, rank: 1, rating: 17, uncertainty: 3}
]
end
test "assign teams" do
members = [
- %{rating: 8, rank: 4, member_id: 100, name: "100"},
- %{rating: 5, rank: 0, member_id: 4, name: "4"},
- %{rating: 6, rank: 0, member_id: 2, name: "2"},
- %{rating: 17, rank: 0, member_id: 3, name: "3"}
+ %{rating: 8, rank: 4, member_id: 100, name: "100", uncertainty: 0},
+ %{rating: 5, rank: 0, member_id: 4, name: "4", uncertainty: 1},
+ %{rating: 6, rank: 0, member_id: 2, name: "2", uncertainty: 2},
+ %{rating: 17, rank: 0, member_id: 3, name: "3", uncertainty: 3}
]
result = SplitOneChevs.assign_teams(members, 2)
@@ -81,15 +84,15 @@ defmodule Teiserver.Battle.SplitOneChevsInternalTest do
assert result.teams == [
%{
members: [
- %{rating: 17, rank: 0, member_id: 3, name: "3"},
- %{rating: 8, rank: 4, member_id: 100, name: "100"}
+ %{member_id: 3, name: "3", rank: 0, rating: 17, uncertainty: 3},
+ %{member_id: 100, name: "100", rank: 4, rating: 8, uncertainty: 0}
],
team_id: 1
},
%{
members: [
- %{rating: 6, rank: 0, member_id: 2, name: "2"},
- %{rating: 5, rank: 0, member_id: 4, name: "4"}
+ %{member_id: 2, name: "2", rank: 0, rating: 6, uncertainty: 2},
+ %{member_id: 4, name: "4", rank: 0, rating: 5, uncertainty: 1}
],
team_id: 2
}
@@ -105,4 +108,76 @@ defmodule Teiserver.Battle.SplitOneChevsInternalTest do
%{members: [], team_id: 3}
]
end
+
+ test "has enough noobs" do
+ expanded_group = [
+ %{
+ count: 2,
+ members: ["Pro1", "Noob1"],
+ group_rating: 13,
+ ratings: [8, 5],
+ ranks: [1, 0],
+ names: ["Pro1", "Noob1"],
+ uncertainties: [0, 1]
+ },
+ %{
+ count: 1,
+ members: ["Noob2"],
+ group_rating: 6,
+ ratings: [6],
+ ranks: [0],
+ names: ["Noob2"],
+ uncertainties: [2]
+ },
+ %{
+ count: 1,
+ members: ["Noob3"],
+ group_rating: 7,
+ ratings: [17],
+ ranks: [0],
+ names: ["Noob3"],
+ uncertainties: [3]
+ }
+ ]
+
+ result = SplitOneChevs.has_enough_noobs?(expanded_group)
+
+ assert result
+ end
+
+ test "not have enough noobs" do
+ expanded_group = [
+ %{
+ count: 2,
+ members: ["B", "A"],
+ group_rating: 13,
+ ratings: [8, 5],
+ ranks: [2, 2],
+ names: ["B", "A"],
+ uncertainties: [0, 1]
+ },
+ %{
+ count: 1,
+ members: ["C"],
+ group_rating: 6,
+ ratings: [6],
+ ranks: [2],
+ names: ["C"],
+ uncertainties: [2]
+ },
+ %{
+ count: 1,
+ members: ["D"],
+ group_rating: 7,
+ ratings: [17],
+ ranks: [2],
+ names: ["D"],
+ uncertainties: [3]
+ }
+ ]
+
+ result = SplitOneChevs.has_enough_noobs?(expanded_group)
+
+ refute result
+ end
end
diff --git a/test/teiserver/battle/split_one_chevs_test.exs b/test/teiserver/battle/split_one_chevs_test.exs
index 408af32cc..cf485ebe5 100644
--- a/test/teiserver/battle/split_one_chevs_test.exs
+++ b/test/teiserver/battle/split_one_chevs_test.exs
@@ -1,7 +1,7 @@
defmodule Teiserver.Battle.SplitOneChevsTest do
@moduledoc """
- Can run tests in this file only by
- mix test test/teiserver/battle/split_one_chevs_test.exs
+ Can run all balance tests via
+ mix test --only balance_test
"""
use ExUnit.Case
@moduletag :balance_test
@@ -28,7 +28,8 @@ defmodule Teiserver.Battle.SplitOneChevsTest do
team_players: %{},
team_sizes: %{},
means: %{},
- stdevs: %{}
+ stdevs: %{},
+ has_parties?: false
}
end
@@ -36,10 +37,10 @@ defmodule Teiserver.Battle.SplitOneChevsTest do
result =
BalanceLib.create_balance(
[
- %{1 => %{rating: 5}},
- %{2 => %{rating: 6}},
- %{3 => %{rating: 7}},
- %{4 => %{rating: 8}}
+ %{1 => %{rating: 5, rank: 2}},
+ %{2 => %{rating: 6, rank: 2}},
+ %{3 => %{rating: 7, rank: 2}},
+ %{4 => %{rating: 8, rank: 2}}
],
4,
algorithm: @split_algo
@@ -52,27 +53,27 @@ defmodule Teiserver.Battle.SplitOneChevsTest do
result =
BalanceLib.create_balance(
[
- %{1 => %{rating: 5}},
- %{2 => %{rating: 6}},
- %{3 => %{rating: 7}},
- %{4 => %{rating: 8}},
- %{5 => %{rating: 9}},
- %{6 => %{rating: 9}}
+ %{1 => %{rating: 5, rank: 2}},
+ %{2 => %{rating: 6, rank: 2}},
+ %{3 => %{rating: 7, rank: 2}},
+ %{4 => %{rating: 8, rank: 2}},
+ %{5 => %{rating: 9, rank: 2}},
+ %{6 => %{rating: 9, rank: 2}}
],
3,
algorithm: @split_algo
)
- assert result.team_players == %{1 => [1, 5], 2 => [2, 6], 3 => [3, 4]}
+ assert result.team_players == %{1 => [5, 2], 2 => [6, 1], 3 => [4, 3]}
end
test "split one chevs simple group" do
result =
BalanceLib.create_balance(
[
- %{4 => %{rating: 5}, 1 => %{rating: 8}},
- %{2 => %{rating: 6}},
- %{3 => %{rating: 7}}
+ %{4 => %{rating: 5, rank: 2}, 1 => %{rating: 8, rank: 2}},
+ %{2 => %{rating: 6, rank: 2}},
+ %{3 => %{rating: 7, rank: 2}}
],
2,
rating_lower_boundary: 100,
@@ -82,28 +83,31 @@ defmodule Teiserver.Battle.SplitOneChevsTest do
algorithm: @split_algo
)
- assert result.team_players == %{1 => [4, 1], 2 => [2, 3]}
+ assert result.team_players == %{1 => [1, 4], 2 => [2, 3]}
end
test "logs FFA" do
result =
BalanceLib.create_balance(
[
- %{"Pro1" => %{rating: 5, rank: 1}},
- %{"Pro2" => %{rating: 6, rank: 1}},
- %{"Noob1" => %{rating: 7, rank: 0}},
- %{"Noob2" => %{rating: 8, rank: 0}}
+ %{"Pro1" => %{rating: 5, rank: 2}},
+ %{"Pro2" => %{rating: 6, rank: 2}},
+ %{"Noob1" => %{rating: 7, rank: 1, uncertainty: 7}},
+ %{"Noob2" => %{rating: 8, rank: 0, uncertainty: 8}}
],
4,
algorithm: @split_algo
)
assert result.logs == [
- "Begin split_one_chevs balance",
- "Pro2 (Chev: 2) picked for Team 1",
- "Pro1 (Chev: 2) picked for Team 2",
- "Noob2 (Chev: 1) picked for Team 3",
- "Noob1 (Chev: 1) picked for Team 4"
+ "Algorithm: split_one_chevs",
+ "---------------------------",
+ "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.",
+ "---------------------------",
+ "Pro2 (6, σ: 0, Chev: 3) picked for Team 1",
+ "Pro1 (5, σ: 0, Chev: 3) picked for Team 2",
+ "Noob1 (7, σ: 7, Chev: 2) picked for Team 3",
+ "Noob2 (8, σ: 8, Chev: 1) picked for Team 4"
]
end
@@ -111,21 +115,47 @@ defmodule Teiserver.Battle.SplitOneChevsTest do
result =
BalanceLib.create_balance(
[
- %{"Pro1" => %{rating: 5, rank: 1}},
- %{"Pro2" => %{rating: 6, rank: 1}},
- %{"Noob1" => %{rating: 7, rank: 0}},
- %{"Noob2" => %{rating: 8, rank: 0}}
+ %{"Pro1" => %{rating: 5, rank: 2}},
+ %{"Pro2" => %{rating: 6, rank: 2}},
+ %{"Noob1" => %{rating: 7, rank: 0, uncertainty: 7.9}},
+ %{"Noob2" => %{rating: 8, rank: 0, uncertainty: 8}}
],
2,
algorithm: @split_algo
)
assert result.logs == [
- "Begin split_one_chevs balance",
- "Pro2 (Chev: 2) picked for Team 1",
- "Pro1 (Chev: 2) picked for Team 2",
- "Noob2 (Chev: 1) picked for Team 2",
- "Noob1 (Chev: 1) picked for Team 1"
+ "Algorithm: split_one_chevs",
+ "---------------------------",
+ "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.",
+ "---------------------------",
+ "Pro2 (6, σ: 0, Chev: 3) picked for Team 1",
+ "Pro1 (5, σ: 0, Chev: 3) picked for Team 2",
+ "Noob1 (7, σ: 7.9, Chev: 1) picked for Team 2",
+ "Noob2 (8, σ: 8, Chev: 1) picked for Team 1"
+ ]
+ end
+
+ test "calls another balancer when no noobs" do
+ result =
+ BalanceLib.create_balance(
+ [
+ %{"A" => %{rating: 5, rank: 2}},
+ %{"B" => %{rating: 6, rank: 2}},
+ %{"C" => %{rating: 7, rank: 2, uncertainty: 7.9}},
+ %{"D" => %{rating: 8, rank: 2, uncertainty: 8}}
+ ],
+ 2,
+ algorithm: @split_algo
+ )
+
+ assert result.logs == [
+ "Not enough new players; will use another balance algorithm.",
+ "---------------------------",
+ "Picked D for team 1, adding 8.0 points for new total of 8.0",
+ "Picked C for team 2, adding 7.0 points for new total of 7.0",
+ "Picked B for team 2, adding 6.0 points for new total of 13.0",
+ "Picked A for team 1, adding 5.0 points for new total of 13.0"
]
end
end
diff --git a/test/teiserver/game/balancer_server_test.exs b/test/teiserver/game/balancer_server_test.exs
new file mode 100644
index 000000000..f338b896e
--- /dev/null
+++ b/test/teiserver/game/balancer_server_test.exs
@@ -0,0 +1,19 @@
+defmodule Teiserver.Game.BalancerServerTest do
+ @moduledoc false
+ use ExUnit.Case
+ @moduletag :balance_test
+ alias Teiserver.Game.BalancerServer
+
+ test "get fuzz multiplier" do
+ result = BalancerServer.get_fuzz_multiplier([])
+ assert result == 0
+
+ result = BalancerServer.get_fuzz_multiplier(algorithm: "loser_picks", fuzz_multiplier: 0.5)
+ assert result == 0.5
+
+ result =
+ BalancerServer.get_fuzz_multiplier(algorithm: "split_one_chevs", fuzz_multiplier: 0.5)
+
+ assert result == 0
+ end
+end