[Raid] Complete roster assigner
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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)));
|
||||
}
|
||||
}
|
||||
@@ -17,15 +17,17 @@ public partial class RaidModule : InteractionModuleBase<SocketInteractionContext
|
||||
private readonly IPlayerInfosRepository _playerInfos;
|
||||
private readonly RaidFormatter _raidFormatter;
|
||||
private readonly RaidRegisterManager _registerManager;
|
||||
private readonly RosterAssigner _rosterAssigner;
|
||||
|
||||
public RaidModule(ILogger<RaidModule> 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<SocketInteractionContext
|
||||
|
||||
if (message is SocketUserMessage userMessage)
|
||||
{
|
||||
raid.AssignRosters(_rosterAssigner);
|
||||
|
||||
await userMessage.ModifyAsync(
|
||||
m => m.Embed = _raidFormatter.RaidEmbed(raid).Build()
|
||||
);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
using Cocotte.Utils;
|
||||
|
||||
namespace Cocotte.Modules.Raids;
|
||||
|
||||
public class RosterAssigner
|
||||
{
|
||||
{
|
||||
public void AssignRosters(IEnumerable<RosterPlayer> 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<RosterInfo>(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<PlayerGroup>();
|
||||
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<PlayerGroup> GroupPlayers(IEnumerable<RosterPlayer> players)
|
||||
{
|
||||
var groups = new List<PlayerGroup>();
|
||||
|
||||
|
||||
// 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<RosterPlayer>();
|
||||
}
|
||||
|
||||
|
||||
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)
|
||||
|
||||
@@ -6,7 +6,7 @@ public static class RosterExtensions
|
||||
{
|
||||
return players.Any(p => p.Role == PlayerRole.Healer);
|
||||
}
|
||||
|
||||
|
||||
public static bool AnyTank(this IEnumerable<RosterPlayer> 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<RosterPlayer> players)
|
||||
{
|
||||
return players.Count(p => p.Role == PlayerRole.Tank);
|
||||
}
|
||||
|
||||
|
||||
public static long TotalFc(this IEnumerable<RosterPlayer> 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());
|
||||
}
|
||||
}
|
||||
31
Cocotte/Utils/EnumerableExtensions.cs
Normal file
31
Cocotte/Utils/EnumerableExtensions.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
namespace Cocotte.Utils;
|
||||
|
||||
public static class EnumerableExtensions
|
||||
{
|
||||
public static TSource MinBy<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector,
|
||||
Func<TSource, TSource, TSource> conflictResolver)
|
||||
{
|
||||
var comparer = Comparer<TKey>.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;
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user