From 0dd77e30e5d6ef399aaab157f4136db2d4cc3fe6 Mon Sep 17 00:00:00 2001 From: Joshua Augustinus Date: Thu, 13 Jun 2024 07:53:26 +1000 Subject: [PATCH 1/9] Add split-one-chevs-v2 Add split-one-chevs-v2 Update moduledoc --- config/dev.exs | 7 +- lib/teiserver/battle/balance/balance_types.ex | 3 +- .../battle/balance/split_one_chevs.ex | 68 ++++++-- lib/teiserver/battle/libs/balance_lib.ex | 114 +++++++++--- lib/teiserver/data/cache_user.ex | 16 +- lib/teiserver/game/servers/balancer_server.ex | 22 ++- lib/teiserver/mix_tasks/fake_playtime.ex | 74 ++++++++ .../controllers/admin/match_controller.ex | 163 +++++++++++++++++- .../templates/admin/match/index.html.heex | 5 +- .../admin/match/server_index.html.heex | 5 +- .../templates/admin/match/show.html.heex | 112 ++++++++++++ .../admin/match/tab_balance.html.heex | 41 +++++ .../admin/match/tab_details.html.heex | 37 ++++ .../admin/match/tab_events.html.heex | 18 ++ .../admin/match/tab_players.html.heex | 109 ++++++++++++ .../admin/match/tab_ratings.html.heex | 81 +++++++++ .../admin/match/user_index.html.heex | 5 +- .../battle/match/section_menu.html.heex | 2 +- mix.exs | 4 +- .../battle/balance_lib_internal_test.exs | 63 ++++++- test/teiserver/battle/balance_lib_test.exs | 12 +- .../cheeky_switcher_smart_balance_test.exs | 7 +- .../battle/loser_picks_balance_test.exs | 28 +-- .../battle/split_one_chevs_internal_test.exs | 53 +++--- .../teiserver/battle/split_one_chevs_test.exs | 75 ++++---- test/teiserver/game/balancer_server_test.exs | 19 ++ 26 files changed, 1003 insertions(+), 140 deletions(-) create mode 100644 lib/teiserver/mix_tasks/fake_playtime.ex create mode 100644 lib/teiserver_web/templates/admin/match/show.html.heex create mode 100644 lib/teiserver_web/templates/admin/match/tab_balance.html.heex create mode 100644 lib/teiserver_web/templates/admin/match/tab_details.html.heex create mode 100644 lib/teiserver_web/templates/admin/match/tab_events.html.heex create mode 100644 lib/teiserver_web/templates/admin/match/tab_players.html.heex create mode 100644 lib/teiserver_web/templates/admin/match/tab_ratings.html.heex create mode 100644 test/teiserver/game/balancer_server_test.exs diff --git a/config/dev.exs b/config/dev.exs index 5931816ed..cec09588c 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -60,7 +60,12 @@ config :teiserver, Teiserver, heartbeat_timeout: nil, enable_discord_bridge: false, enable_hailstorm: true, - accept_all_emails: true + accept_all_emails: true, + + # The balance algorithm to use on the admin/matches/match/:id page + # It is purely used for analysis and not for actual games + # TODO move this into dropdown on admin/matches/match/:id page + analysis_balance_algorithm: "loser_picks" # Watch static and templates for browser reloading. config :teiserver, TeiserverWeb.Endpoint, 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/split_one_chevs.ex b/lib/teiserver/battle/balance/split_one_chevs.ex index 21c0a0342..98ef99304 100644 --- a/lib/teiserver/battle/balance/split_one_chevs.ex +++ b/lib/teiserver/battle/balance/split_one_chevs.ex @@ -1,19 +1,26 @@ 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 + @splitter "---------------------------" + @doc """ Main entry point used by balance_lib See split_one_chevs_internal_test.exs for sample input @@ -30,28 +37,52 @@ 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 + defp round_number(rating_value) when is_float(rating_value) do + rating_value + |> Decimal.from_float() + |> Decimal.round(1) + end + + defp round_number(rating_value) when is_integer(rating_value) do + rating_value + end + @doc """ Assigns teams using algorithm defined in moduledoc See split_one_chevs_internal_test.exs for sample input @@ -59,14 +90,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} (#{round_number(x.rating)}, σ: #{round_number(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..1f56deccf 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 + defp 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 %{ @@ -81,6 +88,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 +106,8 @@ defmodule Teiserver.Battle.BalanceLib do team_players: %{}, team_sizes: %{}, means: %{}, - stdevs: %{} + stdevs: %{}, + has_parties?: false } end @@ -118,6 +132,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 +156,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 +190,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 +209,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 +254,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 +594,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 +613,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 +877,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 0ffcef2b5..e1bc55717 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,13 +1312,15 @@ 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, ~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/mix_tasks/fake_playtime.ex b/lib/teiserver/mix_tasks/fake_playtime.ex new file mode 100644 index 000000000..4bc371f08 --- /dev/null +++ b/lib/teiserver/mix_tasks/fake_playtime.ex @@ -0,0 +1,74 @@ +defmodule Mix.Tasks.Teiserver.FakePlaytime do + @moduledoc """ + Adds fake play time stats to all non bot users + Run with + 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) + user = Teiserver.Account.UserCacheLib.get_user_by_id(user_id) + + user = %{ + user + | rank: rank + } + + CacheUser.update_user(user, true) + + 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/controllers/admin/match_controller.ex b/lib/teiserver_web/controllers/admin/match_controller.ex index 0261e32ea..6500bd999 100644 --- a/lib/teiserver_web/controllers/admin/match_controller.ex +++ b/lib/teiserver_web/controllers/admin/match_controller.ex @@ -68,12 +68,128 @@ defmodule TeiserverWeb.Admin.MatchController do @spec show(Plug.Conn.t(), Map.t()) :: Plug.Conn.t() def show(conn, %{"id" => id}) do + match = + Battle.get_match!(id, + joins: [], + preload: [:members_and_users] + ) + + members = + match.members + |> Enum.sort_by(fn m -> m.user.name end, &<=/2) + |> Enum.sort_by(fn m -> m.team_id end, &<=/2) + + match + |> MatchLib.make_favourite() + |> insert_recently(conn) + + match_name = MatchLib.make_match_name(match) + + rating_logs = + Game.list_rating_logs( + search: [ + match_id: match.id + ] + ) + |> Map.new(fn log -> {log.user_id, log} end) + + # Creates a map where the party_id refers to an integer + # but only includes parties with 2 or more members + parties = + members + |> Enum.group_by(fn m -> m.party_id end) + |> Map.drop([nil]) + |> Map.filter(fn {_id, members} -> Enum.count(members) > 1 end) + |> Map.keys() + |> Enum.zip(Teiserver.Helper.StylingHelper.bright_hex_colour_list()) + |> Map.new() + + # Now for balance related stuff + partied_players = + members + |> Enum.group_by(fn p -> p.party_id end, fn p -> p.user_id end) + + groups = + partied_players + |> Enum.map(fn + # The nil group is players without a party, they need to + # be broken out of the party + {nil, player_id_list} -> + player_id_list + |> Enum.filter(fn userid -> rating_logs[userid] != nil end) + |> Enum.map(fn userid -> + %{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} + end) + end) + |> List.flatten() + + past_balance = + BalanceLib.create_balance(groups, match.team_count, + algorithm: get_analysis_balance_algorithm() + ) + |> Map.put(:balance_mode, :grouped) + + # What about new balance? + new_balance = generate_new_balance_data(match) + + raw_events = + Telemetry.list_simple_match_events(where: [match_id: match.id], preload: [:event_types]) + + events_by_type = + raw_events + |> Enum.group_by( + fn e -> + e.event_type.name + end, + fn _ -> + 1 + end + ) + |> Enum.map(fn {name, vs} -> + {name, Enum.count(vs)} + end) + |> Enum.sort_by(fn v -> v end, &<=/2) + + team_lookup = + members + |> Map.new(fn m -> + {m.user_id, m.team_id} + end) + + events_by_team_and_type = + raw_events + |> Enum.group_by( + fn e -> + {team_lookup[e.user_id] || -1, e.event_type.name} + end, + fn _ -> + 1 + end + ) + |> Enum.map(fn {key, vs} -> + {key, Enum.count(vs)} + end) + |> Enum.sort_by(fn v -> v end, &<=/2) + conn - |> put_flash( - :info, - "/teiserver/admin/matches/:match_id is deprecated in favor of /battle/:id" - ) - |> redirect(to: ~p"/battle/#{id}") + |> assign(:match, match) + |> assign(:match_name, match_name) + |> assign(:members, members) + |> assign(:rating_logs, rating_logs) + |> assign(:parties, parties) + |> assign(:past_balance, past_balance) + |> assign(:new_balance, new_balance) + |> assign(:events_by_type, events_by_type) + |> assign(:events_by_team_and_type, events_by_team_and_type) + |> add_breadcrumb(name: "Show: #{match_name}", url: conn.request_path) + |> render("show.html") end @spec user_show(Plug.Conn.t(), Map.t()) :: Plug.Conn.t() @@ -115,4 +231,41 @@ defmodule TeiserverWeb.Admin.MatchController do |> assign(:matches, matches) |> render("server_index.html") end + + defp generate_new_balance_data(match) do + rating_type = MatchLib.game_type(match.team_size, match.team_count) + + partied_players = + match.members + |> Enum.group_by(fn p -> p.party_id end, fn p -> p.user_id end) + + groups = + partied_players + |> Enum.map(fn + # The nil group is players without a party, they need to + # be broken out of the party + {nil, player_id_list} -> + player_id_list + |> Enum.map(fn userid -> + %{userid => BalanceLib.get_user_rating_rank(userid, rating_type)} + end) + + {_party_id, player_id_list} -> + player_id_list + |> Map.new(fn userid -> + {userid, BalanceLib.get_user_rating_rank(userid, rating_type)} + end) + end) + |> List.flatten() + + BalanceLib.create_balance(groups, match.team_count, + algorithm: get_analysis_balance_algorithm() + ) + |> Map.put(:balance_mode, :grouped) + end + + defp get_analysis_balance_algorithm() do + # TODO move this from config into a dropdown so it can be selected on this page + Application.get_env(:teiserver, Teiserver)[:analysis_balance_algorithm] || "loser_picks" + end end diff --git a/lib/teiserver_web/templates/admin/match/index.html.heex b/lib/teiserver_web/templates/admin/match/index.html.heex index b490f9f17..2665b9bc4 100644 --- a/lib/teiserver_web/templates/admin/match/index.html.heex +++ b/lib/teiserver_web/templates/admin/match/index.html.heex @@ -74,7 +74,10 @@ <%= date_to_str(match.finished, format: :ymd_hms) %> - + Show diff --git a/lib/teiserver_web/templates/admin/match/server_index.html.heex b/lib/teiserver_web/templates/admin/match/server_index.html.heex index 70b7bb807..f02a4b7b1 100644 --- a/lib/teiserver_web/templates/admin/match/server_index.html.heex +++ b/lib/teiserver_web/templates/admin/match/server_index.html.heex @@ -87,7 +87,10 @@ <%= date_to_str(match.finished, format: :ymd_hms) %> - + Show diff --git a/lib/teiserver_web/templates/admin/match/show.html.heex b/lib/teiserver_web/templates/admin/match/show.html.heex new file mode 100644 index 000000000..a0912581e --- /dev/null +++ b/lib/teiserver_web/templates/admin/match/show.html.heex @@ -0,0 +1,112 @@ +<% bsname = view_colour() %> + + + +<%= render( + TeiserverWeb.Admin.GeneralView, + "sub_menu.html", + Map.merge(assigns, %{active: "matches"}) +) %> + +
+
+
+
+ <%= render( + TeiserverWeb.Admin.MatchView, + "section_menu.html", + Map.merge(assigns, %{ + show_search: false, + active: "" + }) + ) %> +
+ +
+   +
+ +

+ <%= @match_name %>      + <%= if @match.winning_team != nil do %> + Team <%= @match.winning_team + 1 %> won +      + <% end %> + Duration: <%= duration_to_str_short(@match.game_duration) %> +

+
+ + +
+
+ <%= render("tab_details.html", assigns) %> +
+ +
+ <%= render("tab_players.html", assigns) %> +
+ + <%= if @rating_logs != %{} and allow?(@conn, "Reviewer") do %> +
+ <%= render("tab_ratings.html", assigns) %> +
+ +
+ <%= render("tab_balance.html", assigns) %> +
+ <% end %> + + <%= if @rating_logs != %{} and allow?(@conn, "Reviewer") do %> +
+ <%= render("tab_events.html", assigns) %> +
+ <% end %> +
+
+
+
+
diff --git a/lib/teiserver_web/templates/admin/match/tab_balance.html.heex b/lib/teiserver_web/templates/admin/match/tab_balance.html.heex new file mode 100644 index 000000000..e5a47bf31 --- /dev/null +++ b/lib/teiserver_web/templates/admin/match/tab_balance.html.heex @@ -0,0 +1,41 @@ +

Based on data at the time

+ + + + + + + + + + + + + + + +
Team 1<%= @past_balance.ratings[1] |> round(2) %>
Team 2<%= @past_balance.ratings[2] |> round(2) %>
Deviation<%= @past_balance.deviation %>
+ + + +
+ +

If balance we made using current ratings

+ + + + + + + + + + + + + + + +
Team 1<%= @new_balance.ratings[1] |> round(2) %>
Team 2<%= @new_balance.ratings[2] |> round(2) %>
Deviation<%= @new_balance.deviation %>
+ + diff --git a/lib/teiserver_web/templates/admin/match/tab_details.html.heex b/lib/teiserver_web/templates/admin/match/tab_details.html.heex new file mode 100644 index 000000000..f583f4e68 --- /dev/null +++ b/lib/teiserver_web/templates/admin/match/tab_details.html.heex @@ -0,0 +1,37 @@ +<%= central_component("detail_line", + label: "Team count", + value: @match.team_count +) %> + +<%= central_component("detail_line", + label: "Team size", + value: @match.team_size +) %> + +
+ +<%= central_component("detail_line", + label: "Started", + value: date_to_str(@match.started, format: :ymd_hms, tz: @tz) +) %> + +<%= central_component("detail_line", + label: "Finished", + value: date_to_str(@match.finished, format: :ymd_hms, tz: @tz) +) %> + +<%= if allow?(@conn, "admin.dev") do %> + <%= central_component("detail_line", + label: "Tag key count", + value: Map.keys(@match.tags) |> Enum.count() + ) %> +<% end %> + +<%= if allow?(@conn, "Moderator") do %> +
+ Match data Match bots +<% end %> diff --git a/lib/teiserver_web/templates/admin/match/tab_events.html.heex b/lib/teiserver_web/templates/admin/match/tab_events.html.heex new file mode 100644 index 000000000..ed4b8595e --- /dev/null +++ b/lib/teiserver_web/templates/admin/match/tab_events.html.heex @@ -0,0 +1,18 @@ +
+
+

By type

+ <.table id="by_type" rows={@events_by_type} table_class="table-sm"> + <:col :let={{name, _}} label="Event"><%= name %> + <:col :let={{_, count}} label="Count"><%= count %> + +
+ +
+

By team and type

+ <.table id="by_type" rows={@events_by_team_and_type} table_class="table-sm"> + <:col :let={{{team, _}, _}} label="Team"><%= team + 1 %> + <:col :let={{{_, name}, _}} label="Event"><%= name %> + <:col :let={{_, count}} label="Count"><%= count %> + +
+
diff --git a/lib/teiserver_web/templates/admin/match/tab_players.html.heex b/lib/teiserver_web/templates/admin/match/tab_players.html.heex new file mode 100644 index 000000000..8652d84ec --- /dev/null +++ b/lib/teiserver_web/templates/admin/match/tab_players.html.heex @@ -0,0 +1,109 @@ +<% bsname = view_colour() %> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + <%= for m <- @members do %> + <% rating = @rating_logs[m.user_id] + party_colour = @parties[m.party_id] + + exit_status = calculate_exit_status(m.left_after, @match.game_duration) + + play_percentage = + if exit_status != :stayed do + (m.left_after / @match.game_duration * 100) |> round + end %> + + + + + + <%= if party_colour do %> + + <% else %> + + <% end %> + + + + + + + + + + + + + + + + + + + + + <% end %> + +
 DamageUnitsMetalEnergy 
Name & PartyTeamPlayDoneTakenKilledProdProdUsedProdUsedRating 
+ <%= if m.team_id == @match.winning_team do %> + + <% end %> + + <%= central_component("icon", icon: m.user.icon) %> + + <%= m.user.name %> +   <%= m.team_id + 1 %> + <%= case exit_status do %> + <% :stayed -> %> + <% :early -> %> +   <%= play_percentage %>% + <% :abandoned -> %> + +   <%= play_percentage %>% + <% :noshow -> %> + + <% end %> + <%= normalize(m.stats["damageDealt"]) %><%= normalize(m.stats["damageReceived"]) %><%= normalize(m.stats["unitsKilled"]) %><%= normalize(m.stats["unitsProduced"]) %><%= normalize(m.stats["metalProduced"]) %><%= normalize(m.stats["metalUsed"]) %><%= normalize(m.stats["energyProduced"]) %><%= normalize(m.stats["energyUsed"]) %> + <%= if rating != nil do %> + <%= rating.value["rating_value"] |> round(2) %> + <% end %> + + + Matches + +
diff --git a/lib/teiserver_web/templates/admin/match/tab_ratings.html.heex b/lib/teiserver_web/templates/admin/match/tab_ratings.html.heex new file mode 100644 index 000000000..a4d15fa84 --- /dev/null +++ b/lib/teiserver_web/templates/admin/match/tab_ratings.html.heex @@ -0,0 +1,81 @@ +<%= if @rating_logs != %{} do %> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + <%= for m <- @members do %> + <% rating = @rating_logs[m.user_id] %> + + + + + + + + + + + + + + + + + + + + + + <% end %> + +
 Rating Change 
NameTeamRatingSkillUncertainty RatingSkillUncertainty 
+ <%= if m.team_id == @match.winning_team do %> + + <% end %> + + <%= central_component("icon", icon: m.user.icon) %> + <%= m.user.name %><%= m.team_id + 1 %><%= rating.value["rating_value"] |> round(2) %><%= rating.value["skill"] |> round(2) %><%= rating.value["uncertainty"] |> round(2) %> <%= rating.value["rating_value_change"] |> round(2) %><%= rating.value["skill_change"] |> round(2) %><%= rating.value["uncertainty_change"] |> round(2) %> + + Show/Hide raw + +
+ +
+<% end %> diff --git a/lib/teiserver_web/templates/admin/match/user_index.html.heex b/lib/teiserver_web/templates/admin/match/user_index.html.heex index f924d6a8a..4ce294b0c 100644 --- a/lib/teiserver_web/templates/admin/match/user_index.html.heex +++ b/lib/teiserver_web/templates/admin/match/user_index.html.heex @@ -96,7 +96,10 @@ <%= date_to_str(match.finished, format: :ymd_hms) %> - + Show diff --git a/lib/teiserver_web/templates/battle/match/section_menu.html.heex b/lib/teiserver_web/templates/battle/match/section_menu.html.heex index 7578d689d..96912e1b0 100644 --- a/lib/teiserver_web/templates/battle/match/section_menu.html.heex +++ b/lib/teiserver_web/templates/battle/match/section_menu.html.heex @@ -37,7 +37,7 @@ active: @active, icon: Teiserver.Admin.AdminLib.icon(), bsname: bsname, - url: ~p"/battle/#{@match.id}" + url: ~p"/teiserver/admin/matches/#{@match.id}" ) %> <% end %> diff --git a/mix.exs b/mix.exs index 8e23a3e54..4058f1bd5 100644 --- a/mix.exs +++ b/mix.exs @@ -131,7 +131,9 @@ defmodule Teiserver.MixProject do defp dialyzer do [ plt_core_path: "priv/plts", - plt_file: {:no_warn, "priv/plts/dialyzer.plt"} + plt_file: {:no_warn, "priv/plts/dialyzer.plt"}, + # https://stackoverflow.com/questions/51208388/how-to-fix-dialyzer-callback-info-about-the-behaviour-is-not-available + plt_add_apps: [:mix] ] end diff --git a/test/teiserver/battle/balance_lib_internal_test.exs b/test/teiserver/battle/balance_lib_internal_test.exs index 173f5737b..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: "User_1", rank: 0, rating: 19}, - user2.id => %{name: "User_2", 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: "User_3", rank: 0, rating: 18}}, - %{user4.id => %{name: "User_4", rank: 0, rating: 15}}, - %{user5.id => %{name: "User_5", 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..dbf02f593 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 } diff --git a/test/teiserver/battle/split_one_chevs_test.exs b/test/teiserver/battle/split_one_chevs_test.exs index 408af32cc..f811fe1d4 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,12 +53,12 @@ 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 @@ -70,9 +71,9 @@ defmodule Teiserver.Battle.SplitOneChevsTest 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, @@ -89,21 +90,24 @@ 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: 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,24 @@ 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 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 From 093c8e4df9e60d674353a55002e0a0ac3d428b04 Mon Sep 17 00:00:00 2001 From: Joshua Augustinus Date: Sat, 15 Jun 2024 20:39:51 +1000 Subject: [PATCH 2/9] Remove admin pages This reverts commit 0dd77e30e5d6ef399aaab157f4136db2d4cc3fe6. --- .../controllers/admin/match_controller.ex | 163 +----------------- lib/teiserver_web/live/battles/match/show.ex | 19 +- .../templates/admin/match/index.html.heex | 5 +- .../admin/match/server_index.html.heex | 5 +- .../templates/admin/match/show.html.heex | 112 ------------ .../admin/match/tab_balance.html.heex | 41 ----- .../admin/match/tab_details.html.heex | 37 ---- .../admin/match/tab_events.html.heex | 18 -- .../admin/match/tab_players.html.heex | 109 ------------ .../admin/match/tab_ratings.html.heex | 81 --------- .../admin/match/user_index.html.heex | 5 +- 11 files changed, 22 insertions(+), 573 deletions(-) delete mode 100644 lib/teiserver_web/templates/admin/match/show.html.heex delete mode 100644 lib/teiserver_web/templates/admin/match/tab_balance.html.heex delete mode 100644 lib/teiserver_web/templates/admin/match/tab_details.html.heex delete mode 100644 lib/teiserver_web/templates/admin/match/tab_events.html.heex delete mode 100644 lib/teiserver_web/templates/admin/match/tab_players.html.heex delete mode 100644 lib/teiserver_web/templates/admin/match/tab_ratings.html.heex diff --git a/lib/teiserver_web/controllers/admin/match_controller.ex b/lib/teiserver_web/controllers/admin/match_controller.ex index 6500bd999..0261e32ea 100644 --- a/lib/teiserver_web/controllers/admin/match_controller.ex +++ b/lib/teiserver_web/controllers/admin/match_controller.ex @@ -68,128 +68,12 @@ defmodule TeiserverWeb.Admin.MatchController do @spec show(Plug.Conn.t(), Map.t()) :: Plug.Conn.t() def show(conn, %{"id" => id}) do - match = - Battle.get_match!(id, - joins: [], - preload: [:members_and_users] - ) - - members = - match.members - |> Enum.sort_by(fn m -> m.user.name end, &<=/2) - |> Enum.sort_by(fn m -> m.team_id end, &<=/2) - - match - |> MatchLib.make_favourite() - |> insert_recently(conn) - - match_name = MatchLib.make_match_name(match) - - rating_logs = - Game.list_rating_logs( - search: [ - match_id: match.id - ] - ) - |> Map.new(fn log -> {log.user_id, log} end) - - # Creates a map where the party_id refers to an integer - # but only includes parties with 2 or more members - parties = - members - |> Enum.group_by(fn m -> m.party_id end) - |> Map.drop([nil]) - |> Map.filter(fn {_id, members} -> Enum.count(members) > 1 end) - |> Map.keys() - |> Enum.zip(Teiserver.Helper.StylingHelper.bright_hex_colour_list()) - |> Map.new() - - # Now for balance related stuff - partied_players = - members - |> Enum.group_by(fn p -> p.party_id end, fn p -> p.user_id end) - - groups = - partied_players - |> Enum.map(fn - # The nil group is players without a party, they need to - # be broken out of the party - {nil, player_id_list} -> - player_id_list - |> Enum.filter(fn userid -> rating_logs[userid] != nil end) - |> Enum.map(fn userid -> - %{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} - end) - end) - |> List.flatten() - - past_balance = - BalanceLib.create_balance(groups, match.team_count, - algorithm: get_analysis_balance_algorithm() - ) - |> Map.put(:balance_mode, :grouped) - - # What about new balance? - new_balance = generate_new_balance_data(match) - - raw_events = - Telemetry.list_simple_match_events(where: [match_id: match.id], preload: [:event_types]) - - events_by_type = - raw_events - |> Enum.group_by( - fn e -> - e.event_type.name - end, - fn _ -> - 1 - end - ) - |> Enum.map(fn {name, vs} -> - {name, Enum.count(vs)} - end) - |> Enum.sort_by(fn v -> v end, &<=/2) - - team_lookup = - members - |> Map.new(fn m -> - {m.user_id, m.team_id} - end) - - events_by_team_and_type = - raw_events - |> Enum.group_by( - fn e -> - {team_lookup[e.user_id] || -1, e.event_type.name} - end, - fn _ -> - 1 - end - ) - |> Enum.map(fn {key, vs} -> - {key, Enum.count(vs)} - end) - |> Enum.sort_by(fn v -> v end, &<=/2) - conn - |> assign(:match, match) - |> assign(:match_name, match_name) - |> assign(:members, members) - |> assign(:rating_logs, rating_logs) - |> assign(:parties, parties) - |> assign(:past_balance, past_balance) - |> assign(:new_balance, new_balance) - |> assign(:events_by_type, events_by_type) - |> assign(:events_by_team_and_type, events_by_team_and_type) - |> add_breadcrumb(name: "Show: #{match_name}", url: conn.request_path) - |> render("show.html") + |> put_flash( + :info, + "/teiserver/admin/matches/:match_id is deprecated in favor of /battle/:id" + ) + |> redirect(to: ~p"/battle/#{id}") end @spec user_show(Plug.Conn.t(), Map.t()) :: Plug.Conn.t() @@ -231,41 +115,4 @@ defmodule TeiserverWeb.Admin.MatchController do |> assign(:matches, matches) |> render("server_index.html") end - - defp generate_new_balance_data(match) do - rating_type = MatchLib.game_type(match.team_size, match.team_count) - - partied_players = - match.members - |> Enum.group_by(fn p -> p.party_id end, fn p -> p.user_id end) - - groups = - partied_players - |> Enum.map(fn - # The nil group is players without a party, they need to - # be broken out of the party - {nil, player_id_list} -> - player_id_list - |> Enum.map(fn userid -> - %{userid => BalanceLib.get_user_rating_rank(userid, rating_type)} - end) - - {_party_id, player_id_list} -> - player_id_list - |> Map.new(fn userid -> - {userid, BalanceLib.get_user_rating_rank(userid, rating_type)} - end) - end) - |> List.flatten() - - BalanceLib.create_balance(groups, match.team_count, - algorithm: get_analysis_balance_algorithm() - ) - |> Map.put(:balance_mode, :grouped) - end - - defp get_analysis_balance_algorithm() do - # TODO move this from config into a dropdown so it can be selected on this page - Application.get_env(:teiserver, Teiserver)[:analysis_balance_algorithm] || "loser_picks" - end end diff --git a/lib/teiserver_web/live/battles/match/show.ex b/lib/teiserver_web/live/battles/match/show.ex index bd02fcf6b..ade438e52 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 @@ -115,20 +115,22 @@ 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: get_analysis_balance_algorithm() + ) |> Map.put(:balance_mode, :grouped) # What about new balance? @@ -223,7 +225,14 @@ 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: get_analysis_balance_algorithm() + ) |> Map.put(:balance_mode, :grouped) end + + defp get_analysis_balance_algorithm() do + # TODO move this from config into a dropdown so it can be selected on this page + Application.get_env(:teiserver, Teiserver)[:analysis_balance_algorithm] || "loser_picks" + end end diff --git a/lib/teiserver_web/templates/admin/match/index.html.heex b/lib/teiserver_web/templates/admin/match/index.html.heex index 2665b9bc4..b490f9f17 100644 --- a/lib/teiserver_web/templates/admin/match/index.html.heex +++ b/lib/teiserver_web/templates/admin/match/index.html.heex @@ -74,10 +74,7 @@ <%= date_to_str(match.finished, format: :ymd_hms) %> - + Show diff --git a/lib/teiserver_web/templates/admin/match/server_index.html.heex b/lib/teiserver_web/templates/admin/match/server_index.html.heex index f02a4b7b1..70b7bb807 100644 --- a/lib/teiserver_web/templates/admin/match/server_index.html.heex +++ b/lib/teiserver_web/templates/admin/match/server_index.html.heex @@ -87,10 +87,7 @@ <%= date_to_str(match.finished, format: :ymd_hms) %> - + Show diff --git a/lib/teiserver_web/templates/admin/match/show.html.heex b/lib/teiserver_web/templates/admin/match/show.html.heex deleted file mode 100644 index a0912581e..000000000 --- a/lib/teiserver_web/templates/admin/match/show.html.heex +++ /dev/null @@ -1,112 +0,0 @@ -<% bsname = view_colour() %> - - - -<%= render( - TeiserverWeb.Admin.GeneralView, - "sub_menu.html", - Map.merge(assigns, %{active: "matches"}) -) %> - -
-
-
-
- <%= render( - TeiserverWeb.Admin.MatchView, - "section_menu.html", - Map.merge(assigns, %{ - show_search: false, - active: "" - }) - ) %> -
- -
-   -
- -

- <%= @match_name %>      - <%= if @match.winning_team != nil do %> - Team <%= @match.winning_team + 1 %> won -      - <% end %> - Duration: <%= duration_to_str_short(@match.game_duration) %> -

-
- - -
-
- <%= render("tab_details.html", assigns) %> -
- -
- <%= render("tab_players.html", assigns) %> -
- - <%= if @rating_logs != %{} and allow?(@conn, "Reviewer") do %> -
- <%= render("tab_ratings.html", assigns) %> -
- -
- <%= render("tab_balance.html", assigns) %> -
- <% end %> - - <%= if @rating_logs != %{} and allow?(@conn, "Reviewer") do %> -
- <%= render("tab_events.html", assigns) %> -
- <% end %> -
-
-
-
-
diff --git a/lib/teiserver_web/templates/admin/match/tab_balance.html.heex b/lib/teiserver_web/templates/admin/match/tab_balance.html.heex deleted file mode 100644 index e5a47bf31..000000000 --- a/lib/teiserver_web/templates/admin/match/tab_balance.html.heex +++ /dev/null @@ -1,41 +0,0 @@ -

Based on data at the time

- - - - - - - - - - - - - - - -
Team 1<%= @past_balance.ratings[1] |> round(2) %>
Team 2<%= @past_balance.ratings[2] |> round(2) %>
Deviation<%= @past_balance.deviation %>
- - - -
- -

If balance we made using current ratings

- - - - - - - - - - - - - - - -
Team 1<%= @new_balance.ratings[1] |> round(2) %>
Team 2<%= @new_balance.ratings[2] |> round(2) %>
Deviation<%= @new_balance.deviation %>
- - diff --git a/lib/teiserver_web/templates/admin/match/tab_details.html.heex b/lib/teiserver_web/templates/admin/match/tab_details.html.heex deleted file mode 100644 index f583f4e68..000000000 --- a/lib/teiserver_web/templates/admin/match/tab_details.html.heex +++ /dev/null @@ -1,37 +0,0 @@ -<%= central_component("detail_line", - label: "Team count", - value: @match.team_count -) %> - -<%= central_component("detail_line", - label: "Team size", - value: @match.team_size -) %> - -
- -<%= central_component("detail_line", - label: "Started", - value: date_to_str(@match.started, format: :ymd_hms, tz: @tz) -) %> - -<%= central_component("detail_line", - label: "Finished", - value: date_to_str(@match.finished, format: :ymd_hms, tz: @tz) -) %> - -<%= if allow?(@conn, "admin.dev") do %> - <%= central_component("detail_line", - label: "Tag key count", - value: Map.keys(@match.tags) |> Enum.count() - ) %> -<% end %> - -<%= if allow?(@conn, "Moderator") do %> -
- Match data Match bots -<% end %> diff --git a/lib/teiserver_web/templates/admin/match/tab_events.html.heex b/lib/teiserver_web/templates/admin/match/tab_events.html.heex deleted file mode 100644 index ed4b8595e..000000000 --- a/lib/teiserver_web/templates/admin/match/tab_events.html.heex +++ /dev/null @@ -1,18 +0,0 @@ -
-
-

By type

- <.table id="by_type" rows={@events_by_type} table_class="table-sm"> - <:col :let={{name, _}} label="Event"><%= name %> - <:col :let={{_, count}} label="Count"><%= count %> - -
- -
-

By team and type

- <.table id="by_type" rows={@events_by_team_and_type} table_class="table-sm"> - <:col :let={{{team, _}, _}} label="Team"><%= team + 1 %> - <:col :let={{{_, name}, _}} label="Event"><%= name %> - <:col :let={{_, count}} label="Count"><%= count %> - -
-
diff --git a/lib/teiserver_web/templates/admin/match/tab_players.html.heex b/lib/teiserver_web/templates/admin/match/tab_players.html.heex deleted file mode 100644 index 8652d84ec..000000000 --- a/lib/teiserver_web/templates/admin/match/tab_players.html.heex +++ /dev/null @@ -1,109 +0,0 @@ -<% bsname = view_colour() %> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - <%= for m <- @members do %> - <% rating = @rating_logs[m.user_id] - party_colour = @parties[m.party_id] - - exit_status = calculate_exit_status(m.left_after, @match.game_duration) - - play_percentage = - if exit_status != :stayed do - (m.left_after / @match.game_duration * 100) |> round - end %> - - - - - - <%= if party_colour do %> - - <% else %> - - <% end %> - - - - - - - - - - - - - - - - - - - - - <% end %> - -
 DamageUnitsMetalEnergy 
Name & PartyTeamPlayDoneTakenKilledProdProdUsedProdUsedRating 
- <%= if m.team_id == @match.winning_team do %> - - <% end %> - - <%= central_component("icon", icon: m.user.icon) %> - - <%= m.user.name %> -   <%= m.team_id + 1 %> - <%= case exit_status do %> - <% :stayed -> %> - <% :early -> %> -   <%= play_percentage %>% - <% :abandoned -> %> - -   <%= play_percentage %>% - <% :noshow -> %> - - <% end %> - <%= normalize(m.stats["damageDealt"]) %><%= normalize(m.stats["damageReceived"]) %><%= normalize(m.stats["unitsKilled"]) %><%= normalize(m.stats["unitsProduced"]) %><%= normalize(m.stats["metalProduced"]) %><%= normalize(m.stats["metalUsed"]) %><%= normalize(m.stats["energyProduced"]) %><%= normalize(m.stats["energyUsed"]) %> - <%= if rating != nil do %> - <%= rating.value["rating_value"] |> round(2) %> - <% end %> - - - Matches - -
diff --git a/lib/teiserver_web/templates/admin/match/tab_ratings.html.heex b/lib/teiserver_web/templates/admin/match/tab_ratings.html.heex deleted file mode 100644 index a4d15fa84..000000000 --- a/lib/teiserver_web/templates/admin/match/tab_ratings.html.heex +++ /dev/null @@ -1,81 +0,0 @@ -<%= if @rating_logs != %{} do %> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - <%= for m <- @members do %> - <% rating = @rating_logs[m.user_id] %> - - - - - - - - - - - - - - - - - - - - - - <% end %> - -
 Rating Change 
NameTeamRatingSkillUncertainty RatingSkillUncertainty 
- <%= if m.team_id == @match.winning_team do %> - - <% end %> - - <%= central_component("icon", icon: m.user.icon) %> - <%= m.user.name %><%= m.team_id + 1 %><%= rating.value["rating_value"] |> round(2) %><%= rating.value["skill"] |> round(2) %><%= rating.value["uncertainty"] |> round(2) %> <%= rating.value["rating_value_change"] |> round(2) %><%= rating.value["skill_change"] |> round(2) %><%= rating.value["uncertainty_change"] |> round(2) %> - - Show/Hide raw - -
- -
-<% end %> diff --git a/lib/teiserver_web/templates/admin/match/user_index.html.heex b/lib/teiserver_web/templates/admin/match/user_index.html.heex index 4ce294b0c..f924d6a8a 100644 --- a/lib/teiserver_web/templates/admin/match/user_index.html.heex +++ b/lib/teiserver_web/templates/admin/match/user_index.html.heex @@ -96,10 +96,7 @@ <%= date_to_str(match.finished, format: :ymd_hms) %> - + Show From 95d352ed78d1265b88a8282614e52ee3dbc9c0d6 Mon Sep 17 00:00:00 2001 From: Joshua Augustinus Date: Sat, 15 Jun 2024 21:06:33 +1000 Subject: [PATCH 3/9] Handle balancer via url Check for bad balancer --- config/dev.exs | 7 +-- lib/teiserver/battle/libs/balance_lib.ex | 12 ++++- lib/teiserver_web/live/battles/match/show.ex | 49 ++++++++++--------- .../live/battles/match/show.html.heex | 4 +- lib/teiserver_web/router.ex | 1 + .../battle/match/section_menu.html.heex | 2 +- 6 files changed, 44 insertions(+), 31 deletions(-) diff --git a/config/dev.exs b/config/dev.exs index cec09588c..5931816ed 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -60,12 +60,7 @@ config :teiserver, Teiserver, heartbeat_timeout: nil, enable_discord_bridge: false, enable_hailstorm: true, - accept_all_emails: true, - - # The balance algorithm to use on the admin/matches/match/:id page - # It is purely used for analysis and not for actual games - # TODO move this into dropdown on admin/matches/match/:id page - analysis_balance_algorithm: "loser_picks" + accept_all_emails: true # Watch static and templates for browser reloading. config :teiserver, TeiserverWeb.Endpoint, diff --git a/lib/teiserver/battle/libs/balance_lib.ex b/lib/teiserver/battle/libs/balance_lib.ex index 1f56deccf..105c95ea3 100644 --- a/lib/teiserver/battle/libs/balance_lib.ex +++ b/lib/teiserver/battle/libs/balance_lib.ex @@ -42,7 +42,7 @@ defmodule Teiserver.Battle.BalanceLib do } end - defp get_default_algorithm() do + def get_default_algorithm() do # For now it's a constant but this could be moved to a configurable value @default_balance_algorithm end @@ -181,6 +181,16 @@ defmodule Teiserver.Battle.BalanceLib do |> Map.put(:time_taken, System.system_time(:microsecond) - start_time) end + def is_valid_algorithm?(algo_name) do + case algorithm_modules()[algo_name] do + nil -> + false + + _ -> + true + end + end + @doc """ Sometimes groups have missing data so we need to refetch it. If we go through balancer_server then all the required data should be there diff --git a/lib/teiserver_web/live/battles/match/show.ex b/lib/teiserver_web/live/battles/match/show.ex index ade438e52..236741043 100644 --- a/lib/teiserver_web/live/battles/match/show.ex +++ b/lib/teiserver_web/live/battles/match/show.ex @@ -16,14 +16,26 @@ defmodule TeiserverWeb.Battle.MatchLive.Show do end @impl true - def handle_params(%{"id" => id} = params, _url, socket) do - socket = - socket - |> assign(:id, String.to_integer(id)) - |> get_match() - |> assign(:tab, socket.assigns.live_action) - - {:noreply, apply_action(socket, socket.assigns.live_action, params)} + def handle_params(params, _url, socket) do + id = Map.get(params, "id") + default_balancer = BalanceLib.get_default_algorithm() + balancer = Map.get(params, "balancer", default_balancer) + + if(!BalanceLib.is_valid_algorithm?(balancer)) do + {:noreply, + socket + |> put_flash(:error, "#{balancer} is not a valid balancer") + |> push_patch(to: "/battle/#{id}/balance/#{default_balancer}")} + else + socket = + socket + |> assign(:id, String.to_integer(id)) + |> assign(:balancer, balancer) + |> get_match() + |> assign(:tab, socket.assigns.live_action) + + {:noreply, apply_action(socket, socket.assigns.live_action, params)} + end end defp apply_action(%{assigns: %{match_name: match_name}} = socket, :overview, _params) do @@ -59,7 +71,7 @@ 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, balancer: balancer, current_user: _current_user}} = socket) do if connected?(socket) do match = Battle.get_match!(id, @@ -128,13 +140,11 @@ defmodule TeiserverWeb.Battle.MatchLive.Show do |> List.flatten() past_balance = - BalanceLib.create_balance(groups, match.team_count, - algorithm: get_analysis_balance_algorithm() - ) + BalanceLib.create_balance(groups, match.team_count, algorithm: balancer) |> Map.put(:balance_mode, :grouped) # What about new balance? - new_balance = generate_new_balance_data(match) + new_balance = generate_new_balance_data(match, balancer) raw_events = Telemetry.list_simple_match_events(where: [match_id: match.id], preload: [:event_types]) @@ -185,6 +195,7 @@ defmodule TeiserverWeb.Battle.MatchLive.Show do |> assign(:new_balance, new_balance) |> assign(:events_by_type, events_by_type) |> assign(:events_by_team_and_type, events_by_team_and_type) + |> assign(:balancer, balancer) else socket |> assign(:match, nil) @@ -196,10 +207,11 @@ defmodule TeiserverWeb.Battle.MatchLive.Show do |> assign(:new_balance, %{}) |> assign(:events_by_type, %{}) |> assign(:events_by_team_and_type, %{}) + |> assign(:balancer, balancer) end end - defp generate_new_balance_data(match) do + defp generate_new_balance_data(match, balancer) do rating_type = MatchLib.game_type(match.team_size, match.team_count) partied_players = @@ -225,14 +237,7 @@ defmodule TeiserverWeb.Battle.MatchLive.Show do end) |> List.flatten() - BalanceLib.create_balance(groups, match.team_count, - algorithm: get_analysis_balance_algorithm() - ) + BalanceLib.create_balance(groups, match.team_count, algorithm: balancer) |> Map.put(:balance_mode, :grouped) end - - defp get_analysis_balance_algorithm() do - # TODO move this from config into a dropdown so it can be selected on this page - Application.get_env(:teiserver, Teiserver)[:analysis_balance_algorithm] || "loser_picks" - 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..19bfe9ab6 100644 --- a/lib/teiserver_web/live/battles/match/show.html.heex +++ b/lib/teiserver_web/live/battles/match/show.html.heex @@ -42,7 +42,7 @@ <% end %> <%= if @rating_logs != %{} and allow?(@current_user, "Overwatch") do %> - <.tab_nav url={~p"/battle/#{@match.id}/balance"} selected={@tab == :balance}> + <.tab_nav url={~p"/battle/#{@match.id}/balance/loser_picks"} selected={@tab == :balance}> Balance <% end %> @@ -284,7 +284,9 @@ <%= if allow?(@current_user, "Overwatch") do %>
+

Balancer: <%= @balancer %>

Based on data at the time

+ diff --git a/lib/teiserver_web/router.ex b/lib/teiserver_web/router.ex index 7851b490a..83d23e118 100644 --- a/lib/teiserver_web/router.ex +++ b/lib/teiserver_web/router.ex @@ -304,6 +304,7 @@ defmodule TeiserverWeb.Router do live "/:id/players", MatchLive.Show, :players live "/:id/ratings", MatchLive.Show, :ratings live "/:id/balance", MatchLive.Show, :balance + live "/:id/balance/:balancer", MatchLive.Show, :balance live "/:id/events", MatchLive.Show, :events end end diff --git a/lib/teiserver_web/templates/battle/match/section_menu.html.heex b/lib/teiserver_web/templates/battle/match/section_menu.html.heex index 96912e1b0..7578d689d 100644 --- a/lib/teiserver_web/templates/battle/match/section_menu.html.heex +++ b/lib/teiserver_web/templates/battle/match/section_menu.html.heex @@ -37,7 +37,7 @@ active: @active, icon: Teiserver.Admin.AdminLib.icon(), bsname: bsname, - url: ~p"/teiserver/admin/matches/#{@match.id}" + url: ~p"/battle/#{@match.id}" ) %> <% end %> From a2c597652e8d45c1c9b555c4e574dc5ffb78fad8 Mon Sep 17 00:00:00 2001 From: Joshua Augustinus Date: Sun, 16 Jun 2024 06:19:46 +1000 Subject: [PATCH 4/9] Add balancer dropdown Refactor minor --- lib/teiserver/battle/libs/balance_lib.ex | 11 +---- lib/teiserver_web/live/battles/match/show.ex | 49 ++++++++++--------- .../live/battles/match/show.html.heex | 15 +++++- lib/teiserver_web/router.ex | 1 - 4 files changed, 41 insertions(+), 35 deletions(-) diff --git a/lib/teiserver/battle/libs/balance_lib.ex b/lib/teiserver/battle/libs/balance_lib.ex index 105c95ea3..b3d11f512 100644 --- a/lib/teiserver/battle/libs/balance_lib.ex +++ b/lib/teiserver/battle/libs/balance_lib.ex @@ -60,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() @@ -181,16 +182,6 @@ defmodule Teiserver.Battle.BalanceLib do |> Map.put(:time_taken, System.system_time(:microsecond) - start_time) end - def is_valid_algorithm?(algo_name) do - case algorithm_modules()[algo_name] do - nil -> - false - - _ -> - true - end - end - @doc """ Sometimes groups have missing data so we need to refetch it. If we go through balancer_server then all the required data should be there diff --git a/lib/teiserver_web/live/battles/match/show.ex b/lib/teiserver_web/live/battles/match/show.ex index 236741043..f93bc9197 100644 --- a/lib/teiserver_web/live/battles/match/show.ex +++ b/lib/teiserver_web/live/battles/match/show.ex @@ -11,31 +11,24 @@ defmodule TeiserverWeb.Battle.MatchLive.Show do |> assign(:site_menu_active, "match") |> assign(:view_colour, Teiserver.Battle.MatchLib.colours()) |> assign(:tab, "details") + |> assign( + :balancer_options, + BalanceLib.get_allowed_algorithms(true) + ) + |> assign(:balancer, BalanceLib.get_default_algorithm()) {:ok, socket} end @impl true - def handle_params(params, _url, socket) do - id = Map.get(params, "id") - default_balancer = BalanceLib.get_default_algorithm() - balancer = Map.get(params, "balancer", default_balancer) - - if(!BalanceLib.is_valid_algorithm?(balancer)) do - {:noreply, - socket - |> put_flash(:error, "#{balancer} is not a valid balancer") - |> push_patch(to: "/battle/#{id}/balance/#{default_balancer}")} - else - socket = - socket - |> assign(:id, String.to_integer(id)) - |> assign(:balancer, balancer) - |> get_match() - |> assign(:tab, socket.assigns.live_action) - - {:noreply, apply_action(socket, socket.assigns.live_action, params)} - end + def handle_params(%{"id" => id} = params, _url, socket) do + socket = + socket + |> assign(:id, String.to_integer(id)) + |> get_match() + |> assign(:tab, socket.assigns.live_action) + + {:noreply, apply_action(socket, socket.assigns.live_action, params)} end defp apply_action(%{assigns: %{match_name: match_name}} = socket, :overview, _params) do @@ -195,7 +188,6 @@ defmodule TeiserverWeb.Battle.MatchLive.Show do |> assign(:new_balance, new_balance) |> assign(:events_by_type, events_by_type) |> assign(:events_by_team_and_type, events_by_team_and_type) - |> assign(:balancer, balancer) else socket |> assign(:match, nil) @@ -207,7 +199,6 @@ defmodule TeiserverWeb.Battle.MatchLive.Show do |> assign(:new_balance, %{}) |> assign(:events_by_type, %{}) |> assign(:events_by_team_and_type, %{}) - |> assign(:balancer, balancer) end end @@ -240,4 +231,18 @@ defmodule TeiserverWeb.Battle.MatchLive.Show do BalanceLib.create_balance(groups, match.team_count, algorithm: balancer) |> Map.put(:balance_mode, :grouped) end + + @doc """ + Handles the dropdown for balancer changing + """ + @impl true + def handle_event("update-balancer", event, socket) do + [key] = event["_target"] + value = event[key] + + {:noreply, + socket + |> assign(:balancer, 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 19bfe9ab6..5dfc103d7 100644 --- a/lib/teiserver_web/live/battles/match/show.html.heex +++ b/lib/teiserver_web/live/battles/match/show.html.heex @@ -42,7 +42,7 @@ <% end %> <%= if @rating_logs != %{} and allow?(@current_user, "Overwatch") do %> - <.tab_nav url={~p"/battle/#{@match.id}/balance/loser_picks"} selected={@tab == :balance}> + <.tab_nav url={~p"/battle/#{@match.id}/balance"} selected={@tab == :balance}> Balance <% end %> @@ -284,7 +284,18 @@ <%= if allow?(@current_user, "Overwatch") do %>
-

Balancer: <%= @balancer %>

+
+ <.input + type="select" + label="Balancer" + options={@balancer_options} + name="balancer" + value={@balancer} + phx-change="update-balancer" + /> + +
+

Based on data at the time

diff --git a/lib/teiserver_web/router.ex b/lib/teiserver_web/router.ex index 83d23e118..7851b490a 100644 --- a/lib/teiserver_web/router.ex +++ b/lib/teiserver_web/router.ex @@ -304,7 +304,6 @@ defmodule TeiserverWeb.Router do live "/:id/players", MatchLive.Show, :players live "/:id/ratings", MatchLive.Show, :ratings live "/:id/balance", MatchLive.Show, :balance - live "/:id/balance/:balancer", MatchLive.Show, :balance live "/:id/events", MatchLive.Show, :events end end From 2cd66d9e4ed14bbdc4931d55178c1621ae5abe18 Mon Sep 17 00:00:00 2001 From: Joshua Augustinus Date: Sun, 16 Jun 2024 10:06:48 +1000 Subject: [PATCH 5/9] Fix mix tasks Minor update --- lib/teiserver/mix_tasks/fake_data.ex | 2 +- lib/teiserver/mix_tasks/fake_playtime.ex | 10 +--------- lib/teiserver_web/live/battles/match/show.ex | 4 +++- lib/teiserver_web/live/battles/match/show.html.heex | 12 ++++++------ 4 files changed, 11 insertions(+), 17 deletions(-) diff --git a/lib/teiserver/mix_tasks/fake_data.ex b/lib/teiserver/mix_tasks/fake_data.ex index 1b322b40b..2ca10e0f6 100644 --- a/lib/teiserver/mix_tasks/fake_data.ex +++ b/lib/teiserver/mix_tasks/fake_data.ex @@ -96,7 +96,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 index 4bc371f08..72fa3cb72 100644 --- a/lib/teiserver/mix_tasks/fake_playtime.ex +++ b/lib/teiserver/mix_tasks/fake_playtime.ex @@ -36,16 +36,8 @@ defmodule Mix.Tasks.Teiserver.FakePlaytime do # Now recalculate ranks # This calc would usually be done in do_login rank = CacheUser.calculate_rank(user_id) - user = Teiserver.Account.UserCacheLib.get_user_by_id(user_id) - user = %{ - user - | rank: rank - } - - CacheUser.update_user(user, true) - - Account.update_user_stat(user.id, %{ + Account.update_user_stat(user_id, %{ rank: rank }) end diff --git a/lib/teiserver_web/live/battles/match/show.ex b/lib/teiserver_web/live/battles/match/show.ex index f93bc9197..0b20dd9e3 100644 --- a/lib/teiserver_web/live/battles/match/show.ex +++ b/lib/teiserver_web/live/battles/match/show.ex @@ -54,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 diff --git a/lib/teiserver_web/live/battles/match/show.html.heex b/lib/teiserver_web/live/battles/match/show.html.heex index 5dfc103d7..94f221395 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,18 +282,18 @@ <% end %> - <%= if allow?(@current_user, "Overwatch") do %> + <%= if allow_any?(@current_user, ["Overwatch", "Contributor"]) do %>
-
- <.input + + <.input type="select" label="Balancer" options={@balancer_options} name="balancer" value={@balancer} phx-change="update-balancer" - /> - + /> +

Based on data at the time

From 182e0347a32b9d8164e315d3a20389b3d46759b0 Mon Sep 17 00:00:00 2001 From: Joshua Augustinus Date: Tue, 25 Jun 2024 14:22:01 +1000 Subject: [PATCH 6/9] Call teifion's algo when not enough noobs More improvements --- .../battle/balance/cheeky_switcher_smart.ex | 9 +-- .../battle/balance/split_one_chevs.ex | 45 ++++++++---- lib/teiserver/helpers/number_helper.ex | 16 +++++ .../battle/split_one_chevs_internal_test.exs | 72 +++++++++++++++++++ .../teiserver/battle/split_one_chevs_test.exs | 27 ++++++- 5 files changed, 148 insertions(+), 21 deletions(-) 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 98ef99304..5ca3643f5 100644 --- a/lib/teiserver/battle/balance/split_one_chevs.ex +++ b/lib/teiserver/battle/balance/split_one_chevs.ex @@ -18,6 +18,7 @@ defmodule Teiserver.Battle.Balance.SplitOneChevs do """ alias Teiserver.Battle.Balance.SplitOneChevsTypes, as: ST alias Teiserver.Battle.Balance.BalanceTypes, as: BT + import Teiserver.Helper.NumberHelper, only: [format: 1] @splitter "---------------------------" @@ -26,10 +27,34 @@ defmodule Teiserver.Battle.Balance.SplitOneChevs do 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 noobs; calling another balancer.", @splitter, result.logs] + |> List.flatten() + + Map.put(result, :logs, new_logs) + end + end + + @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 """ @@ -73,16 +98,6 @@ defmodule Teiserver.Battle.Balance.SplitOneChevs do |> List.flatten() end - defp round_number(rating_value) when is_float(rating_value) do - rating_value - |> Decimal.from_float() - |> Decimal.round(1) - end - - defp round_number(rating_value) when is_integer(rating_value) do - rating_value - end - @doc """ Assigns teams using algorithm defined in moduledoc See split_one_chevs_internal_test.exs for sample input @@ -104,7 +119,7 @@ defmodule Teiserver.Battle.Balance.SplitOneChevs do username = x.name new_log = - "#{username} (#{round_number(x.rating)}, σ: #{round_number(x.uncertainty)}, Chev: #{x.rank + 1}) picked for Team #{picking_team.team_id}" + "#{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/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/test/teiserver/battle/split_one_chevs_internal_test.exs b/test/teiserver/battle/split_one_chevs_internal_test.exs index dbf02f593..9ff7f975b 100644 --- a/test/teiserver/battle/split_one_chevs_internal_test.exs +++ b/test/teiserver/battle/split_one_chevs_internal_test.exs @@ -108,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 f811fe1d4..c501090d2 100644 --- a/test/teiserver/battle/split_one_chevs_test.exs +++ b/test/teiserver/battle/split_one_chevs_test.exs @@ -64,7 +64,7 @@ defmodule Teiserver.Battle.SplitOneChevsTest do 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 @@ -83,7 +83,7 @@ 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 @@ -135,4 +135,27 @@ defmodule Teiserver.Battle.SplitOneChevsTest do "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 noobs; calling another balancer.", + "---------------------------", + "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 From 867ab5d2480ce9d2b029d8020c6c33de3f990529 Mon Sep 17 00:00:00 2001 From: Joshua Augustinus Date: Sat, 29 Jun 2024 03:44:10 +1000 Subject: [PATCH 7/9] Updates based on Lexon feedback --- .../battle/balance/split_one_chevs.ex | 5 +++++ lib/teiserver/mix_tasks/fake_data.ex | 3 +++ lib/teiserver/mix_tasks/fake_playtime.ex | 4 +++- lib/teiserver_web/live/battles/match/show.ex | 22 ++++++++++--------- .../live/battles/match/show.html.heex | 10 ++++----- 5 files changed, 28 insertions(+), 16 deletions(-) diff --git a/lib/teiserver/battle/balance/split_one_chevs.ex b/lib/teiserver/battle/balance/split_one_chevs.ex index 5ca3643f5..1ec783751 100644 --- a/lib/teiserver/battle/balance/split_one_chevs.ex +++ b/lib/teiserver/battle/balance/split_one_chevs.ex @@ -44,6 +44,11 @@ defmodule Teiserver.Battle.Balance.SplitOneChevs do 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 = diff --git a/lib/teiserver/mix_tasks/fake_data.ex b/lib/teiserver/mix_tasks/fake_data.ex index 2ca10e0f6..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( diff --git a/lib/teiserver/mix_tasks/fake_playtime.ex b/lib/teiserver/mix_tasks/fake_playtime.ex index 72fa3cb72..f07cbf66a 100644 --- a/lib/teiserver/mix_tasks/fake_playtime.ex +++ b/lib/teiserver/mix_tasks/fake_playtime.ex @@ -1,7 +1,9 @@ defmodule Mix.Tasks.Teiserver.FakePlaytime do @moduledoc """ Adds fake play time stats to all non bot users - Run with + This will also be called by the teiserver.fakedata task + + If you want to run this task invidually, use: mix teiserver.fake_playtime """ diff --git a/lib/teiserver_web/live/battles/match/show.ex b/lib/teiserver_web/live/battles/match/show.ex index 0b20dd9e3..09ff0660c 100644 --- a/lib/teiserver_web/live/battles/match/show.ex +++ b/lib/teiserver_web/live/battles/match/show.ex @@ -12,10 +12,10 @@ defmodule TeiserverWeb.Battle.MatchLive.Show do |> assign(:view_colour, Teiserver.Battle.MatchLib.colours()) |> assign(:tab, "details") |> assign( - :balancer_options, + :algorithm_options, BalanceLib.get_allowed_algorithms(true) ) - |> assign(:balancer, BalanceLib.get_default_algorithm()) + |> assign(:algorithm, BalanceLib.get_default_algorithm()) {:ok, socket} end @@ -66,7 +66,9 @@ defmodule TeiserverWeb.Battle.MatchLive.Show do # {:noreply, assign(socket, :tab, tab)} # end - defp get_match(%{assigns: %{id: id, balancer: balancer, 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, @@ -135,11 +137,11 @@ defmodule TeiserverWeb.Battle.MatchLive.Show do |> List.flatten() past_balance = - BalanceLib.create_balance(groups, match.team_count, algorithm: balancer) + 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, balancer) + new_balance = generate_new_balance_data(match, algorithm) raw_events = Telemetry.list_simple_match_events(where: [match_id: match.id], preload: [:event_types]) @@ -204,7 +206,7 @@ defmodule TeiserverWeb.Battle.MatchLive.Show do end end - defp generate_new_balance_data(match, balancer) do + defp generate_new_balance_data(match, algorithm) do rating_type = MatchLib.game_type(match.team_size, match.team_count) partied_players = @@ -230,21 +232,21 @@ defmodule TeiserverWeb.Battle.MatchLive.Show do end) |> List.flatten() - BalanceLib.create_balance(groups, match.team_count, algorithm: balancer) + BalanceLib.create_balance(groups, match.team_count, algorithm: algorithm) |> Map.put(:balance_mode, :grouped) end @doc """ - Handles the dropdown for balancer changing + Handles the dropdown for algorithm changing """ @impl true - def handle_event("update-balancer", event, socket) do + def handle_event("update-algorithm", event, socket) do [key] = event["_target"] value = event[key] {:noreply, socket - |> assign(:balancer, value) + |> 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 94f221395..6cf6094b0 100644 --- a/lib/teiserver_web/live/battles/match/show.html.heex +++ b/lib/teiserver_web/live/battles/match/show.html.heex @@ -287,11 +287,11 @@
<.input type="select" - label="Balancer" - options={@balancer_options} - name="balancer" - value={@balancer} - phx-change="update-balancer" + label="Balance Algorithm" + options={@algorithm_options} + name="algorithm" + value={@algorithm} + phx-change="update-algorithm" />
From 729d61085d1130592c0c1347bc7a201b1be4ab71 Mon Sep 17 00:00:00 2001 From: Joshua Augustinus Date: Wed, 3 Jul 2024 09:55:44 +1000 Subject: [PATCH 8/9] Minor updates --- lib/teiserver/battle/balance/split_one_chevs.ex | 2 +- lib/teiserver_web/live/battles/match/ratings.html.heex | 4 +++- test/teiserver/battle/split_one_chevs_test.exs | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/teiserver/battle/balance/split_one_chevs.ex b/lib/teiserver/battle/balance/split_one_chevs.ex index 1ec783751..ecedd8ad7 100644 --- a/lib/teiserver/battle/balance/split_one_chevs.ex +++ b/lib/teiserver/battle/balance/split_one_chevs.ex @@ -37,7 +37,7 @@ defmodule Teiserver.Battle.Balance.SplitOneChevs do result = Teiserver.Battle.Balance.LoserPicks.perform(expanded_group, team_count, opts) new_logs = - ["Not enough noobs; calling another balancer.", @splitter, result.logs] + ["Not enough new players; will use another balance algorithm.", @splitter, result.logs] |> List.flatten() Map.put(result, :logs, new_logs) diff --git a/lib/teiserver_web/live/battles/match/ratings.html.heex b/lib/teiserver_web/live/battles/match/ratings.html.heex index ca93328ec..550df28aa 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 %> - + <% else %> diff --git a/test/teiserver/battle/split_one_chevs_test.exs b/test/teiserver/battle/split_one_chevs_test.exs index c501090d2..cf485ebe5 100644 --- a/test/teiserver/battle/split_one_chevs_test.exs +++ b/test/teiserver/battle/split_one_chevs_test.exs @@ -150,7 +150,7 @@ defmodule Teiserver.Battle.SplitOneChevsTest do ) assert result.logs == [ - "Not enough noobs; calling another balancer.", + "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", From f9a2ccb183b9ac090c2051dec829fe1b1f71f60f Mon Sep 17 00:00:00 2001 From: Joshua Augustinus Date: Wed, 10 Jul 2024 18:20:59 +1000 Subject: [PATCH 9/9] Minor fixes to pass automated checks --- lib/teiserver_web/live/battles/match/ratings.html.heex | 2 +- mix.exs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/teiserver_web/live/battles/match/ratings.html.heex b/lib/teiserver_web/live/battles/match/ratings.html.heex index 550df28aa..4c750f485 100644 --- a/lib/teiserver_web/live/battles/match/ratings.html.heex +++ b/lib/teiserver_web/live/battles/match/ratings.html.heex @@ -90,7 +90,7 @@ <%= if log.match do %> <% else %> 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
<%= log.match.map %> + <%= live_redirect "#{log.match.map}", to: ~p"/battle/#{log.match_id}" %> + <%= log.match.team_size * log.match.team_count %><%= log.value["reason"] || "No match" %>
- <%= live_redirect "#{log.match.map}", to: ~p"/battle/#{log.match_id}" %> + <%= live_redirect("#{log.match.map}", to: ~p"/battle/#{log.match_id}") %> <%= log.match.team_size * log.match.team_count %>