Skip to content

Commit

Permalink
League Points Calculation + Task Scheduling (#195)
Browse files Browse the repository at this point in the history
Co-authored-by: Felix T.J. Dietrich <[email protected]>
  • Loading branch information
GODrums and FelixTJDietrich authored Dec 4, 2024
1 parent bc8d264 commit 566ae85
Show file tree
Hide file tree
Showing 7 changed files with 221 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,9 @@ public class User extends BaseGitServiceEntity {
@ToString.Exclude
private Set<PullRequestReviewComment> reviewComments = new HashSet<>();

// Current ranking points for the leaderboard leagues
private int leaguePoints;

public enum Type {
USER, ORGANIZATION, BOT
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@ public interface UserRepository extends JpaRepository<User, Long> {
""")
Optional<User> findByLogin(@Param("login") String login);

@Query("""
SELECT u
FROM User u
LEFT JOIN FETCH u.mergedPullRequests
WHERE u.login = :login
""")
Optional<User> findByLoginWithEagerMergedPullRequests(@Param("login") String login);

@Query("""
SELECT u
FROM User u
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package de.tum.in.www1.hephaestus.leaderboard;

import de.tum.in.www1.hephaestus.leaderboard.tasks.LeaguePointsUpdateTask;
import de.tum.in.www1.hephaestus.leaderboard.tasks.SlackWeeklyLeaderboardTask;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand Down Expand Up @@ -41,6 +42,9 @@ public class LeaderboardTaskScheduler {
@Autowired
private SlackWeeklyLeaderboardTask slackWeeklyLeaderboardTask;

@Autowired
private LeaguePointsUpdateTask leaguePointsUpdateTask;


@EventListener(ApplicationReadyEvent.class)
public void activateTaskScheduler() {
Expand All @@ -61,7 +65,7 @@ public void activateTaskScheduler() {
}

scheduleSlackMessage(cron);

scheduleLeaguePointsUpdate(cron);
}

/**
Expand All @@ -78,4 +82,9 @@ private void scheduleSlackMessage(String cron) {
logger.info("Scheduling Slack message to run with {}", cron);
taskScheduler.schedule(slackWeeklyLeaderboardTask, new CronTrigger(cron));
}

private void scheduleLeaguePointsUpdate(String cron) {
logger.info("Scheduling league points update to run with {}", cron);
taskScheduler.schedule(leaguePointsUpdateTask, new CronTrigger(cron));
}
}
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;
}
}
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());
}

}
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>
1 change: 1 addition & 0 deletions server/application-server/src/main/resources/db/master.xml
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@
<include file="./changelog/0000000000000_initial_schema.xml" relativeToChangelogFile="true"/>
<include file="./changelog/1731511193057_changelog.xml" relativeToChangelogFile="true"/>
<include file="./changelog/1732399349184_changelog.xml" relativeToChangelogFile="true"/>
<include file="./changelog/1732966971482_changelog.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>

0 comments on commit 566ae85

Please sign in to comment.