From de572779e431f8e70e4b8ddf5c3f52d988218e25 Mon Sep 17 00:00:00 2001 From: Liam Date: Thu, 25 Apr 2024 06:34:49 +0100 Subject: [PATCH] impl +1 raw per global pp --- .../fascinated/backend/common/MathUtils.java | 28 +++ .../backend/leaderboard/Leaderboard.java | 17 ++ .../leaderboard/LeaderboardCurvePoint.java | 11 ++ .../impl/ScoreSaberLeaderboard.java | 164 ++++++++++++++++++ .../backend/model/account/Account.java | 11 ++ .../model/leaderboard/Leaderboard.java | 2 + .../fascinated/backend/model/score/Score.java | 9 +- .../backend/repository/ScoreRepository.java | 9 + .../backend/service/AccountService.java | 29 +++- 9 files changed, 270 insertions(+), 10 deletions(-) create mode 100644 src/main/java/cc/fascinated/backend/common/MathUtils.java create mode 100644 src/main/java/cc/fascinated/backend/leaderboard/Leaderboard.java create mode 100644 src/main/java/cc/fascinated/backend/leaderboard/LeaderboardCurvePoint.java create mode 100644 src/main/java/cc/fascinated/backend/leaderboard/impl/ScoreSaberLeaderboard.java diff --git a/src/main/java/cc/fascinated/backend/common/MathUtils.java b/src/main/java/cc/fascinated/backend/common/MathUtils.java new file mode 100644 index 0000000..3285700 --- /dev/null +++ b/src/main/java/cc/fascinated/backend/common/MathUtils.java @@ -0,0 +1,28 @@ +package cc.fascinated.backend.common; + +public class MathUtils { + + /** + * Clamps a value between a minimum and maximum. + * + * @param value The value to clamp. + * @param min The minimum value. + * @param max The maximum value. + * @return The clamped value. + */ + public static double clamp(double value, double min, double max) { + return Math.max(min, Math.min(max, value)); + } + + /** + * Linearly interpolates between two values. + * + * @param a The first value. + * @param b The second value. + * @param t The interpolation value. + * @return The interpolated value. + */ + public static double lerp(double a, double b, double t) { + return a + t * (b - a); + } +} diff --git a/src/main/java/cc/fascinated/backend/leaderboard/Leaderboard.java b/src/main/java/cc/fascinated/backend/leaderboard/Leaderboard.java new file mode 100644 index 0000000..bbb7fea --- /dev/null +++ b/src/main/java/cc/fascinated/backend/leaderboard/Leaderboard.java @@ -0,0 +1,17 @@ +package cc.fascinated.backend.leaderboard; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor @Getter +public class Leaderboard { + /** + * The name of the leaderboard. + */ + private final String name; + + /** + * The curve of the leaderboard. + */ + private final LeaderboardCurvePoint[] curve; +} diff --git a/src/main/java/cc/fascinated/backend/leaderboard/LeaderboardCurvePoint.java b/src/main/java/cc/fascinated/backend/leaderboard/LeaderboardCurvePoint.java new file mode 100644 index 0000000..dcbe1d0 --- /dev/null +++ b/src/main/java/cc/fascinated/backend/leaderboard/LeaderboardCurvePoint.java @@ -0,0 +1,11 @@ +package cc.fascinated.backend.leaderboard; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter @AllArgsConstructor +public class LeaderboardCurvePoint { + + private final double a; + private final double b; +} diff --git a/src/main/java/cc/fascinated/backend/leaderboard/impl/ScoreSaberLeaderboard.java b/src/main/java/cc/fascinated/backend/leaderboard/impl/ScoreSaberLeaderboard.java new file mode 100644 index 0000000..18b6f84 --- /dev/null +++ b/src/main/java/cc/fascinated/backend/leaderboard/impl/ScoreSaberLeaderboard.java @@ -0,0 +1,164 @@ +package cc.fascinated.backend.leaderboard.impl; + +import cc.fascinated.backend.common.MathUtils; +import cc.fascinated.backend.leaderboard.Leaderboard; +import cc.fascinated.backend.leaderboard.LeaderboardCurvePoint; +import cc.fascinated.backend.model.score.Score; +import lombok.extern.log4j.Log4j2; + +import java.util.ArrayList; +import java.util.List; + +@Log4j2(topic = "ScoreSaber Leaderboard") +public class ScoreSaberLeaderboard extends Leaderboard { + public static final ScoreSaberLeaderboard INSTANCE = new ScoreSaberLeaderboard(); + + /** + * The base multiplier for stars. + */ + private final double starMultiplier = 42.11; + + /** + * no idea, ngl + */ + private final double weightCoefficient = 0.965; + + public ScoreSaberLeaderboard() { + super("ScoreSaber", new LeaderboardCurvePoint[] { + new LeaderboardCurvePoint(1.0, 5.367394282890631), + new LeaderboardCurvePoint(0.9995, 5.019543595874787), + new LeaderboardCurvePoint(0.999, 4.715470646416203), + new LeaderboardCurvePoint(0.99825, 4.325027383589547), + new LeaderboardCurvePoint(0.9975, 3.996793606763322), + new LeaderboardCurvePoint(0.99625, 3.5526145337555373), + new LeaderboardCurvePoint(0.995, 3.2022017597337955), + new LeaderboardCurvePoint(0.99375, 2.9190155639254955), + new LeaderboardCurvePoint(0.9925, 2.685667856592722), + new LeaderboardCurvePoint(0.99125, 2.4902905794106913), + new LeaderboardCurvePoint(0.99, 2.324506282149922), + new LeaderboardCurvePoint(0.9875, 2.058947159052738), + new LeaderboardCurvePoint(0.985, 1.8563887693647105), + new LeaderboardCurvePoint(0.9825, 1.697536248647543), + new LeaderboardCurvePoint(0.98, 1.5702410055532239), + new LeaderboardCurvePoint(0.9775, 1.4664726399289512), + new LeaderboardCurvePoint(0.975, 1.3807102743105126), + new LeaderboardCurvePoint(0.9725, 1.3090333065057616), + new LeaderboardCurvePoint(0.97, 1.2485807759957321), + new LeaderboardCurvePoint(0.965, 1.1552120359501035), + new LeaderboardCurvePoint(0.96, 1.0871883573850478), + new LeaderboardCurvePoint(0.955, 1.0388633331418984), + new LeaderboardCurvePoint(0.95, 1.0), + new LeaderboardCurvePoint(0.94, 0.9417362980580238), + new LeaderboardCurvePoint(0.93, 0.9039994071865736), + new LeaderboardCurvePoint(0.92, 0.8728710341448851), + new LeaderboardCurvePoint(0.91, 0.8488375988124467), + new LeaderboardCurvePoint(0.9, 0.825756123560842), + new LeaderboardCurvePoint(0.875, 0.7816934560296046), + new LeaderboardCurvePoint(0.85, 0.7462290664143185), + new LeaderboardCurvePoint(0.825, 0.7150465663454271), + new LeaderboardCurvePoint(0.8, 0.6872268862950283), + new LeaderboardCurvePoint(0.75, 0.6451808210101443), + new LeaderboardCurvePoint(0.7, 0.6125565959114954), + new LeaderboardCurvePoint(0.65, 0.5866010012767576), + new LeaderboardCurvePoint(0.6, 0.18223233667439062), + new LeaderboardCurvePoint(0.0, 0.0) + }); + } + + /** + * Gets the modifier for the given accuracy. + * + * @param accuracy The accuracy. + * @return The modifier. + */ + public double getModifier(double accuracy) { + accuracy = MathUtils.clamp(accuracy, 0, 100) / 100; + + LeaderboardCurvePoint prev = this.getCurve()[1]; + for (LeaderboardCurvePoint point : this.getCurve()) { + if (point.getA() <= accuracy) { + double distance = (prev.getA() - accuracy) / (prev.getA() - point.getA()); + return MathUtils.lerp(prev.getB(), point.getB(), distance); + } + prev = point; + } + return 0; + } + + /** + * Gets the pp for the given accuracy and stars. + * + * @param accuracy The accuracy. + * @param stars The stars. + * @return The pp. + */ + public double getPP(double accuracy, double stars) { + double pp = stars * this.starMultiplier; + double modifier = this.getModifier(accuracy); + return modifier * pp; + } + + /** + * Gets the total pp for the given scores. + * + * @param scores The scores. + * @return The total pp. + */ + private double getTotalPP(List scores, int startIdx) { + double totalPP = 0; + for (int i = 0; i < scores.size(); i++) { + totalPP += Math.pow(this.weightCoefficient, i + startIdx) * scores.get(i).getPp(); + } + return totalPP; + } + + /** + * Gets the pp at the given index for the given scores. + * + * @param bottomScores The scores. + * @param idx The index. + * @return The pp. + */ + private double getRawPPAtIdx(List bottomScores, int idx, double expected) { + double oldBottomPP = this.getTotalPP(bottomScores, idx); + double newBottomPP = this.getTotalPP(bottomScores, idx + 1); + + return (expected + oldBottomPP - newBottomPP) / Math.pow(this.weightCoefficient, idx); + } + + /** + * Gets the raw pp per global pp for the given scores. + * + * @param scores The scores. + * @param expectedPP The expected pp. + * @return The raw pp per global pp. + */ + public double getRawPerGlobalPP(List scores, double expectedPP) { + int left = 0; + int right = scores.size() - 1; + int boundaryIdx = -1; + + while (left <= right) { + int mid = (left + right) / 2; + double bottomPP = this.getTotalPP(scores.subList(mid, scores.size()), mid); + + List bottomSlice = new ArrayList<>(scores.subList(mid, scores.size())); + bottomSlice.add(0, scores.get(mid)); + double modifiedBottomPP = this.getTotalPP(bottomSlice, mid); + double diff = modifiedBottomPP - bottomPP; + + if (diff > expectedPP) { + boundaryIdx = mid; + left = mid + 1; + } else { + right = mid - 1; + } + } + + if (boundaryIdx == -1) { + return this.getRawPPAtIdx(scores, 0, expectedPP); + } else { + return this.getRawPPAtIdx(scores.subList(boundaryIdx + 1, scores.size()), boundaryIdx + 1, expectedPP); + } + } +} diff --git a/src/main/java/cc/fascinated/backend/model/account/Account.java b/src/main/java/cc/fascinated/backend/model/account/Account.java index eafa8c8..2af43b5 100644 --- a/src/main/java/cc/fascinated/backend/model/account/Account.java +++ b/src/main/java/cc/fascinated/backend/model/account/Account.java @@ -8,9 +8,14 @@ import lombok.Getter; import lombok.Setter; import lombok.SneakyThrows; import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.mapping.Document; import java.util.Date; +/** + * An account for ScoreSaber. + */ +@Document @AllArgsConstructor @Getter @Setter public class Account { @@ -45,6 +50,11 @@ public class Account { */ private double performancePoints; + /** + * The amount of raw pp to get 1 global performance point. + */ + private double rawPerGlobalPerformancePoints; + /** * The rank for this account. */ @@ -117,6 +127,7 @@ public class Account { Bio.fromRaw(token.getBio()), token.getCountry(), token.getPp(), + -1, token.getRank(), token.getCountryRank(), token.getRole(), diff --git a/src/main/java/cc/fascinated/backend/model/leaderboard/Leaderboard.java b/src/main/java/cc/fascinated/backend/model/leaderboard/Leaderboard.java index 2f5e050..aad8cd1 100644 --- a/src/main/java/cc/fascinated/backend/model/leaderboard/Leaderboard.java +++ b/src/main/java/cc/fascinated/backend/model/leaderboard/Leaderboard.java @@ -5,12 +5,14 @@ import cc.fascinated.backend.model.score.Score; import cc.fascinated.backend.model.token.ScoreSaberLeaderboardToken; import lombok.AllArgsConstructor; import lombok.Getter; +import org.springframework.data.mongodb.core.mapping.Document; import java.util.Date; /** * A leaderboard for a song. */ +@Document @AllArgsConstructor @Getter public class Leaderboard { /** diff --git a/src/main/java/cc/fascinated/backend/model/score/Score.java b/src/main/java/cc/fascinated/backend/model/score/Score.java index 1a1c37d..8c5fe16 100644 --- a/src/main/java/cc/fascinated/backend/model/score/Score.java +++ b/src/main/java/cc/fascinated/backend/model/score/Score.java @@ -8,11 +8,18 @@ import lombok.AllArgsConstructor; import lombok.Getter; import lombok.Setter; import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.index.CompoundIndex; +import org.springframework.data.mongodb.core.mapping.Document; import java.util.ArrayList; import java.util.Date; import java.util.List; +/** + * A score for an account. + */ +@Document +@CompoundIndex(name = "accountId_leaderboardId", def = "{'accountId' : 1, 'leaderboardId' : 1}") @AllArgsConstructor @Getter @Setter public class Score { /** @@ -39,7 +46,7 @@ public class Score { /** * The PP for this score. */ - private double pp; + private Double pp; /** * The weight for this score. diff --git a/src/main/java/cc/fascinated/backend/repository/ScoreRepository.java b/src/main/java/cc/fascinated/backend/repository/ScoreRepository.java index ac96bf8..2635b0d 100644 --- a/src/main/java/cc/fascinated/backend/repository/ScoreRepository.java +++ b/src/main/java/cc/fascinated/backend/repository/ScoreRepository.java @@ -20,6 +20,15 @@ public interface ScoreRepository extends MongoRepository { @Query("{ 'accountId' : ?0 }") List getScoresForAccount(String accountId); + /** + * Gets the ranked scores for an account. + * + * @param accountId The id of the account. + * @return The ranked scores for the account. + */ + @Query(value = "{ 'accountId' : ?0, 'pp' : { $gt : 0 } }", sort = "{ 'pp' : -1 }") + List getRankedScoresForAccount(String accountId); + /** * Gets the scores sorted by the newest for an account. * diff --git a/src/main/java/cc/fascinated/backend/service/AccountService.java b/src/main/java/cc/fascinated/backend/service/AccountService.java index 3afc29b..57f7cd4 100644 --- a/src/main/java/cc/fascinated/backend/service/AccountService.java +++ b/src/main/java/cc/fascinated/backend/service/AccountService.java @@ -1,9 +1,11 @@ package cc.fascinated.backend.service; import cc.fascinated.backend.common.Timer; +import cc.fascinated.backend.leaderboard.impl.ScoreSaberLeaderboard; import cc.fascinated.backend.model.account.Account; import cc.fascinated.backend.model.token.ScoreSaberAccountToken; import cc.fascinated.backend.repository.AccountRepository; +import cc.fascinated.backend.repository.ScoreRepository; import lombok.extern.log4j.Log4j2; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @@ -25,14 +27,20 @@ public class AccountService { */ private final AccountRepository accountRepository; + /** + * The {@link ScoreRepository} instance. + */ + private final ScoreRepository scoreRepository; + /** * The {@link ScoreSaberService} instance. */ private final ScoreSaberService scoreSaberService; @Autowired - public AccountService(AccountRepository accountRepository, ScoreSaberService scoreSaberService) { + public AccountService(AccountRepository accountRepository, ScoreRepository scoreRepository, ScoreSaberService scoreSaberService) { this.accountRepository = accountRepository; + this.scoreRepository = scoreRepository; this.scoreSaberService = scoreSaberService; Timer.scheduleRepeating(() -> { @@ -63,10 +71,7 @@ public class AccountService { if (optionalAccount.isEmpty()) { log.info("Account '{}' not found in the database. Fetching from ScoreSaber API.", id); - Account account = Account.fromToken(scoreSaberService.getAccount(id)); - updateAccount(account); // Fetch the scores for the account. - accountRepository.save(account); // Save the account to the database. - return account; + return updateAccount(Account.fromToken(scoreSaberService.getAccount(id))); } log.info("Account '{}' found in the database.", id); return optionalAccount.get(); @@ -78,21 +83,27 @@ public class AccountService { * * @param account The account. */ - public void updateAccount(Account account) { + public Account updateAccount(Account account) { String id = account.getId(); // Fetch the account from the ScoreSaber API. ScoreSaberAccountToken accountToken = scoreSaberService.getAccount(id); // Fetch the account from the ScoreSaber API. if (accountToken == null) { log.warn("Account '{}' not found in the ScoreSaber API.", id); - return; + return null; } // Update the account with the new token. - Account updatedAccount = Account.fromToken(accountToken); - account = accountRepository.save(updatedAccount); // Save the account to the database. + account = Account.fromToken(accountToken); // Fetch the scores for the account. scoreSaberService.updateScores(account); + + // Set the raw pp per +1 global pp + double rawPerGlobalPP = ScoreSaberLeaderboard.INSTANCE.getRawPerGlobalPP(scoreRepository.getRankedScoresForAccount(id), 1); + account.setRawPerGlobalPerformancePoints(rawPerGlobalPP); + + // Save the account to the database. + return accountRepository.save(account); } }