[Raid] Complete roster assigner

This commit is contained in:
2022-12-07 23:26:19 +01:00
parent 12507b0fed
commit 0535655f58
8 changed files with 171 additions and 47 deletions

View File

@@ -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;

View File

@@ -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)));
}
}

View File

@@ -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()
);

View File

@@ -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)

View File

@@ -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)

View File

@@ -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());
}
}

View 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;
}
}

View File

@@ -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));
}
}