Exemple #1
0
        public void CalculateTraitsWithMods(BeatmapTraits nomodTraits, Mods mods, GameMode mode, BeatmapTraits expectedTraits)
        {
            // Act
            var traits = BeatmapTraitsLogic.CalculateTraitsWithMods(nomodTraits, mods, mode);

            // Assert
            traits.Should().BeEquivalentTo(expectedTraits,
                                           o => o.Using <double>(ctx => ctx.Subject.Should().BeApproximately(ctx.Expectation, 0.01)).WhenTypeIs <double>());
        }
Exemple #2
0
        public async Task <IReadOnlyList <Recommendation> > GetRecommendationsAsync(IProgress <double> progressHandler)
        {
            var progress = 0.0;

            progressHandler.Report(progress);

            // Get user's top plays
            var ownTopPlays = (await _dataService.GetUserTopPlaysAsync(UserId, GameMode))
                              .OrderByDescending(p => p.PerformancePoints) // sort by PP
                              .ToArray();

            // Get user's top maps
            var ownTopPlaysBeatmapIds = ownTopPlays.Select(p => p.BeatmapId).ToArray();

            // Truncate user's top plays
            ownTopPlays = ownTopPlays.Take(30).ToArray();

            // If user doesn't have any plays - throw
            if (!ownTopPlays.Any())
            {
                throw new RecommendationsUnavailableException($"User [{UserId}] doesn't have any top plays in [{GameMode}].");
            }

            // Set boundaries for recommendations based on PP
            var minPP = ownTopPlays.Average(p => p.PerformancePoints);
            var maxPP = minPP * 1.25;

            // Prepare buffer for plays which will serve as base for recommendations
            var candidatePlays = new List <Play>();

            // Go through user's top plays
            await ownTopPlays.ParallelForEachAsync(async ownTopPlay =>
            {
                // Get the map's top plays
                var mapTopPlays =
                    (await _dataService.GetBeatmapTopPlaysAsync(ownTopPlay.BeatmapId, GameMode, ownTopPlay.Mods))
                    .OrderBy(p => Math.Abs(p.PerformancePoints - ownTopPlay.PerformancePoints)) // order by PP similarity
                    .Take(20)                                                                   // only take top 20
                    .ToArray();

                // Progress
                progressHandler.Report(progress += 0.05 / ownTopPlays.Length);

                // Go through those top plays
                await mapTopPlays.ParallelForEachAsync(async mapTopPlay =>
                {
                    // Get top plays of that user
                    var otherUserTopPlays = (await _dataService.GetUserTopPlaysAsync(mapTopPlay.PlayerId, GameMode))
                                            .Where(p => p.Rank >= PlayRank.S)                                           // only S ranks
                                            .Where(p => p.PerformancePoints >= minPP)                                   // limit by minPP
                                            .Where(p => p.PerformancePoints <= maxPP)                                   // limit by maxPP
                                            .OrderBy(p => Math.Abs(p.PerformancePoints - ownTopPlay.PerformancePoints)) // order by PP similarity
                                            .Take(20);                                                                  // only take top 20

                    // Add these plays to candidates
                    candidatePlays.AddRange(otherUserTopPlays);

                    // Progress
                    progressHandler.Report(progress += 0.85 / mapTopPlays.Length / ownTopPlays.Length);
                });
            });

            // Group candidate plays by beatmap
            var candidatePlaysGroups = candidatePlays
                                       .GroupBy(p => p.BeatmapId)                          // group
                                       .Where(g => !ownTopPlaysBeatmapIds.Contains(g.Key)) // filter out maps that the user has top plays on
                                       .OrderByDescending(g => g.Count())                  // sort by number of times the map appears in candidate plays
                                       .Take(200)                                          // only take top 200
                                       .ToArray();

            // Assemble recommendations
            var result = new List <Recommendation>();
            await candidatePlaysGroups.ParallelForEachAsync(async group =>
            {
                var count = group.Count();

                // Get median play based on PP
                var play = group.OrderBy(p => p.PerformancePoints).ElementAt(count / 2);

                // Get beatmap data
                var beatmap = await _dataService.GetBeatmapAsync(play.BeatmapId, GameMode);

                // Calculate traits with mods
                var traitsWithMods = BeatmapTraitsLogic.CalculateTraitsWithMods(beatmap.Traits, play.Mods, GameMode);

                // Add recommendation to the list
                var recommendation = new Recommendation(beatmap, count, play.Mods, traitsWithMods, play.Accuracy, play.PerformancePoints);
                result.Add(recommendation);

                // Progress
                progressHandler.Report(progress += 0.1 / candidatePlaysGroups.Length);
            });

            // Return recommendations sorted by weight
            return(result.OrderByDescending(r => r.Weight).ToArray());
        }