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
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:
parent
23814d8cfd
commit
de572779e4
28
src/main/java/cc/fascinated/backend/common/MathUtils.java
Normal file
28
src/main/java/cc/fascinated/backend/common/MathUtils.java
Normal file
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user