impl +1 raw per global pp
All checks were successful
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Successful in 1m20s

This commit is contained in:
Lee 2024-04-25 06:34:49 +01:00
parent 23814d8cfd
commit de572779e4
9 changed files with 270 additions and 10 deletions

@ -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);
}
}

@ -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;
}

@ -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;
}

@ -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<Score> 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<Score> 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<Score> 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<Score> 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);
}
}
}

@ -8,9 +8,14 @@ import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import lombok.SneakyThrows; import lombok.SneakyThrows;
import org.springframework.data.annotation.Id; import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
import java.util.Date; import java.util.Date;
/**
* An account for ScoreSaber.
*/
@Document
@AllArgsConstructor @AllArgsConstructor
@Getter @Setter @Getter @Setter
public class Account { public class Account {
@ -45,6 +50,11 @@ public class Account {
*/ */
private double performancePoints; private double performancePoints;
/**
* The amount of raw pp to get 1 global performance point.
*/
private double rawPerGlobalPerformancePoints;
/** /**
* The rank for this account. * The rank for this account.
*/ */
@ -117,6 +127,7 @@ public class Account {
Bio.fromRaw(token.getBio()), Bio.fromRaw(token.getBio()),
token.getCountry(), token.getCountry(),
token.getPp(), token.getPp(),
-1,
token.getRank(), token.getRank(),
token.getCountryRank(), token.getCountryRank(),
token.getRole(), token.getRole(),

@ -5,12 +5,14 @@ import cc.fascinated.backend.model.score.Score;
import cc.fascinated.backend.model.token.ScoreSaberLeaderboardToken; import cc.fascinated.backend.model.token.ScoreSaberLeaderboardToken;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Getter; import lombok.Getter;
import org.springframework.data.mongodb.core.mapping.Document;
import java.util.Date; import java.util.Date;
/** /**
* A leaderboard for a song. * A leaderboard for a song.
*/ */
@Document
@AllArgsConstructor @Getter @AllArgsConstructor @Getter
public class Leaderboard { public class Leaderboard {
/** /**

@ -8,11 +8,18 @@ import lombok.AllArgsConstructor;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import org.springframework.data.annotation.Id; 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.ArrayList;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
/**
* A score for an account.
*/
@Document
@CompoundIndex(name = "accountId_leaderboardId", def = "{'accountId' : 1, 'leaderboardId' : 1}")
@AllArgsConstructor @Getter @Setter @AllArgsConstructor @Getter @Setter
public class Score { public class Score {
/** /**
@ -39,7 +46,7 @@ public class Score {
/** /**
* The PP for this score. * The PP for this score.
*/ */
private double pp; private Double pp;
/** /**
* The weight for this score. * The weight for this score.

@ -20,6 +20,15 @@ public interface ScoreRepository extends MongoRepository<Score, String> {
@Query("{ 'accountId' : ?0 }") @Query("{ 'accountId' : ?0 }")
List<Score> getScoresForAccount(String accountId); List<Score> 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<Score> getRankedScoresForAccount(String accountId);
/** /**
* Gets the scores sorted by the newest for an account. * Gets the scores sorted by the newest for an account.
* *

@ -1,9 +1,11 @@
package cc.fascinated.backend.service; package cc.fascinated.backend.service;
import cc.fascinated.backend.common.Timer; 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.account.Account;
import cc.fascinated.backend.model.token.ScoreSaberAccountToken; import cc.fascinated.backend.model.token.ScoreSaberAccountToken;
import cc.fascinated.backend.repository.AccountRepository; import cc.fascinated.backend.repository.AccountRepository;
import cc.fascinated.backend.repository.ScoreRepository;
import lombok.extern.log4j.Log4j2; import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@ -25,14 +27,20 @@ public class AccountService {
*/ */
private final AccountRepository accountRepository; private final AccountRepository accountRepository;
/**
* The {@link ScoreRepository} instance.
*/
private final ScoreRepository scoreRepository;
/** /**
* The {@link ScoreSaberService} instance. * The {@link ScoreSaberService} instance.
*/ */
private final ScoreSaberService scoreSaberService; private final ScoreSaberService scoreSaberService;
@Autowired @Autowired
public AccountService(AccountRepository accountRepository, ScoreSaberService scoreSaberService) { public AccountService(AccountRepository accountRepository, ScoreRepository scoreRepository, ScoreSaberService scoreSaberService) {
this.accountRepository = accountRepository; this.accountRepository = accountRepository;
this.scoreRepository = scoreRepository;
this.scoreSaberService = scoreSaberService; this.scoreSaberService = scoreSaberService;
Timer.scheduleRepeating(() -> { Timer.scheduleRepeating(() -> {
@ -63,10 +71,7 @@ public class AccountService {
if (optionalAccount.isEmpty()) { if (optionalAccount.isEmpty()) {
log.info("Account '{}' not found in the database. Fetching from ScoreSaber API.", id); log.info("Account '{}' not found in the database. Fetching from ScoreSaber API.", id);
Account account = Account.fromToken(scoreSaberService.getAccount(id)); return updateAccount(Account.fromToken(scoreSaberService.getAccount(id)));
updateAccount(account); // Fetch the scores for the account.
accountRepository.save(account); // Save the account to the database.
return account;
} }
log.info("Account '{}' found in the database.", id); log.info("Account '{}' found in the database.", id);
return optionalAccount.get(); return optionalAccount.get();
@ -78,21 +83,27 @@ public class AccountService {
* *
* @param account The account. * @param account The account.
*/ */
public void updateAccount(Account account) { public Account updateAccount(Account account) {
String id = account.getId(); String id = account.getId();
// Fetch the account from the ScoreSaber API. // Fetch the account from the ScoreSaber API.
ScoreSaberAccountToken accountToken = scoreSaberService.getAccount(id); // Fetch the account from the ScoreSaber API. ScoreSaberAccountToken accountToken = scoreSaberService.getAccount(id); // Fetch the account from the ScoreSaber API.
if (accountToken == null) { if (accountToken == null) {
log.warn("Account '{}' not found in the ScoreSaber API.", id); log.warn("Account '{}' not found in the ScoreSaber API.", id);
return; return null;
} }
// Update the account with the new token. // Update the account with the new token.
Account updatedAccount = Account.fromToken(accountToken); account = Account.fromToken(accountToken);
account = accountRepository.save(updatedAccount); // Save the account to the database.
// Fetch the scores for the account. // Fetch the scores for the account.
scoreSaberService.updateScores(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);
} }
} }