예제 #1
0
        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);
            }
        }
예제 #2
0
        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();
            }
        }
예제 #3
0
        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);
                }
            }
        }
예제 #6
0
        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());
        }
예제 #8
0
        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());
        }
예제 #9
0
        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());
        }
예제 #12
0
        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);
            }
        }
예제 #13
0
        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);
        }
예제 #14
0
        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());
        }
예제 #15
0
        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
            });
        }
예제 #16
0
 public abstract IList <ScoreboardSummaryEntry> GetPeerTeams(CompetitionRound round, CompleteScoreboardSummary divisionScoreboard, ScoreboardSummaryEntry teamInfo);
예제 #17
0
        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);
            }
        }
예제 #18
0
        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();
            }
        }
예제 #19
0
        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();
        }
예제 #20
0
        // [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);
                }
            }
        }
예제 #21
0
        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());
        }
예제 #22
0
        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());
            }