public static async Task SerializeAsync(TextWriter target, CompleteScoreboardSummary summary, IDictionary <TeamId, ScoreboardDetails> teamDetails, CompetitionRound round = 0) { using (JsonWriter jw = new JsonTextWriter(target)) { jw.CloseOutput = false; // write await jw.WriteStartObjectAsync().ConfigureAwait(false); await jw.WritePropertyNameAsync("summary").ConfigureAwait(false); var serializer = JsonSerializer.CreateDefault(); // serialize serializer.Serialize(jw, summary); await jw.WritePropertyNameAsync("teams").ConfigureAwait(false); serializer.Serialize(jw, teamDetails); await jw.WritePropertyNameAsync("round").ConfigureAwait(false); await jw.WriteValueAsync((int)round).ConfigureAwait(false); await jw.WriteEndObjectAsync().ConfigureAwait(false); await jw.FlushAsync().ConfigureAwait(false); } }
public void Deserialize(string rawJson) { deserializedJsonLock.EnterWriteLock(); try { JObject obj = JObject.Parse(rawJson); summary = obj["summary"].ToObject <CompleteScoreboardSummary>(); teamDetails = obj["teams"].ToObject <Dictionary <TeamId, ScoreboardDetails> >(); // workaround, see #18 summary.SnapshotTimestamp = summary.SnapshotTimestamp.ToUniversalTime(); foreach (var teamData in teamDetails.Values) { teamData.SnapshotTimestamp = teamData.SnapshotTimestamp.ToUniversalTime(); } try { Round = (CompetitionRound)obj["round"].Value <int>(); } catch { Round = 0; } Metadata.StaticSummaryLine = "CCS Archive" + (Round == 0 ? string.Empty : (", " + Round.ToStringCamelCaseToSpace())); } finally { deserializedJsonLock.ExitWriteLock(); } }
public static async Task <string> SerializeAsync(CompleteScoreboardSummary summary, IDictionary <TeamId, ScoreboardDetails> teamDetails, CompetitionRound round = 0) { StreamWriter sw = null; StreamReader sr = null; try { using (var memStr = new MemoryStream()) { sw = new StreamWriter(memStr); sr = new StreamReader(memStr); // write await SerializeAsync(sw, summary, teamDetails, round).ConfigureAwait(false); // read memStr.Position = 0; return(await sr.ReadToEndAsync().ConfigureAwait(false)); } } finally { sw?.Dispose(); sr?.Dispose(); } }
public async Task GenerateHistogramAsync(DivisionWithCategory?divisionWithCategory, Tier?tier, string imageName, string locCode) { using (Context.Channel.EnterTypingState()) { if (imageName != null && !ScoreRetrievalService.Metadata.SupportsInexpensiveDetailQueries) { throw new InvalidOperationException("Per-image histograms are not supported on online score providers. Use the `datasource` command to select an offline score provider."); } CompleteScoreboardSummary scoreboard = await ScoreRetrievalService.GetScoreboardAsync(new ScoreboardFilterInfo(divisionWithCategory?.Division, tier)).ConfigureAwait(false); decimal[] data = await scoreboard.TeamList .Conditionally(locCode != null, tle => tle.Where(t => t.Location == locCode)) .Conditionally(divisionWithCategory?.Category != null, tle => tle.Where(t => t.Category == divisionWithCategory.Value.Category)) .TernaryAsync(imageName == null, x => x.Select(datum => (decimal)datum.TotalScore).ToAsyncEnumerable(), x => x.Select(t => ScoreRetrievalService.GetDetailsAsync(t.TeamId)) .ToTaskResultEnumerable() .Select(t => t.Images.SingleOrDefault(i => i.ImageName == imageName)) .Where(i => i != null) .Select(i => (decimal)i.Score)) .ToArrayAsync().ConfigureAwait(false); Array.Sort(data); Models.User userSettings = await Preferences.Database.FindOneAsync <Models.User>(u => u.Id == Context.User.Id).ConfigureAwait(false); ColorPresets.HistogramColorPreset histogramColorScheme = (userSettings?.DiscordTheme ?? "dark") == "light" ? ColorPresets.DiscordLight : ColorPresets.DiscordDark; using (var memStr = new System.IO.MemoryStream()) { await GraphProvider.WriteHistogramPngAsync(data, "Score", "Frequency", datum => datum.ToString("0.0#"), histogramColorScheme, memStr).ConfigureAwait(false); memStr.Position = 0; var histogramEmbed = new EmbedBuilder() .WithTitle("CyberPatriot Score Analysis") .WithDescription(Utilities.JoinNonNullNonEmpty(" | ", imageName.AppendPrependIfNonEmpty("`"), divisionWithCategory?.Division.ToStringCamelCaseToSpace(), tier, divisionWithCategory?.Category?.ToCanonicalName(), LocationResolutionService.GetFullNameOrNull(locCode)) .CoalesceBlank("All Teams")) .AddInlineField("Teams", data.Length) .AddInlineField("Mean", $"{data.Average():0.##}") .AddInlineField("Standard Deviation", $"{data.StandardDeviation():0.##}") .AddInlineField("First Quartile", $"{data.Take(data.Length / 2).ToArray().Median():0.##}") .AddInlineField("Median", $"{data.Median():0.##}") .AddInlineField("Third Quartile", $"{data.Skip(data.Length / 2).ToArray().Median():0.##}") .AddInlineField("Min Score", $"{data.Min()}") .AddInlineField("Max Score", $"{data.Max()}") .WithTimestamp(scoreboard.SnapshotTimestamp) .WithFooter(ScoreRetrievalService.Metadata.StaticSummaryLine) .WithImageUrl("attachment://histogram.png"); // Discord API requirement to use the uploaded histogram await Context.Channel.SendFileAsync(memStr, "histogram.png", embed : histogramEmbed.Build()).ConfigureAwait(false); } } }
public async Task ExportSummaryCommandAsync() { using (Context.Channel.EnterTypingState()) { var scoreboardTask = ScoreRetrievalService.GetScoreboardAsync(ScoreboardFilterInfo.NoFilter); var targetWriter = new System.IO.StringWriter(); await targetWriter.WriteLineAsync("TeamId,Division,Category,Location,Tier,ImageCount,PlayTime,Score,Warnings").ConfigureAwait(false); CompleteScoreboardSummary scoreboard = await scoreboardTask.ConfigureAwait(false); foreach (var team in scoreboard.TeamList) { await targetWriter.WriteLineAsync($"{team.TeamId},{team.Division.ToStringCamelCaseToSpace()},{(!team.Category.HasValue ? string.Empty : team.Category.Value.ToCanonicalName())},{team.Location},{(team.Tier.HasValue ? team.Tier.Value.ToString() : string.Empty)},{team.ImageCount},{team.PlayTime.ToHoursMinutesSecondsString()},{ScoreRetrievalService.Metadata.FormattingOptions.FormatScore(team.TotalScore)},{team.Warnings.ToConciseString()}").ConfigureAwait(false); } TimeZoneInfo tz = await Preferences.GetTimeZoneAsync(Context.Guild, Context.User).ConfigureAwait(false); string tzAbbr = tz.GetAbbreviations().Generic; DateTimeOffset snapshotTimestamp = TimeZoneInfo.ConvertTime(scoreboard.SnapshotTimestamp, tz); using (var targetStream = new System.IO.MemoryStream(System.Text.Encoding.UTF8.GetBytes(targetWriter.GetStringBuilder().ToString()))) { await Context.Channel.SendFileAsync(targetStream, "scoreboard.csv", $"Scoreboard summary CSV export\nScore timestamp: {snapshotTimestamp:g} {tzAbbr}\nExported: {TimeZoneInfo.ConvertTime(DateTimeOffset.UtcNow, tz):g} {tzAbbr}").ConfigureAwait(false); } } }
public async Task GetServiceLeaderboardImplementationAsync(string category, Tier?tier, int pageNumber) { using (Context.Channel.EnterTypingState()) { CompleteScoreboardSummary teamScore = await ScoreRetrievalService.GetScoreboardAsync(new ScoreboardFilterInfo(Division.AllService, tier)).ConfigureAwait(false); if (teamScore == null) { throw new Exception("Error obtaining scoreboard."); } // validate category string realCategory = teamScore.TeamList.Where(t => CategoryEquals(t, category)).Select(t => t.Category).Distinct().SingleIfOne(); if (realCategory == null) { throw new ArgumentException("The given category was not found - it was either ambiguous or invalid.", nameof(category)); } await ReplyAsync(ScoreEmbedBuilder.CreateTopLeaderboardEmbed(teamScore, pageNumber: pageNumber, customFilter: new ScoreboardMessageBuilderService.CustomFiltrationInfo() { Predicate = t => t.Category == realCategory, FilterDescription = realCategory }, timeZone: await Preferences.GetTimeZoneAsync(Context.Guild, Context.User).ConfigureAwait(false))).ConfigureAwait(false); } }
public string CreateTopLeaderboardEmbed(CompleteScoreboardSummary scoreboard, CustomFiltrationInfo customFilter = null, TimeZoneInfo timeZone = null, int pageNumber = 1, int pageSize = 15) { if (pageSize <= 0) { throw new ArgumentOutOfRangeException(nameof(pageSize)); } Func <ScoreboardSummaryEntry, bool> predicate = customFilter?.Predicate == null ? _ => true : customFilter.Predicate; int pageCount = (int)(Math.Ceiling((((double)scoreboard.TeamList.Count(predicate)) / pageSize))); if (--pageNumber < 0 || pageNumber >= pageCount) { throw new ArgumentOutOfRangeException(nameof(pageNumber)); } var stringBuilder = new StringBuilder(); stringBuilder.Append("**CyberPatriot Scoreboard"); if (scoreboard.Filter.Division.HasValue) { stringBuilder.Append(", ").Append(Utilities.ToStringCamelCaseToSpace(scoreboard.Filter.Division.Value)); if (scoreboard.Filter.Tier != null) { stringBuilder.Append(' ').Append(scoreboard.Filter.Tier); } } else if (scoreboard.Filter.Tier != null) { stringBuilder.Append(", ").Append(scoreboard.Filter.Tier).Append(" Tier"); } if (customFilter?.FilterDescription != null) { stringBuilder.Append(", ").Append(customFilter.FilterDescription); } if (pageCount > 1) { stringBuilder.Append(" (Page ").Append(pageNumber + 1).Append(" of ").Append(pageCount).Append(')'); } stringBuilder.AppendLine("**"); stringBuilder.Append("*As of: "); DateTimeOffset timestamp = timeZone == null ? scoreboard.SnapshotTimestamp : TimeZoneInfo.ConvertTime(scoreboard.SnapshotTimestamp, timeZone); stringBuilder.AppendFormat("{0:g}", timestamp); stringBuilder.Append(' ').Append(timeZone == null ? "UTC" : TimeZoneNames.TZNames.GetAbbreviationsForTimeZone(timeZone.Id, "en-US").Generic).AppendLine("*"); stringBuilder.AppendLine("```"); // FIXME time display logic according to FormattingOptions scoreboard.TeamList.Where(predicate).Skip(pageNumber * pageSize).Take(pageSize) .Select((team, i) => stringBuilder.AppendFormat("#{0,-5}{1,-7}{2,4}{6,6}{7,10}{3,16}{4,7}{5,4}", i + 1 + (pageNumber * pageSize), team.TeamId, team.Location, ScoreRetrieverMetadata.FormattingOptions.FormatScoreForLeaderboard(team.TotalScore), team.Advancement.HasValue ? team.Advancement.Value.ToConciseString() : string.Format("{0:hh\\:mm}", team.PlayTime), team.Warnings.ToConciseString(), team.Division.ToConciseString(), team.Tier).AppendLine()) .Last().AppendLine("```"); if (scoreboard.OriginUri != null) { stringBuilder.AppendLine(scoreboard.OriginUri.ToString()); } return(stringBuilder.ToString()); }
public string CreateTopLeaderboardEmbed(CompleteScoreboardSummary scoreboard, TimeZoneInfo timeZone = null, int pageNumber = 1, int pageSize = 15) { if (pageSize <= 0) { throw new ArgumentOutOfRangeException(nameof(pageSize)); } int pageCount = (int)(Math.Ceiling((((double)scoreboard.TeamList.Count()) / pageSize))); if (--pageNumber < 0 || pageNumber >= pageCount) { throw new ArgumentOutOfRangeException(nameof(pageNumber)); } var stringBuilder = new StringBuilder(); stringBuilder.Append("**CyberPatriot Scoreboard"); string filterDesc = Utilities.JoinNonNullNonEmpty(", ", scoreboard.Filter.Division.HasValue ? Utilities.ToStringCamelCaseToSpace(scoreboard.Filter.Division.Value) + " Division" : null, scoreboard.Filter.Category?.ToCanonicalName(), scoreboard.Filter.Tier, LocationResolution.GetFullNameOrNull(scoreboard.Filter.Location) ); if (filterDesc.Length > 0) { stringBuilder.Append(", "); stringBuilder.Append(filterDesc); } if (pageCount > 1) { stringBuilder.Append(" (Page ").Append(pageNumber + 1).Append(" of ").Append(pageCount).Append(')'); } stringBuilder.AppendLine("**"); stringBuilder.Append("*As of: "); DateTimeOffset timestamp = timeZone == null ? scoreboard.SnapshotTimestamp : TimeZoneInfo.ConvertTime(scoreboard.SnapshotTimestamp, timeZone); stringBuilder.AppendFormat("{0:g}", timestamp); stringBuilder.Append(' ').Append(timeZone == null ? "UTC" : TimeZoneNames.TZNames.GetAbbreviationsForTimeZone(timeZone.Id, "en-US").Generic).AppendLine("*"); stringBuilder.AppendLine("```"); bool conciseDivision = (scoreboard.Filter.Division.HasValue && scoreboard.Filter.Division.Value != Division.AllService) || !scoreboard.TeamList.Any(x => x.Category.HasValue); scoreboard.TeamList.Skip(pageNumber * pageSize).Take(pageSize) .Select((team, i) => stringBuilder.AppendLine(GetTeamLeaderboardEntry(team, i + 1 + (pageNumber * pageSize), useAbbreviatedDivision: conciseDivision))) .Last().AppendLine("```"); if (scoreboard.OriginUri != null) { stringBuilder.AppendLine(scoreboard.OriginUri.ToString()); } return(stringBuilder.ToString()); }
public async Task GetLeaderboardAsync(Division division, Tier tier, int pageNumber = 1) { using (Context.Channel.EnterTypingState()) { CompleteScoreboardSummary teamScore = await ScoreRetrievalService.GetScoreboardAsync(new ScoreboardFilterInfo(division, tier)).ConfigureAwait(false); if (teamScore == null) { throw new Exception("Error obtaining scoreboard."); } await ReplyAsync(ScoreEmbedBuilder.CreateTopLeaderboardEmbed(teamScore, pageNumber: pageNumber, timeZone: await Preferences.GetTimeZoneAsync(Context.Guild, Context.User).ConfigureAwait(false))).ConfigureAwait(false); } }
public async Task GeneratePeerLeaderboardAsync(TeamId team) { using (Context.Channel.EnterTypingState()) { ScoreboardDetails teamDetails = await ScoreRetrievalService.GetDetailsAsync(team).ConfigureAwait(false); if (teamDetails == null) { throw new Exception("Error obtaining team score."); } CompleteScoreboardSummary peerScoreboard = await ScoreRetrievalService.GetScoreboardAsync(CompetitionRoundLogicService.GetPeerFilter(ScoreRetrievalService.Round, teamDetails.Summary)).ConfigureAwait(false); await ReplyAsync(ScoreEmbedBuilder.CreatePeerLeaderboardEmbed(teamDetails.TeamId, peerScoreboard, timeZone: await Preferences.GetTimeZoneAsync(Context.Guild, Context.User).ConfigureAwait(false))).ConfigureAwait(false); } }
public string CreatePeerLeaderboardEmbed(TeamId teamId, CompleteScoreboardSummary scoreboard, IList <ScoreboardSummaryEntry> peerTeams, TimeZoneInfo timeZone = null, int topTeams = 3, int nearbyTeams = 5) { // var peerTeams = CompetitionLogic.GetPeerTeams(ScoreRetriever.Round, scoreboard, teamDetails.Summary); var stringBuilder = new StringBuilder(); stringBuilder.AppendLine("**CyberPatriot Scoreboard**"); DateTimeOffset timestamp = timeZone == null ? scoreboard.SnapshotTimestamp : TimeZoneInfo.ConvertTime(scoreboard.SnapshotTimestamp, timeZone); stringBuilder.AppendFormat("*Competing against: {0} | As of: ", teamId); stringBuilder.AppendFormat("{0:g}", timestamp); stringBuilder.Append(' ').Append(timeZone == null ? "UTC" : TimeZoneNames.TZNames.GetAbbreviationsForTimeZone(timeZone.Id, "en-US").Generic).AppendLine("*"); stringBuilder.AppendLine("```bash"); // zero-based rank of the given team int pos = peerTeams.IndexOfWhere(team => team.TeamId == teamId); if (pos < nearbyTeams + topTeams + 1) { peerTeams.Take(nearbyTeams + pos + 1) .Select((team, i) => stringBuilder.AppendFormat("{8}{0,-5}{1,-7}{2,4}{6,6}{7,10}{3,16}{4,7}{5,4}", i + 1, team.TeamId, team.Location, ScoreRetrieverMetadata.FormattingOptions.FormatScoreForLeaderboard(team.TotalScore), team.Advancement.HasValue ? team.Advancement.Value.ToConciseString() : string.Format("{0:hh\\:mm}", team.PlayTime), team.Warnings.ToConciseString(), team.Division.ToConciseString(), team.Tier, team.TeamId == teamId ? ">" : "#").AppendLine()) .Consume(); } else { peerTeams.Take(topTeams) .Select((team, i) => stringBuilder.AppendFormat("#{0,-5}{1,-7}{2,4}{6,6}{7,10}{3,16}{4,7}{5,4}", i + 1, team.TeamId, team.Location, ScoreRetrieverMetadata.FormattingOptions.FormatScoreForLeaderboard(team.TotalScore), team.Advancement.HasValue ? team.Advancement.Value.ToConciseString() : string.Format("{0:hh\\:mm}", team.PlayTime), team.Warnings.ToConciseString(), team.Division.ToConciseString(), team.Tier).AppendLine()) .Consume(); stringBuilder.AppendLine("..."); peerTeams.Skip(pos - nearbyTeams) .Take(nearbyTeams) .Select((team, i) => stringBuilder.AppendFormat("#{0,-5}{1,-7}{2,4}{6,6}{7,10}{3,16}{4,7}{5,4}", i + pos - nearbyTeams + 1, team.TeamId, team.Location, ScoreRetrieverMetadata.FormattingOptions.FormatScoreForLeaderboard(team.TotalScore), team.Advancement.HasValue ? team.Advancement.Value.ToConciseString() : string.Format("{0:hh\\:mm}", team.PlayTime), team.Warnings.ToConciseString(), team.Division.ToConciseString(), team.Tier).AppendLine()) .Consume(); ScoreboardSummaryEntry thisTeamDetails = peerTeams.Single(t => t.TeamId == teamId); stringBuilder.AppendFormat(">{0,-5}{1,-7}{2,4}{6,6}{7,10}{3,16}{4,7}{5,4}", pos + 1, thisTeamDetails.TeamId, thisTeamDetails.Location, ScoreRetrieverMetadata.FormattingOptions.FormatScoreForLeaderboard(thisTeamDetails.TotalScore), thisTeamDetails.Advancement.HasValue ? thisTeamDetails.Advancement.Value.ToConciseString() : string.Format("{0:hh\\:mm}", thisTeamDetails.PlayTime), thisTeamDetails.Warnings.ToConciseString(), thisTeamDetails.Division.ToConciseString(), thisTeamDetails.Tier).AppendLine(); // since pos and i are both zero-based, i + pos + 2 returns correct team rank for teams after given team peerTeams.Skip(pos + 1) .Take(nearbyTeams) .Select((team, i) => stringBuilder.AppendFormat("#{0,-5}{1,-7}{2,4}{6,6}{7,10}{3,16}{4,7}{5,4}", i + pos + 2, team.TeamId, team.Location, ScoreRetrieverMetadata.FormattingOptions.FormatScoreForLeaderboard(team.TotalScore), team.Advancement.HasValue ? team.Advancement.Value.ToConciseString() : string.Format("{0:hh\\:mm}", team.PlayTime), team.Warnings.ToConciseString(), team.Division.ToConciseString(), team.Tier).AppendLine()) .Consume(); } stringBuilder.AppendLine("```"); if (scoreboard.OriginUri != null) { stringBuilder.AppendLine(scoreboard.OriginUri.ToString()); } return(stringBuilder.ToString()); }
public async Task GetLocationLeaderboardImplementationAsync(string location, ScoreboardFilterInfo filterInfo, int pageNumber) { using (Context.Channel.EnterTypingState()) { CompleteScoreboardSummary teamScore = await ScoreRetrievalService.GetScoreboardAsync(filterInfo).ConfigureAwait(false); if (teamScore == null) { throw new Exception("Error obtaining scoreboard."); } await ReplyAsync(ScoreEmbedBuilder.CreateTopLeaderboardEmbed(teamScore, pageNumber: pageNumber, customFilter: new ScoreboardMessageBuilderService.CustomFiltrationInfo() { Predicate = t => t.Location == location, FilterDescription = location // TODO full name of state? }, timeZone: await Preferences.GetTimeZoneAsync(Context.Guild, Context.User).ConfigureAwait(false))).ConfigureAwait(false); } }
protected bool IsSummaryValid(CompleteScoreboardSummary returnedSummary) { if (returnedSummary?.TeamList == null) { return(false); } if (returnedSummary.Filter == ScoreboardFilterInfo.NoFilter) { // NoFilter responses should have at least one team if (returnedSummary.TeamList.Count < 1) { return(false); } if (returnedSummary.TeamList.First().TeamId == default(TeamId)) { return(false); } } return(true); }
public override IList <ScoreboardSummaryEntry> GetPeerTeams(CompetitionRound round, CompleteScoreboardSummary divisionScoreboard, ScoreboardSummaryEntry teamDetails) { // make a clone because we'll mutate this later divisionScoreboard = divisionScoreboard.Clone().WithFilter(teamDetails.Division, null); if (teamDetails.Division == Division.MiddleSchool) { // middle school doesn't have tiers or categories return(divisionScoreboard.TeamList); } // open/service if ((teamDetails.Division == Division.Open && round > CompetitionRound.Round2) || (teamDetails.Division == Division.AllService && round == CompetitionRound.Round3)) { // In open past R2, tier matters, but that's it // In all service R3, category doesn't* matter, just tier // See issue #14 return(divisionScoreboard.WithFilter(teamDetails.Division, teamDetails.Tier).TeamList); } // open/service, service: category matters; open: no tiers if (teamDetails.Division == Division.Open) { // unknown round - if our candidate team has a tier, filter by tier, otherwise return the whole division if (round == 0 && teamDetails.Tier != null) { return(divisionScoreboard.WithFilter(teamDetails.Division, teamDetails.Tier).TeamList); } // either R1 or R2 // safe to return the whole division as a peer list return(divisionScoreboard.TeamList); } // all-service round where category matters ("R0" we default to factoring in category) // filter by tier, where available if (round > CompetitionRound.Round2) { divisionScoreboard.WithFilter(Division.AllService, teamDetails.Tier); } // just need to filter the list by category if (teamDetails.Category == null) { // silent fail return(divisionScoreboard.TeamList); } // there might be some A.S. teams whose categories we don't know // they get treated as not-my-problem, that is, not part of my category return(divisionScoreboard.TeamList.Where(t => t.Category == teamDetails.Category).ToIList()); }
public virtual TeamDetailRankingInformation GetRankingInformation(CompetitionRound round, CompleteScoreboardSummary divisionScoreboard, ScoreboardSummaryEntry teamInfo) { divisionScoreboard = divisionScoreboard.Clone().WithFilter(teamInfo.Division, null); // may be equal to division scoreboard, that's fine var tierScoreboard = divisionScoreboard.Clone().WithFilter(teamInfo.Division, teamInfo.Tier); var peers = GetPeerTeams(round, divisionScoreboard, teamInfo); var summaryComparer = BuildSummaryComparer(teamInfo.TeamId); return(new TeamDetailRankingInformation() { TeamId = teamInfo.TeamId, Peers = peers, PeerIndex = peers.IndexOfWhere(summaryComparer), PeerCount = peers.Count, DivisionIndex = divisionScoreboard.TeamList.IndexOfWhere(summaryComparer), DivisionCount = divisionScoreboard.TeamList.Count, TierIndex = tierScoreboard.TeamList.IndexOfWhere(summaryComparer), TierCount = tierScoreboard.TeamList.Count }); }
public abstract IList <ScoreboardSummaryEntry> GetPeerTeams(CompetitionRound round, CompleteScoreboardSummary divisionScoreboard, ScoreboardSummaryEntry teamInfo);
async Task TeamPlacementChangeNotificationTimer(TimerStateWrapper state) { try { using (var databaseContext = _database.OpenContext <Models.Guild>(false)) using (var guildSettingEnumerator = databaseContext.FindAllAsync().GetEnumerator()) { CompleteScoreboardSummary masterScoreboard = null; Dictionary <TeamId, int> teamIdsToPeerIndexes = new Dictionary <TeamId, int>(); while (await guildSettingEnumerator.MoveNext().ConfigureAwait(false)) { Models.Guild guildSettings = guildSettingEnumerator.Current; if (guildSettings?.ChannelSettings == null || guildSettings.ChannelSettings.Count == 0) { return; } IGuild guild = _discord.GetGuild(guildSettings.Id); foreach (var chanSettings in guildSettings.ChannelSettings.Values) { if (chanSettings?.MonitoredTeams == null || chanSettings.MonitoredTeams.Count == 0) { continue; } IGuildChannel rawChan = await guild.GetChannelAsync(chanSettings.Id).ConfigureAwait(false); if (!(rawChan is ITextChannel chan)) { continue; } masterScoreboard = await _scoreRetriever.GetScoreboardAsync(ScoreboardFilterInfo.NoFilter).ConfigureAwait(false); foreach (TeamId monitored in chanSettings.MonitoredTeams) { int masterScoreboardIndex = masterScoreboard.TeamList.IndexOfWhere(scoreEntry => scoreEntry.TeamId == monitored); if (masterScoreboardIndex == -1) { continue; } // TODO efficiency: we're refiltering every loop iteration ScoreboardSummaryEntry monitoredEntry = masterScoreboard.TeamList[masterScoreboardIndex]; int peerIndex = masterScoreboard.Clone().WithFilter(_competitionLogic.GetPeerFilter(_scoreRetriever.Round, monitoredEntry)).TeamList.IndexOf(monitoredEntry); teamIdsToPeerIndexes[monitored] = peerIndex; // we've obtained all information, now compare to past data if (state.PreviousTeamListIndexes != null && state.PreviousTeamListIndexes.TryGetValue(monitored, out int prevPeerIndex)) { int indexDifference = peerIndex - prevPeerIndex; if (indexDifference != 0) { StringBuilder announceMessage = new StringBuilder(); announceMessage.Append("**"); announceMessage.Append(monitored); announceMessage.Append("**"); if (indexDifference > 0) { announceMessage.Append(" rose "); } else { announceMessage.Append(" fell "); indexDifference *= -1; } var teamDetails = await _scoreRetriever.GetDetailsAsync(monitored).ConfigureAwait(false); announceMessage.Append(Utilities.Pluralize("place", indexDifference)); announceMessage.Append(" to **"); announceMessage.Append(Utilities.AppendOrdinalSuffix(peerIndex + 1)); announceMessage.Append(" place**."); await chan.SendMessageAsync( announceMessage.ToString(), embed : _messageBuilder .CreateTeamDetailsEmbed( teamDetails, masterScoreboard, _competitionLogic.GetPeerFilter(_scoreRetriever.Round, teamDetails.Summary)) .Build()).ConfigureAwait(false); } } } } } state.PreviousTeamListIndexes = teamIdsToPeerIndexes; } } catch (Exception ex) { await _logService.LogApplicationMessageAsync(LogSeverity.Error, "Error in team monitor timer task", ex).ConfigureAwait(false); } }
public async Task ResolveBackendAsync(IServiceProvider provider = null, int upperSearchBound = -1) { await _backendResolutionLock.WaitAsync().ConfigureAwait(false); var errors = new List <Exception>(); try { provider = provider ?? _provider; IScoreRetrievalService backend = null; int selInd = -1; for (int i = 0; i < (upperSearchBound == -1 ? _backendOptions.Count : upperSearchBound); i++) { var candidateBackendTaskFactory = _backendOptions[i]; try { // construct and initialize the backend var candidateBackend = await candidateBackendTaskFactory(provider).ConfigureAwait(false); if (candidateBackend == null) { errors.Add(new NullReferenceException("Candidate backend task factory returned null.")); continue; } // try getting a summary to "test" the backend CompleteScoreboardSummary returnedSummary = await candidateBackend.GetScoreboardAsync(ScoreboardFilterInfo.NoFilter).ConfigureAwait(false); if (!IsSummaryValid(returnedSummary)) { // invalid summary errors.Add(new Exception("Invalid summary.")); continue; } else { // this backend is valid selInd = i; backend = candidateBackend; // first valid backend wins break; } } catch (Exception e) { // invalid summary, or failed constructor errors.Add(e); continue; } } if (backend == null) { if (upperSearchBound == -1) { // we searched all possible backends await _log(Microsoft.Extensions.Logging.LogLevel.Error, "Could not find an IScoreRetrievalService for fallback, continuing with invalid service.", source : nameof(FallbackScoreRetrievalService)).ConfigureAwait(false); throw new AggregateException("No valid IScoreRetrievalService found.", errors); } else { // we tried and failed to replace the backend with a higher-priority one, so now check lower priority ones // use Backend.Backend to get the cache's backend if (IsSummaryValid(await Backend.Backend.GetScoreboardAsync(ScoreboardFilterInfo.NoFilter).ConfigureAwait(false))) { // update the refresh time to now, keep the existing backend _lastBackendRefresh = DateTimeOffset.UtcNow; } else { // current backend has failed: try again but allow deferring to lower-priority things await ResolveBackendAsync(provider, -1).ConfigureAwait(false); } return; } } _selectedBackendIndex = selInd; // wrap it in a cache var csrs = new CachingScoreRetrievalService(backend); _cacheConfigurator(csrs); Backend = csrs; _lastBackendRefresh = DateTimeOffset.UtcNow; } finally { _backendResolutionLock.Release(); } }
static void Main(string[] args) { Console.WriteLine("Enter path to CSV file:"); string path = Console.ReadLine(); Console.WriteLine("Enter timestamp:"); string timestamp = Console.ReadLine(); Console.WriteLine("Enter path to all service category map file (or empty string):"); string servicePath = Console.ReadLine(); Console.WriteLine("Enter round number:"); int roundNumber = int.Parse(Console.ReadLine()); Console.WriteLine("Enter origin URI:"); string originUri = Console.ReadLine(); Dictionary <TeamId, string> categories = new Dictionary <TeamId, string>(); if (servicePath != "") { categories = File.ReadAllLines(servicePath).Select(x => x.Split(':')).Where(x => TeamId.TryParse(x[0], out TeamId _)).ToDictionary(x => TeamId.Parse(x[0]), x => x[1]); } var lines = File.ReadAllLines(path); CompleteScoreboardSummary summary = new CompleteScoreboardSummary(); summary.TeamList = new List <ScoreboardSummaryEntry>(); summary.SnapshotTimestamp = DateTimeOffset.Parse(timestamp); summary.OriginUri = string.IsNullOrEmpty(originUri) ? null : new Uri(originUri); Console.WriteLine("Loading score data"); foreach (string[] data in lines.Skip(1).Select(line => line.Split(','))) { ScoreboardSummaryEntry entry = new ScoreboardSummaryEntry { TeamId = TeamId.Parse(data[0]), Division = Enum.Parse <Division>(data[1].Replace(" ", ""), true), Category = string.IsNullOrEmpty(data[2]) ? categories.TryGetValue(TeamId.Parse(data[0]), out string c) ? c : null : data[2], Location = data[3], Tier = Enum.TryParse <Tier>(data[4], true, out Tier t) ? t : (Tier?)null, ImageCount = int.Parse(data[5]), PlayTime = ParseTimeSpan(data[6]), TotalScore = int.Parse(data[7]), Warnings = (data[8].Contains("M") ? ScoreWarnings.MultiImage : 0) | (data[8].Contains("T") ? ScoreWarnings.TimeOver : 0) }; summary.TeamList.Add(entry); } Console.WriteLine("Generating output data"); var o = new Output { round = roundNumber, summary = summary, teams = summary.TeamList.Select(x => new ScoreboardDetails { Images = new List <ScoreboardImageDetails> { new ScoreboardImageDetails { ImageName = "All Points", Penalties = 0, PlayTime = x.PlayTime, PointsPossible = x.ImageCount * 100, Score = x.TotalScore, VulnerabilitiesFound = 0, VulnerabilitiesRemaining = 0, Warnings = x.Warnings } }, ImageScoresOverTime = null, OriginUri = null, ScoreTime = x.PlayTime, SnapshotTimestamp = DateTimeOffset.Parse(timestamp), Summary = x }).ToDictionary(x => x.TeamId, x => x) }; File.WriteAllText("scores.json", JsonConvert.SerializeObject(o)); Console.WriteLine("Done"); Console.ReadKey(); }
// [Command(HistogramCommandName), Alias("scoregraph", "scorestats", "statistics"), Summary("Generates a histogram of the given tier's scores for the given image within the given state on the current CyberPatriot leaderboard.")] // [Priority(-1)] // public Task HistogramCommandAsync([OverrideTypeReader(typeof(LocationTypeReader))] string location, Division div, Tier tier, string imageName) => GenerateHistogramAsync(new ScoreboardFilterInfo(div, tier), imageName, location); public async Task GenerateHistogramAsync(ScoreboardFilterInfo filter, string imageName, string locCode) { using (Context.Channel.EnterTypingState()) { var descBuilder = new System.Text.StringBuilder(); if (filter.Division.HasValue) { descBuilder.Append(' ').Append(filter.Division.Value.ToStringCamelCaseToSpace()); } if (filter.Tier.HasValue) { descBuilder.Append(' ').Append(filter.Tier.Value); } if (imageName != null) { throw new NotSupportedException("Per-image histograms are not yet supported."); // unreachable code - not implemented on the data-aggregation/filter side, but this code Should Work:tm: for constructing the title #pragma warning disable 0162 if (descBuilder.Length > 0) { descBuilder.Append(": "); } descBuilder.Append(imageName); #pragma warning restore 0162 } CompleteScoreboardSummary scoreboard = await ScoreRetrievalService.GetScoreboardAsync(filter).ConfigureAwait(false); decimal[] data = scoreboard.TeamList .Conditionally(locCode != null, tle => tle.Where(t => t.Location == locCode)) // nasty hack .Select(datum => decimal.TryParse(ScoreRetrievalService.Metadata.FormattingOptions.FormatScore(datum.TotalScore), out decimal d) ? d : datum.TotalScore) .OrderBy(d => d).ToArray(); using (var memStr = new System.IO.MemoryStream()) { await GraphProvider.WriteHistogramPngAsync(data, "Score", "Frequency", datum => datum.ToString("0.0#"), BitmapProvider.Color.Parse("#32363B"), BitmapProvider.Color.Parse("#7289DA"), BitmapProvider.Color.White, BitmapProvider.Color.Gray, memStr).ConfigureAwait(false); memStr.Position = 0; // This shouldn't be necessary, Discord's API supports embedding attached images // BUT discord.net does not, see #796 var httpClient = new System.Net.Http.HttpClient(); var imagePostMessage = new System.Net.Http.StreamContent(memStr); imagePostMessage.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("image/png"); Task <System.Net.Http.HttpResponseMessage> uploadUrlResponseTask = httpClient.PutAsync("https://transfer.sh/histogram.png", imagePostMessage); var histogramEmbed = new EmbedBuilder() .WithTitle("CyberPatriot Score Analysis") .WithDescription(Utilities.JoinNonNullNonEmpty(" | ", filter.Division?.ToStringCamelCaseToSpace(), filter.Tier, locCode).CoalesceBlank("All Teams")) .AddInlineField("Teams", data.Length) .AddInlineField("Mean", $"{data.Average():0.##}") .AddInlineField("Standard Deviation", $"{data.StandardDeviation():0.##}") .AddInlineField("First Quartile", $"{data.Take(data.Length / 2).ToArray().Median():0.##}") .AddInlineField("Median", $"{data.Median():0.##}") .AddInlineField("Third Quartile", $"{data.Skip(data.Length / 2).ToArray().Median():0.##}") .AddInlineField("Min Score", $"{data.Min()}") .AddInlineField("Max Score", $"{data.Max()}") .WithImageUrl(await(await uploadUrlResponseTask.ConfigureAwait(false)).Content.ReadAsStringAsync().ConfigureAwait(false)) .WithTimestamp(scoreboard.SnapshotTimestamp) .WithFooter(ScoreRetrievalService.Metadata.StaticSummaryLine); await Context.Channel.SendMessageAsync("", embed : histogramEmbed).ConfigureAwait(false); } } }
public string CreatePeerLeaderboardEmbed(TeamId teamId, CompleteScoreboardSummary peerScoreboard, TimeZoneInfo timeZone = null, int topTeams = 3, int nearbyTeams = 5) { // var peerTeams = CompetitionLogic.GetPeerTeams(ScoreRetriever.Round, scoreboard, teamDetails.Summary); var stringBuilder = new StringBuilder(); stringBuilder.AppendLine("**CyberPatriot Scoreboard**"); DateTimeOffset timestamp = timeZone == null ? peerScoreboard.SnapshotTimestamp : TimeZoneInfo.ConvertTime(peerScoreboard.SnapshotTimestamp, timeZone); stringBuilder.AppendFormat("*Competing against: {0} | As of: ", teamId); stringBuilder.AppendFormat("{0:g}", timestamp); stringBuilder.Append(' ').Append(timeZone == null ? "UTC" : TimeZoneNames.TZNames.GetAbbreviationsForTimeZone(timeZone.Id, "en-US").Generic).AppendLine("*"); var peerTeams = peerScoreboard.TeamList; stringBuilder.AppendFormat("*{0} competes in: {1} Division", teamId, peerScoreboard.Filter.Division.ToStringCamelCaseToSpace()); if (peerScoreboard.Filter.Category.HasValue) { stringBuilder.AppendFormat(", {0}", peerScoreboard.Filter.Category.Value.ToCanonicalName()); } if (peerScoreboard.Filter.Tier.HasValue) { stringBuilder.AppendFormat(", {0} Tier", peerScoreboard.Filter.Tier.Value); } if (peerScoreboard.Filter.Location != null) { stringBuilder.AppendFormat(", {0}", peerScoreboard.Filter.Location); } stringBuilder.AppendLine("*"); bool conciseDivision = !peerTeams.Any(x => x.Category != null); stringBuilder.AppendLine("```bash"); // zero-based rank of the given team int pos = peerTeams.IndexOfWhere(team => team.TeamId == teamId); if (pos < nearbyTeams + topTeams + 1) { peerTeams.Take(nearbyTeams + pos + 1) .Select((team, i) => stringBuilder.AppendLine(GetTeamLeaderboardEntry(team, i + 1, useAbbreviatedDivision: conciseDivision, prefix: team.TeamId == teamId ? ">" : "#"))) .Consume(); } else { peerTeams.Take(topTeams) .Select((team, i) => stringBuilder.AppendLine(GetTeamLeaderboardEntry(team, i + 1, useAbbreviatedDivision: conciseDivision))) .Consume(); stringBuilder.AppendLine("..."); peerTeams.Skip(pos - nearbyTeams) .Take(nearbyTeams) .Select((team, i) => stringBuilder.AppendLine(GetTeamLeaderboardEntry(team, i + pos - nearbyTeams + 1, useAbbreviatedDivision: conciseDivision))) .Consume(); ScoreboardSummaryEntry thisTeamDetails = peerTeams.Single(t => t.TeamId == teamId); stringBuilder.AppendLine(GetTeamLeaderboardEntry(thisTeamDetails, pos + 1, useAbbreviatedDivision: conciseDivision, prefix: ">")); // since pos and i are both zero-based, i + pos + 2 returns correct team rank for teams after given team peerTeams.Skip(pos + 1) .Take(nearbyTeams) .Select((team, i) => stringBuilder.AppendLine(GetTeamLeaderboardEntry(team, i + pos + 2, useAbbreviatedDivision: conciseDivision))) .Consume(); } stringBuilder.AppendLine("```"); if (peerScoreboard.OriginUri != null) { stringBuilder.AppendLine(peerScoreboard.OriginUri.ToString()); } return(stringBuilder.ToString()); }
public EmbedBuilder CreateTeamDetailsEmbed(ScoreboardDetails teamScore, CompleteScoreboardSummary completeScoreboard = null, ScoreboardFilterInfo peerFilter = default(ScoreboardFilterInfo), TimeZoneInfo timeZone = null) { if (teamScore == null) { throw new ArgumentNullException(nameof(teamScore)); } var builder = new EmbedBuilder() .WithTimestamp(teamScore.SnapshotTimestamp) .WithTitle("Team " + teamScore.TeamId) .WithDescription(Utilities.JoinNonNullNonEmpty(" | ", CompetitionLogic.GetEffectiveDivisionDescriptor(teamScore.Summary), teamScore.Summary.Tier, LocationResolution.GetFullName(teamScore.Summary.Location))) .WithFooter(ScoreRetrieverMetadata.StaticSummaryLine); if (!string.IsNullOrWhiteSpace(teamScore.Comment)) { builder.Description += "\n"; builder.Description += teamScore.Comment; } // scoreboard link if (teamScore.OriginUri != null) { builder.Url = teamScore.OriginUri.ToString(); } // location -> flag in thumbnail Uri flagUrl = LocationResolution?.GetFlagUriOrNull(teamScore.Summary.Location); if (flagUrl != null) { builder.ThumbnailUrl = flagUrl.ToString(); } // tier -> color on side // colors borrowed from AFA's spreadsheet if (teamScore.Summary.Tier.HasValue) { switch (teamScore.Summary.Tier.Value) { case Tier.Platinum: // tweaked from AFA spreadsheet to be more distinct from silver // AFA original is #DAE3F3 builder.WithColor(183, 201, 243); break; case Tier.Gold: builder.WithColor(0xFF, 0xE6, 0x99); break; case Tier.Silver: // tweaked from AFA spreadsheet to be more distinct from platinum, and to look less white // AFA original is #F2F2F2 builder.WithColor(0x90, 0x90, 0x90); break; } } // TODO image lookup for location? e.g. thumbnail with flag? foreach (var item in teamScore.Images) { string penaltyAppendage = item.Penalties != 0 ? " - " + Utilities.Pluralize("penalty", item.Penalties) : string.Empty; bool overtime = (item.Warnings & ScoreWarnings.TimeOver) == ScoreWarnings.TimeOver; bool multiimage = (item.Warnings & ScoreWarnings.MultiImage) == ScoreWarnings.MultiImage; string warningAppendage = string.Empty; const string multiImageStr = "**M**ulti-instance"; const string overTimeStr = "**T**ime"; if (overtime || multiimage) { warningAppendage = "\nWarnings: "; } if (overtime && multiimage) { warningAppendage += multiImageStr + ", " + overTimeStr; } else if (overtime || multiimage) { warningAppendage += multiimage ? multiImageStr : overTimeStr; } string vulnsString = ScoreFormattingOptions.EvaluateNumericDisplay(ScoreRetrieverMetadata.FormattingOptions.VulnerabilityDisplay, item.VulnerabilitiesFound, item.VulnerabilitiesRemaining + item.VulnerabilitiesFound) ? $" ({item.VulnerabilitiesFound}/{item.VulnerabilitiesFound + item.VulnerabilitiesRemaining} vulns{penaltyAppendage})" : string.Empty; string playTimeStr = ScoreFormattingOptions.EvaluateNumericDisplay(ScoreRetrieverMetadata.FormattingOptions.TimeDisplay, item.PlayTime) ? $" in {item.PlayTime.ToHoursMinutesSecondsString()}" : string.Empty; builder.AddField('`' + item.ImageName + $": {ScoreRetrieverMetadata.FormattingOptions.FormatScore(item.Score)}pts`", $"{ScoreRetrieverMetadata.FormattingOptions.FormatScore(item.Score)} points{vulnsString}{playTimeStr}{warningAppendage}"); } string totalScoreTimeAppendage = string.Empty; if (ScoreFormattingOptions.EvaluateNumericDisplay(ScoreRetrieverMetadata.FormattingOptions.TimeDisplay, teamScore.Summary.PlayTime)) { totalScoreTimeAppendage = $" in {teamScore.Summary.PlayTime.ToHoursMinutesSecondsString()}"; } string totalPointsAppendage = string.Empty; if (teamScore.Images.All(i => i.PointsPossible != -1)) { totalPointsAppendage = "\n" + ScoreRetrieverMetadata.FormattingOptions.FormatScore(teamScore.Images.Sum(i => i.PointsPossible)) + " points possible"; } builder.AddInlineField("Total Score", $"{ScoreRetrieverMetadata.FormattingOptions.FormatScore(teamScore.Summary.TotalScore)} points" + totalScoreTimeAppendage + totalPointsAppendage); if (teamScore.Summary.Warnings != 0) { string warningsOverview = null; if ((teamScore.Summary.Warnings & ScoreWarnings.MultiImage) == ScoreWarnings.MultiImage) { warningsOverview = "Multiple Instances"; } if ((teamScore.Summary.Warnings & ScoreWarnings.TimeOver) == ScoreWarnings.TimeOver) { if (warningsOverview == null) { warningsOverview = ""; } else { warningsOverview += "\n"; } warningsOverview += "Time Limit Exceeded"; } builder.AddInlineField("Warnings", warningsOverview); } var timingFieldBuilder = new StringBuilder(); if (ScoreFormattingOptions.EvaluateNumericDisplay(ScoreRetrieverMetadata.FormattingOptions.TimeDisplay, teamScore.ScoreTime)) { if (timingFieldBuilder.Length > 0) { timingFieldBuilder.AppendLine(); } timingFieldBuilder.AppendFormat("Score achieved in {0}", teamScore.ScoreTime.ToHoursMinutesSecondsString()); } DateTimeOffset?maxImageTime = null; if (teamScore.ImageScoresOverTime != null) { foreach (var dto in teamScore.ImageScoresOverTime.Select(x => x.Value.Keys.Last())) { if (!maxImageTime.HasValue || dto > maxImageTime.Value) { maxImageTime = dto; } } } if (maxImageTime.HasValue) { if (timingFieldBuilder.Length > 0) { timingFieldBuilder.AppendLine(); } timingFieldBuilder.Append("Score last updated:"); timingFieldBuilder.AppendLine().Append("\u00A0\u00A0"); //NBSP x2 if (DateTimeOffset.UtcNow - maxImageTime.Value < TimeSpan.FromDays(1)) { timingFieldBuilder.Append((DateTimeOffset.UtcNow - maxImageTime.Value).ToLongString(showSeconds: false)).Append(" ago"); } else { DateTimeOffset timestamp = timeZone == null ? maxImageTime.Value : TimeZoneInfo.ConvertTime(maxImageTime.Value, timeZone); timingFieldBuilder.AppendFormat("{0:g} ", timestamp); timingFieldBuilder.Append(TimeZoneNames.TZNames.GetAbbreviationsForTimeZone(timeZone.Id, "en-US").Generic.Replace("UTC Time", "UTC")); } } if (timingFieldBuilder.Length > 0) { builder.AddInlineField("Timing", timingFieldBuilder.ToString()); } if (completeScoreboard != null) { var peerList = completeScoreboard.Clone().WithFilter(peerFilter).TeamList; int myIndexInPeerList = peerList.IndexOfWhere(x => x.TeamId == teamScore.TeamId); double rawPercentile = 1.0 - ((myIndexInPeerList + 1.0) / peerList.Count); int multipliedPercentile = (int)Math.Round(rawPercentile * 1000); int intPart = multipliedPercentile / 10; int floatPart = multipliedPercentile % 10; builder.AddInlineField("Rank", $"{Utilities.AppendOrdinalSuffix(myIndexInPeerList + 1)} place\n{(floatPart == 0 ? Utilities.AppendOrdinalSuffix(intPart) : $"{intPart}.{Utilities.AppendOrdinalSuffix(floatPart)}")} percentile"); StringBuilder marginBuilder = new StringBuilder(); if (myIndexInPeerList > 0) { double marginUnderFirst = peerList[0].TotalScore - teamScore.Summary.TotalScore; marginBuilder.AppendLine($"{ScoreRetrieverMetadata.FormattingOptions.FormatLabeledScoreDifference(marginUnderFirst)} under 1st place"); } if (myIndexInPeerList >= 2) { double marginUnderAbove = peerList[myIndexInPeerList - 1].TotalScore - teamScore.Summary.TotalScore; marginBuilder.AppendLine($"{ScoreRetrieverMetadata.FormattingOptions.FormatLabeledScoreDifference(marginUnderAbove)} under {Utilities.AppendOrdinalSuffix(myIndexInPeerList)} place"); } if (myIndexInPeerList < peerList.Count - 1) { double marginAboveUnder = teamScore.Summary.TotalScore - peerList[myIndexInPeerList + 1].TotalScore; marginBuilder.AppendLine($"{ScoreRetrieverMetadata.FormattingOptions.FormatLabeledScoreDifference(marginAboveUnder)} above {Utilities.AppendOrdinalSuffix(myIndexInPeerList + 2)} place"); } // TODO division- and round-specific margins builder.AddInlineField("Margin", marginBuilder.ToString()); StringBuilder standingFieldBuilder = new StringBuilder(); IList <ScoreboardSummaryEntry> subPeer = null; string subPeerLabel = null; if (!peerFilter.Category.HasValue && teamScore.Summary.Category.HasValue) { var myCategory = teamScore.Summary.Category.Value; subPeer = peerList.Where(x => x.Category == myCategory).ToIList(); subPeerLabel = " in category"; } else if (peerFilter.Location == null && teamScore.Summary.Location != null) { var myLocation = teamScore.Summary.Location; subPeer = peerList.Where(x => x.Location == myLocation).ToIList(); subPeerLabel = " in state"; } if (subPeerLabel != null) { standingFieldBuilder.AppendLine(Utilities.AppendOrdinalSuffix(subPeer.IndexOfWhere(x => x.TeamId == teamScore.TeamId) + 1) + " of " + Utilities.Pluralize("peer team", subPeer.Count) + subPeerLabel); } standingFieldBuilder.AppendLine(Utilities.AppendOrdinalSuffix(myIndexInPeerList + 1) + " of " + Utilities.Pluralize("peer team", peerList.Count)); // if peer teams != div+tier teams if ((peerFilter.Category.HasValue || peerFilter.Location != null) && peerFilter.Tier.HasValue) { // tier ranking, differing from peer ranking var tierTeams = completeScoreboard.Clone().WithFilter(new ScoreboardFilterInfo(peerFilter.Division, peerFilter.Tier)).TeamList; standingFieldBuilder.AppendLine(Utilities.AppendOrdinalSuffix(tierTeams.IndexOfWhere(x => x.TeamId == teamScore.TeamId) + 1) + " of " + Utilities.Pluralize("team", tierTeams.Count) + " in tier"); } if (peerFilter.Category.HasValue || peerFilter.Location != null || peerFilter.Tier.HasValue) { // division ranking, differing from peer ranking var divTeams = completeScoreboard.Clone().WithFilter(new ScoreboardFilterInfo(peerFilter.Division, null)).TeamList; standingFieldBuilder.AppendLine(Utilities.AppendOrdinalSuffix(divTeams.IndexOfWhere(x => x.TeamId == teamScore.TeamId) + 1) + " of " + Utilities.Pluralize("team", divTeams.Count) + " in division"); } builder.AddInlineField("Standing", standingFieldBuilder.ToString()); }