Skip to content

Commit

Permalink
Fixed all-time stats (#108)
Browse files Browse the repository at this point in the history
* all time stats show only 1 statistic per profile

* providing player dto in leaderboard endpoint

* chore: update openapi types

---------

Co-authored-by: Thiies <Thiies@users.noreply.github.com>
  • Loading branch information
Thiies and Thiies authored Nov 21, 2024
1 parent 1e6643d commit 5307b9b
Show file tree
Hide file tree
Showing 4 changed files with 65 additions and 35 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

@Data
public class LeaderboardEntryDto {
private String playerId;
private PlayerDto playerDto;
private int totalPoints = 0;
private int totalGames = 0;
private int totalMoves = 0;
Expand Down
94 changes: 62 additions & 32 deletions api/src/main/java/pro/beerpong/api/service/LeaderboardService.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Service;
import pro.beerpong.api.model.dto.*;
import pro.beerpong.api.repository.PlayerRepository;
import pro.beerpong.api.util.RankingAlgorithm;

import java.time.Duration;
import java.time.ZonedDateTime;
import java.util.Comparator;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
Expand All @@ -16,16 +19,18 @@

@Service
public class LeaderboardService {
private final RuleMoveService ruleMoveService;
private final MatchService matchService;

private static final double K_FACTOR = 10D;
private static final int ELO_DIVIDER = 400;

private final RuleMoveService ruleMoveService;
private final MatchService matchService;
private final PlayerRepository playerRepository;

@Autowired
public LeaderboardService(RuleMoveService ruleMoveService, MatchService matchService) {
public LeaderboardService(RuleMoveService ruleMoveService, MatchService matchService, PlayerRepository playerRepository) {
this.ruleMoveService = ruleMoveService;
this.matchService = matchService;
this.playerRepository = playerRepository;
}

public LeaderboardDto generateLeaderboard(GroupDto group, String scope, @Nullable String seasonId) {
Expand Down Expand Up @@ -62,14 +67,29 @@ public LeaderboardDto generateLeaderboard(GroupDto group, String scope, @Nullabl
}

Map<String, LeaderboardEntryDto> entries = Maps.newHashMap();
Map<String, String> memberToPlayer = Maps.newHashMap();
Map<String, String> memberToProfile = Maps.newHashMap();

players.forEach(playerDto -> {
var dto = new LeaderboardEntryDto();

dto.setPlayerId(playerDto.getId());
var dto = entries.getOrDefault(playerDto.getProfile().getId(), new LeaderboardEntryDto());

// if no player is saved we set the player
if (dto.getPlayerDto() == null) {
dto.setPlayerDto(playerDto);
// if the current player is from the active season we set him
} else if (playerDto.getSeason().getEndDate() == null) {
dto.setPlayerDto(playerDto);
// if the season of the current player is closer to now than the saved season we set the newer player
} else if (dto.getPlayerDto().getSeason().getEndDate() != null && Duration.between(
ZonedDateTime.now(),
playerDto.getSeason().getEndDate())
.compareTo(Duration.between(
ZonedDateTime.now(),
dto.getPlayerDto().getSeason().getEndDate()
)) < 0) {
dto.setPlayerDto(playerDto);
}

entries.put(playerDto.getId(), dto);
entries.put(playerDto.getProfile().getId(), dto);
});

// go through all matches sorted by date, starting with the earliest
Expand All @@ -87,23 +107,22 @@ public LeaderboardDto generateLeaderboard(GroupDto group, String scope, @Nullabl

// go through all team members
teamMembers.forEach(teamMemberDto -> {
// create leaderboard entries for all members
if (!entries.containsKey(teamMemberDto.getPlayerId())) {
var dto = new LeaderboardEntryDto();
// save player id by member id
if (!memberToProfile.containsKey(teamMemberDto.getId())) {
var player = playerRepository.findById(teamMemberDto.getPlayerId()).orElse(null);

dto.setPlayerId(teamMemberDto.getPlayerId());
if (player == null) {
return;
}

entries.put(teamMemberDto.getPlayerId(), dto);
memberToProfile.put(teamMemberDto.getId(), player.getProfile().getId());
}

// save player id by member id
if (!memberToPlayer.containsKey(teamMemberDto.getTeamId())) {
memberToPlayer.put(teamMemberDto.getId(), teamMemberDto.getPlayerId());
}
var profileId = memberToProfile.get(teamMemberDto.getId());

// add game and team size to entry
entries.get(teamMemberDto.getPlayerId()).addTotalGames();
entries.get(teamMemberDto.getPlayerId()).addTotalTeamSize(teamMembers.size());
entries.get(profileId).addTotalGames();
entries.get(profileId).addTotalTeamSize(teamMembers.size());
});

AtomicInteger teamPoints = new AtomicInteger();
Expand All @@ -113,13 +132,13 @@ public LeaderboardDto generateLeaderboard(GroupDto group, String scope, @Nullabl
matchDto.getMatchMoves().stream()
.filter(dto -> teamMembers.stream().anyMatch(teamMemberDto -> teamMemberDto.getId().equals(dto.getTeamMemberId())))
.forEach(dto -> {
if (!memberToPlayer.containsKey(dto.getTeamMemberId()) ||
!entries.containsKey(memberToPlayer.get(dto.getTeamMemberId()))) {
if (!memberToProfile.containsKey(dto.getTeamMemberId()) ||
!entries.containsKey(memberToProfile.get(dto.getTeamMemberId()))) {
return;
}

// get entry and points for this move
var entry = entries.get(memberToPlayer.get(dto.getTeamMemberId()));
var entry = entries.get(memberToProfile.get(dto.getTeamMemberId()));
var points = ruleMoveService.getPointsById(dto.getMoveId());

if (points == null) {
Expand All @@ -142,8 +161,12 @@ public LeaderboardDto generateLeaderboard(GroupDto group, String scope, @Nullabl
// if pointsForTeam > 0 add gained pointsForTeam to every team members entry
if (points.getSecond() > 0) {
teamMembers.forEach(teamMemberDto -> {
if (entries.containsKey(teamMemberDto.getPlayerId())) {
entries.get(teamMemberDto.getPlayerId()).addTotalPoints(points.getSecond() * dto.getValue());
if (memberToProfile.containsKey(teamMemberDto.getId())) {
var profileId = memberToProfile.get(teamMemberDto.getId());

if (entries.containsKey(profileId)) {
entries.get(profileId).addTotalPoints(points.getSecond() * dto.getValue());
}
}
});
}
Expand Down Expand Up @@ -178,12 +201,16 @@ public LeaderboardDto generateLeaderboard(GroupDto group, String scope, @Nullabl
var loosingTeam = matchDto.getTeams().getFirst().getId().equals(winningTeam.getId()) ? matchDto.getTeams().get(1) : matchDto.getTeams().getFirst();

// calculate team elo averages
var winnerEloAvg = calcTeamEloAverage(entries, matchDto, winningTeam);
var looserEloAvg = calcTeamEloAverage(entries, matchDto, loosingTeam);
var winnerEloAvg = calcTeamEloAverage(entries, memberToProfile, matchDto, winningTeam);
var looserEloAvg = calcTeamEloAverage(entries, memberToProfile, matchDto, loosingTeam);

// calculate elo for all team members
matchDto.getTeamMembers().forEach(teamMemberDto -> {
var entry = entries.get(teamMemberDto.getPlayerId());
if (!memberToProfile.containsKey(teamMemberDto.getId())) {
return;
}

var entry = entries.get(memberToProfile.get(teamMemberDto.getId()));

// win: 1.0, loss: 0.0, no draw possible
var score = teamMemberDto.getTeamId().equals(winningTeam.getId()) ? 1.0D : 0.0D;
Expand All @@ -209,7 +236,8 @@ public LeaderboardDto generateLeaderboard(GroupDto group, String scope, @Nullabl

// sort the stream based on the current algorithm
switch (value) {
case AVERAGE -> stream = stream.sorted((o1, o2) -> Double.compare(o2.getAveragePointsPerMatch(), o1.getAveragePointsPerMatch()));
case AVERAGE ->
stream = stream.sorted((o1, o2) -> Double.compare(o2.getAveragePointsPerMatch(), o1.getAveragePointsPerMatch()));
case ELO -> stream = stream.sorted((o1, o2) -> Double.compare(o2.getElo(), o1.getElo()));
}

Expand All @@ -223,10 +251,12 @@ public LeaderboardDto generateLeaderboard(GroupDto group, String scope, @Nullabl
return dto;
}

private double calcTeamEloAverage(Map<String, LeaderboardEntryDto> entries, MatchDto matchDto, TeamDto teamDto) {
private double calcTeamEloAverage(Map<String, LeaderboardEntryDto> entries, Map<String, String> memberToProfile, MatchDto matchDto, TeamDto teamDto) {
return matchDto.getTeamMembers().stream()
.filter(teamMemberDto -> teamMemberDto.getTeamId().equals(teamDto.getId()) && entries.containsKey(teamMemberDto.getPlayerId()))
.map(teamMemberDto -> entries.get(teamMemberDto.getPlayerId()).getElo())
.filter(teamMemberDto -> teamMemberDto.getTeamId().equals(teamDto.getId()) &&
memberToProfile.containsKey(teamMemberDto.getId()) &&
entries.containsKey(memberToProfile.get(teamMemberDto.getId())))
.map(teamMemberDto -> entries.get(memberToProfile.get(teamMemberDto.getId())).getElo())
.reduce(Double::sum)
.orElse(0D) /
matchDto.getTeamMembers().stream()
Expand Down
2 changes: 1 addition & 1 deletion mobile-app/api/generated/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -1602,7 +1602,7 @@
"LeaderboardEntryDto": {
"type": "object",
"properties": {
"playerId": { "type": "string" },
"playerDto": { "$ref": "#/components/schemas/PlayerDto" },
"totalPoints": { "type": "integer", "format": "int32" },
"totalGames": { "type": "integer", "format": "int32" },
"totalMoves": { "type": "integer", "format": "int32" },
Expand Down
2 changes: 1 addition & 1 deletion mobile-app/openapi/openapi.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ declare namespace Components {
entries?: LeaderboardEntryDto[];
}
export interface LeaderboardEntryDto {
playerId?: string;
playerDto?: PlayerDto;
totalPoints?: number; // int32
totalGames?: number; // int32
totalMoves?: number; // int32
Expand Down

0 comments on commit 5307b9b

Please sign in to comment.