From c30aac7d5c35d96fbced0536c7f958dda23fd6a9 Mon Sep 17 00:00:00 2001 From: Tillerino Date: Mon, 27 Mar 2023 19:39:11 +0200 Subject: [PATCH] idea: tune down recommendations difficulty based on lower top plays? --- .../RecommendationRequest.java | 28 +++++++++++++-- .../RecommendationRequestParser.java | 24 ++++++++++++- .../RecommendationsManager.java | 12 +++++++ .../tillerino/tillerinobot/TestBackend.java | 1 + .../RecommendationRequestParserTest.java | 19 ++++++++++ .../RecommendationsManagerTest.java | 35 ++++++++++++++++--- 6 files changed, 112 insertions(+), 7 deletions(-) diff --git a/tillerinobot/src/main/java/tillerino/tillerinobot/recommendations/RecommendationRequest.java b/tillerinobot/src/main/java/tillerino/tillerinobot/recommendations/RecommendationRequest.java index 67a6951d..7dd68299 100644 --- a/tillerinobot/src/main/java/tillerino/tillerinobot/recommendations/RecommendationRequest.java +++ b/tillerinobot/src/main/java/tillerino/tillerinobot/recommendations/RecommendationRequest.java @@ -6,8 +6,8 @@ import org.tillerino.osuApiModel.types.BitwiseMods; -import lombok.Getter; import lombok.Builder; +import lombok.Getter; import tillerino.tillerinobot.predicates.RecommendationPredicate; @Builder @@ -15,7 +15,8 @@ public record RecommendationRequest( boolean nomod, Model model, @BitwiseMods long requestedMods, - List predicates + List predicates, + Shift difficultyShift ) { public RecommendationRequest { predicates = new ArrayList<>(predicates); @@ -32,6 +33,8 @@ public static class RecommendationRequestBuilder { private List predicates = new ArrayList<>(); + private Shift difficultyShift = Shift.NONE; + public RecommendationRequestBuilder requestedMods(@BitwiseMods long requestedMods) { this.requestedMods = requestedMods; return this; @@ -51,4 +54,25 @@ public List getPredicates() { } } + /** + * Modifies the difficulty of recommendations. + */ + static enum Shift { + /** + * Regular strength. + */ + NONE, + /** + * The player is weak compared to their top scores. Recommendations are easier. + */ + SUCC, + /** + * Even weaker than {@link #SUCC} + */ + SUCCER, + /** + * Even weaker than {@link #SUCCERBERG} + */ + SUCCERBERG + } } diff --git a/tillerinobot/src/main/java/tillerino/tillerinobot/recommendations/RecommendationRequestParser.java b/tillerinobot/src/main/java/tillerino/tillerinobot/recommendations/RecommendationRequestParser.java index 133cd200..3035945d 100644 --- a/tillerinobot/src/main/java/tillerino/tillerinobot/recommendations/RecommendationRequestParser.java +++ b/tillerinobot/src/main/java/tillerino/tillerinobot/recommendations/RecommendationRequestParser.java @@ -19,6 +19,7 @@ import tillerino.tillerinobot.predicates.PredicateParser; import tillerino.tillerinobot.predicates.RecommendationPredicate; import tillerino.tillerinobot.recommendations.RecommendationRequest.RecommendationRequestBuilder; +import tillerino.tillerinobot.recommendations.RecommendationRequest.Shift; @RequiredArgsConstructor(onConstructor = @__(@Inject)) public class RecommendationRequestParser { @@ -49,7 +50,8 @@ public RecommendationRequest parseSamplerSettings(OsuApiUser apiUser, @Nonnull S continue; if (!parseEngines(param, settingsBuilder, apiUser) && !parseMods(param, settingsBuilder) - && !parsePredicates(param, settingsBuilder, apiUser, lang)) { + && !parsePredicates(param, settingsBuilder, apiUser, lang) + && !parseOther(param, settingsBuilder, apiUser)) { throw new UserException(lang.invalidChoice(param, STANDARD_SYNTAX)); } } @@ -157,4 +159,24 @@ private boolean parsePredicates(String param, RecommendationRequestBuilder setti } return false; } + + private boolean parseOther(String param, RecommendationRequestBuilder settingsBuilder, OsuApiUser apiUser) + throws SQLException, IOException { + String lowerCase = param.toLowerCase(); + if (backend.getDonator(apiUser.getUserId()) > 0) { + switch (lowerCase) { + case "succ": + settingsBuilder.difficultyShift(Shift.SUCC); + return true; + case "succer": + settingsBuilder.difficultyShift(Shift.SUCCER); + return true; + } + if (getLevenshteinDistance(lowerCase, "succerberg") <= 2) { + settingsBuilder.difficultyShift(Shift.SUCCERBERG); + return true; + } + } + return false; + } } diff --git a/tillerinobot/src/main/java/tillerino/tillerinobot/recommendations/RecommendationsManager.java b/tillerinobot/src/main/java/tillerino/tillerinobot/recommendations/RecommendationsManager.java index 0d8761b1..5ffb7ece 100644 --- a/tillerinobot/src/main/java/tillerino/tillerinobot/recommendations/RecommendationsManager.java +++ b/tillerinobot/src/main/java/tillerino/tillerinobot/recommendations/RecommendationsManager.java @@ -1,5 +1,6 @@ package tillerino.tillerinobot.recommendations; +import static java.util.stream.Collectors.toList; import static java.util.stream.Collectors.toSet; import static org.tillerino.osuApiModel.Mods.Nightcore; import static org.tillerino.osuApiModel.Mods.getEffectiveMods; @@ -47,6 +48,7 @@ import tillerino.tillerinobot.data.util.ThreadLocalAutoCommittingEntityManager; import tillerino.tillerinobot.lang.Language; import tillerino.tillerinobot.predicates.RecommendationPredicate; +import tillerino.tillerinobot.recommendations.RecommendationRequest.Shift; /** * Communicates with the backend and creates recommendations samplers as well as caching information. @@ -188,6 +190,16 @@ private Sampler loadSampler(@UserId i exclude.add(play.getBeatmapid()); } + if (settings.difficultyShift() != Shift.NONE) { + int limit = switch (settings.difficultyShift()) { + case SUCC -> topPlays.size() / 2; + case SUCCER -> topPlays.size() / 4; + case SUCCERBERG -> 5; + default -> throw new IllegalStateException(); + }; + topPlays = topPlays.stream().sorted(Comparator.comparingDouble(TopPlay::getPp)).limit(limit).collect(toList()); + } + Collection recommendations = recommender.loadRecommendations(topPlays, exclude, settings.model(), settings.nomod(), settings.requestedMods()); diff --git a/tillerinobot/src/test/java/tillerino/tillerinobot/TestBackend.java b/tillerinobot/src/test/java/tillerino/tillerinobot/TestBackend.java index ddaa2abd..cc0143d8 100644 --- a/tillerinobot/src/test/java/tillerino/tillerinobot/TestBackend.java +++ b/tillerinobot/src/test/java/tillerino/tillerinobot/TestBackend.java @@ -351,6 +351,7 @@ public Collection loadRecommendations(List topPlays } double equivalentPp(List plays) { + plays = new ArrayList<>(plays); Collections.sort(plays, Comparator.comparingDouble(TopPlay::getPp).reversed()); double ppSum = 0; double partialSum = 0; diff --git a/tillerinobot/src/test/java/tillerino/tillerinobot/recommendations/RecommendationRequestParserTest.java b/tillerinobot/src/test/java/tillerino/tillerinobot/recommendations/RecommendationRequestParserTest.java index 4da8f96e..d34f078c 100644 --- a/tillerinobot/src/test/java/tillerino/tillerinobot/recommendations/RecommendationRequestParserTest.java +++ b/tillerinobot/src/test/java/tillerino/tillerinobot/recommendations/RecommendationRequestParserTest.java @@ -3,6 +3,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.when; import org.junit.Test; @@ -20,6 +21,7 @@ import tillerino.tillerinobot.predicates.MapLength; import tillerino.tillerinobot.predicates.NumericPropertyPredicate; import tillerino.tillerinobot.predicates.StarDiff; +import tillerino.tillerinobot.recommendations.RecommendationRequest.Shift; @RunWith(MockitoJUnitRunner.class) public class RecommendationRequestParserTest { @@ -35,6 +37,12 @@ private RecommendationRequest parse(String settings) throws Exception { return recommendationRequestParser.parseSamplerSettings(user, settings, new Default()); } + @Test + public void defaultSettings() throws Exception { + RecommendationRequest request = parse(""); + assertThat(request).returns(Shift.NONE, RecommendationRequest::difficultyShift); + } + @Test public void testModContradiction() throws Exception { when(backend.getDonator(anyInt())).thenReturn(1); @@ -82,4 +90,15 @@ public void parseNap() throws Exception { .hasFieldOrPropertyWithValue("model", Model.NAP) .hasFieldOrPropertyWithValue("requestedMods", 64L); } + + @Test + public void testSucc() throws Exception { + assertThatThrownBy(() -> parse("succ")).isInstanceOf(UserException.class); + + doReturn(1).when(backend).getDonator(1); + assertThat(parse("succ")).returns(Shift.SUCC, RecommendationRequest::difficultyShift); + assertThat(parse("succer")).returns(Shift.SUCCER, RecommendationRequest::difficultyShift); + // typo; long word + assertThat(parse("sucerberg")).returns(Shift.SUCCERBERG, RecommendationRequest::difficultyShift); + } } diff --git a/tillerinobot/src/test/java/tillerino/tillerinobot/recommendations/RecommendationsManagerTest.java b/tillerinobot/src/test/java/tillerino/tillerinobot/recommendations/RecommendationsManagerTest.java index a34c42cb..60801ec1 100644 --- a/tillerinobot/src/test/java/tillerino/tillerinobot/recommendations/RecommendationsManagerTest.java +++ b/tillerinobot/src/test/java/tillerino/tillerinobot/recommendations/RecommendationsManagerTest.java @@ -1,18 +1,18 @@ package tillerino.tillerinobot.recommendations; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; +import static org.junit.Assert.*; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.verify; import java.io.IOException; import java.sql.SQLException; -import java.util.Collections; +import java.util.ArrayList; +import java.util.Comparator; import java.util.List; import javax.inject.Inject; @@ -153,4 +153,31 @@ public void gamma5NotRestricted() throws Exception { user = backend.downloadUser("guy"); assertThat(manager.getRecommendation(user, "gamma5", new Default())).isNotNull(); } + + @Test + public void succ() throws Exception { + runShift("succ", 25); + } + + @Test + public void succer() throws Exception { + runShift("succer", 12); + } + + @Test + public void succerberg() throws Exception { + runShift("succerberg", 5); + } + + private void runShift(String mode, int limit) throws IOException, SQLException, UserException, InterruptedException { + backend.hintUser("guy", true, 123, 1000); + user = backend.downloadUser("guy"); + + List topPlays = new ArrayList<>(recommender.loadTopPlays(user.getUserId())); + assertThat(topPlays).hasSize(50); + topPlays.sort(Comparator.comparingDouble(TopPlay::getPp)); + + assertThat(manager.getRecommendation(user, mode, new Default())).isNotNull(); + verify(recommender).loadRecommendations(argThat(l -> l.equals(topPlays.subList(0, limit))), any(), eq(Model.GAMMA8), anyBoolean(), anyLong()); + } }