-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
League Points Calculation + Task Scheduling (#195)
Co-authored-by: Felix T.J. Dietrich <[email protected]>
- Loading branch information
1 parent
bc8d264
commit 566ae85
Showing
7 changed files
with
221 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
119 changes: 119 additions & 0 deletions
119
...r/src/main/java/de/tum/in/www1/hephaestus/leaderboard/LeaguePointsCalculationService.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,119 @@ | ||
package de.tum.in.www1.hephaestus.leaderboard; | ||
|
||
import de.tum.in.www1.hephaestus.gitprovider.pullrequest.PullRequest; | ||
import de.tum.in.www1.hephaestus.gitprovider.user.User; | ||
import java.time.OffsetDateTime; | ||
import org.slf4j.Logger; | ||
import org.slf4j.LoggerFactory; | ||
import org.springframework.stereotype.Service; | ||
|
||
@Service | ||
public class LeaguePointsCalculationService { | ||
private final Logger logger = LoggerFactory.getLogger(LeaguePointsCalculationService.class); | ||
|
||
// Starting points for new players | ||
public static int POINTS_DEFAULT = 1000; | ||
// Upper bound for first reducting in the k-factor | ||
public static int POINTS_THRESHOLD_HIGH = 1750; | ||
// Lower bound for first reducting in the k-factor | ||
public static int POINTS_THRESHOLD_LOW = 1250; | ||
// Minimum amount of points to decay each cycle | ||
public static int DECAY_MINIMUM = 10; | ||
// Factor to determine how much of the current points are decayed each cycle | ||
public static double DECAY_FACTOR = 0.05; | ||
// K-factors depending on the player's league points | ||
public static double K_FACTOR_NEW_PLAYER = 2.0; | ||
public static double K_FACTOR_LOW_POINTS = 1.5; | ||
public static double K_FACTOR_MEDIUM_POINTS = 1.2; | ||
public static double K_FACTOR_HIGH_POINTS = 1.1; | ||
|
||
|
||
|
||
public int calculateNewPoints(User user, LeaderboardEntryDTO entry) { | ||
// Initialize points for new players | ||
if (user.getLeaguePoints() == 0) { | ||
user.setLeaguePoints(POINTS_DEFAULT); | ||
} | ||
|
||
int oldPoints = user.getLeaguePoints(); | ||
|
||
double kFactor = getKFactor(user); | ||
// Base decay | ||
int decay = calculateDecay(user.getLeaguePoints()); | ||
// Bonus based on leaderboard score | ||
int performanceBonus = calculatePerformanceBonus(entry.score()); | ||
// Additional bonus for placements | ||
int placementBonus = calculatePlacementBonus(entry.rank()); | ||
// Calculate final point change | ||
int pointChange = (int) (kFactor * (performanceBonus + placementBonus - decay)); | ||
// Apply minimum change to prevent extreme swings | ||
int newPoints = Math.max(1, oldPoints + pointChange); | ||
|
||
logger.info("Points calculation: old={}, k={}, decay={}, performanceBonus={}, placement={}, pointchange={}, new={}", | ||
oldPoints, kFactor, decay, performanceBonus, placementBonus, pointChange, newPoints); | ||
|
||
return newPoints; | ||
} | ||
|
||
/** | ||
* Calculate the K factor for the user based on their current points and if they are a new player. | ||
* The K-factor is used to control the sensitivity of the rating system to changes in the leaderboard. | ||
* New players have a higher K-factor to allow them to quickly reach their true skill level. | ||
* @param user | ||
* @return K factor | ||
* @see <a href="https://en.wikipedia.org/wiki/Elo_rating_system#Most_accurate_K-factor">Wikipedia: Most accurate K-factor</a> | ||
*/ | ||
private double getKFactor(User user) { | ||
if (isNewPlayer(user)) { | ||
return K_FACTOR_NEW_PLAYER; | ||
} else if (user.getLeaguePoints() < POINTS_THRESHOLD_LOW) { | ||
return K_FACTOR_LOW_POINTS; | ||
} else if (user.getLeaguePoints() < POINTS_THRESHOLD_HIGH) { | ||
return K_FACTOR_MEDIUM_POINTS; | ||
} else { | ||
return K_FACTOR_HIGH_POINTS; | ||
} | ||
} | ||
|
||
/** | ||
* Check if the user's earliest merged pull request is within the last 30 days. | ||
* @param user | ||
* @return true if the pull request is within the last 30 days | ||
*/ | ||
private boolean isNewPlayer(User user) { | ||
return user.getMergedPullRequests().stream() | ||
.filter(PullRequest::isMerged) | ||
.map(PullRequest::getMergedAt) | ||
.noneMatch(date -> date.isAfter(OffsetDateTime.now().minusDays(30))); | ||
} | ||
|
||
/** | ||
* Calculate the base decay in points based on the current points. | ||
* @param currentPoints Current amount of league points | ||
* @return Amount of decay points | ||
*/ | ||
private int calculateDecay(int currentPoints) { | ||
// decay a part of the current points, at least DECAY_MINIMUM points | ||
return currentPoints > 0 ? Math.max(DECAY_MINIMUM, (int)(currentPoints * DECAY_FACTOR)) : 0; | ||
} | ||
|
||
/** | ||
* Calculate the bonus points based on the leaderboard score. | ||
* @param score Leaderboard score | ||
* @return Bonus points | ||
*/ | ||
private int calculatePerformanceBonus(int score) { | ||
// Convert leaderboard score directly to points with diminishing returns | ||
return (int)(Math.sqrt(score) * 10); | ||
} | ||
|
||
/** | ||
* Calculate the bonus points based on the placement in the leaderboard. | ||
* @param placement Placement in the leaderboard | ||
* @return Bonus points | ||
*/ | ||
private int calculatePlacementBonus(int placement) { | ||
// Bonus for top 3 placements | ||
return placement <= 3 ? 20 * (4 - placement) : 0; | ||
} | ||
} |
70 changes: 70 additions & 0 deletions
70
...ver/src/main/java/de/tum/in/www1/hephaestus/leaderboard/tasks/LeaguePointsUpdateTask.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
package de.tum.in.www1.hephaestus.leaderboard.tasks; | ||
|
||
import de.tum.in.www1.hephaestus.gitprovider.user.UserRepository; | ||
import de.tum.in.www1.hephaestus.leaderboard.LeaderboardEntryDTO; | ||
import de.tum.in.www1.hephaestus.leaderboard.LeaderboardService; | ||
import de.tum.in.www1.hephaestus.leaderboard.LeaguePointsCalculationService; | ||
import jakarta.transaction.Transactional; | ||
import java.time.DayOfWeek; | ||
import java.time.OffsetDateTime; | ||
import java.time.temporal.TemporalAdjusters; | ||
import java.util.List; | ||
import java.util.Optional; | ||
import java.util.function.Consumer; | ||
import org.springframework.beans.factory.annotation.Autowired; | ||
import org.springframework.beans.factory.annotation.Value; | ||
import org.springframework.stereotype.Component; | ||
|
||
@Component | ||
public class LeaguePointsUpdateTask implements Runnable { | ||
|
||
@Value("${hephaestus.leaderboard.schedule.day}") | ||
private String scheduledDay; | ||
|
||
@Value("${hephaestus.leaderboard.schedule.time}") | ||
private String scheduledTime; | ||
|
||
@Autowired | ||
private UserRepository userRepository; | ||
@Autowired | ||
private LeaderboardService leaderboardService; | ||
@Autowired | ||
private LeaguePointsCalculationService leaguePointsCalculationService; | ||
|
||
@Override | ||
@Transactional | ||
public void run() { | ||
List<LeaderboardEntryDTO> leaderboard = getLatestLeaderboard(); | ||
leaderboard.forEach(updateLeaderboardEntry()); | ||
} | ||
|
||
/** | ||
* Update ranking points of a user based on its leaderboard entry. | ||
* @return {@code Consumer} that updates {@code leaguePoints} based on its leaderboard entry. | ||
*/ | ||
private Consumer<? super LeaderboardEntryDTO> updateLeaderboardEntry() { | ||
return entry -> { | ||
var user = userRepository.findByLoginWithEagerMergedPullRequests(entry.user().login()).orElseThrow(); | ||
int newPoints = leaguePointsCalculationService.calculateNewPoints(user, entry); | ||
user.setLeaguePoints(newPoints); | ||
userRepository.save(user); | ||
}; | ||
} | ||
|
||
/** | ||
* Retrieves the latest leaderboard based on the scheduled time of the environment. | ||
* @return List of {@code LeaderboardEntryDTO} representing the latest leaderboard | ||
*/ | ||
private List<LeaderboardEntryDTO> getLatestLeaderboard() { | ||
String[] timeParts = scheduledTime.split(":"); | ||
OffsetDateTime before = OffsetDateTime.now() | ||
.with(TemporalAdjusters.previousOrSame(DayOfWeek.of(Integer.parseInt(scheduledDay)))) | ||
.withHour(Integer.parseInt(timeParts[0])) | ||
.withMinute(timeParts.length > 0 ? Integer.parseInt(timeParts[1]) : 0) | ||
.withSecond(0) | ||
.withNano(0); | ||
OffsetDateTime after = before.minusWeeks(1); | ||
return leaderboardService.createLeaderboard(after, before, Optional.empty()); | ||
} | ||
|
||
} |
10 changes: 10 additions & 0 deletions
10
server/application-server/src/main/resources/db/changelog/1732966971482_changelog.xml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
<?xml version="1.1" encoding="UTF-8" standalone="no"?> | ||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext" xmlns:pro="http://www.liquibase.org/xml/ns/pro" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog-ext http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd http://www.liquibase.org/xml/ns/pro http://www.liquibase.org/xml/ns/pro/liquibase-pro-latest.xsd http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd"> | ||
<changeSet author="godrums" id="1732966971482-1"> | ||
<addColumn tableName="user"> | ||
<column name="league_points" type="integer" defaultValue="0"> | ||
<constraints nullable="false"/> | ||
</column> | ||
</addColumn> | ||
</changeSet> | ||
</databaseChangeLog> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters