Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

League Points Calculation + Task Scheduling #195

Merged
merged 13 commits into from
Dec 4, 2024
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
GODrums marked this conversation as resolved.
Show resolved Hide resolved
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,89 @@
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);

public int calculateNewPoints(User user, LeaderboardEntryDTO entry) {
// Initialize new players with 1000 points
FelixTJDietrich marked this conversation as resolved.
Show resolved Hide resolved
if (user.getLeaguePoints() == 0) {
user.setLeaguePoints(1000);
}

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(0, oldPoints + pointChange);
FelixTJDietrich marked this conversation as resolved.
Show resolved Hide resolved

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.
* @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 2.0;
} else if (user.getLeaguePoints() < 1400) {
return 1.5;
} else if (user.getLeaguePoints() < 1800) {
return 1.2;
} else {
return 1.1;
}
}

/**
* 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)
.anyMatch(date -> date.isAfter(OffsetDateTime.now().minusDays(30)));
GODrums marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* Calculate the base decay in points based on the current points.
* @param currentPoints
* @return Amount of decay points
*/
private int calculateDecay(int currentPoints) {
// 5% decay of current points, minimum 10 points if they have any points
return currentPoints > 0 ? Math.max(10, (int)(currentPoints * 0.05)) : 0;
GODrums marked this conversation as resolved.
Show resolved Hide resolved
}

private int calculatePerformanceBonus(int score) {
// Convert leaderboard score directly to points with diminishing returns
return (int)(Math.sqrt(score) * 10);
}

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>
Loading