diff --git a/frontend/.env b/frontend/.env index 945d03d9..6129c0e6 100644 --- a/frontend/.env +++ b/frontend/.env @@ -1 +1,2 @@ REACT_APP_auth.signupWithPasswordEnabled=false +REACT_APP_eloServiceEnabled=false diff --git a/frontend/src/lib/api/dto/MainPageData.ts b/frontend/src/lib/api/dto/MainPageData.ts index 27bccb5d..d639131f 100644 --- a/frontend/src/lib/api/dto/MainPageData.ts +++ b/frontend/src/lib/api/dto/MainPageData.ts @@ -20,6 +20,7 @@ export interface UserDto { roles?: string[] reputation?: number globalScore?: number + eloPoints?: number; } export interface ListDto { diff --git a/frontend/src/pages/MainPage/UserPane.tsx b/frontend/src/pages/MainPage/UserPane.tsx index 768a42db..7ecd4723 100644 --- a/frontend/src/pages/MainPage/UserPane.tsx +++ b/frontend/src/pages/MainPage/UserPane.tsx @@ -4,8 +4,10 @@ import Gravatar, {GravatarType} from "components/Gravatar"; import {Link} from "react-router-dom"; import {AiOutlineTrophy} from "react-icons/ai"; import {FaRegHeart} from "react-icons/fa"; +import { FaChartLine } from "react-icons/fa6"; import React from "react"; + export function UserPane( { user @@ -16,6 +18,8 @@ export function UserPane( let loc = useLoc() let transliterate = useTransliterate() + const eloServiceEnabled = process.env.REACT_APP_eloServiceEnabled === 'true'; + return
{user.reputation || 0}
+ + {eloServiceEnabled && ( +
+ + {user.eloPoints || "Unrated"} +
+ )}
; diff --git a/src/main/java/com/chessgrinder/chessgrinder/controller/UserController.java b/src/main/java/com/chessgrinder/chessgrinder/controller/UserController.java index eb794e06..cc48cc38 100644 --- a/src/main/java/com/chessgrinder/chessgrinder/controller/UserController.java +++ b/src/main/java/com/chessgrinder/chessgrinder/controller/UserController.java @@ -26,6 +26,7 @@ import java.time.LocalDate; import java.util.*; +import java.util.stream.Collectors; @RestController @RequestMapping("/user") diff --git a/src/main/java/com/chessgrinder/chessgrinder/dto/EloUpdateResultDto.java b/src/main/java/com/chessgrinder/chessgrinder/dto/EloUpdateResultDto.java new file mode 100644 index 00000000..a6f95268 --- /dev/null +++ b/src/main/java/com/chessgrinder/chessgrinder/dto/EloUpdateResultDto.java @@ -0,0 +1,11 @@ +package com.chessgrinder.chessgrinder.dto; + +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class EloUpdateResultDto { + private final int whiteNewElo; + private final int blackNewElo; +} diff --git a/src/main/java/com/chessgrinder/chessgrinder/dto/UserDto.java b/src/main/java/com/chessgrinder/chessgrinder/dto/UserDto.java index 3890d1f6..1917491a 100644 --- a/src/main/java/com/chessgrinder/chessgrinder/dto/UserDto.java +++ b/src/main/java/com/chessgrinder/chessgrinder/dto/UserDto.java @@ -36,6 +36,8 @@ public class UserDto { */ private List badges; + private int eloPoints; + private List roles; private int reputation; diff --git a/src/main/java/com/chessgrinder/chessgrinder/entities/ParticipantEntity.java b/src/main/java/com/chessgrinder/chessgrinder/entities/ParticipantEntity.java index 8200fb97..0f6fd826 100644 --- a/src/main/java/com/chessgrinder/chessgrinder/entities/ParticipantEntity.java +++ b/src/main/java/com/chessgrinder/chessgrinder/entities/ParticipantEntity.java @@ -41,6 +41,12 @@ public class ParticipantEntity extends AbstractAuditingEntity { @Column(name = "nickname") private String nickname; + @Column(name = "initial_elo_points") + private int initialEloPoints; + + @Column (name ="final_elo_points") + private int finalEloPoints; + @Nonnull @Column(name = "score") private BigDecimal score; diff --git a/src/main/java/com/chessgrinder/chessgrinder/entities/TournamentEntity.java b/src/main/java/com/chessgrinder/chessgrinder/entities/TournamentEntity.java index d0763224..ff07e94d 100644 --- a/src/main/java/com/chessgrinder/chessgrinder/entities/TournamentEntity.java +++ b/src/main/java/com/chessgrinder/chessgrinder/entities/TournamentEntity.java @@ -51,6 +51,10 @@ public class TournamentEntity extends AbstractAuditingEntity { @Enumerated(EnumType. STRING) private TournamentStatus status; + @Nullable + @Column(name = "has_elo_calculated") + private boolean hasEloCalculated; + /** * Number of allowed rounds (not actual number of rounds) */ diff --git a/src/main/java/com/chessgrinder/chessgrinder/entities/UserEntity.java b/src/main/java/com/chessgrinder/chessgrinder/entities/UserEntity.java index 58c950a1..49132067 100644 --- a/src/main/java/com/chessgrinder/chessgrinder/entities/UserEntity.java +++ b/src/main/java/com/chessgrinder/chessgrinder/entities/UserEntity.java @@ -61,6 +61,9 @@ public class UserEntity extends AbstractAuditingEntity { @Column(name = "reputation") private int reputation = 0; + @Column(name = "elo_points") + private int eloPoints = 0; + @Builder.Default @Transient //won't be created in DB private BigDecimal globalScore = BigDecimal.valueOf(-1); diff --git a/src/main/java/com/chessgrinder/chessgrinder/mappers/UserMapper.java b/src/main/java/com/chessgrinder/chessgrinder/mappers/UserMapper.java index 897b273e..8bc2547b 100644 --- a/src/main/java/com/chessgrinder/chessgrinder/mappers/UserMapper.java +++ b/src/main/java/com/chessgrinder/chessgrinder/mappers/UserMapper.java @@ -34,6 +34,7 @@ public UserDto toDto( .name(user.getName()) .roles(user.getRoles().stream().map(RoleEntity::getName).collect(Collectors.toList())) .reputation(user.getReputation()) + .eloPoints(user.getEloPoints()) .globalScore(user.getGlobalScore()) .build(); } diff --git a/src/main/java/com/chessgrinder/chessgrinder/security/SecurityUtil.java b/src/main/java/com/chessgrinder/chessgrinder/security/SecurityUtil.java index cdc9ee8a..97f039e4 100644 --- a/src/main/java/com/chessgrinder/chessgrinder/security/SecurityUtil.java +++ b/src/main/java/com/chessgrinder/chessgrinder/security/SecurityUtil.java @@ -43,4 +43,7 @@ public static UserEntity tryGetUserEntity(@Nullable Authentication authenticatio private SecurityUtil() { } + public static boolean isAuthorizedUser(UserEntity user) { + return user != null && user.getId() != null; + } } diff --git a/src/main/java/com/chessgrinder/chessgrinder/service/AdvancedEloCalculationStrategy.java b/src/main/java/com/chessgrinder/chessgrinder/service/AdvancedEloCalculationStrategy.java new file mode 100644 index 00000000..a5cbf793 --- /dev/null +++ b/src/main/java/com/chessgrinder/chessgrinder/service/AdvancedEloCalculationStrategy.java @@ -0,0 +1,58 @@ +package com.chessgrinder.chessgrinder.service; + +import com.chessgrinder.chessgrinder.dto.EloUpdateResultDto; +import com.chessgrinder.chessgrinder.enums.MatchResult; +import org.springframework.stereotype.Component; + +@Component +public class AdvancedEloCalculationStrategy implements EloCalculationStrategy { + + private static final double K_FACTOR = 32; + private static final int UNRATED_WIN_POINTS = 5; + private static final int UNRATED_LOSE_POINTS = -5; + private static final int UNRATED_DRAW_POINTS = 1; + + public static double calculateExpectedScore(int whiteElo, int blackElo) { + return 1.0 / (1 + Math.pow(10, (blackElo - whiteElo) / 400.0)); + } + + public static int calculateNewElo(int whiteElo, int blackElo, double score) { + double expectedScore = calculateExpectedScore(whiteElo, blackElo); + return (int) Math.round(whiteElo + K_FACTOR * (score - expectedScore)); + } + + + @Override + public EloUpdateResultDto calculateElo(int whiteElo, int blackElo, MatchResult result, boolean bothUsersAuthorized) { + + if (result == null) { + return EloUpdateResultDto.builder() + .whiteNewElo(whiteElo) + .blackNewElo(blackElo) + .build(); + } + + int whitePoints = 0; + int blackPoints = 0; + + if (result == MatchResult.WHITE_WIN) { + whitePoints = bothUsersAuthorized ? (calculateNewElo(whiteElo, blackElo, 1.0) - whiteElo) : UNRATED_WIN_POINTS; + blackPoints = bothUsersAuthorized ? (calculateNewElo(blackElo, whiteElo, 0) - blackElo) : UNRATED_LOSE_POINTS; + } else if (result == MatchResult.BLACK_WIN) { + whitePoints = bothUsersAuthorized ? (calculateNewElo(whiteElo, blackElo, 0) - whiteElo) : UNRATED_LOSE_POINTS; + blackPoints = bothUsersAuthorized ? (calculateNewElo(blackElo, whiteElo, 1.0) - blackElo) : UNRATED_WIN_POINTS; + } else if (result == MatchResult.DRAW) { + whitePoints = bothUsersAuthorized ? (calculateNewElo(whiteElo, blackElo, 0.5) - whiteElo) : UNRATED_DRAW_POINTS; + blackPoints = bothUsersAuthorized ? (calculateNewElo(blackElo, whiteElo, 0.5) - blackElo) : UNRATED_DRAW_POINTS; + } + + int whiteNewElo = whiteElo + whitePoints; + int blackNewElo = blackElo + blackPoints; + + return EloUpdateResultDto.builder() + .whiteNewElo(whiteNewElo) + .blackNewElo(blackNewElo) + .build(); + } + +} diff --git a/src/main/java/com/chessgrinder/chessgrinder/service/DefaultEloCalculationStrategy.java b/src/main/java/com/chessgrinder/chessgrinder/service/DefaultEloCalculationStrategy.java new file mode 100644 index 00000000..2edb64ca --- /dev/null +++ b/src/main/java/com/chessgrinder/chessgrinder/service/DefaultEloCalculationStrategy.java @@ -0,0 +1,46 @@ +package com.chessgrinder.chessgrinder.service; + +import com.chessgrinder.chessgrinder.dto.EloUpdateResultDto; +import com.chessgrinder.chessgrinder.enums.MatchResult; +import org.springframework.stereotype.Component; + + +public class DefaultEloCalculationStrategy implements EloCalculationStrategy { + + private static final int WIN_POINTS = 10; + private static final int LOSE_POINTS = -10; + private static final int UNRATED_WIN_POINTS = 5; + private static final int UNRATED_LOSE_POINTS = -5; + + @Override + public EloUpdateResultDto calculateElo(int whiteElo, int blackElo, MatchResult result, boolean bothUsersAuthorized) { + + if (result == null) { + return EloUpdateResultDto.builder() + .whiteNewElo(whiteElo) + .blackNewElo(blackElo) + .build(); + } + int whitePoints = 0; + int blackPoints = 0; + + int winPoints = bothUsersAuthorized ? WIN_POINTS : UNRATED_WIN_POINTS; + int losePoints = bothUsersAuthorized ? LOSE_POINTS : UNRATED_LOSE_POINTS; + + if (result == MatchResult.WHITE_WIN) { + whitePoints = winPoints; + blackPoints = losePoints; + } else if (result == MatchResult.BLACK_WIN) { + whitePoints = losePoints; + blackPoints = winPoints; + } + + int whiteNewElo = whiteElo + whitePoints; + int blackNewElo = blackElo + blackPoints; + + return EloUpdateResultDto.builder() + .whiteNewElo(whiteNewElo) + .blackNewElo(blackNewElo) + .build(); + } +} diff --git a/src/main/java/com/chessgrinder/chessgrinder/service/EloCalculationStrategy.java b/src/main/java/com/chessgrinder/chessgrinder/service/EloCalculationStrategy.java new file mode 100644 index 00000000..69e0e1d1 --- /dev/null +++ b/src/main/java/com/chessgrinder/chessgrinder/service/EloCalculationStrategy.java @@ -0,0 +1,8 @@ +package com.chessgrinder.chessgrinder.service; + +import com.chessgrinder.chessgrinder.dto.EloUpdateResultDto; +import com.chessgrinder.chessgrinder.enums.MatchResult; + +public interface EloCalculationStrategy { + EloUpdateResultDto calculateElo(int whiteElo, int blackElo, MatchResult result, boolean bothUsersAuthorized); +} diff --git a/src/main/java/com/chessgrinder/chessgrinder/service/EloService.java b/src/main/java/com/chessgrinder/chessgrinder/service/EloService.java new file mode 100644 index 00000000..4891ab25 --- /dev/null +++ b/src/main/java/com/chessgrinder/chessgrinder/service/EloService.java @@ -0,0 +1,10 @@ +package com.chessgrinder.chessgrinder.service; + +import com.chessgrinder.chessgrinder.entities.TournamentEntity; + + +public interface EloService { + + void processTournamentAndUpdateElo(TournamentEntity tournament); + void rollbackEloChanges(TournamentEntity tournament); +} diff --git a/src/main/java/com/chessgrinder/chessgrinder/service/EloServiceImpl.java b/src/main/java/com/chessgrinder/chessgrinder/service/EloServiceImpl.java new file mode 100644 index 00000000..f2e8abcf --- /dev/null +++ b/src/main/java/com/chessgrinder/chessgrinder/service/EloServiceImpl.java @@ -0,0 +1,189 @@ +package com.chessgrinder.chessgrinder.service; + +import com.chessgrinder.chessgrinder.dto.EloUpdateResultDto; +import com.chessgrinder.chessgrinder.entities.*; +import com.chessgrinder.chessgrinder.repositories.ParticipantRepository; +import com.chessgrinder.chessgrinder.repositories.TournamentRepository; +import com.chessgrinder.chessgrinder.repositories.UserRepository; +import com.chessgrinder.chessgrinder.security.SecurityUtil; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +@RequiredArgsConstructor +@Service +public class EloServiceImpl implements EloService { + + private final UserEloInitializerService userEloInitializerService; + private final EloCalculationStrategy eloCalculationStrategy; + private final ParticipantRepository participantRepository; + private final UserRepository userRepository; + private final TournamentRepository tournamentRepository; + + + @Value("${chessgrinder.feature.eloServiceEnabled:false}") + private boolean eloServiceEnabled; + + @Override + @Transactional + public void processTournamentAndUpdateElo(TournamentEntity tournament) { + + if (!eloServiceEnabled) { + System.out.println("Elo Service is disabled, skipping processing."); + return; + } + + Map currentEloMap = initializeAuthorizedParticipantsElo(tournament); + + processMatches(tournament, currentEloMap); + + finalizeTournament(tournament, currentEloMap); + } + + private Map initializeAuthorizedParticipantsElo(TournamentEntity tournament) { + Map currentEloMap = new HashMap<>(); + + for (RoundEntity round : tournament.getRounds()) { + for (MatchEntity match : round.getMatches()) { + initializeParticipantElo(match.getParticipant1(), currentEloMap); + initializeParticipantElo(match.getParticipant2(), currentEloMap); + } + } + return currentEloMap; + } + + private void initializeParticipantElo(ParticipantEntity participant, Map currentEloMap) { + if (participant == null || participant.getUser() == null) { + return; + } + + UserEntity user = participant.getUser(); + + userEloInitializerService.setDefaultEloIfNeeded(user, SecurityUtil.isAuthorizedUser(user)); + + if (participant.getInitialEloPoints() == 0) { + participant.setInitialEloPoints(user.getEloPoints()); + participantRepository.save(participant); + } + + currentEloMap.putIfAbsent(participant.getId(), participant.getInitialEloPoints()); + } + + private void processMatches(TournamentEntity tournament, Map currentEloMap) { + for (RoundEntity round : tournament.getRounds()) { + for (MatchEntity match : round.getMatches()) { + processSingleMatch(match, currentEloMap); + } + } + } + + private void processSingleMatch(MatchEntity match, Map currentEloMap) { + ParticipantEntity participant1 = match.getParticipant1(); + ParticipantEntity participant2 = match.getParticipant2(); + + if (participant1 == null || participant2 == null) { + return; + } + + UserEntity user1 = participant1.getUser(); + UserEntity user2 = participant2.getUser(); + + boolean isUser1Authorized = SecurityUtil.isAuthorizedUser(user1); + boolean isUser2Authorized = SecurityUtil.isAuthorizedUser(user2); + + if (!isUser1Authorized && !isUser2Authorized) { + return; + } + + setInitialElo(participant1, user1, isUser1Authorized, currentEloMap); + setInitialElo(participant2, user2, isUser2Authorized, currentEloMap); + + calculateAndApplyElo(match, participant1, participant2, currentEloMap); + } + + private void setInitialElo(ParticipantEntity participant, UserEntity user, boolean isAuthorized, Map currentEloMap) { + if (isAuthorized && participant.getInitialEloPoints() == 0) { + participant.setInitialEloPoints(user.getEloPoints()); + currentEloMap.put(participant.getId(), user.getEloPoints()); + participantRepository.save(participant); + } + } + + private void calculateAndApplyElo(MatchEntity match, ParticipantEntity participant1, ParticipantEntity participant2, Map currentEloMap) { + int whiteElo = currentEloMap.getOrDefault(participant1.getId(), 1200); // начальный Elo, если не установлен + int blackElo = currentEloMap.getOrDefault(participant2.getId(), 1200); + + boolean bothUsersAuthorized = SecurityUtil.isAuthorizedUser(participant1.getUser()) && SecurityUtil.isAuthorizedUser(participant2.getUser()); + + EloUpdateResultDto updateResult = eloCalculationStrategy.calculateElo(whiteElo, blackElo, match.getResult(), bothUsersAuthorized); + + int whiteEloChange = updateResult.getWhiteNewElo() - whiteElo; + int blackEloChange = updateResult.getBlackNewElo() - blackElo; + + + currentEloMap.put(participant1.getId(), whiteElo + whiteEloChange); + currentEloMap.put(participant2.getId(), blackElo + blackEloChange); + + participant1.setFinalEloPoints(participant1.getFinalEloPoints() + whiteEloChange); + participant2.setFinalEloPoints(participant2.getFinalEloPoints() + blackEloChange); + + participantRepository.save(participant1); + participantRepository.save(participant2); + } + + + private void finalizeTournament(TournamentEntity tournament, Map currentEloMap) { + for (Map.Entry entry : currentEloMap.entrySet()) { + ParticipantEntity participant = participantRepository.findById(entry.getKey()).orElse(null); + if (participant != null) { + UserEntity user = participant.getUser(); + + if (SecurityUtil.isAuthorizedUser(user)) { + int newElo = user.getEloPoints() + participant.getFinalEloPoints(); + user.setEloPoints(newElo); + userRepository.save(user); + } + } + } + + tournament.setHasEloCalculated(true); + tournamentRepository.save(tournament); + } + + @Override + public void rollbackEloChanges(TournamentEntity tournament) { + + if (!eloServiceEnabled) { + System.out.println("Elo Service is disabled, rollback skipped."); + return; + } + + Set uniqueParticipants = tournament.getRounds().stream() + .flatMap(round -> round.getMatches().stream()) + .flatMap(match -> Stream.of(match.getParticipant1(), match.getParticipant2())) + .filter(participant -> participant != null && participant.getUser() != null) // Фильтруем только участников с пользователями + .collect(Collectors.toSet()); + + for (ParticipantEntity participant : uniqueParticipants) { + UserEntity user = participant.getUser(); + + if (participant.getFinalEloPoints() != 0) { + int originalElo = user.getEloPoints() - participant.getFinalEloPoints(); + user.setEloPoints(originalElo); + userRepository.save(user); + } + + participant.setFinalEloPoints(0); + participantRepository.save(participant); // Сохраняем изменения для участника + + } + + tournament.setHasEloCalculated(false); + tournamentRepository.save(tournament); + } +} diff --git a/src/main/java/com/chessgrinder/chessgrinder/service/RoundService.java b/src/main/java/com/chessgrinder/chessgrinder/service/RoundService.java index 65016aa0..43755afd 100644 --- a/src/main/java/com/chessgrinder/chessgrinder/service/RoundService.java +++ b/src/main/java/com/chessgrinder/chessgrinder/service/RoundService.java @@ -105,6 +105,7 @@ public void finishRound(UUID tournamentId, Integer roundNumber) { throw new IllegalStateException("Can not finish round with unknown match result"); } } + roundEntity.setFinished(true); roundRepository.save(roundEntity); try { @@ -164,7 +165,7 @@ public void makePairings(UUID tournamentId, Integer roundNumber) { List participantEntities = participantRepository.findByTournamentId(tournamentId); List participantDtos = participantMapper.toDto(participantEntities); - TournamentEntity tournament = tournamentRepository.findById(tournamentId).get(); + TournamentEntity tournament = tournamentRepository.findById(tournamentId).orElseThrow(); List> allMatchesInTheTournament = tournament.getRounds().stream() .filter(RoundEntity::isFinished) @@ -204,6 +205,9 @@ public void makePairings(UUID tournamentId, Integer roundNumber) { public void updateResults(UUID tournamentId) { List matches = matchRepository.findFinishedByTournamentId(tournamentId); + + matches.forEach(this::reverseEloUpdate); + // Map pointsMap = new HashMap<>(); Map> enemiesMap = new HashMap<>(); @@ -266,6 +270,11 @@ public void updateResults(UUID tournamentId) { participantRepository.saveAll(participants); } + private void reverseEloUpdate(MatchEntity match) { + + + } + private static Comparator compareParticipantEntityByPersonalEncounterWinnerFirst(List tournamentRoundEntities) { return (participant1, participant2) -> { ParticipantEntity winnerBetweenTwoParticipants = findWinnerBetweenTwoParticipants(participant1, participant2, tournamentRoundEntities); diff --git a/src/main/java/com/chessgrinder/chessgrinder/service/TournamentService.java b/src/main/java/com/chessgrinder/chessgrinder/service/TournamentService.java index 4078350c..ba856c5f 100644 --- a/src/main/java/com/chessgrinder/chessgrinder/service/TournamentService.java +++ b/src/main/java/com/chessgrinder/chessgrinder/service/TournamentService.java @@ -19,6 +19,7 @@ import java.util.UUID; import java.util.stream.Collectors; + @Component @RequiredArgsConstructor @Slf4j @@ -27,6 +28,7 @@ public class TournamentService { private final RoundRepository roundRepository; private final TournamentMapper tournamentMapper; private final RoundService roundService; + private final EloServiceImpl eloService; private static final int DEFAULT_ROUNDS_NUMBER = 6; private static final int MIN_ROUNDS_NUMBER = 0; private static final int MAX_ROUNDS_NUMBER = 99; @@ -59,19 +61,35 @@ public TournamentDto createTournament(LocalDateTime date) { .build(); roundRepository.save(firstRoundEntity); + return tournamentMapper.toDto(tournamentEntity); } public void startTournament(UUID tournamentId) { + tournamentRepository.findById(tournamentId).ifPresent(tournament -> { + if (tournament.getStatus() == TournamentStatus.FINISHED && tournament.isHasEloCalculated()) { + try { + eloService.rollbackEloChanges(tournament); + tournament.setHasEloCalculated(false); + } catch (Exception e) { + log.error("Could not revert Elo changes when reopening the tournament", e); + throw new RuntimeException("Error reverting Elo changes when reopening the tournament", e); + } + } tournament.setStatus(TournamentStatus.ACTIVE); + tournamentRepository.save(tournament); }); } public void finishTournament(UUID tournamentId) { + TournamentEntity tournament = tournamentRepository.findById(tournamentId) + .orElseThrow(() -> new IllegalArgumentException("Tournament not found with ID: " + tournamentId)); + List rounds = roundRepository.findByTournamentId(tournamentId); + boolean allRoundsFinished = true; for (RoundEntity round : rounds) { @@ -79,12 +97,10 @@ public void finishTournament(UUID tournamentId) { boolean allMatchesHaveResults = round.getMatches().stream() .allMatch(match -> match.getResult() != null); - if (allMatchesHaveResults) { round.setFinished(true); roundRepository.save(round); - } - else { + } else { allRoundsFinished = false; break; } @@ -98,14 +114,20 @@ public void finishTournament(UUID tournamentId) { try { roundService.updateResults(tournamentId); + } catch (Exception e) { log.error("Could not update results", e); } - tournamentRepository.findById(tournamentId).ifPresent(tournament -> { - tournament.setStatus(TournamentStatus.FINISHED); - tournamentRepository.save(tournament); - }); + try { + eloService.processTournamentAndUpdateElo(tournament); + } catch (Exception e) { + log.error("Could not finalize Elo ratings", e); + } + + tournament.setStatus(TournamentStatus.FINISHED); + tournamentRepository.save(tournament); + } public void deleteTournament(UUID tournamentId) { diff --git a/src/main/java/com/chessgrinder/chessgrinder/service/UserEloInitializerService.java b/src/main/java/com/chessgrinder/chessgrinder/service/UserEloInitializerService.java new file mode 100644 index 00000000..b0893ab7 --- /dev/null +++ b/src/main/java/com/chessgrinder/chessgrinder/service/UserEloInitializerService.java @@ -0,0 +1,22 @@ +package com.chessgrinder.chessgrinder.service; + +import com.chessgrinder.chessgrinder.entities.UserEntity; +import com.chessgrinder.chessgrinder.repositories.UserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@RequiredArgsConstructor +@Service +public class UserEloInitializerService { + + private static final int DEFAULT_ELO_POINTS = 1200; + + private final UserRepository userRepository; + + public void setDefaultEloIfNeeded(UserEntity user, boolean isAuthorized) { + if (isAuthorized && user.getEloPoints() == 0) { + user.setEloPoints(DEFAULT_ELO_POINTS); + userRepository.save(user); + } + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 767dd912..0dea0d58 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -17,3 +17,4 @@ spring.flyway.enabled=true ## ChessGrinder application properties #chessgrinder.security.adminEmail=foo@example.com chessgrinder.feature.auth.signupWithPasswordEnabled=false +chessgrinder.feature.eloServiceEnabled=false diff --git a/src/main/resources/db/migration/V202408211800__alter_table__users_table__add_elo_column.sql b/src/main/resources/db/migration/V202408211800__alter_table__users_table__add_elo_column.sql new file mode 100644 index 00000000..b8a9e2fd --- /dev/null +++ b/src/main/resources/db/migration/V202408211800__alter_table__users_table__add_elo_column.sql @@ -0,0 +1,2 @@ +ALTER TABLE users_table + ADD COLUMN elo_points INT DEFAULT 0; diff --git a/src/main/resources/db/migration/V202408271500__alter_table__participants_table__add__initial_elo_column.sql b/src/main/resources/db/migration/V202408271500__alter_table__participants_table__add__initial_elo_column.sql new file mode 100644 index 00000000..9fa3a919 --- /dev/null +++ b/src/main/resources/db/migration/V202408271500__alter_table__participants_table__add__initial_elo_column.sql @@ -0,0 +1,2 @@ +ALTER TABLE participants_table + ADD COLUMN initial_elo_points INT DEFAULT 0; diff --git a/src/main/resources/db/migration/V202409051800__alter_table__tournaments_table__add__has_elo_calculated_column.sql b/src/main/resources/db/migration/V202409051800__alter_table__tournaments_table__add__has_elo_calculated_column.sql new file mode 100644 index 00000000..24de841a --- /dev/null +++ b/src/main/resources/db/migration/V202409051800__alter_table__tournaments_table__add__has_elo_calculated_column.sql @@ -0,0 +1,2 @@ +ALTER TABLE tournaments_table + ADD COLUMN has_elo_calculated Boolean DEFAULT false; diff --git a/src/main/resources/db/migration/V202409101300__alter_table__participants_table__add__final_elo_points_column.sql b/src/main/resources/db/migration/V202409101300__alter_table__participants_table__add__final_elo_points_column.sql new file mode 100644 index 00000000..cac3f17f --- /dev/null +++ b/src/main/resources/db/migration/V202409101300__alter_table__participants_table__add__final_elo_points_column.sql @@ -0,0 +1,2 @@ +ALTER TABLE participants_table + ADD COLUMN final_elo_points INT DEFAULT 0; diff --git a/src/test/java/com/chessgrinder/chessgrinder/service/AdvancedEloCalculationStrategyTest.java b/src/test/java/com/chessgrinder/chessgrinder/service/AdvancedEloCalculationStrategyTest.java new file mode 100644 index 00000000..55afccef --- /dev/null +++ b/src/test/java/com/chessgrinder/chessgrinder/service/AdvancedEloCalculationStrategyTest.java @@ -0,0 +1,74 @@ +package com.chessgrinder.chessgrinder.service; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.*; + + +public class AdvancedEloCalculationStrategyTest { + + + @Test + public void testCalculateExpectedScore() { + + int whiteElo = 1600; + int blackElo = 1600; + double expectedScore = AdvancedEloCalculationStrategy.calculateExpectedScore(whiteElo, blackElo); + assertEquals(0.5, expectedScore, 0.01); + + whiteElo = 2000; + blackElo = 1600; + expectedScore = AdvancedEloCalculationStrategy.calculateExpectedScore(whiteElo, blackElo); + assertEquals(0.91, expectedScore, 0.01); + + whiteElo = 1600; + blackElo = 2000; + expectedScore = AdvancedEloCalculationStrategy.calculateExpectedScore(whiteElo, blackElo); + assertEquals(0.09, expectedScore, 0.01); + } + + + @Test + public void testCalculateNewElo() { + + //same Elo scenario + int whiteElo = 1600; + int blackElo = 1600; + double score = 1; // white won + + int whiteNewElo = AdvancedEloCalculationStrategy.calculateNewElo(whiteElo, blackElo, score); + int blackNewElo = AdvancedEloCalculationStrategy.calculateNewElo(blackElo, whiteElo, 0); // black lost + + assertEquals(1616, whiteNewElo); + assertEquals(1584, blackNewElo); + + // High-rated player won + whiteElo = 2000; + blackElo = 1600; + score = 1; // white won + + whiteNewElo = AdvancedEloCalculationStrategy.calculateNewElo(whiteElo, blackElo, score); + blackNewElo = AdvancedEloCalculationStrategy.calculateNewElo(blackElo, whiteElo, 0); // black lost + + assertEquals(2003, whiteNewElo); + assertEquals(1597, blackNewElo); + + // low-rated player won + score = 1; // black won + blackNewElo = AdvancedEloCalculationStrategy.calculateNewElo(blackElo, whiteElo, score); + whiteNewElo = AdvancedEloCalculationStrategy.calculateNewElo(whiteElo, blackElo, 0); // white lost + + assertEquals(1629, blackNewElo); + assertEquals(1971, whiteNewElo); + + // Draw scenario for big elo diff + whiteElo = 2000; + blackElo = 1400; + score = 0.5; // draw + whiteNewElo = AdvancedEloCalculationStrategy.calculateNewElo(whiteElo, blackElo, score); + blackNewElo = AdvancedEloCalculationStrategy.calculateNewElo(blackElo, whiteElo, score); + + assertEquals(1985, whiteNewElo); + assertEquals(1415, blackNewElo); + } +} diff --git a/src/test/java/com/chessgrinder/chessgrinder/service/EloServiceImplTest.java b/src/test/java/com/chessgrinder/chessgrinder/service/EloServiceImplTest.java new file mode 100644 index 00000000..e4c606bd --- /dev/null +++ b/src/test/java/com/chessgrinder/chessgrinder/service/EloServiceImplTest.java @@ -0,0 +1,702 @@ +package com.chessgrinder.chessgrinder.service; + +import com.chessgrinder.chessgrinder.dto.EloUpdateResultDto; +import com.chessgrinder.chessgrinder.entities.*; +import com.chessgrinder.chessgrinder.enums.MatchResult; +import com.chessgrinder.chessgrinder.enums.TournamentStatus; +import com.chessgrinder.chessgrinder.repositories.ParticipantRepository; +import com.chessgrinder.chessgrinder.repositories.TournamentRepository; +import com.chessgrinder.chessgrinder.repositories.UserRepository; +import com.chessgrinder.chessgrinder.security.SecurityUtil; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.test.util.ReflectionTestUtils; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.*; + +import static org.junit.jupiter.api.Assertions.*; + +public class EloServiceImplTest { + + private EloServiceImpl eloService; + private UserEloInitializerService userEloInitializerService; + private DefaultEloCalculationStrategy defaultEloCalculationStrategy; + private ParticipantRepository participantRepository; + private TournamentRepository tournamentRepository; + private UserRepository userRepository; + + @BeforeEach + public void setUp() { + userRepository = new InMemoryUserRepository(); + userEloInitializerService = new UserEloInitializerService(userRepository); + + defaultEloCalculationStrategy = new DefaultEloCalculationStrategy() { + @Override + public EloUpdateResultDto calculateElo(int whiteElo, int blackElo, MatchResult result, boolean bothUsersAuthorized) { + if (result == MatchResult.DRAW) { + return EloUpdateResultDto.builder() + .whiteNewElo(whiteElo) + .blackNewElo(blackElo) + .build(); + } + int points = bothUsersAuthorized ? 10 : 5; + int playerNewElo = result == MatchResult.WHITE_WIN ? whiteElo + points : whiteElo - points; + int opponentNewElo = result == MatchResult.WHITE_WIN ? blackElo - points : blackElo + points; + return EloUpdateResultDto.builder() + .whiteNewElo(playerNewElo) + .blackNewElo(opponentNewElo) + .build(); + } + }; + + participantRepository = new InMemoryParticipantRepository(); + tournamentRepository = new InMemoryTournamentRepository(); + userRepository = new InMemoryUserRepository(); + + eloService = new EloServiceImpl(userEloInitializerService, defaultEloCalculationStrategy, participantRepository,userRepository, tournamentRepository); + ReflectionTestUtils.setField(eloService, "eloServiceEnabled", true); + } + + @Test + public void testTwoAuthorizedUsersMatch() { + UserEntity user1 = new UserEntity(); + user1.setId(UUID.randomUUID()); + user1.setUsername("user1"); + user1.setEloPoints(1200); + + UserEntity user2 = new UserEntity(); + user2.setId(UUID.randomUUID()); + user2.setUsername("user2"); + user2.setEloPoints(1200); + + + ParticipantEntity participant1 = new ParticipantEntity(); + participant1.setId(UUID.randomUUID()); + participant1.setUser(user1); + + ParticipantEntity participant2 = new ParticipantEntity(); + participant2.setId(UUID.randomUUID()); + participant2.setUser(user2); + + MatchEntity match = new MatchEntity(); + match.setParticipant1(participant1); + match.setParticipant2(participant2); + match.setResult(MatchResult.WHITE_WIN); + + + RoundEntity round = new RoundEntity(); + round.setMatches(List.of(match)); + round.setFinished(true); // помечаем раунд как завершенный + + // Создаем турнир и добавляем раунд + TournamentEntity tournament = new TournamentEntity(); + tournament.setId(UUID.randomUUID()); + tournament.setName("Test Tournament"); + tournament.setRounds(List.of(round)); // добавляем раунд в турнир + tournament.setStatus(TournamentStatus.ACTIVE); + + + eloService.processTournamentAndUpdateElo(tournament); + + assertEquals(1210, user1.getEloPoints()); + assertEquals(1190, user2.getEloPoints()); + } + + @Test + public void testEloRecalculationAfterMatchResultChange() { + // Step 1: Создаем двух пользователей + UserEntity user1 = new UserEntity(); + user1.setId(UUID.randomUUID()); + user1.setUsername("user1"); + user1.setEloPoints(1200); + + UserEntity user2 = new UserEntity(); + user2.setId(UUID.randomUUID()); + user2.setUsername("user2"); + user2.setEloPoints(1200); + + // Step 2: Создаем участников + ParticipantEntity participant1 = new ParticipantEntity(); + participant1.setId(UUID.randomUUID()); + participant1.setUser(user1); + + + ParticipantEntity participant2 = new ParticipantEntity(); + participant2.setId(UUID.randomUUID()); + participant2.setUser(user2); + + + // Step 3: Создаем матч с результатом WHITE_WIN + MatchEntity match = new MatchEntity(); + match.setParticipant1(participant1); + match.setParticipant2(participant2); + match.setResult(MatchResult.WHITE_WIN); + + // Step 4: Создаем раунд и добавляем матч в раунд + RoundEntity round = new RoundEntity(); + round.setMatches(List.of(match)); + round.setFinished(true); + + // Step 5: Создаем турнир и добавляем раунд + TournamentEntity tournament = new TournamentEntity(); + tournament.setId(UUID.randomUUID()); + tournament.setName("Test Tournament"); + tournament.setRounds(List.of(round)); + tournament.setStatus(TournamentStatus.ACTIVE); + + // Step 6: Рассчитываем начальный рейтинг ELO + eloService.processTournamentAndUpdateElo(tournament); + + assertEquals(1210, user1.getEloPoints()); + assertEquals(1190, user2.getEloPoints()); + + // Step 7: Изменяем результат матча на ничью (DRAW) + match.setResult(MatchResult.BLACK_WIN); + + + // Step 8: Откатываем изменения рейтинга ELO + eloService.rollbackEloChanges(tournament); + + // Проверяем, что рейтинги были откатаны + assertEquals(1200, user1.getEloPoints()); + assertEquals(1200, user2.getEloPoints()); + + // Step 9: Пересчитываем ELO с учетом нового результата + eloService.processTournamentAndUpdateElo(tournament); + + // Step 10: Проверяем, что рейтинги изменились корректно + assertEquals(1190, user1.getEloPoints()); + assertEquals(1210, user2.getEloPoints()); + } + + @Test + public void testEloRecalculationAfterNoChange() { + // Step 1: Создаем двух пользователей + UserEntity user1 = new UserEntity(); + user1.setId(UUID.randomUUID()); + user1.setUsername("user1"); + user1.setEloPoints(1200); + + UserEntity user2 = new UserEntity(); + user2.setId(UUID.randomUUID()); + user2.setUsername("user2"); + user2.setEloPoints(1200); + + // Step 2: Создаем участников + ParticipantEntity participant1 = new ParticipantEntity(); + participant1.setId(UUID.randomUUID()); + participant1.setUser(user1); + + + ParticipantEntity participant2 = new ParticipantEntity(); + participant2.setId(UUID.randomUUID()); + participant2.setUser(user2); + + + // Step 3: Создаем матч с результатом WHITE_WIN + MatchEntity match = new MatchEntity(); + match.setParticipant1(participant1); + match.setParticipant2(participant2); + match.setResult(MatchResult.WHITE_WIN); + + // Step 4: Создаем раунд и добавляем матч в раунд + RoundEntity round = new RoundEntity(); + round.setMatches(List.of(match)); + round.setFinished(true); + + // Step 5: Создаем турнир и добавляем раунд + TournamentEntity tournament = new TournamentEntity(); + tournament.setId(UUID.randomUUID()); + tournament.setName("Test Tournament"); + tournament.setRounds(List.of(round)); + tournament.setStatus(TournamentStatus.ACTIVE); + + // Step 6: Рассчитываем начальный рейтинг ELO + eloService.processTournamentAndUpdateElo(tournament); + + assertEquals(1210, user1.getEloPoints()); + assertEquals(1190, user2.getEloPoints()); + + // Step 7: Изменяем результат матча на ничью (DRAW) + match.setResult(MatchResult.WHITE_WIN); + + + // Step 8: Откатываем изменения рейтинга ELO + eloService.rollbackEloChanges(tournament); + + // Проверяем, что рейтинги были откатаны + assertEquals(1200, user1.getEloPoints()); + assertEquals(1200, user2.getEloPoints()); + + // Step 9: Пересчитываем ELO с учетом нового результата + eloService.processTournamentAndUpdateElo(tournament); + + // Step 10: Проверяем, что рейтинги изменились корректно + assertEquals(1210, user1.getEloPoints()); + assertEquals(1190, user2.getEloPoints()); + } + @Test + public void testAuthorizedVsUnauthorizedUserMatch() { + + UserEntity user1 = new UserEntity(); + user1.setId(UUID.randomUUID()); + user1.setUsername("user1"); + user1.setEloPoints(1200); + + UserEntity user2 = new UserEntity(); + user2.setId(null); + user2.setUsername("user2"); + user2.setEloPoints(0); + + ParticipantEntity participant1 = new ParticipantEntity(); + participant1.setId(UUID.randomUUID()); + participant1.setUser(user1); + + ParticipantEntity participant2 = new ParticipantEntity(); + participant2.setId(UUID.randomUUID()); + participant2.setUser(user2); + + boolean isUser1Authorized = SecurityUtil.isAuthorizedUser(user1); + boolean isUser2Authorized = SecurityUtil.isAuthorizedUser(user2); + + assertTrue(isUser1Authorized); + assertFalse(isUser2Authorized); + + + MatchEntity match = new MatchEntity(); + match.setParticipant1(participant1); + match.setParticipant2(participant2); + match.setResult(MatchResult.WHITE_WIN); + + + // Создаем раунд и добавляем матч + RoundEntity round = new RoundEntity(); + round.setMatches(List.of(match)); + round.setFinished(true); // помечаем раунд как завершенный + + // Создаем турнир и добавляем раунд + TournamentEntity tournament = new TournamentEntity(); + tournament.setId(UUID.randomUUID()); + tournament.setName("Test Tournament"); + tournament.setRounds(List.of(round)); // добавляем раунд в турнир + tournament.setStatus(TournamentStatus.ACTIVE); // статус турнира "В процессе" + + eloService.processTournamentAndUpdateElo(tournament); + + // Проверяем, что рейтинг ELO авторизованного пользователя был корректно обновлен, + // а неавторизованный пользователь остался без рейтинга + assertEquals(1205, user1.getEloPoints()); + assertEquals(0, user2.getEloPoints()); + } + + @Test + public void testDrawMatch() { + UserEntity user1 = new UserEntity(); + user1.setId(UUID.randomUUID()); + user1.setUsername("user1"); + user1.setEloPoints(1200); + + UserEntity user2 = new UserEntity(); + user2.setId(UUID.randomUUID()); + user2.setUsername("user2"); + user2.setEloPoints(1200); + + ParticipantEntity participant1 = new ParticipantEntity(); + participant1.setId(UUID.randomUUID()); + participant1.setUser(user1); + + ParticipantEntity participant2 = new ParticipantEntity(); + participant2.setId(UUID.randomUUID()); + participant2.setUser(user2); + + // Создаем матч с ничьей + MatchEntity match = new MatchEntity(); + match.setParticipant1(participant1); + match.setParticipant2(participant2); + match.setResult(MatchResult.DRAW); + + // Создаем раунд и добавляем матч + RoundEntity round = new RoundEntity(); + round.setMatches(List.of(match)); + round.setFinished(true); // помечаем раунд как завершенный + + // Создаем турнир и добавляем раунд + TournamentEntity tournament = new TournamentEntity(); + tournament.setId(UUID.randomUUID()); + tournament.setName("Test Tournament"); + tournament.setRounds(List.of(round)); // добавляем раунд в турнир + tournament.setStatus(TournamentStatus.ACTIVE); + + eloService.processTournamentAndUpdateElo(tournament); + + assertEquals(1200, user1.getEloPoints()); + assertEquals(1200, user2.getEloPoints()); + } + + @Test + public void testGeneralScenarioWithMultipleRounds() { + + UserEntity user1 = new UserEntity(); + user1.setId(UUID.randomUUID()); + user1.setUsername("user1"); + user1.setEloPoints(1200); + + UserEntity user2 = new UserEntity(); + user2.setId(UUID.randomUUID()); + user2.setUsername("user2"); + user2.setEloPoints(1200); + + UserEntity user3 = new UserEntity(); + user3.setId(null); + user3.setUsername("user3"); + user3.setEloPoints(0); + + UserEntity user4 = new UserEntity(); + user4.setId(null); + user4.setUsername("user4"); + user4.setEloPoints(0); + + + ParticipantEntity participant1 = new ParticipantEntity(); + participant1.setId(UUID.randomUUID()); + participant1.setUser(user1); + + ParticipantEntity participant2 = new ParticipantEntity(); + participant2.setId(UUID.randomUUID()); + participant2.setUser(user2); + + ParticipantEntity participant3 = new ParticipantEntity(); + participant3.setId(UUID.randomUUID()); + participant3.setUser(user3); + + ParticipantEntity participant4 = new ParticipantEntity(); + participant4.setId(UUID.randomUUID()); + participant4.setUser(user4); + + + boolean isUser1Authorized = SecurityUtil.isAuthorizedUser(user1); + boolean isUser2Authorized = SecurityUtil.isAuthorizedUser(user2); + boolean isUser3Authorized = SecurityUtil.isAuthorizedUser(user3); + boolean isUser4Authorized = SecurityUtil.isAuthorizedUser(user4); + + assertTrue(isUser1Authorized); // user1 авторизован + assertTrue(isUser2Authorized); // user2 авторизован + assertFalse(isUser3Authorized); // user3 неавторизован + assertFalse(isUser4Authorized); // user4 неавторизован + + MatchEntity match1 = new MatchEntity(); + match1.setParticipant1(participant1); + match1.setParticipant2(participant2); + match1.setResult(MatchResult.WHITE_WIN); + + MatchEntity match2 = new MatchEntity(); + match2.setParticipant1(participant3); + match2.setParticipant2(participant4); + match2.setResult(MatchResult.BLACK_WIN); + + MatchEntity match3 = new MatchEntity(); + match3.setParticipant1(participant1); + match3.setParticipant2(participant3); + match3.setResult(MatchResult.WHITE_WIN); + + MatchEntity match4 = new MatchEntity(); + match4.setParticipant1(participant2); + match4.setParticipant2(participant4); + match4.setResult(MatchResult.DRAW); + + // Создаем раунды и добавляем матчи + RoundEntity round1 = new RoundEntity(); + round1.setMatches(List.of(match1, match2)); + round1.setFinished(true); + + RoundEntity round2 = new RoundEntity(); + round2.setMatches(List.of(match3, match4)); + round2.setFinished(true); + + // Создаем турнир и добавляем раунды + TournamentEntity tournament = new TournamentEntity(); + tournament.setId(UUID.randomUUID()); + tournament.setName("Test Tournament"); + tournament.setRounds(List.of(round1, round2)); // добавляем раунды в турнир + tournament.setStatus(TournamentStatus.ACTIVE); // статус турнира "В процессе" + + // Обрабатываем турнир и обновляем рейтинг + eloService.processTournamentAndUpdateElo(tournament); + + + assertEquals(1215, user1.getEloPoints()); // user1 выиграл 2 раза: один раз против авторизованного (+10) и один раз против неавторизованного (+5) + assertEquals(1190, user2.getEloPoints()); // user2 проиграл 1 раз (-10) и сыграл вничью (0) + assertEquals(0, user3.getEloPoints()); // user3 неавторизован, его рейтинг не должен измениться + assertEquals(0, user4.getEloPoints()); // user4 неавторизован, его рейтинг не должен измениться + } + + private static class InMemoryParticipantRepository implements ParticipantRepository { + private final Map data = new HashMap<>(); + + @Override + public S save(S participant) { + if (participant.getId() == null) { + participant.setId(UUID.randomUUID()); // Присваиваем новый UUID, если ID отсутствует + } + data.put(participant.getId(), participant); + return participant; + } + + @Override + public Iterable saveAll(Iterable entities) { + return null; + } + + @Override + public Optional findById(UUID id) { + return Optional.ofNullable(data.get(id)); // Возвращаем Optional, чтобы соответствовать CrudRepository + } + + @Override + public boolean existsById(UUID uuid) { + return false; + } + + public List findAll() { + return new ArrayList<>(data.values()); + } + + @Override + public Iterable findAllById(Iterable uuids) { + return null; + } + + @Override + public long count() { + return 0; + } + + @Override + public void deleteById(UUID uuid) { + + } + + @Override + public void delete(ParticipantEntity entity) { + + } + + @Override + public void deleteAllById(Iterable uuids) { + + } + + @Override + public void deleteAll(Iterable entities) { + + } + + @Override + public void deleteAll() { + + } + + @Override + public List findByTournamentId(UUID tournamentId) { + return List.of(); + } + + @Override + public ParticipantEntity findByTournamentIdAndUserId(UUID tournamentId, UUID userId) { + return null; + } + + @Override + public List findAllByUserId(UUID userId) { + return List.of(); + } + + @Override + public Iterable findAll(Sort sort) { + return null; + } + + @Override + public Page findAll(Pageable pageable) { + return null; + } + } + + private static class InMemoryTournamentRepository implements TournamentRepository { + private final Map data = new HashMap<>(); + + + + @Override + public S save(S entity) { + return null; + } + + @Override + public Iterable saveAll(Iterable entities) { + return null; + } + + @Override + public Optional findById(UUID uuid) { + return Optional.empty(); + } + + @Override + public boolean existsById(UUID uuid) { + return false; + } + + @Override + public List findAll() { + return List.of(); + } + + @Override + public Iterable findAllById(Iterable uuids) { + return null; + } + + @Override + public long count() { + return 0; + } + + @Override + public void deleteById(UUID uuid) { + + } + + @Override + public void delete(TournamentEntity entity) { + + } + + @Override + public void deleteAllById(Iterable uuids) { + + } + + @Override + public void deleteAll(Iterable entities) { + + } + + @Override + public void deleteAll() { + + } + + @Override + public Iterable findAll(Sort sort) { + return null; + } + + @Override + public Page findAll(Pageable pageable) { + return null; + } + } + + private static class InMemoryUserRepository implements UserRepository { + private final Map data = new HashMap<>(); + + @Override + public S save(S user) { + if (user.getId() == null) { + user.setId(UUID.randomUUID()); // Присваиваем новый UUID, если ID отсутствует + } + data.put(user.getId(), user); + return user; // Возвращаем сохраненный объект, чтобы соответствовать CrudRepository + } + + @Override + public Iterable saveAll(Iterable entities) { + return null; + } + + @Override + public Optional findById(UUID id) { + return Optional.ofNullable(data.get(id)); // Возвращаем Optional, чтобы соответствовать CrudRepository + } + + @Override + public boolean existsById(UUID uuid) { + return false; + } + + public List findAll() { + return new ArrayList<>(data.values()); + } + + @Override + public Iterable findAllById(Iterable uuids) { + return null; + } + + @Override + public long count() { + return 0; + } + + @Override + public void deleteById(UUID uuid) { + + } + + @Override + public void delete(UserEntity entity) { + + } + + @Override + public void deleteAllById(Iterable uuids) { + + } + + @Override + public void deleteAll(Iterable entities) { + + } + + @Override + public void deleteAll() { + + } + + @Override + public UserEntity findByUsername(String userName) { + return null; + } + + @Override + public void addReputation(UUID userId, Integer amount) { + + } + + @Override + public List findAllByBadgeId(UUID badgeId) { + return List.of(); + } + + @Override + public BigDecimal getGlobalScore(UUID userId, LocalDateTime globalScoreFromDate, LocalDateTime globalScoreToDate) { + return null; + } + + @Override + public Iterable findAll(Sort sort) { + return null; + } + + @Override + public Page findAll(Pageable pageable) { + return null; + } + } +}