diff --git a/PlayNext.UnitTests/Model/Score/GameScore/GameScoreBySeriesCalculatorTests.cs b/PlayNext.UnitTests/Model/Score/GameScore/GameScoreBySeriesCalculatorTests.cs index f46c38f..f19b587 100644 --- a/PlayNext.UnitTests/Model/Score/GameScore/GameScoreBySeriesCalculatorTests.cs +++ b/PlayNext.UnitTests/Model/Score/GameScore/GameScoreBySeriesCalculatorTests.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using AutoFixture.Xunit2; +using PlayNext.Model.Score.GameScore; using Playnite.SDK.Models; using Xunit; @@ -11,63 +12,214 @@ public class GameScoreBySeriesCalculatorTests { [Theory] [AutoData] - public void Calculate_NoGamesMatchSeriesWithScore_AllScoreIs0( + public void Calculate_GamesListIsEmpty_ScoreListIsEmpty( + CalculateSeriesScoreBy calculateSeriesScoreBy, + Dictionary attributeScore, + GameScoreBySeriesCalculator sut) + { + // Arrange + var games = Array.Empty(); + + // Act + var result = sut.Calculate(calculateSeriesScoreBy, games, attributeScore); + + // Arrange + Assert.Empty(result); + } + + [Theory] + [AutoData] + public void Calculate_AttributeScoreListIsEmpty_ScoreListIsEmpty( + CalculateSeriesScoreBy calculateSeriesScoreBy, + Game[] games, + GameScoreBySeriesCalculator sut) + { + // Arrange + var attributeScore = new Dictionary(); + + // Act + var result = sut.Calculate(calculateSeriesScoreBy, games, attributeScore); + + // Arrange + Assert.Empty(result); + } + + [Theory] + [AutoData] + public void Calculate_NoGamesMatchSeriesWithScore_ScoreListIsEmpty( + CalculateSeriesScoreBy calculateSeriesScoreBy, Dictionary attributeScore, Game[] games, GameScoreBySeriesCalculator sut) { // Act - var result = sut.Calculate(games, attributeScore); + var result = sut.Calculate(calculateSeriesScoreBy, games, attributeScore); // Arrange - Assert.All(result, x => Assert.Equal(0, x.Value)); + Assert.Empty(result); } [Theory] [AutoData] - public void Calculate_OneGameMatchesSeriesWithScore_ThatGamesScoreIs100( + public void Calculate_OneGameMatchesSeriesWithScore_ThatGameScoreIs100( + CalculateSeriesScoreBy calculateSeriesScoreBy, Dictionary attributeScore, Game[] games, GameScoreBySeriesCalculator sut) { // Arrange - games.Last().SeriesIds.Add(attributeScore.Last().Key); + var seriesAttributeScore = attributeScore.Last(); + games.Last().SeriesIds.Add(seriesAttributeScore.Key); // Act - var result = sut.Calculate(games, attributeScore); + var result = sut.Calculate(calculateSeriesScoreBy, games, attributeScore); // Arrange var actualGame = Assert.Single(result, x => x.Value != 0); Assert.Equal(100, actualGame.Value); } - } - public class GameScoreBySeriesCalculator - { - public Dictionary Calculate(IEnumerable games, Dictionary attributeScore) + [Theory] + [AutoData] + public void Calculate_TwoGamesMatchesSeriesWithScoreAndCalculatingByReleaseDate_OlderReleaseDateGameIs100NewerOneIs50Score( + Dictionary attributeScore, + Game[] games, + GameScoreBySeriesCalculator sut) + { + // Arrange + var seriesAttributeScore = attributeScore.Last(); + var olderGame = games.Last(); + var newerGame = games.First(); + olderGame.SeriesIds.Add(seriesAttributeScore.Key); + newerGame.SeriesIds.Add(seriesAttributeScore.Key); + olderGame.ReleaseDate = new ReleaseDate(1988); + newerGame.ReleaseDate = new ReleaseDate(2002, 05, 12); + + // Act + var result = sut.Calculate(CalculateSeriesScoreBy.ReleaseDate, games, attributeScore); + + // Arrange + var olderGameScore = result[olderGame.Id]; + var newerGameScore = result[newerGame.Id]; + Assert.Equal(100, olderGameScore); + Assert.Equal(50, newerGameScore); + } + + [Theory] + [AutoData] + public void Calculate_ThreeGamesMatchesSeriesWithScoreAndCalculatingByReleaseDate_OlderReleaseDateGameIs100NewerOneIs67NewestIs33Score( + Dictionary attributeScore, + Game[] games, + GameScoreBySeriesCalculator sut) { - var maxScore = 0f; - var gamesWithSeriesScores = games.ToDictionary(x => x.Id, x => x.SeriesIds.ToDictionary(s => s, s => - { - if (!attributeScore.TryGetValue(s, out var value)) - { - return 0; - } - - if (value > maxScore) - { - maxScore = value; - } - - return value; - })); - - if (maxScore == 0) - { - return new Dictionary(); - } - - return gamesWithSeriesScores.ToDictionary(x => x.Key, x => x.Value.Values.Sum() * 100 / maxScore); + // Arrange + var seriesAttributeScore = attributeScore.Last(); + var olderGame = games.Last(); + var newerGame = games.First(); + var newestGame = games[1]; + olderGame.SeriesIds.Add(seriesAttributeScore.Key); + newerGame.SeriesIds.Add(seriesAttributeScore.Key); + newestGame.SeriesIds.Add(seriesAttributeScore.Key); + olderGame.ReleaseDate = new ReleaseDate(1988); + newerGame.ReleaseDate = new ReleaseDate(2002, 05, 12); + newestGame.ReleaseDate = new ReleaseDate(2002, 05, 13); + + // Act + var result = sut.Calculate(CalculateSeriesScoreBy.ReleaseDate, games, attributeScore); + + // Arrange + var olderGameScore = result[olderGame.Id]; + var newerGameScore = result[newerGame.Id]; + var newestGameScore = result[newestGame.Id]; + Assert.Equal(100, olderGameScore); + Assert.Equal(200f / 3, newerGameScore, 0.5); + Assert.Equal(100f / 3, newestGameScore, 0.5); + } + + [Theory] + [AutoData] + public void Calculate_TwoGamesMatchesSeriesWithScoreAndCalculatingBySortingName_FirstGameIs100SecondIs50Score( + Dictionary attributeScore, + Game[] games, + GameScoreBySeriesCalculator sut) + { + // Arrange + var seriesAttributeScore = attributeScore.Last(); + var olderGame = games.Last(); + var newerGame = games.First(); + olderGame.SeriesIds.Add(seriesAttributeScore.Key); + newerGame.SeriesIds.Add(seriesAttributeScore.Key); + olderGame.SortingName = "ABC1"; + newerGame.SortingName = "ABC2"; + + // Act + var result = sut.Calculate(CalculateSeriesScoreBy.SortingName, games, attributeScore); + + // Arrange + var olderGameScore = result[olderGame.Id]; + var newerGameScore = result[newerGame.Id]; + Assert.Equal(100, olderGameScore); + Assert.Equal(50, newerGameScore); + } + + [Theory] + [AutoData] + public void Calculate_ThreeGamesMatchesSeriesWithScoreAndCalculatingBySortingName_FirstIs100SecondIs67ThirdIs33Score( + Dictionary attributeScore, + Game[] games, + GameScoreBySeriesCalculator sut) + { + // Arrange + var seriesAttributeScore = attributeScore.Last(); + var olderGame = games.Last(); + var newerGame = games.First(); + var newestGame = games[1]; + olderGame.SeriesIds.Add(seriesAttributeScore.Key); + newerGame.SeriesIds.Add(seriesAttributeScore.Key); + newestGame.SeriesIds.Add(seriesAttributeScore.Key); + olderGame.SortingName = "ABC1"; + newerGame.SortingName = "ABC2"; + newestGame.SortingName = "ABC3"; + + // Act + var result = sut.Calculate(CalculateSeriesScoreBy.SortingName, games, attributeScore); + + // Arrange + var olderGameScore = result[olderGame.Id]; + var newerGameScore = result[newerGame.Id]; + var newestGameScore = result[newestGame.Id]; + Assert.Equal(100, olderGameScore); + Assert.Equal(200f / 3, newerGameScore, 0.5); + Assert.Equal(100f / 3, newestGameScore, 0.5); + } + + [Theory] + [AutoData] + public void Calculate_OneGameWithOneSeriesOneGameWithTwoSeriesWithScoreAndScoreIsAllEqual_TwoSeriesGameIs100( + CalculateSeriesScoreBy calculateSeriesScoreBy, + Dictionary attributeScore, + float score, + Game[] games, + GameScoreBySeriesCalculator sut) + { + // Arrange + attributeScore = attributeScore.ToDictionary(x => x.Key, x => score); + var firstSeries = attributeScore.Last(); + var secondSeries = attributeScore.First(); + var gameWithOneSeries = games.Last(); + var gameWithTwoSeries = games.First(); + gameWithOneSeries.SeriesIds.Add(firstSeries.Key); + gameWithTwoSeries.SeriesIds.Add(firstSeries.Key); + gameWithTwoSeries.SeriesIds.Add(secondSeries.Key); + + // Act + var result = sut.Calculate(calculateSeriesScoreBy, games, attributeScore); + + // Arrange + var gameWithOneSeriesScore = result[gameWithOneSeries.Id]; + var gameWithTwoSeriesScore = result[gameWithTwoSeries.Id]; + Assert.Equal(100, gameWithTwoSeriesScore); + Assert.NotEqual(100, gameWithOneSeriesScore); } } } \ No newline at end of file diff --git a/PlayNext/Model/Score/GameScore/CalculateSeriesScoreBy.cs b/PlayNext/Model/Score/GameScore/CalculateSeriesScoreBy.cs new file mode 100644 index 0000000..0b387f9 --- /dev/null +++ b/PlayNext/Model/Score/GameScore/CalculateSeriesScoreBy.cs @@ -0,0 +1,8 @@ +namespace PlayNext.Model.Score.GameScore +{ + public enum CalculateSeriesScoreBy + { + ReleaseDate, + SortingName, + } +} \ No newline at end of file diff --git a/PlayNext/Model/Score/GameScore/GameScoreBySeriesCalculator.cs b/PlayNext/Model/Score/GameScore/GameScoreBySeriesCalculator.cs new file mode 100644 index 0000000..82e44e2 --- /dev/null +++ b/PlayNext/Model/Score/GameScore/GameScoreBySeriesCalculator.cs @@ -0,0 +1,115 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Playnite.SDK.Models; + +namespace PlayNext.Model.Score.GameScore +{ + public class GameScoreBySeriesCalculator + { + public Dictionary Calculate( + CalculateSeriesScoreBy calculateSeriesScoreBy, + IEnumerable games, + Dictionary attributeScore) + { + if (!games.Any()) + { + return new Dictionary(); + } + + var gamesWithSeriesScores = GetGamesWithSeriesScores(games, attributeScore); + var seriesWithGames = GetSeriesWithGames(games, gamesWithSeriesScores); + var gamesWithScores = GetGamesWithSummedUpScores(calculateSeriesScoreBy, gamesWithSeriesScores, seriesWithGames); + + var maxScore = gamesWithScores.Max(x => x.Value); + if (maxScore == 0) + { + return new Dictionary(); + } + + return CalculateGamesWithNormalizedScores(gamesWithScores, maxScore); + } + + private static Dictionary> GetGamesWithSeriesScores(IEnumerable games, Dictionary attributeScore) + { + return games.ToDictionary(x => x.Id, x => x.SeriesIds.ToDictionary(s => s, s => + { + if (!attributeScore.TryGetValue(s, out var value)) + { + return 0; + } + + return value; + })); + } + + private static Dictionary> GetSeriesWithGames(IEnumerable games, Dictionary> gamesWithSeriesScores) + { + return gamesWithSeriesScores + .SelectMany(x => x.Value.Keys.Select(s => new { SeriesId = s, Game = games.First(g => g.Id == x.Key) })) + .GroupBy(x => x.SeriesId) + .ToDictionary(x => x.Key, x => x.Select(g => g.Game)); + } + + private static Dictionary GetGamesWithSummedUpScores(CalculateSeriesScoreBy calculateSeriesScoreBy, Dictionary> gamesWithSeriesScores, Dictionary> seriesWithGames) + { + return gamesWithSeriesScores.ToDictionary(x => x.Key, x => + { + var score = 0f; + foreach (var seriesScore in x.Value) + { + var scoreMultiplier = GetScoreMultiplierByPositionInSeries(x.Key, calculateSeriesScoreBy, seriesWithGames, seriesScore); + score += seriesScore.Value * scoreMultiplier; + } + + return score; + }); + } + + private static Dictionary CalculateGamesWithNormalizedScores(Dictionary gamesWithScores, float maxScore) + { + return gamesWithScores.ToDictionary(x => x.Key, x => x.Value * 100 / maxScore); + } + + private static float GetScoreMultiplierByPositionInSeries( + Guid gameId, + CalculateSeriesScoreBy calculateSeriesScoreBy, + Dictionary> seriesWithGames, + KeyValuePair seriesScore) + { + var seriesGames = seriesWithGames[seriesScore.Key].ToList(); + seriesGames.Sort((a, b) => + { + if (calculateSeriesScoreBy == CalculateSeriesScoreBy.ReleaseDate) + { + if (a.ReleaseDate != null && b.ReleaseDate != null) + { + return a.ReleaseDate.Value.Date.CompareTo(b.ReleaseDate.Value.Date); + } + + if (a.ReleaseDate != null) + { + return -1; + } + + if (b.ReleaseDate != null) + { + return 1; + } + } + + if (calculateSeriesScoreBy == CalculateSeriesScoreBy.SortingName) + { + return string.Compare(a.SortingName, b.SortingName, StringComparison.CurrentCulture); + } + + throw new NotSupportedException("Not supported series calculation method"); + }); + + var seriesGameIds = seriesGames.Select(g => g.Id).ToList(); + var gameCountInSeries = seriesGameIds.Count; + var scoreMultiplier = ((float)gameCountInSeries - seriesGameIds.IndexOf(gameId)) / gameCountInSeries; + return scoreMultiplier; + } + } +} \ No newline at end of file diff --git a/PlayNext/PlayNext.csproj b/PlayNext/PlayNext.csproj index f349657..f5ec503 100644 --- a/PlayNext/PlayNext.csproj +++ b/PlayNext/PlayNext.csproj @@ -71,7 +71,9 @@ + + diff --git a/ci/Changelog.txt b/ci/Changelog.txt index a9321c8..a76f249 100644 --- a/ci/Changelog.txt +++ b/ci/Changelog.txt @@ -3,6 +3,8 @@ v1.5.0------------------------- ----------- Series ----------- Extract StartPage Settings ----------- Improve game length algorithm (deviation by 0.5 from max length is the limit) +----------- Allow switching off sidebar item +----------- Fix init calculations v1.4.0 - Added minimum cover count for use with start page alignments