Skip to content

Commit

Permalink
Version 2.0, restructured code. Improved performance. A lot of fixes.
Browse files Browse the repository at this point in the history
  • Loading branch information
Mesharsky committed Aug 25, 2024
1 parent d7cfb01 commit 6d00b26
Show file tree
Hide file tree
Showing 14 changed files with 291 additions and 301 deletions.
12 changes: 8 additions & 4 deletions BalanceLogic/PlayerManagement.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ namespace Mesharsky_TeamBalance;

public partial class Mesharsky_TeamBalance
{
private static readonly ConcurrentDictionary<ulong, Player> playerCache = new();
private static readonly ConcurrentDictionary<ulong, PlayerStats> playerCache = new();

public static void UpdatePlayerTeamsInCache()
{
Expand All @@ -24,11 +24,15 @@ public static void UpdatePlayerTeamsInCache()
}
else
{
var newPlayer = new Player
var newPlayer = new PlayerStats
{
PlayerName = player.PlayerName,
PlayerSteamID = player.SteamID,
Team = (int)player.Team,
Kills = player.ActionTrackingServices!.MatchStats.Kills,
Assists = player.ActionTrackingServices!.MatchStats.Assists,
Deaths = player.ActionTrackingServices.MatchStats.Deaths,
Damage = player.ActionTrackingServices.MatchStats.Damage,
Score = player.Score
};

Expand All @@ -37,11 +41,11 @@ public static void UpdatePlayerTeamsInCache()
}
}

public static List<Player> GetPlayersForRebalance()
public static List<PlayerStats> GetPlayersForRebalance()
{
var players = playerCache.Values
.Where(p => p.Team == (int)CsTeam.CounterTerrorist || p.Team == (int)CsTeam.Terrorist)
.OrderByDescending(p => Config?.PluginSettings.UsePerformanceScore == true ? p.PerformanceScore : p.Score)
.OrderByDescending(p => p.PerformanceScore)
.ToList();

PrintDebugMessage($"Total valid players for rebalance: {players.Count}");
Expand Down
38 changes: 31 additions & 7 deletions BalanceLogic/TeamBalance.cs
Original file line number Diff line number Diff line change
@@ -1,29 +1,48 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Modules.Utils;

namespace Mesharsky_TeamBalance;

public partial class Mesharsky_TeamBalance
{
public bool GlobalBalanceMade = false;

private void AttemptBalanceTeams()
{
PrintDebugMessage("Attempting to balance teams...");

// Reset balance flag before attempting balance
GlobalBalanceMade = false;

if (!ShouldTeamsBeRebalanced())
return;

PrintDebugMessage("Balancing teams...");

var players = GetPlayersForRebalance();
if (players == null || players.Count == 0)
{
PrintDebugMessage("No players available for rebalancing.");
return;
}

bool balanceMade = RebalancePlayers(players);
GlobalBalanceMade = balanceMade;

if (balanceMade)
if (GlobalBalanceMade)
{
GlobalBalanceMade = true;
var ctPlayerCount = balanceStats.CT.Stats.Count;
var tPlayerCount = balanceStats.T.Stats.Count;
var ctTotalScore = balanceStats.CT.TotalPerformanceScore;
var tTotalScore = balanceStats.T.TotalPerformanceScore;

Server.PrintToChatAll($" {ChatColors.Red}[Team Balance] {ChatColors.Default}Teams have been balanced..");
Server.PrintToChatAll($" {ChatColors.Red}[Team Balance] CT: {ctPlayerCount} players, {ctTotalScore} score");
Server.PrintToChatAll($" {ChatColors.Red}[Team Balance] T: {tPlayerCount} players, {tTotalScore} score.");
}
else
{
GlobalBalanceMade = false;
Server.PrintToChatAll($" {ChatColors.Red}[Team Balance] {ChatColors.Default}No need for team balance at this moment.");
}
}

Expand All @@ -33,12 +52,17 @@ private static bool ShouldTeamsBeRebalanced()

UpdatePlayerTeamsInCache();

var players = playerCache.Values.ToList();
var players = playerCache?.Values.ToList();
if (players == null || players.Count == 0)
{
PrintDebugMessage("No players found for rebalancing.");
return false;
}

int ctPlayerCount = players.Count(p => p.Team == (int)CsTeam.CounterTerrorist);
int tPlayerCount = players.Count(p => p.Team == (int)CsTeam.Terrorist);

if (ctPlayerCount + tPlayerCount < Config?.PluginSettings.MinPlayers)
if (ctPlayerCount + tPlayerCount < (Config?.PluginSettings.MinPlayers ?? 0))
{
PrintDebugMessage("Not enough players to balance.");
return false;
Expand All @@ -52,13 +76,13 @@ private static bool ShouldTeamsBeRebalanced()
? players.Where(p => p.Team == (int)CsTeam.Terrorist).Sum(p => p.PerformanceScore)
: players.Where(p => p.Team == (int)CsTeam.Terrorist).Sum(p => p.Score);

if (ctScore > tScore * Config?.PluginSettings.MaxScoreBalanceRatio || tScore > ctScore * Config?.PluginSettings.MaxScoreBalanceRatio)
if (ctScore > tScore * (Config?.PluginSettings.MaxScoreBalanceRatio ?? 1.0f) || tScore > ctScore * (Config?.PluginSettings.MaxScoreBalanceRatio ?? 1.0f))
{
PrintDebugMessage("Score difference is too high. Balance required.");
return true;
}

if (Math.Abs(ctPlayerCount - tPlayerCount) > Config?.PluginSettings.MaxTeamSizeDifference)
if (Math.Abs(ctPlayerCount - tPlayerCount) > (Config?.PluginSettings.MaxTeamSizeDifference ?? 1))
{
PrintDebugMessage("Team sizes are not equal. Balance needed.");
return true;
Expand Down
231 changes: 28 additions & 203 deletions BalanceLogic/TeamBalancingLogic.cs
Original file line number Diff line number Diff line change
@@ -1,226 +1,51 @@
using CounterStrikeSharp.API.Modules.Utils;

namespace Mesharsky_TeamBalance;

public partial class Mesharsky_TeamBalance
{
private static bool RebalancePlayers(List<Player> players)
{
PrintDebugMessage("Starting player rebalance...");

var currentRound = GetCurrentRound();

var ctTeam = players.Where(p => p.Team == (int)CsTeam.CounterTerrorist).ToList();
var tTeam = players.Where(p => p.Team == (int)CsTeam.Terrorist).ToList();

var ctTotalScore = ctTeam.Sum(p => p.PerformanceScore);
var tTotalScore = tTeam.Sum(p => p.PerformanceScore);

PrintDebugMessage($"Initial CT Score: {ctTotalScore}, T Score: {tTotalScore}, CT Players: {ctTeam.Count}, T Players: {tTeam.Count}");

// Determine balance trigger reasons
bool sizeDifferenceTriggered = Math.Abs(ctTeam.Count - tTeam.Count) > Config?.PluginSettings.MaxTeamSizeDifference;
bool scoreDifferenceTriggered = Math.Abs(ctTotalScore - tTotalScore) > (tTotalScore * Config?.PluginSettings.MaxScoreBalanceRatio);
private readonly BalanceStats balanceStats = new BalanceStats();

PrintDebugMessage($"Balance Triggered by Size Difference: {sizeDifferenceTriggered}, Score Difference: {scoreDifferenceTriggered}");

// Step 1: Ensure the teams are within MaxTeamSizeDifference by MOVING players
int attempts = 0;
while (sizeDifferenceTriggered && Math.Abs(ctTeam.Count - tTeam.Count) > Config?.PluginSettings.MaxTeamSizeDifference)
{
attempts++;
var difference = ctTeam.Count - tTeam.Count;

if (difference > 0)
{
var playerToMove = ctTeam.OrderByDescending(p => p.PerformanceScore).First();
MovePlayer(playerToMove, tTeam, ctTeam, ref ctTotalScore, ref tTotalScore);
}
else
{
var playerToMove = tTeam.OrderByDescending(p => p.PerformanceScore).First();
MovePlayer(playerToMove, ctTeam, tTeam, ref tTotalScore, ref ctTotalScore);
}

if (attempts >= 10)
{
PrintDebugMessage("Maximum move attempts reached. Exiting to prevent infinite loop.");
break;
}
}

// Step 2: Balance teams by TRADING players to minimize performance score differences
if (scoreDifferenceTriggered || !sizeDifferenceTriggered)
{
SwapPlayersToBalance(ctTeam, tTeam, ref ctTotalScore, ref tTotalScore, currentRound);
}

// Step 3: Apply changes if any balancing was performed
bool balanceMade = ApplyTeamChanges(ctTeam, tTeam, currentRound);

PrintDebugMessage($"Rebalance complete || Final CT Score: {ctTotalScore}, Final T Score: {tTotalScore}, Final CT Players: {ctTeam.Count}, Final T Players: {tTeam.Count}");
return balanceMade;
}

private static void SwapPlayersToBalance(List<Player> ctTeam, List<Player> tTeam, ref float ctTotalScore, ref float tTotalScore, int currentRound)
private bool RebalancePlayers(List<PlayerStats> players)
{
int maxAttempts = 10;
int attempts = 0;

while (Math.Abs(ctTotalScore - tTotalScore) > Config?.PluginSettings.MaxScoreBalanceRatio && attempts < maxAttempts)
{
attempts++;

var difference = Math.Abs(ctTotalScore - tTotalScore);
PrintDebugMessage($"Attempt {attempts}: Current Score Difference = {difference}, Max Allowed = {tTotalScore * Config?.PluginSettings.MaxScoreBalanceRatio}");

// Find the best players to swap
var bestCtPlayerToMove = FindBestPlayerToMove(ctTeam, tTeam, ctTotalScore, tTotalScore, currentRound, difference);
var bestTPlayerToMove = FindBestPlayerToMove(tTeam, ctTeam, tTotalScore, ctTotalScore, currentRound, difference);

// If both players are found, proceed with the trade
if (bestCtPlayerToMove.HasValue && bestTPlayerToMove.HasValue)
{
var ctPlayer = bestCtPlayerToMove.Value.Item1;
var tPlayer = bestTPlayerToMove.Value.Item1;

// Ensure both players are valid before proceeding
if (ctPlayer != null && tPlayer != null)
{
TradePlayers(ctPlayer, tPlayer, ctTeam, tTeam, ref ctTotalScore, ref tTotalScore);
}
else
{
PrintDebugMessage("One of the players is null. Skipping trade.");
break;
}
}
else
{
// If no valid players are found for trading, exit the loop
PrintDebugMessage("No valid players found for trading. Exiting swap loop.");
break;
}

// Safety check: Break out of the loop if no further meaningful trades can be made
if (attempts > 1 && Math.Abs(ctTotalScore - tTotalScore) == difference)
{
PrintDebugMessage("Score difference unchanged after trade attempt. Exiting swap loop.");
break;
}
}

if (attempts >= maxAttempts)
{
PrintDebugMessage("Maximum attempts reached. Exiting swap loop to prevent infinite loop.");
}
}


// Find the best player to move based on minimizing the performance difference
private static (Player, float)? FindBestPlayerToMove(List<Player> fromTeam, List<Player> toTeam, float fromTeamScore, float toTeamScore, int currentRound, float difference)
{
var potentialPlayers = fromTeam
.Where(p => p != null && CanMovePlayer(fromTeam, toTeam, p, currentRound) && p.PerformanceScore <= difference)
.Select(p => (p, Math.Abs(fromTeamScore - p.PerformanceScore - (toTeamScore + p.PerformanceScore))))
.OrderBy(result => result.Item2)
.ToList();

// Debug logging to understand why no valid players are found
if (potentialPlayers.Count == 0)
{
PrintDebugMessage("No players eligible for moving under current conditions.");
}
else
{
PrintDebugMessage($"Found {potentialPlayers.Count} potential players for trading. Top candidate: {potentialPlayers.First().Item1.PlayerName} with score difference: {potentialPlayers.First().Item2}");
}
PrintDebugMessage("Starting player rebalance...");

return potentialPlayers.FirstOrDefault();
}
// Collect current stats
balanceStats.GetStats(players);

private static void TradePlayers(Player ctPlayer, Player tPlayer, List<Player> ctTeam, List<Player> tTeam, ref float ctTotalScore, ref float tTotalScore)
{
// Validate that both players are still on their respective teams
if (!ctTeam.Contains(ctPlayer) || !tTeam.Contains(tPlayer))
{
PrintDebugMessage("Player no longer in the original team. Skipping trade.");
return;
}
// Pre-checks and debug messages
var ctPlayerCount = balanceStats.CT.Stats.Count;
var tPlayerCount = balanceStats.T.Stats.Count;
var ctTotalScore = balanceStats.CT.TotalPerformanceScore;
var tTotalScore = balanceStats.T.TotalPerformanceScore;

// Safeguard: Ensure trade will reduce the score imbalance
float newCtTotalScore = ctTotalScore - ctPlayer.PerformanceScore + tPlayer.PerformanceScore;
float newTTotalScore = tTotalScore - tPlayer.PerformanceScore + ctPlayer.PerformanceScore;
PrintDebugMessage($"Current CT Team size: {ctPlayerCount}, T Team size: {tPlayerCount}");
PrintDebugMessage($"Current CT Score: {ctTotalScore}, T Score: {tTotalScore}");

if (Math.Abs(newCtTotalScore - newTTotalScore) >= Math.Abs(ctTotalScore - tTotalScore))
if (Math.Abs(ctPlayerCount - tPlayerCount) > Config?.PluginSettings.MaxTeamSizeDifference)
{
PrintDebugMessage("Trade would worsen score imbalance. Skipping trade.");
return;
PrintDebugMessage($"Team size difference exceeds the allowed max_team_size_difference: {Config?.PluginSettings.MaxTeamSizeDifference}. Correction needed.");
}

// Perform the trade
ctTeam.Remove(ctPlayer);
tTeam.Remove(tPlayer);

ctTeam.Add(tPlayer);
tTeam.Add(ctPlayer);

// Adjust the team scores
ctTotalScore = newCtTotalScore;
tTotalScore = newTTotalScore;

PrintDebugMessage($"Traded {ctPlayer.PlayerName} with {tPlayer.PlayerName}. New scores: CT = {ctTotalScore}, T = {tTotalScore}");
}

private static void MovePlayer(Player player, List<Player> toTeam, List<Player> fromTeam, ref float fromTeamScore, ref float toTeamScore, bool forceMove = false)
{
if (player == null || fromTeam == null || toTeam == null)

if (Math.Abs(ctTotalScore - tTotalScore) > Config?.PluginSettings.MaxScoreBalanceRatio)
{
PrintDebugMessage("Invalid operation. Either player or team is null.");
return;
PrintDebugMessage($"Score difference exceeds the allowed score_balance_ratio: {Config?.PluginSettings.MaxScoreBalanceRatio}. Correction needed.");
}

// Safeguard: Do not move if the move would make score differences worse unless forceMove is true
float projectedFromTeamScore = fromTeamScore - player.PerformanceScore;
float projectedToTeamScore = toTeamScore + player.PerformanceScore;

if (Math.Abs(projectedFromTeamScore - projectedToTeamScore) > Math.Abs(fromTeamScore - toTeamScore) && !forceMove)
// Step 1: Balance team sizes
if (balanceStats.ShouldMoveLowestScorers())
{
PrintDebugMessage("Move would worsen score imbalance. Skipping move.");
return;
balanceStats.MoveLowestScorersFromBiggerTeam();
}

fromTeam.Remove(player);
toTeam.Add(player);

fromTeamScore -= player.PerformanceScore;
toTeamScore += player.PerformanceScore;

PrintDebugMessage($"Moved {player.PlayerName} to the opposite team. New scores: CT = {fromTeamScore}, T = {toTeamScore}");
}

private static bool ApplyTeamChanges(List<Player> ctTeam, List<Player> tTeam, int currentRound)
{
bool balanceMade = false;

foreach (var player in ctTeam)
// Step 2: Balance teams by performance scores
if (!balanceStats.TeamsAreEqualScore())
{
if (player.Team != (int)CsTeam.CounterTerrorist)
{
ChangePlayerTeam(player.PlayerSteamID, CsTeam.CounterTerrorist);
balanceMade = true;
}
balanceStats.BalanceTeamsByPerformance();
}

foreach (var player in tTeam)
{
if (player.Team != (int)CsTeam.Terrorist)
{
ChangePlayerTeam(player.PlayerSteamID, CsTeam.Terrorist);
balanceMade = true;
}
}
// Step 3: Apply the team assignments
balanceStats.AssignPlayerTeams();

return balanceMade;
PrintDebugMessage($"Rebalance complete || Final CT Score: {balanceStats.CT.TotalPerformanceScore}, Final T Score: {balanceStats.T.TotalPerformanceScore}, Final CT Players: {balanceStats.CT.Stats.Count}, Final T Players: {balanceStats.T.Stats.Count}");
return true;
}
}
}
Loading

0 comments on commit 6d00b26

Please sign in to comment.