Skip to content

Commit

Permalink
Respect avoids balancer with party supports (#435)
Browse files Browse the repository at this point in the history
  • Loading branch information
jauggy authored Oct 17, 2024
1 parent 6638535 commit fc6fbee
Show file tree
Hide file tree
Showing 19 changed files with 1,469 additions and 77 deletions.
3 changes: 0 additions & 3 deletions lib/teiserver/account.ex
Original file line number Diff line number Diff line change
Expand Up @@ -1570,9 +1570,6 @@ defmodule Teiserver.Account do
@spec check_block_status(T.userid(), [T.userid()]) :: :ok | :blocking | :blocked
defdelegate check_block_status(userid, userid_list), to: RelationshipLib

@spec check_avoid_status(T.userid(), [T.userid()]) :: :ok | :avoiding | :avoided
defdelegate check_avoid_status(userid, userid_list), to: RelationshipLib

@spec profile_view_permissions(
T.user(),
T.user(),
Expand Down
98 changes: 62 additions & 36 deletions lib/teiserver/account/libs/relationship_lib.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ defmodule Teiserver.Account.RelationshipLib do
alias Teiserver.Account.AuthLib
alias Teiserver.Data.Types, as: T
alias Phoenix.PubSub
alias Teiserver.Repo

@spec colour :: atom
def colour(), do: :success
Expand Down Expand Up @@ -169,13 +170,14 @@ defmodule Teiserver.Account.RelationshipLib do
end)
end

# Blocks will be treated as avoids, but avoids will not be treated as blocks
@spec list_userids_avoiding_this_userid(T.userid()) :: [T.userid()]
def list_userids_avoiding_this_userid(userid) do
Teiserver.cache_get_or_store(:account_avoiding_this_cache, userid, fn ->
Account.list_relationships(
where: [
to_user_id: userid,
state: "avoid"
state_in: ["block", "avoid"]
],
select: [:from_user_id]
)
Expand All @@ -185,13 +187,14 @@ defmodule Teiserver.Account.RelationshipLib do
end)
end

# Blocks will be treated as avoids, but avoids will not be treated as blocks
@spec list_userids_avoided_by_userid(T.userid()) :: [T.userid()]
def list_userids_avoided_by_userid(userid) do
Teiserver.cache_get_or_store(:account_avoid_cache, userid, fn ->
Account.list_relationships(
where: [
from_user_id: userid,
state: "avoid"
state_in: ["block", "avoid"]
],
select: [:to_user_id]
)
Expand All @@ -201,13 +204,15 @@ defmodule Teiserver.Account.RelationshipLib do
end)
end

# Get userids blocked by a userid
# Avoids do not count as blocks
@spec list_userids_blocked_by_userid(T.userid()) :: [T.userid()]
def list_userids_blocked_by_userid(userid) do
Teiserver.cache_get_or_store(:account_block_cache, userid, fn ->
Account.list_relationships(
where: [
from_user_id: userid,
state_in: ["avoid", "block"]
state_in: ["block"]
],
select: [:to_user_id]
)
Expand All @@ -217,13 +222,15 @@ defmodule Teiserver.Account.RelationshipLib do
end)
end

# Get userids blocking a userid
# Avoids do not count as blocks
@spec list_userids_blocking_this_userid(T.userid()) :: [T.userid()]
def list_userids_blocking_this_userid(userid) do
Teiserver.cache_get_or_store(:account_blocking_this_cache, userid, fn ->
Account.list_relationships(
where: [
to_user_id: userid,
state_in: ["avoid", "block"]
state_in: ["block"]
],
select: [:from_user_id]
)
Expand Down Expand Up @@ -298,7 +305,6 @@ defmodule Teiserver.Account.RelationshipLib do

@spec check_block_status(T.userid(), [T.userid()]) :: :ok | :blocking | :blocked
def check_block_status(userid, userid_list) do
user = Account.get_user_by_id(userid)
userid_count = Enum.count(userid_list) |> max(1)

block_count_needed = Config.get_site_config_cache("lobby.Block count to prevent join")
Expand Down Expand Up @@ -330,37 +336,57 @@ defmodule Teiserver.Account.RelationshipLib do
end
end

@spec check_avoid_status(T.userid(), [T.userid()]) :: :ok | :avoiding | :avoided
def check_avoid_status(userid, userid_list) do
user = Account.get_user_by_id(userid)
userid_count = Enum.count(userid_list) |> max(1)

avoid_count_needed = Config.get_site_config_cache("lobby.Avoid count to prevent playing")

avoid_percentage_needed =
Config.get_site_config_cache("lobby.Avoid percentage to prevent playing")

being_avoided_count =
userid
|> list_userids_avoiding_this_userid()
|> Enum.count(fn uid -> Enum.member?(userid_list, uid) end)

avoiding_count =
userid
|> list_userids_avoided_by_userid()
|> Enum.count(fn uid -> Enum.member?(userid_list, uid) end)

being_avoided_percentage = being_avoided_count / userid_count * 100
avoiding_percentage = avoiding_count / userid_count * 100
# Given a list of players in lobby, gets all relevant avoids
# Use limit to only pull a maximum number from the database (prefer oldest)
# Use player_limit to only pull a maximum number per player (prefer oldest)
def get_lobby_avoids(player_ids, limit, player_limit) do
query = """
select from_user_id, to_user_id from (
select from_user_id, to_user_id, state, inserted_at, row_number() over (partition by arel.from_user_id order by arel.inserted_at asc) as rn
from account_relationships arel
where state in('avoid','block')
and from_user_id =ANY($1)
and to_user_id =ANY($2)
and state in('avoid','block')
) as ar
where
rn <= $4
order by inserted_at asc
limit $3
"""

results = Ecto.Adapters.SQL.query!(Repo, query, [player_ids, player_ids, limit, player_limit])

results.rows
end

cond do
# You are being avoided
being_avoided_percentage >= avoid_percentage_needed -> :avoided
being_avoided_count >= avoid_count_needed -> :avoided
# You are avoiding
avoiding_percentage >= avoid_percentage_needed -> :avoiding
avoiding_count >= avoid_count_needed -> :avoiding
true -> :ok
end
def get_lobby_avoids(player_ids, limit, player_limit, minimum_time_hours) do
query = """
select from_user_id, to_user_id from (
select from_user_id, to_user_id, state, inserted_at, row_number() over (partition by arel.from_user_id order by arel.inserted_at asc) as rn
from account_relationships arel
where state in('avoid','block')
and from_user_id =ANY($1)
and to_user_id =ANY($2)
and state in('avoid','block')
and inserted_at <= (now() - interval '#{minimum_time_hours} hours')
) as ar
where
rn <= $4
order by inserted_at asc
limit $3
"""

# Not able to use mimimum_time_hours as parameter so have to add into the sql string

results =
Ecto.Adapters.SQL.query!(Repo, query, [
player_ids,
player_ids,
limit,
player_limit
])

results.rows
end
end
172 changes: 172 additions & 0 deletions lib/teiserver/battle/balance/brute_force_avoid.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
defmodule Teiserver.Battle.Balance.BruteForceAvoid do
@moduledoc """
Overview:
Go through every possible combination and pick the best one. Each combination is given a score
Score = team rating penalty + broken avoid penalty + broken party penalty + broken max team rating diff penalty
team rating penalty = difference in team ratings
broken avoid penalty = broken avoid count * avoid importance
broken party penalty = broken party count * party importance
broken max team rating diff penalty = max team diff importance (if the team rating diff > @max_team_diff)
Only use for games with two teams.
This is not a balance algorithm that is callable players. It's a lib that can be used by another balance algorithm.
"""
alias Teiserver.Config
alias Teiserver.Battle.Balance.BruteForceAvoidTypes, as: BF
require Integer

# Parties will be split if team diff is too large. It either uses absolute value or percentage
# See get_max_team_diff function below for full details
@max_team_diff_abs 10
@max_team_diff_importance 10000
@party_importance 1000
@avoid_importance 10

def get_best_combo(players, avoids, parties) do
potential_teams = potential_teams(length(players))
get_best_combo(potential_teams, players, avoids, parties)
end

@spec get_best_combo([integer()], [BF.player()], [[number()]], [[number()]]) ::
BF.combo_result()
def get_best_combo(combos, players, avoids, parties) do
players_with_index = Enum.with_index(players)

# Go through every possibility and get the combination with the lowest score
result =
Enum.map(combos, fn x ->
team = get_players_from_indexes(x, players_with_index)
result = score_combo(team, players, avoids, parties)
Map.put(result, :first_team, team)
end)
|> Enum.min_by(fn z ->
z.score
end)

first_team = result.first_team

second_team =
players
|> Enum.filter(fn x ->
!Enum.any?(first_team, fn y ->
y.id == x.id
end)
end)

Map.put(result, :second_team, second_team)
end

@spec potential_teams(integer()) :: any()
def potential_teams(num_players) do
Teiserver.Helper.CombinationsHelper.get_combinations(num_players)
end

# Parties/avoids will be ignored if the team rating diff is too large
# This function returns the allowed team difference
# By default, it is either 10 rating points or 5% of a single team rating - whichever is larger
defp get_max_team_diff(total_lobby_rating, num_teams) do
# This value is 10% in dev but 5% in production. Can be adjusted by Admin
percentage_of_team = Config.get_site_config_cache("teiserver.Max deviation") / 100
max(total_lobby_rating / num_teams * percentage_of_team, @max_team_diff_abs)
end

@spec score_combo([BF.player()], [BF.player()], [[number()]], [[number()]]) :: any()
def score_combo(first_team, all_players, avoids, parties) do
first_team_rating = get_team_rating(first_team)
both_team_rating = get_team_rating(all_players)
rating_diff_penalty = abs(both_team_rating - first_team_rating * 2)
num_teams = 2

max_team_diff_penalty =
cond do
rating_diff_penalty > get_max_team_diff(both_team_rating, num_teams) ->
@max_team_diff_importance

true ->
0
end

# If max_team_diff_penalty is non zero don't even bother calculating avoid and party penalty
# since it's likely we'll discard this combo
{broken_avoid_penalty, broken_party_penalty} =
case max_team_diff_penalty do
0 ->
{count_broken_avoids(first_team, avoids) * @avoid_importance,
count_broken_parties(first_team, parties) * @party_importance}

_ ->
{0, 0}
end

score =
rating_diff_penalty + broken_avoid_penalty + broken_party_penalty + max_team_diff_penalty

%{
score: score,
rating_diff_penalty: rating_diff_penalty,
broken_avoid_penalty: broken_avoid_penalty,
broken_party_penalty: broken_party_penalty
}
end

def get_players_from_indexes(player_indexes, players_with_index) do
players_with_index
|> Enum.filter(fn {_player, index} -> index in player_indexes end)
|> Enum.map(fn {player, _index} -> player end)
end

def count_broken_parties(first_team, parties) do
Enum.count(parties, fn party ->
is_party_broken?(first_team, party)
end)
end

@spec is_party_broken?([BF.player()], [String.t()]) :: any()
def is_party_broken?(team, party) do
count =
Enum.count(party, fn x ->
Enum.any?(team, fn y ->
y.id == x
end)
end)

cond do
# Nobody from this party is on this team. Therefore unbroken.
count == 0 -> false
# Everyone from this party is on this team. Therefore unbroken.
count == length(party) -> false
# Otherwise, this party is broken.
true -> true
end
end

def count_broken_avoids(first_team, avoids) do
Enum.count(avoids, fn avoid ->
is_avoid_broken?(first_team, avoid)
end)
end

@spec is_avoid_broken?([BF.player()], [[any()]]) :: any()
def is_avoid_broken?(team, avoids) do
count =
Enum.count(avoids, fn x ->
Enum.any?(team, fn y ->
y.id == x
end)
end)

cond do
# One person from avoid on this team. The other must be on other team. Avoid is respected.
count == 1 -> false
# Otherwise avoid is broken
true -> true
end
end

defp get_team_rating(players) do
Enum.reduce(players, 0, fn x, acc ->
acc + x.rating
end)
end
end
26 changes: 26 additions & 0 deletions lib/teiserver/battle/balance/brute_force_avoid_types.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
defmodule Teiserver.Battle.Balance.BruteForceAvoidTypes do
@moduledoc false

@type player :: %{
rating: float(),
id: any(),
name: String.t()
}
@type team :: %{
players: [player],
id: integer()
}
@type input_data :: %{
players: [player],
parties: [String.t()]
}

@type combo_result :: %{
broken_avoid_penalty: number(),
broken_party_penalty: number(),
rating_diff_penalty: number(),
score: number(),
first_team: [player()],
second_team: [player()]
}
end
Loading

0 comments on commit fc6fbee

Please sign in to comment.