From 0535655f58beebed950cf1220f14dcce1796c984 Mon Sep 17 00:00:00 2001 From: Eveldee Date: Wed, 7 Dec 2022 23:26:19 +0100 Subject: [PATCH] [Raid] Complete roster assigner --- Cocotte/Modules/Raids/Raid.cs | 9 +-- Cocotte/Modules/Raids/RaidFormatter.cs | 7 +-- Cocotte/Modules/Raids/RaidModule.cs | 6 +- Cocotte/Modules/Raids/RaidModuleDebug.cs | 62 ++++++++++++++++++++ Cocotte/Modules/Raids/RosterAssigner.cs | 69 ++++++++++++++++------- Cocotte/Modules/Raids/RosterExtensions.cs | 20 +++++-- Cocotte/Utils/EnumerableExtensions.cs | 31 ++++++++++ Cocotte/Utils/RaidExtensions.cs | 14 ----- 8 files changed, 171 insertions(+), 47 deletions(-) create mode 100644 Cocotte/Utils/EnumerableExtensions.cs delete mode 100644 Cocotte/Utils/RaidExtensions.cs diff --git a/Cocotte/Modules/Raids/Raid.cs b/Cocotte/Modules/Raids/Raid.cs index 227fbf9..5c4222e 100644 --- a/Cocotte/Modules/Raids/Raid.cs +++ b/Cocotte/Modules/Raids/Raid.cs @@ -14,10 +14,6 @@ public class Raid { Id = id; DateTime = dateTime; - -#if DEBUG - this.AddTestPlayers(); -#endif } public bool AddPlayer(RosterPlayer rosterPlayer) @@ -55,6 +51,11 @@ public class Raid return _players.Remove(id); } + public void AssignRosters(RosterAssigner assigner) + { + assigner.AssignRosters(_players.Values, 8); + } + public override bool Equals(object? other) { return other is Raid roster && roster.Id == Id; diff --git a/Cocotte/Modules/Raids/RaidFormatter.cs b/Cocotte/Modules/Raids/RaidFormatter.cs index 502b292..68b4e28 100644 --- a/Cocotte/Modules/Raids/RaidFormatter.cs +++ b/Cocotte/Modules/Raids/RaidFormatter.cs @@ -41,12 +41,11 @@ public class RaidFormatter var nonSubstitute = rosterPlayers.Where(p => !p.Substitute); var substitute = rosterPlayers.Where(p => p.Substitute); - var separatorLength = Math.Max(nonSubstitute.Select(p => p.Name.Length).Max(), substitute.Select(p => p.Name.Length).Max()); + var separatorLength = players.Select(p => p.Name.Length).Max(); separatorLength = (int) ((separatorLength + 13) * 0.49); // Don't ask why, it just works - // Todo add Total FC number return new EmbedFieldBuilder() - .WithName($"Roster {rosterNumber}") + .WithName($"Roster {rosterNumber} ({nonSubstitute.Sum(p => p.Fc)} FC)") .WithValue($"{string.Join("\n", nonSubstitute.Select(FormatRosterPlayer))}\n{new string('━', separatorLength)}\n{string.Join("\n", substitute.Select(FormatRosterPlayer))}") .WithIsInline(true); } @@ -55,6 +54,6 @@ public class RaidFormatter .WithColor(Colors.CocotteBlue) .WithTitle(":crossed_swords: Raid") .WithDescription($"**Date:** {TimestampTag.FromDateTime(raid.DateTime, TimestampTagStyles.LongDateTime)}") - .WithFields(raid.Rosters.Select(r => RosterEmbed(r.Key, r))); + .WithFields(raid.Rosters.OrderBy(r => r.Key).Select(r => RosterEmbed(r.Key, r))); } } \ No newline at end of file diff --git a/Cocotte/Modules/Raids/RaidModule.cs b/Cocotte/Modules/Raids/RaidModule.cs index da1ac1a..1e9e2c4 100644 --- a/Cocotte/Modules/Raids/RaidModule.cs +++ b/Cocotte/Modules/Raids/RaidModule.cs @@ -17,15 +17,17 @@ public partial class RaidModule : InteractionModuleBase logger, IRaidsRepository raids, IPlayerInfosRepository playerInfos, - RaidFormatter raidFormatter, RaidRegisterManager registerManager) + RaidFormatter raidFormatter, RaidRegisterManager registerManager, RosterAssigner rosterAssigner) { _logger = logger; _raids = raids; _playerInfos = playerInfos; _raidFormatter = raidFormatter; _registerManager = registerManager; + _rosterAssigner = rosterAssigner; } [EnabledInDm(false)] @@ -293,6 +295,8 @@ public partial class RaidModule : InteractionModuleBase m.Embed = _raidFormatter.RaidEmbed(raid).Build() ); diff --git a/Cocotte/Modules/Raids/RaidModuleDebug.cs b/Cocotte/Modules/Raids/RaidModuleDebug.cs index b24709c..9965e54 100644 --- a/Cocotte/Modules/Raids/RaidModuleDebug.cs +++ b/Cocotte/Modules/Raids/RaidModuleDebug.cs @@ -26,6 +26,68 @@ public partial class RaidModule await AddTestPlayer(message, PlayerRole.Healer); } + [MessageCommand("Fill roster")] + public async Task FillRoster(IMessage message) + { + if (message is IUserMessage userMessage && userMessage.Author.IsBot) + { + if (_raids.TryGetRaid(userMessage.Id, out var raid)) + { + // Add 3 healers + for (int i = 0; i < 3; i++) + { + raid.AddPlayer(new RosterPlayer( + (ulong) Random.Shared.NextInt64(), + $"Healer{Random.Shared.Next(1, 100)}", + PlayerRole.Healer, + (uint) (1000 * Random.Shared.Next(30, 60))) + ); + } + + // Add 3 tanks + for (int i = 0; i < 3; i++) + { + raid.AddPlayer(new RosterPlayer( + (ulong) Random.Shared.NextInt64(), + $"Tank{Random.Shared.Next(1, 100)}", + PlayerRole.Tank, + (uint) (1000 * Random.Shared.Next(30, 60))) + ); + } + + // Add 8 dps + for (int i = 0; i < 8; i++) + { + raid.AddPlayer(new RosterPlayer( + (ulong) Random.Shared.NextInt64(), + $"Dps{Random.Shared.Next(1, 100)}", + PlayerRole.Dps, + (uint) (1000 * Random.Shared.Next(30, 60))) + ); + } + + // Fill rest with substitutes + for (int i = 0; i < 6; i++) + { + raid.AddPlayer(new RosterPlayer( + (ulong) Random.Shared.NextInt64(), + $"Dps{Random.Shared.Next(1, 100)}", + PlayerRole.Dps, + (uint) (1000 * Random.Shared.Next(30, 60)), + true) + ); + } + + await UpdateRaidRosterEmbed(raid); + } + } + + await RespondAsync( + embed: EmbedUtils.SuccessEmbed($"Successfully filled the roster").Build(), + ephemeral: true + ); + } + private async Task AddTestPlayer(IMessage message, PlayerRole playerRole) { if (message is IUserMessage userMessage && userMessage.Author.IsBot) diff --git a/Cocotte/Modules/Raids/RosterAssigner.cs b/Cocotte/Modules/Raids/RosterAssigner.cs index 192cc64..90cdeca 100644 --- a/Cocotte/Modules/Raids/RosterAssigner.cs +++ b/Cocotte/Modules/Raids/RosterAssigner.cs @@ -1,18 +1,20 @@ +using Cocotte.Utils; + namespace Cocotte.Modules.Raids; public class RosterAssigner -{ +{ public void AssignRosters(IEnumerable players, uint playersPerRoster) { // Start by grouping players var groups = GroupPlayers(players.OrderByDescending(p => p.Fc)); - + // Create rosters var neededRosters = (int)Math.Ceiling(players.Count(p => !p.Substitute) / (double)playersPerRoster); - var rosters = new List(Enumerable.Repeat(new RosterInfo(), neededRosters)); - + var rosters = Enumerable.Range(0, neededRosters).Select(_ => new RosterInfo()).ToList(); + // Todo Check when there's more than max players per roster - + // First pass: assign healers and tanks // Always assign to the group which have the least amount of healer/tank, biased towards healers // Skip groups without players @@ -21,15 +23,15 @@ public class RosterAssigner { if (group.Players.AnyHealer()) { - var nextHealerRoster = rosters.MinBy(r => r.RealHealerCount()); - - nextHealerRoster!.AddGroup(group); + var nextHealerRoster = rosters.MinBy(r => r.RealHealerCount(), (x, y) => x.TotalRealFc > y.TotalRealFc ? y : x); + + nextHealerRoster.AddGroup(group); } else if (group.Players.AnyTank()) { - var nextTankRoster = rosters.MinBy(r => r.RealTankCount()); + var nextTankRoster = rosters.MinBy(r => r.RealTankCount(), (x, y) => x.TotalRealFc < y.TotalRealFc ? x : y); - nextTankRoster!.AddGroup(group); + nextTankRoster.AddGroup(group); } // Those groups will be used to assign dps, they should still be in descending order of FC else @@ -37,30 +39,59 @@ public class RosterAssigner dpsGroup.Add(group); } } - + // Third pass: assign dps foreach (var group in dpsGroup) { var nextDpsRoster = rosters.MinBy(r => r.TotalRealFc); - + + nextDpsRoster!.AddGroup(group); + } + + // Last pass: do the same but with substitutes + dpsGroup = new List(); + foreach (var group in groups.Where(g => g.AllSubstitutes)) + { + if (group.Substitutes.AnyHealer()) + { + var nextHealerRoster = rosters.MinBy(r => r.TotalHealerCount(), (x, y) => x.TotalFc > y.TotalFc ? y : x); + + nextHealerRoster.AddGroup(group); + } + else if (group.Substitutes.AnyTank()) + { + var nextTankRoster = rosters.MinBy(r => r.TotalTankCount(), (x, y) => x.TotalFc < y.TotalFc ? x : y); + + nextTankRoster.AddGroup(group); + } + // Those groups will be used to assign dps, they should still be in descending order of FC + else + { + dpsGroup.Add(group); + } + } + + // Third pass: assign dps + foreach (var group in dpsGroup) + { + var nextDpsRoster = rosters.MinBy(r => r.TotalFc); + nextDpsRoster!.AddGroup(group); } - // Last pass: fill with substitute - // Assign rosters for (int i = 0; i < rosters.Count; i++) { var roster = rosters[i]; - roster.AssignRosterNumer(i); + roster.AssignRosterNumer(i + 1); } } private IList GroupPlayers(IEnumerable players) { var groups = new List(); - + // Todo create groups from player preferences foreach (var rosterPlayer in players) { @@ -90,7 +121,7 @@ public class RosterInfo { _groups.Add(group); } - + public void AssignRosterNumer(int rosterNumber) { foreach (var group in _groups) @@ -115,7 +146,7 @@ public class PlayerGroup { _players = new List(); } - + public PlayerGroup(params RosterPlayer[] players) { _players = players; @@ -125,7 +156,7 @@ public class PlayerGroup { _players.Add(player); } - + public void AssignRosterNumer(int rosterNumber) { foreach (var rosterPlayer in _players) diff --git a/Cocotte/Modules/Raids/RosterExtensions.cs b/Cocotte/Modules/Raids/RosterExtensions.cs index b4c4fc9..8e76c37 100644 --- a/Cocotte/Modules/Raids/RosterExtensions.cs +++ b/Cocotte/Modules/Raids/RosterExtensions.cs @@ -6,7 +6,7 @@ public static class RosterExtensions { return players.Any(p => p.Role == PlayerRole.Healer); } - + public static bool AnyTank(this IEnumerable players) { return players.Any(p => p.Role == PlayerRole.Tank); @@ -16,24 +16,34 @@ public static class RosterExtensions { return players.Count(p => p.Role == PlayerRole.Healer); } - + public static int TankCount(this IEnumerable players) { return players.Count(p => p.Role == PlayerRole.Tank); } - + public static long TotalFc(this IEnumerable players) { return players.Sum(p => p.Fc); } - + public static int RealHealerCount(this RosterInfo rosterInfo) { return rosterInfo.PlayerGroups.Sum(g => g.Players.HealerCount()); } - + + public static int TotalHealerCount(this RosterInfo rosterInfo) + { + return rosterInfo.PlayerGroups.Concat(rosterInfo.SubstituteGroups).Sum(g => g.Players.Concat(g.Substitutes).HealerCount()); + } + public static int RealTankCount(this RosterInfo rosterInfo) { return rosterInfo.PlayerGroups.Sum(g => g.Players.TankCount()); } + + public static int TotalTankCount(this RosterInfo rosterInfo) + { + return rosterInfo.PlayerGroups.Concat(rosterInfo.SubstituteGroups).Sum(g => g.Players.Concat(g.Substitutes).TankCount()); + } } \ No newline at end of file diff --git a/Cocotte/Utils/EnumerableExtensions.cs b/Cocotte/Utils/EnumerableExtensions.cs new file mode 100644 index 0000000..bade6e3 --- /dev/null +++ b/Cocotte/Utils/EnumerableExtensions.cs @@ -0,0 +1,31 @@ +namespace Cocotte.Utils; + +public static class EnumerableExtensions +{ + public static TSource MinBy(this IEnumerable source, Func keySelector, + Func conflictResolver) + { + var comparer = Comparer.Default; + + var min = source.First(); + var minKey = keySelector(min); + + foreach (var element in source.Skip(1)) + { + var key = keySelector(element); + + if (comparer.Compare(key, minKey) < 0) + { + min = element; + minKey = key; + } + else if (comparer.Compare(key, minKey) == 0) + { + min = conflictResolver(min, element); + minKey = keySelector(min); + } + } + + return min; + } +} \ No newline at end of file diff --git a/Cocotte/Utils/RaidExtensions.cs b/Cocotte/Utils/RaidExtensions.cs deleted file mode 100644 index b70b92a..0000000 --- a/Cocotte/Utils/RaidExtensions.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Cocotte.Modules.Raids; - -namespace Cocotte.Utils; - -public static class RaidExtensions -{ - public static void AddTestPlayers(this Raid raid) - { - raid.AddPlayer(new RosterPlayer(0, "YamaRaja", PlayerRole.Healer, 30000, false)); - raid.AddPlayer(new RosterPlayer(1, "Zaku", PlayerRole.Dps, 40000, false)); - raid.AddPlayer(new RosterPlayer(2, "Juchi", PlayerRole.Tank, 40000, false)); - raid.AddPlayer(new RosterPlayer(3, "Akeno", PlayerRole.Dps, 40000, true)); - } -} \ No newline at end of file