Skip to content
This repository has been archived by the owner on Jul 16, 2023. It is now read-only.

Commit

Permalink
Add progress reporting
Browse files Browse the repository at this point in the history
  • Loading branch information
Tyrrrz committed Dec 30, 2017
1 parent 8a8fb5b commit 45d2314
Show file tree
Hide file tree
Showing 5 changed files with 54 additions and 17 deletions.
5 changes: 3 additions & 2 deletions OsuHelper/Services/IRecommendationService.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using OsuHelper.Models;

namespace OsuHelper.Services
{
public interface IRecommendationService
{
Task<IReadOnlyList<Recommendation>> GetRecommendationsAsync();
Task<IReadOnlyList<Recommendation>> GetRecommendationsAsync(IProgress<double> progress);
}
}
47 changes: 35 additions & 12 deletions OsuHelper/Services/RecommendationService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,32 +25,46 @@ public RecommendationService(ISettingsService settingsService, IDataService data
_beatmapProcessorService = beatmapProcessorService;
}

public async Task<IReadOnlyList<Recommendation>> GetRecommendationsAsync()
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 hasn't set any scores in given game mode.");

// Set boundaries for recommendations based on PP
var minPP = ownTopPlays.Take(30).Average(p => p.PerformancePoints);
var maxPP = minPP*1.25;
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 30 plays
await ownTopPlays.Take(30).ParallelForEachAsync(async ownTopPlay =>
// 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
.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 =>
Expand All @@ -60,20 +74,25 @@ await mapTopPlays.ParallelForEachAsync(async mapTopPlay =>
.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
.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 => !ownTopPlays.Select(p => p.BeatmapId).Contains(g.Key)) // filter out maps that the user has top plays on
.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
.Take(200) // only take top 200
.ToArray();

// Assemble recommendations
var result = new List<Recommendation>();
Expand All @@ -82,7 +101,7 @@ 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);
var play = group.OrderBy(p => p.PerformancePoints).ElementAt(count / 2);

// Get beatmap data
var beatmap = await _dataService.GetBeatmapAsync(play.BeatmapId, GameMode);
Expand All @@ -91,8 +110,12 @@ await candidatePlaysGroups.ParallelForEachAsync(async group =>
var traitsWithMods = _beatmapProcessorService.CalculateTraitsWithMods(beatmap, play.Mods);

// Add recommendation to the list
var recommendation = new Recommendation(beatmap, count, play.Mods, traitsWithMods, play.Accuracy, play.PerformancePoints);
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
Expand Down
2 changes: 2 additions & 0 deletions OsuHelper/ViewModels/IMainViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ public interface IMainViewModel
bool IsBusy { get; }
bool HasData { get; }

double Progress { get; }

IReadOnlyList<Recommendation> Recommendations { get; }
Recommendation SelectedRecommendation { get; }

Expand Down
15 changes: 13 additions & 2 deletions OsuHelper/ViewModels/MainViewModel.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Net;
using GalaSoft.MvvmLight;
Expand All @@ -18,6 +19,7 @@ public class MainViewModel : ViewModelBase, IMainViewModel
private readonly IRecommendationService _recommendationService;

private bool _isBusy;
private double _progress;
private IReadOnlyList<Recommendation> _recommendations;
private Recommendation _selectedRecommendation;

Expand All @@ -33,6 +35,12 @@ private set

public bool HasData => Recommendations.NotNullAndAny();

public double Progress
{
get => _progress;
private set => Set(ref _progress, value);
}

public IReadOnlyList<Recommendation> Recommendations
{
get => _recommendations;
Expand Down Expand Up @@ -101,10 +109,12 @@ private async void PopulateRecommendations()
}

IsBusy = true;
Progress = 0;

try
{
Recommendations = await _recommendationService.GetRecommendationsAsync();
var progressHandler = new Progress<double>(p => Progress = p);
Recommendations = await _recommendationService.GetRecommendationsAsync(progressHandler);
_cacheService.Store("LastRecommendations", Recommendations);
}
catch (RecommendationsUnavailableException ex)
Expand All @@ -118,6 +128,7 @@ private async void PopulateRecommendations()
}

IsBusy = false;
Progress = 0;
}
}
}
2 changes: 1 addition & 1 deletion OsuHelper/Views/MainWindow.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@
<ProgressBar
Grid.Row="1"
Background="{DynamicResource PrimaryHueMidBrush}"
IsIndeterminate="{Binding IsBusy}" />
Value="{Binding Progress, Mode=OneWay}" />

<Grid Grid.Row="2">
<!-- Recommendations -->
Expand Down

0 comments on commit 45d2314

Please sign in to comment.