From 80052cb588c9fcd26744d2338904afd17bbc27d6 Mon Sep 17 00:00:00 2001 From: Bhoopesh <82646284+bhoopesh369@users.noreply.github.com> Date: Tue, 13 Feb 2024 20:04:16 +0530 Subject: [PATCH] feat: pvp auto-match (#6) * feat: pvp auto-match * fix: add tiers for pvp leaderboard * update: add pvp shedulers --- docs/spec/CodeCharacter-API.yml | 24 ++- .../codecharacter/core/LeaderboardApi.kt | 2 +- .../delta/codecharacter/dtos/MatchModeDto.kt | 5 +- .../codecharacter/dtos/UserMatchStatDto.kt | 13 +- .../codecharacter/dtos/UserMatchStatsDto.kt | 35 ++++ .../dtos/UserMatchStatsInnerDto.kt | 15 +- .../code/code_revision/CodeRevisionService.kt | 1 - .../codecharacter/server/game/GameService.kt | 9 +- .../leaderboard/LeaderboardController.kt | 3 +- .../server/match/MatchModeEnum.kt | 1 + .../server/match/MatchService.kt | 170 ++++++++++++++---- .../server/match/PvPAutoMatchEntity.kt | 8 + .../server/match/PvPAutoMatchRepository.kt | 7 + .../server/match/PvPMatchRepository.kt | 1 + .../server/pvp_game/PvPGameService.kt | 6 +- .../server/schedulers/SchedulingService.kt | 3 + .../user/public_user/PublicUserEntity.kt | 1 + .../user/public_user/PublicUserRepository.kt | 4 + .../user/public_user/PublicUserService.kt | 155 ++++++++++++---- .../rating_history/RatingHistoryService.kt | 97 ++++++++++ .../server/SecurityTestConfiguration.kt | 4 +- .../server/leaderboard/LeaderboardTest.kt | 4 + .../server/match/MatchServiceTest.kt | 3 + 23 files changed, 469 insertions(+), 102 deletions(-) create mode 100644 library/src/main/kotlin/delta/codecharacter/dtos/UserMatchStatsDto.kt create mode 100644 server/src/main/kotlin/delta/codecharacter/server/match/PvPAutoMatchEntity.kt create mode 100644 server/src/main/kotlin/delta/codecharacter/server/match/PvPAutoMatchRepository.kt diff --git a/docs/spec/CodeCharacter-API.yml b/docs/spec/CodeCharacter-API.yml index 900c3e9..00040a7 100644 --- a/docs/spec/CodeCharacter-API.yml +++ b/docs/spec/CodeCharacter-API.yml @@ -446,6 +446,11 @@ paths: in: query name: size description: Size of the page + - schema: + $ref: '#/components/schemas/TierType' + in: query + name: tier + description: Leaderboard Tier description: Get PvP leaderboard parameters: [] @@ -1661,23 +1666,31 @@ paths: components: schemas: UserMatchStats: - title: UserMatchStats + title: UserMatchStat + type: array + description: User Match Stats array model + items: + anyOf: + - $ref: '#/components/schemas/UserMatchStat' + required: + - stat + + UserMatchStat: + title: UserMatchStat type: object - description: User Match Stats model + description: User Match Stat model properties: avgAtk: type: number - default: 0 dc_wins: type: number - default: 0 coins: type: number - default: 0 required: - avgAtk - dc_wins - coins + PasswordLoginRequest: title: PasswordLoginRequest type: object @@ -2599,6 +2612,7 @@ components: - DAILYCHALLENGE - PVP - SELFPVP + - AUTOPVP description: Match Mode Verdict: type: string diff --git a/library/src/main/kotlin/delta/codecharacter/core/LeaderboardApi.kt b/library/src/main/kotlin/delta/codecharacter/core/LeaderboardApi.kt index a17976a..fa4b82f 100644 --- a/library/src/main/kotlin/delta/codecharacter/core/LeaderboardApi.kt +++ b/library/src/main/kotlin/delta/codecharacter/core/LeaderboardApi.kt @@ -73,7 +73,7 @@ interface LeaderboardApi { value = ["/pvpleaderboard"], produces = ["application/json"] ) - fun getPvPLeaderboard(@Parameter(description = "Index of the page") @Valid @RequestParam(value = "page", required = false) page: kotlin.Int?,@Parameter(description = "Size of the page") @Valid @RequestParam(value = "size", required = false) size: kotlin.Int?): ResponseEntity> { + fun getPvPLeaderboard(@Parameter(description = "Index of the page") @Valid @RequestParam(value = "page", required = false) page: kotlin.Int?,@Parameter(description = "Size of the page") @Valid @RequestParam(value = "size", required = false) size: kotlin.Int?,@Parameter(description = "Leaderboard Tier", schema = Schema(allowableValues = ["TIER_PRACTICE", "TIER1", "TIER2", "TIER3", "TIER4"])) @Valid @RequestParam(value = "tier", required = false) tier: TierTypeDto?): ResponseEntity> { return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) } } diff --git a/library/src/main/kotlin/delta/codecharacter/dtos/MatchModeDto.kt b/library/src/main/kotlin/delta/codecharacter/dtos/MatchModeDto.kt index 1904ec6..29b8813 100644 --- a/library/src/main/kotlin/delta/codecharacter/dtos/MatchModeDto.kt +++ b/library/src/main/kotlin/delta/codecharacter/dtos/MatchModeDto.kt @@ -16,7 +16,7 @@ import io.swagger.v3.oas.annotations.media.Schema /** * Match Mode -* Values: SELF,MANUAL,AUTO,DAILYCHALLENGE,PVP,SELFPVP +* Values: SELF,MANUAL,AUTO,DAILYCHALLENGE,PVP,SELFPVP,AUTOPVP */ enum class MatchModeDto(val value: kotlin.String) { @@ -25,6 +25,7 @@ enum class MatchModeDto(val value: kotlin.String) { @JsonProperty("AUTO") AUTO("AUTO"), @JsonProperty("DAILYCHALLENGE") DAILYCHALLENGE("DAILYCHALLENGE"), @JsonProperty("PVP") PVP("PVP"), - @JsonProperty("SELFPVP") SELFPVP("SELFPVP") + @JsonProperty("SELFPVP") SELFPVP("SELFPVP"), + @JsonProperty("AUTOPVP") AUTOPVP("AUTOPVP") } diff --git a/library/src/main/kotlin/delta/codecharacter/dtos/UserMatchStatDto.kt b/library/src/main/kotlin/delta/codecharacter/dtos/UserMatchStatDto.kt index 4e3c1bf..6ed48d4 100644 --- a/library/src/main/kotlin/delta/codecharacter/dtos/UserMatchStatDto.kt +++ b/library/src/main/kotlin/delta/codecharacter/dtos/UserMatchStatDto.kt @@ -12,24 +12,23 @@ import jakarta.validation.constraints.Pattern import jakarta.validation.constraints.Size import jakarta.validation.Valid import io.swagger.v3.oas.annotations.media.Schema -import java.math.BigDecimal /** * User Match Stat model - * @param avgAtk - * @param dcWins - * @param coins + * @param avgAtk + * @param dcWins + * @param coins */ data class UserMatchStatDto( @Schema(example = "null", required = true, description = "") - @get:JsonProperty("avgAtk", required = true) val avgAtk: java.math.BigDecimal = BigDecimal.ZERO, + @get:JsonProperty("avgAtk", required = true) val avgAtk: java.math.BigDecimal, @Schema(example = "null", required = true, description = "") - @get:JsonProperty("dc_wins", required = true) val dcWins: java.math.BigDecimal = BigDecimal.ZERO, + @get:JsonProperty("dc_wins", required = true) val dcWins: java.math.BigDecimal, @Schema(example = "null", required = true, description = "") - @get:JsonProperty("coins", required = true) val coins: java.math.BigDecimal = BigDecimal.ZERO + @get:JsonProperty("coins", required = true) val coins: java.math.BigDecimal ) { } diff --git a/library/src/main/kotlin/delta/codecharacter/dtos/UserMatchStatsDto.kt b/library/src/main/kotlin/delta/codecharacter/dtos/UserMatchStatsDto.kt new file mode 100644 index 0000000..552fc68 --- /dev/null +++ b/library/src/main/kotlin/delta/codecharacter/dtos/UserMatchStatsDto.kt @@ -0,0 +1,35 @@ +package delta.codecharacter.dtos + +import java.util.Objects +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.DecimalMax +import jakarta.validation.constraints.DecimalMin +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Pattern +import jakarta.validation.constraints.Size +import jakarta.validation.Valid +import io.swagger.v3.oas.annotations.media.Schema + +/** + * User Match Stats model + * @param avgAtk + * @param dcWins + * @param coins + */ +data class UserMatchStatsDto( + + @Schema(example = "null", required = true, description = "") + @get:JsonProperty("avgAtk", required = true) val avgAtk: java.math.BigDecimal, + + @Schema(example = "null", required = true, description = "") + @get:JsonProperty("dc_wins", required = true) val dcWins: java.math.BigDecimal, + + @Schema(example = "null", required = true, description = "") + @get:JsonProperty("coins", required = true) val coins: java.math.BigDecimal +) { + +} + diff --git a/library/src/main/kotlin/delta/codecharacter/dtos/UserMatchStatsInnerDto.kt b/library/src/main/kotlin/delta/codecharacter/dtos/UserMatchStatsInnerDto.kt index f49aa72..1ccf1ac 100644 --- a/library/src/main/kotlin/delta/codecharacter/dtos/UserMatchStatsInnerDto.kt +++ b/library/src/main/kotlin/delta/codecharacter/dtos/UserMatchStatsInnerDto.kt @@ -13,24 +13,23 @@ import jakarta.validation.constraints.Pattern import jakarta.validation.constraints.Size import jakarta.validation.Valid import io.swagger.v3.oas.annotations.media.Schema -import java.math.BigDecimal /** - * - * @param avgAtk - * @param dcWins - * @param coins + * + * @param avgAtk + * @param dcWins + * @param coins */ data class UserMatchStatsInnerDto( @Schema(example = "null", required = true, description = "") - @get:JsonProperty("avgAtk", required = true) val avgAtk: java.math.BigDecimal = BigDecimal.ZERO, + @get:JsonProperty("avgAtk", required = true) val avgAtk: java.math.BigDecimal, @Schema(example = "null", required = true, description = "") - @get:JsonProperty("dc_wins", required = true) val dcWins: java.math.BigDecimal = BigDecimal.ZERO, + @get:JsonProperty("dc_wins", required = true) val dcWins: java.math.BigDecimal, @Schema(example = "null", required = true, description = "") - @get:JsonProperty("coins", required = true) val coins: java.math.BigDecimal = BigDecimal.ZERO + @get:JsonProperty("coins", required = true) val coins: java.math.BigDecimal ) { } diff --git a/server/src/main/kotlin/delta/codecharacter/server/code/code_revision/CodeRevisionService.kt b/server/src/main/kotlin/delta/codecharacter/server/code/code_revision/CodeRevisionService.kt index 13ee845..5e11856 100644 --- a/server/src/main/kotlin/delta/codecharacter/server/code/code_revision/CodeRevisionService.kt +++ b/server/src/main/kotlin/delta/codecharacter/server/code/code_revision/CodeRevisionService.kt @@ -15,7 +15,6 @@ import java.util.UUID class CodeRevisionService(@Autowired private val codeRevisionRepository: CodeRevisionRepository) { fun createCodeRevision(userId: UUID, createCodeRevisionRequestDto: CreateCodeRevisionRequestDto) { - println(createCodeRevisionRequestDto) val (code, message, language) = createCodeRevisionRequestDto val parentCodeRevision = codeRevisionRepository diff --git a/server/src/main/kotlin/delta/codecharacter/server/game/GameService.kt b/server/src/main/kotlin/delta/codecharacter/server/game/GameService.kt index 8238785..3fbc9af 100644 --- a/server/src/main/kotlin/delta/codecharacter/server/game/GameService.kt +++ b/server/src/main/kotlin/delta/codecharacter/server/game/GameService.kt @@ -80,12 +80,13 @@ class GameService( destruction = destructionPercentage, coinsUsed = coinsUsed, status = gameStatus ) val game = gameRepository.save(newGameEntity) - if(!codeTutorialMatchRepository.findById(game.matchId).isPresent) { - gameLogService.saveGameLog(game.id, gameResult.log) - } - else{ + + if(codeTutorialMatchRepository.findById(game.matchId).isPresent) { gameRepository.deleteById(game.id) + return game } + + gameLogService.saveGameLog(game.id, gameResult.log) return game } } diff --git a/server/src/main/kotlin/delta/codecharacter/server/leaderboard/LeaderboardController.kt b/server/src/main/kotlin/delta/codecharacter/server/leaderboard/LeaderboardController.kt index cf4fa3c..9d644da 100644 --- a/server/src/main/kotlin/delta/codecharacter/server/leaderboard/LeaderboardController.kt +++ b/server/src/main/kotlin/delta/codecharacter/server/leaderboard/LeaderboardController.kt @@ -23,7 +23,8 @@ class LeaderboardController(@Autowired private val publicUserService: PublicUser override fun getPvPLeaderboard( page: Int?, size: Int?, + tier: TierTypeDto?, ): ResponseEntity> { - return ResponseEntity.ok(publicUserService.getPvPLeaderboard(page, size)) + return ResponseEntity.ok(publicUserService.getPvPLeaderboard(page, size, tier)) } } diff --git a/server/src/main/kotlin/delta/codecharacter/server/match/MatchModeEnum.kt b/server/src/main/kotlin/delta/codecharacter/server/match/MatchModeEnum.kt index b5b27be..64fedd5 100644 --- a/server/src/main/kotlin/delta/codecharacter/server/match/MatchModeEnum.kt +++ b/server/src/main/kotlin/delta/codecharacter/server/match/MatchModeEnum.kt @@ -5,5 +5,6 @@ enum class MatchModeEnum { SELFPVP, MANUAL, AUTO, + AUTOPVP, PVP } diff --git a/server/src/main/kotlin/delta/codecharacter/server/match/MatchService.kt b/server/src/main/kotlin/delta/codecharacter/server/match/MatchService.kt index dfb8601..bfc6f2a 100644 --- a/server/src/main/kotlin/delta/codecharacter/server/match/MatchService.kt +++ b/server/src/main/kotlin/delta/codecharacter/server/match/MatchService.kt @@ -2,7 +2,6 @@ package delta.codecharacter.server.match import com.fasterxml.jackson.databind.ObjectMapper import delta.codecharacter.dtos.* -import delta.codecharacter.server.code.Code import delta.codecharacter.server.code.LanguageEnum import delta.codecharacter.server.code.code_revision.CodeRevisionService import delta.codecharacter.server.code.latest_code.LatestCodeService @@ -71,6 +70,7 @@ class MatchService( @Autowired private val simpMessagingTemplate: SimpMessagingTemplate, @Autowired private val mapValidator: MapValidator, @Autowired private val autoMatchRepository: AutoMatchRepository, + @Autowired private val pvPAutoMatchRepository: PvPAutoMatchRepository, @Autowired private val statsService : StatsService, @Autowired private val pvPMatchRepository: PvPMatchRepository, @Autowired private val gameRepository: GameRepository, @@ -131,8 +131,6 @@ class MatchService( if (codeRevisionId1==codeRevisionId2) { throw CustomException(HttpStatus.BAD_REQUEST, "Codes must be different") } - println(codeRevisionId1) - println(codeRevisionId2) val (code1, language1) = getCodeFromRevision(userId, codeRevisionId1, CodeTypeDto.PVP) val (code2, language2) = getCodeFromRevision(userId, codeRevisionId2, CodeTypeDto.PVP) val matchId = UUID.randomUUID() @@ -194,7 +192,11 @@ class MatchService( return matchId } - private fun createPvPMatch(publicUser: PublicUserEntity, publicOpponent: PublicUserEntity) : UUID { + private fun createPvPMatch( + publicUser: PublicUserEntity, + publicOpponent: PublicUserEntity, + mode: MatchModeEnum + ) : UUID { val userId = publicUser.userId val opponentId = publicOpponent.userId @@ -209,7 +211,7 @@ class MatchService( PvPMatchEntity( id = matchId, game = game, - mode = MatchModeEnum.PVP, + mode = mode, verdict = MatchVerdictEnum.TIE, createdAt = Instant.now(), totalPoints = 0, @@ -220,6 +222,12 @@ class MatchService( pvPGameService.sendPvPGameRequest(game, GameCode(userCode, userLanguage), GameCode(opponentCode, opponentLanguage)) + if (mode == MatchModeEnum.AUTOPVP) { + logger.info( + "Auto match started between ${match.player1.username} and ${match.player2.username}" + ) + } + return matchId } @@ -231,11 +239,11 @@ class MatchService( throw CustomException(HttpStatus.BAD_REQUEST, "You cannot play against yourself") } return when(mode) { - MatchModeEnum.MANUAL, MatchModeEnum.AUTO -> { + MatchModeEnum.MANUAL, MatchModeEnum.AUTO-> { createNormalMatch(publicUser, publicOpponent, mode) } - MatchModeEnum.PVP -> { - createPvPMatch(publicUser, publicOpponent) + MatchModeEnum.PVP, MatchModeEnum.AUTOPVP -> { + createPvPMatch(publicUser, publicOpponent, mode) } else -> { throw CustomException(HttpStatus.BAD_REQUEST, "MatchMode does not exist") @@ -324,7 +332,6 @@ class MatchService( gameService.sendGameRequest(game, code, language, map) } fun createMatch(userId: UUID, createMatchRequestDto: CreateMatchRequestDto) { - println(createMatchRequestDto) when (createMatchRequestDto.mode) { MatchModeDto.SELF -> { val (_, _, mapRevisionId, codeRevisionId, _) = createMatchRequestDto @@ -334,7 +341,7 @@ class MatchService( val (_, _, _, codeRevisionId1, codeRevisionId2) = createMatchRequestDto createPvPSelfMatch(userId, codeRevisionId1, codeRevisionId2) } - MatchModeDto.MANUAL, MatchModeDto.AUTO , MatchModeDto.PVP -> { + MatchModeDto.MANUAL, MatchModeDto.AUTO , MatchModeDto.PVP, MatchModeDto.AUTOPVP -> { if (createMatchRequestDto.opponentUsername == null) { throw CustomException(HttpStatus.BAD_REQUEST, "Opponent ID is required") } @@ -363,6 +370,23 @@ class MatchService( } } + fun createPvPAutoMatch() { + val topNUsers = publicUserService.getPvPTopNUsers() + val userIds = topNUsers.map { it.userId } + val usernames = topNUsers.map { it.username } + logger.info("PvP Auto matches started for users: $usernames") + pvPAutoMatchRepository.deleteAll() + userIds.forEachIndexed { i, userId -> + run { + for (j in i + 1 until userIds.size) { + val opponentUsername = usernames[j] + val pvPMatchId = createDualMatch(userId, opponentUsername, MatchModeEnum.AUTOPVP) + pvPAutoMatchRepository.save(PvPAutoMatchEntity(pvPMatchId, 0)) + } + } + } + } + private fun mapMatchEntitiesToDtos(matchEntities: List): List { return matchEntities.map { matchEntity -> MatchDto( @@ -563,6 +587,7 @@ class MatchService( if (match.mode == MatchModeEnum.AUTO) { if (match.games.any { game -> game.status == GameStatusEnum.EXECUTE_ERROR }) { val autoMatch = autoMatchRepository.findById(match.id).get() + // If both games are executed and one of them is execute error, then retry the match if (autoMatch.tries < 2) { autoMatchRepository.delete(autoMatch) val newMatchId = @@ -655,7 +680,7 @@ class MatchService( userIds = userIds.toList(), matches = matches ) publicUserService.updateAutoMatchWinsLosses( - userIds.toList(), userIdWinMap, userIdLossMap, userIdTieMap + userIds.toList(), userIdWinMap, userIdLossMap, userIdTieMap, MatchModeEnum.AUTO ) val newRatings = ratingHistoryService.updateAndGetAutoMatchRatings(userIds.toList(), matches) @@ -723,9 +748,9 @@ class MatchService( dailyChallengeMatchRepository.save(updatedMatch) } } else if(pvPMatchRepository.findById(matchId).isPresent) { - val updatedGame = pvPGameService.updateGameStatus(gameStatusUpdateJson) + val (updatedGame, player1HasErrors, player2HasErrors) = pvPGameService.updateGameStatus(gameStatusUpdateJson) val match = pvPMatchRepository.findById(updatedGame.matchId).get() - if (match.game.matchId == updatedGame.matchId) { + if (match.mode != MatchModeEnum.AUTOPVP && match.game.matchId == updatedGame.matchId) { simpMessagingTemplate.convertAndSend( "/updates/${match.player1.userId}", mapper.writeValueAsString( @@ -738,44 +763,113 @@ class MatchService( ) ) } - if (match.game.status == PvPGameStatusEnum.EXECUTED) { + if (match.mode != MatchModeEnum.SELFPVP && + (match.game.status == PvPGameStatusEnum.EXECUTED || match.game.status == PvPGameStatusEnum.EXECUTE_ERROR) + ) { + + if (match.mode == MatchModeEnum.AUTOPVP) { + if (match.game.status == PvPGameStatusEnum.EXECUTE_ERROR) { + val pvPAutoMatch = pvPAutoMatchRepository.findById(match.id).get() + if (pvPAutoMatch.tries < 2) { + pvPAutoMatchRepository.delete(pvPAutoMatch) + val newMatchId = + createDualMatch(match.player1.userId, match.player2.username, MatchModeEnum.AUTOPVP) + pvPAutoMatchRepository.save(PvPAutoMatchEntity(newMatchId, pvPAutoMatch.tries + 1)) + return + } + } + } val verdict = verdictAlgorithm.getPvPVerdict( - match.game.status == PvPGameStatusEnum.EXECUTE_ERROR, + player1HasErrors, match.game.scorePlayer1, - match.game.status == PvPGameStatusEnum.EXECUTE_ERROR, + player2HasErrors, match.game.scorePlayer2, ) val finishedMatch = match.copy(verdict = verdict) val (newUserRating, newOpponentRating) = ratingHistoryService.updateRating(match.player1.userId, match.player2.userId, verdict, ratingType = RatingType.PVP) - publicUserService.updatePublicPvPRating( - userId = match.player1.userId, - isInitiator = true, - verdict = verdict, - newRating = newUserRating - ) - publicUserService.updatePublicPvPRating( - userId = match.player2.userId, - isInitiator = false, - verdict = verdict, - newRating = newOpponentRating - ) + if (match.mode == MatchModeEnum.PVP) { + if (( + match.player1.pvPTier == TierTypeDto.TIER2 && + match.player2.pvPTier == TierTypeDto.TIER2 + ) || + ( + match.player1.pvPTier == TierTypeDto.TIER_PRACTICE && + match.player2.pvPTier == TierTypeDto.TIER_PRACTICE + ) + ) { + publicUserService.updatePublicPvPRating( + userId = match.player1.userId, + isInitiator = true, + verdict = verdict, + newRating = newUserRating + ) + + publicUserService.updatePublicPvPRating( + userId = match.player2.userId, + isInitiator = false, + verdict = verdict, + newRating = newOpponentRating + ) + } + notificationService.sendNotification( + match.player1.userId, + "Match Result", + "${ + when (verdict) { + MatchVerdictEnum.PLAYER1 -> "Won" + MatchVerdictEnum.PLAYER2 -> "Lost" + MatchVerdictEnum.TIE -> "Tied" + } + } against ${match.player2.username}", + ) + } pvPMatchRepository.save(finishedMatch) - notificationService.sendNotification( - match.player1.userId, - "Match Result", - "${ - when (verdict) { - MatchVerdictEnum.PLAYER1 -> "Won" - MatchVerdictEnum.PLAYER2 -> "Lost" - MatchVerdictEnum.TIE -> "Tied" + + if (match.mode == MatchModeEnum.AUTOPVP) { + if (pvPAutoMatchRepository.findAll().all { autoMatch -> + val status = pvPMatchRepository.findById(autoMatch.matchId).get().game.status + status == PvPGameStatusEnum.EXECUTED || status == PvPGameStatusEnum.EXECUTE_ERROR + }) { + val matches = + pvPMatchRepository.findByIdIn(pvPAutoMatchRepository.findAll().map { it.matchId }) + val userIds = + matches.map { it.player1.userId }.toSet() + + matches.map { it.player2.userId }.toSet() + val (userIdWinMap, userIdLossMap, userIdTieMap) = + ratingHistoryService.updateTotalWinsTiesLossesPvP( + userIds = userIds.toList(), matches = matches + ) + publicUserService.updateAutoMatchWinsLosses( + userIds.toList(), userIdWinMap, userIdLossMap, userIdTieMap, MatchModeEnum.AUTOPVP + ) + val newRatings = + ratingHistoryService.updateAndGetPvPAutoMatchRatings(userIds.toList(), matches) + newRatings.forEach { (userId, newRating) -> + publicUserService.updateAutoMatchPvPRating(userId = userId, newRating = newRating.rating) } - } against ${match.player2.username}", - ) + logger.info("PvP LeaderBoard Tier Promotion and Demotion started") + publicUserService.promotePvPTiers() + } + notificationService.sendNotification( + match.player1.userId, + "Auto Match Result", + "${ + when (verdict) { + MatchVerdictEnum.PLAYER1 -> "Won" + MatchVerdictEnum.PLAYER2 -> "Lost" + MatchVerdictEnum.TIE -> "Tied" + } + } against ${match.player2.username}", + ) + logger.info( + "Match between ${match.player1.username} and ${match.player2.username} completed with verdict $verdict" + ) + } } } else if(codeTutorialMatchRepository.findById(matchId).isPresent) { diff --git a/server/src/main/kotlin/delta/codecharacter/server/match/PvPAutoMatchEntity.kt b/server/src/main/kotlin/delta/codecharacter/server/match/PvPAutoMatchEntity.kt new file mode 100644 index 0000000..af0f875 --- /dev/null +++ b/server/src/main/kotlin/delta/codecharacter/server/match/PvPAutoMatchEntity.kt @@ -0,0 +1,8 @@ +package delta.codecharacter.server.match + +import org.springframework.data.annotation.Id +import org.springframework.data.mongodb.core.mapping.Document +import java.util.UUID + +@Document(collection = "pvp_auto_match") +data class PvPAutoMatchEntity(@Id val matchId: UUID, val tries: Int) diff --git a/server/src/main/kotlin/delta/codecharacter/server/match/PvPAutoMatchRepository.kt b/server/src/main/kotlin/delta/codecharacter/server/match/PvPAutoMatchRepository.kt new file mode 100644 index 0000000..ccb51b4 --- /dev/null +++ b/server/src/main/kotlin/delta/codecharacter/server/match/PvPAutoMatchRepository.kt @@ -0,0 +1,7 @@ +package delta.codecharacter.server.match + +import org.springframework.data.mongodb.repository.MongoRepository +import org.springframework.stereotype.Repository +import java.util.UUID + +@Repository interface PvPAutoMatchRepository : MongoRepository diff --git a/server/src/main/kotlin/delta/codecharacter/server/match/PvPMatchRepository.kt b/server/src/main/kotlin/delta/codecharacter/server/match/PvPMatchRepository.kt index cc0aad8..ccf5915 100644 --- a/server/src/main/kotlin/delta/codecharacter/server/match/PvPMatchRepository.kt +++ b/server/src/main/kotlin/delta/codecharacter/server/match/PvPMatchRepository.kt @@ -10,4 +10,5 @@ import java.util.UUID interface PvPMatchRepository : MongoRepository { fun findTop10ByOrderByTotalPointsDesc(): List fun findByPlayer1OrderByCreatedAtDesc(player1: PublicUserEntity, pageRequest: PageRequest): List + fun findByIdIn(matchIds: List): List } diff --git a/server/src/main/kotlin/delta/codecharacter/server/pvp_game/PvPGameService.kt b/server/src/main/kotlin/delta/codecharacter/server/pvp_game/PvPGameService.kt index a1e1261..5680404 100644 --- a/server/src/main/kotlin/delta/codecharacter/server/pvp_game/PvPGameService.kt +++ b/server/src/main/kotlin/delta/codecharacter/server/pvp_game/PvPGameService.kt @@ -51,7 +51,7 @@ class PvPGameService( rabbitTemplate.convertAndSend("gamePvpRequestQueue", mapper.writeValueAsString(pvPGameRequest)) } - fun updateGameStatus(gameStatusUpdateJson: String): PvPGameEntity { + fun updateGameStatus(gameStatusUpdateJson: String): Triple { val gameStatusUpdateEntity = mapper.readValue(gameStatusUpdateJson, PvPGameStatusUpdateEntity::class.java) val oldPvPGameEntity = @@ -60,7 +60,7 @@ class PvPGameService( } if(gameStatusUpdateEntity.gameResultPlayer1 == null || gameStatusUpdateEntity.gameResultPlayer2 == null) { val newPvPGameEntity = oldPvPGameEntity.copy(status = gameStatusUpdateEntity.gameStatus) - return pvPGameRepository.save(newPvPGameEntity) + return Triple(pvPGameRepository.save(newPvPGameEntity), false, false) } val gameResultPlayer1 = gameStatusUpdateEntity.gameResultPlayer1 @@ -82,6 +82,6 @@ class PvPGameService( val pvPGame = pvPGameRepository.save(newPvPGameEntity) pvPGameLogService.savePvPGameLog(pvPGame.matchId, gameResultPlayer1.log, gameResultPlayer2.log) - return pvPGame + return Triple(pvPGame, gameResultPlayer1.hasErrors, gameResultPlayer2.hasErrors) } } diff --git a/server/src/main/kotlin/delta/codecharacter/server/schedulers/SchedulingService.kt b/server/src/main/kotlin/delta/codecharacter/server/schedulers/SchedulingService.kt index 4dd4c29..408209d 100644 --- a/server/src/main/kotlin/delta/codecharacter/server/schedulers/SchedulingService.kt +++ b/server/src/main/kotlin/delta/codecharacter/server/schedulers/SchedulingService.kt @@ -31,6 +31,7 @@ class SchedulingService( fun updateTempLeaderboard() { logger.info("Practice phase ended!!") publicUserService.resetRatingsAfterPracticePhase() + publicUserService.resetPvPRatingsAfterPracticePhase() codeRevisionService.resetCodeRevisionAfterPracticePhase() latestCodeService.resetLatestCodeAfterPracticePhase() lockedCodeService.resetLockedCodeAfterPracticePhase() @@ -38,11 +39,13 @@ class SchedulingService( lockedMapService.resetLockedMapAfterPracticePhase() mapRevisionService.resetMapRevisionAfterPracticePhase() publicUserService.updateLeaderboardAfterPracticePhase() + publicUserService.updatePvPLeaderboardAfterPracticePhase() } @Scheduled(cron = "\${environment.promote-demote-time}", zone = "GMT+5:30") fun createAutoMatch() { matchService.createAutoMatch() + matchService.createPvPAutoMatch() } } diff --git a/server/src/main/kotlin/delta/codecharacter/server/user/public_user/PublicUserEntity.kt b/server/src/main/kotlin/delta/codecharacter/server/user/public_user/PublicUserEntity.kt index 4f85568..aa6264b 100644 --- a/server/src/main/kotlin/delta/codecharacter/server/user/public_user/PublicUserEntity.kt +++ b/server/src/main/kotlin/delta/codecharacter/server/user/public_user/PublicUserEntity.kt @@ -15,6 +15,7 @@ data class PublicUserEntity( val college: String, val avatarId: Int, val tier: TierTypeDto, + val pvPTier: TierTypeDto, val tutorialLevel: Int, val codeTutorialLevel: Int, val rating: Double, diff --git a/server/src/main/kotlin/delta/codecharacter/server/user/public_user/PublicUserRepository.kt b/server/src/main/kotlin/delta/codecharacter/server/user/public_user/PublicUserRepository.kt index 210a627..58164df 100644 --- a/server/src/main/kotlin/delta/codecharacter/server/user/public_user/PublicUserRepository.kt +++ b/server/src/main/kotlin/delta/codecharacter/server/user/public_user/PublicUserRepository.kt @@ -12,4 +12,8 @@ interface PublicUserRepository : MongoRepository { fun findAllByTier(tier: TierTypeDto?, pageRequest: PageRequest): List fun findAllByTier(tier: TierTypeDto?): List + + fun findAllByPvPTier(pvPTier: TierTypeDto?, pageRequest: PageRequest): List + + fun findAllByPvPTier(pvPTier: TierTypeDto?): List } diff --git a/server/src/main/kotlin/delta/codecharacter/server/user/public_user/PublicUserService.kt b/server/src/main/kotlin/delta/codecharacter/server/user/public_user/PublicUserService.kt index 480f030..acccb4d 100644 --- a/server/src/main/kotlin/delta/codecharacter/server/user/public_user/PublicUserService.kt +++ b/server/src/main/kotlin/delta/codecharacter/server/user/public_user/PublicUserService.kt @@ -12,6 +12,7 @@ import delta.codecharacter.dtos.UserStatsDto import delta.codecharacter.dtos.PvPUserStatsDto import delta.codecharacter.server.daily_challenge.DailyChallengeEntity import delta.codecharacter.server.exception.CustomException +import delta.codecharacter.server.match.MatchModeEnum import delta.codecharacter.server.match.MatchVerdictEnum import delta.codecharacter.server.user.rating_history.RatingType import org.slf4j.Logger @@ -59,8 +60,10 @@ class PublicUserService(@Autowired private val publicUserRepository: PublicUserR pvPTies = 0, score = 0.0, // tier = TierTypeDto.TIER_PRACTICE, //TODO: Automatically assign tier2 to players + // pvPTier = TierTypeDto.TIER_PRACTICE, // registering after practice phase tier = TierTypeDto.TIER2, + pvPTier = TierTypeDto.TIER2, tutorialLevel = 1, dailyChallengeHistory = HashMap(), pvpRating = 1500.0, @@ -81,6 +84,18 @@ class PublicUserService(@Autowired private val publicUserRepository: PublicUserR logger.info("Leaderboard tier set during the start of game phase") } + fun updatePvPLeaderboardAfterPracticePhase() { + val publicUsers = publicUserRepository.findAll() + publicUsers.forEachIndexed { index, user -> + if (index < tier1Players.toInt()) { + publicUserRepository.save(user.copy(pvPTier = TierTypeDto.TIER1)) + } else { + publicUserRepository.save(user.copy(pvPTier = TierTypeDto.TIER2)) + } + } + logger.info("PvP Leaderboard tier set during the start of game phase") + } + fun resetRatingsAfterPracticePhase() { val users = publicUserRepository.findAll() users.forEach { user -> @@ -89,6 +104,14 @@ class PublicUserService(@Autowired private val publicUserRepository: PublicUserR logger.info("Ratings reset after practice phase done") } + fun resetPvPRatingsAfterPracticePhase() { + val users = publicUserRepository.findAll() + users.forEach { user -> + publicUserRepository.save(user.copy(pvpRating = 1500.0, pvPWins = 0, pvPTies = 0, pvPLosses = 0)) + } + logger.info("PvP Ratings reset after practice phase done") + } + fun promoteTiers() { val topPlayersInTier2 = publicUserRepository.findAllByTier( @@ -122,6 +145,39 @@ class PublicUserService(@Autowired private val publicUserRepository: PublicUserR } } + fun promotePvPTiers() { + val topPlayersInTier2 = + publicUserRepository.findAllByPvPTier( + TierTypeDto.TIER2, + PageRequest.of(0, topPlayer.toInt(), Sort.by(Sort.Order.desc("pvpRating"))) + ) + val bottomPlayersInTier1 = + publicUserRepository.findAllByPvPTier( + TierTypeDto.TIER1, + PageRequest.of(0, topPlayer.toInt(), Sort.by(Sort.Order.asc("pvpRating"))) + ) + topPlayersInTier2.forEach { users -> + val updatedToTier1User = publicUserRepository.save(users.copy(pvPTier = TierTypeDto.TIER1)) + if (updatedToTier1User.pvPTier == TierTypeDto.TIER1) { + logger.info("UserName ${updatedToTier1User.username} got promoted to PvP TIER1") + } else { + logger.error( + "Error occurred while updating ${updatedToTier1User.username} (UserName) to PvP TIER1" + ) + } + } + bottomPlayersInTier1.forEach { users -> + val updateToTier2User = publicUserRepository.save(users.copy(pvPTier = TierTypeDto.TIER2)) + if (updateToTier2User.pvPTier == TierTypeDto.TIER2) { + logger.info("UserName ${updateToTier2User.username} got demoted to PvP TIER2") + } else { + logger.error( + "Error occurred while updating ${updateToTier2User.username} (UserName) to PvP TIER2" + ) + } + } + } + fun getLeaderboard(page: Int?, size: Int?, tier: TierTypeDto?): List { val pageRequest = PageRequest.of( @@ -171,32 +227,39 @@ class PublicUserService(@Autowired private val publicUserRepository: PublicUserR fun getPvPLeaderboard( page: Int?, - size: Int? + size: Int?, + tier: TierTypeDto?, ): List { val pageRequest = - PageRequest.of( - page ?: 0, - size ?: 10, - Sort.by(Sort.Order.desc("pvpRating"), Sort.Order.desc("wins"), Sort.Order.asc("username")) - ) - return publicUserRepository.findAll(pageRequest).content.map { + PageRequest.of( + page ?: 0, + size ?: 10, + Sort.by(Sort.Order.desc("pvpRating"), Sort.Order.desc("pvPWins"), Sort.Order.asc("username")) + ) + val publicUsers = + if (tier == null) { + publicUserRepository.findAll(pageRequest).content + } else { + publicUserRepository.findAllByPvPTier(tier, pageRequest) + } + return publicUsers.map { PvPLeaderBoardResponseDto( - user = - PublicUserDto( - username = it.username, - name = it.name, - tier = TierTypeDto.valueOf(it.tier.name), - country = it.country, - college = it.college, - avatarId = it.avatarId, - ), - stats = - PvPUserStatsDto( - rating = BigDecimal(it.pvpRating), - wins = it.pvPWins, - losses = it.pvPLosses, - ties = it.pvPTies, - ), + user = + PublicUserDto( + username = it.username, + name = it.name, + tier = TierTypeDto.valueOf(it.pvPTier.name), + country = it.country, + college = it.college, + avatarId = it.avatarId, + ), + stats = + PvPUserStatsDto( + rating = BigDecimal(it.pvpRating), + wins = it.pvPWins, + losses = it.pvPLosses, + ties = it.pvPTies + ), ) } } @@ -352,24 +415,50 @@ class PublicUserService(@Autowired private val publicUserRepository: PublicUserR publicUserRepository.save(updatedUser) logger.info("Ratings updated for ${user.username}") } + + fun updateAutoMatchPvPRating(userId: UUID, newRating: Double) { + val user = publicUserRepository.findById(userId).get() + val updatedUser = user.copy(pvpRating = newRating) + publicUserRepository.save(updatedUser) + logger.info("PvP Ratings updated for ${user.username}") + } + fun updateAutoMatchWinsLosses( userIds: List, userIdWinsMap: Map, userIdLoss: Map, - userIdTies: Map + userIdTies: Map, + autoMatchType: MatchModeEnum ) { - logger.info("Updating wins and losses for $userIds") + logger.info("Updating normal wins and losses for $userIds") userIds.forEach { val user = publicUserRepository.findById(it).get() - val updatedUser = - user.copy( - wins = user.wins + userIdWinsMap[it]!!, - losses = user.losses + userIdLoss[it]!!, - ties = user.ties + userIdTies[it]!! - ) + val updatedUser: PublicUserEntity + when (autoMatchType) { + MatchModeEnum.AUTO -> { + updatedUser = + user.copy( + wins = user.wins + userIdWinsMap[it]!!, + losses = user.losses + userIdLoss[it]!!, + ties = user.ties + userIdTies[it]!! + ) + } + MatchModeEnum.AUTOPVP -> { + updatedUser = + user.copy( + pvPWins = user.pvPWins + userIdWinsMap[it]!!, + pvPLosses = user.pvPLosses + userIdLoss[it]!!, + pvPTies = user.pvPTies + userIdTies[it]!! + ) + } + else -> { + updatedUser = user + } + } publicUserRepository.save(updatedUser) } } + fun getPublicUser(userId: UUID): PublicUserEntity { return publicUserRepository.findById(userId).get() } @@ -409,4 +498,8 @@ class PublicUserService(@Autowired private val publicUserRepository: PublicUserR fun getTopNUsers(): List { return publicUserRepository.findAllByTier(TierTypeDto.TIER1).sortedByDescending { it.rating } } + + fun getPvPTopNUsers(): List { + return publicUserRepository.findAllByPvPTier(TierTypeDto.TIER1).sortedByDescending { it.pvpRating } + } } diff --git a/server/src/main/kotlin/delta/codecharacter/server/user/rating_history/RatingHistoryService.kt b/server/src/main/kotlin/delta/codecharacter/server/user/rating_history/RatingHistoryService.kt index 30bc745..ba6d9cc 100644 --- a/server/src/main/kotlin/delta/codecharacter/server/user/rating_history/RatingHistoryService.kt +++ b/server/src/main/kotlin/delta/codecharacter/server/user/rating_history/RatingHistoryService.kt @@ -5,6 +5,7 @@ import delta.codecharacter.server.logic.rating.GlickoRating import delta.codecharacter.server.logic.rating.RatingAlgorithm import delta.codecharacter.server.match.MatchEntity import delta.codecharacter.server.match.MatchVerdictEnum +import delta.codecharacter.server.match.PvPMatchEntity import org.springframework.beans.factory.annotation.Autowired import org.springframework.stereotype.Service import java.math.BigDecimal @@ -144,6 +145,49 @@ class RatingHistoryService( verdicts ) } + + private fun getNewRatingAfterPvPAutoMatches( + userId: UUID, + userRatings: Map, + pvPAutoMatches: List + ): GlickoRating { + val userAsPlayer1Matches = pvPAutoMatches.filter { it.player1.userId == userId } + val userAsPlayer2Matches = pvPAutoMatches.filter { it.player2.userId == userId } + + val usersWeightedRatingDeviations = + userRatings + .map { + it.key to + ratingAlgorithm.getWeightedRatingDeviationSinceLastCompetition( + it.value.ratingDeviation, it.value.validFrom + ) + } + .toMap() + + val ratingsForUserAsPlayer1 = + userAsPlayer1Matches.map { match -> + GlickoRating(match.player2.pvpRating, usersWeightedRatingDeviations[match.player2.userId]!!) + } + val verdictsForUserAsPlayer1 = + userAsPlayer1Matches.map { match -> convertVerdictToMatchResult(match.verdict) } + + val ratingsForUserAsPlayer2 = + userAsPlayer2Matches.map { match -> + GlickoRating(match.player1.pvpRating, usersWeightedRatingDeviations[match.player1.userId]!!) + } + val verdictsForUserAsPlayer2 = + userAsPlayer2Matches.map { match -> 1.0 - convertVerdictToMatchResult(match.verdict) } + + val ratings = ratingsForUserAsPlayer1 + ratingsForUserAsPlayer2 + val verdicts = verdictsForUserAsPlayer1 + verdictsForUserAsPlayer2 + + return ratingAlgorithm.calculateNewRating( + GlickoRating(userRatings[userId]!!.rating, usersWeightedRatingDeviations[userId]!!), + ratings, + verdicts + ) + } + fun updateTotalWinsTiesLosses( userIds: List, matches: List @@ -169,6 +213,33 @@ class RatingHistoryService( } return Triple(userIdWinMap.toMap(), userIdLossMap.toMap(), userIdTieMap.toMap()) } + + fun updateTotalWinsTiesLossesPvP( + userIds: List, + matches: List + ): Triple, Map, Map> { + val userIdWinMap = userIds.associateWith { 0 }.toMutableMap() + val userIdLossMap = userIds.associateWith { 0 }.toMutableMap() + val userIdTieMap = userIds.associateWith { 0 }.toMutableMap() + matches.forEach { match -> + when (match.verdict) { + MatchVerdictEnum.PLAYER1 -> { + userIdWinMap[match.player1.userId] = userIdWinMap[match.player1.userId]!! + 1 + userIdLossMap[match.player2.userId] = userIdLossMap[match.player2.userId]!! + 1 + } + MatchVerdictEnum.PLAYER2 -> { + userIdWinMap[match.player2.userId] = userIdWinMap[match.player2.userId]!! + 1 + userIdLossMap[match.player1.userId] = userIdLossMap[match.player1.userId]!! + 1 + } + MatchVerdictEnum.TIE -> { + userIdTieMap[match.player1.userId] = userIdTieMap[match.player1.userId]!! + 1 + userIdTieMap[match.player2.userId] = userIdTieMap[match.player2.userId]!! + 1 + } + } + } + return Triple(userIdWinMap.toMap(), userIdLossMap.toMap(), userIdTieMap.toMap()) + } + fun updateAndGetAutoMatchRatings( userIds: List, matches: List @@ -193,7 +264,33 @@ class RatingHistoryService( ) ) } + return newRatings + } + fun updateAndGetPvPAutoMatchRatings( + userIds: List, + matches: List + ): Map { + val userRatings = + userIds.associateWith { userId -> + ratingHistoryRepository.findFirstByUserIdAndRatingTypeOrderByValidFromDesc(userId,RatingType.PVP) + } + val newRatings = + userIds.associateWith { userId -> + getNewRatingAfterPvPAutoMatches(userId, userRatings, matches) + } + val currentInstant = Instant.now() + newRatings.forEach { (userId, rating) -> + ratingHistoryRepository.save( + RatingHistoryEntity( + userId = userId, + rating = rating.rating, + ratingDeviation = rating.ratingDeviation, + validFrom = currentInstant, + ratingType = RatingType.PVP + ) + ) + } return newRatings } } diff --git a/server/src/test/kotlin/delta/codecharacter/server/SecurityTestConfiguration.kt b/server/src/test/kotlin/delta/codecharacter/server/SecurityTestConfiguration.kt index f7e62a2..77956f2 100644 --- a/server/src/test/kotlin/delta/codecharacter/server/SecurityTestConfiguration.kt +++ b/server/src/test/kotlin/delta/codecharacter/server/SecurityTestConfiguration.kt @@ -87,6 +87,7 @@ class TestAttributes { pvPLosses = 7, pvPTies = 3, tier = TierTypeDto.TIER_PRACTICE, + pvPTier = TierTypeDto.TIER_PRACTICE, score = 0.0, dailyChallengeHistory = hashMapOf(0 to DailyChallengeHistory(0.0, dailyChallengeCode)), tutorialLevel = 1, @@ -110,11 +111,12 @@ class TestAttributes { pvPLosses = 7, pvPTies = 3, tier = TierTypeDto.TIER_PRACTICE, + pvPTier = TierTypeDto.TIER_PRACTICE, score = 0.0, dailyChallengeHistory = hashMapOf(0 to DailyChallengeHistory(0.0, dailyChallengeCode)), tutorialLevel = 1, pvpRating = 1000.0, - codeTutorialLevel = 1 + codeTutorialLevel = 1, ) } } diff --git a/server/src/test/kotlin/delta/codecharacter/server/leaderboard/LeaderboardTest.kt b/server/src/test/kotlin/delta/codecharacter/server/leaderboard/LeaderboardTest.kt index 7fc568e..74bc279 100644 --- a/server/src/test/kotlin/delta/codecharacter/server/leaderboard/LeaderboardTest.kt +++ b/server/src/test/kotlin/delta/codecharacter/server/leaderboard/LeaderboardTest.kt @@ -30,6 +30,7 @@ internal class LeaderboardTest { college = "college", avatarId = 1, tier = TierTypeDto.TIER1, + pvPTier = TierTypeDto.TIER1, tutorialLevel = 1, codeTutorialLevel = 1, rating = 2000.0, @@ -54,6 +55,7 @@ internal class LeaderboardTest { college = "college", avatarId = 1, tier = TierTypeDto.TIER1, + pvPTier = TierTypeDto.TIER1, tutorialLevel = 1, codeTutorialLevel = 1, rating = 1800.0, @@ -78,6 +80,7 @@ internal class LeaderboardTest { college = "college", avatarId = 1, tier = TierTypeDto.TIER2, + pvPTier = TierTypeDto.TIER2, tutorialLevel = 1, codeTutorialLevel = 1, rating = 1600.0, @@ -102,6 +105,7 @@ internal class LeaderboardTest { college = "college", avatarId = 1, tier = TierTypeDto.TIER2, + pvPTier = TierTypeDto.TIER2, tutorialLevel = 1, codeTutorialLevel = 1, rating = 1500.0, diff --git a/server/src/test/kotlin/delta/codecharacter/server/match/MatchServiceTest.kt b/server/src/test/kotlin/delta/codecharacter/server/match/MatchServiceTest.kt index 0240860..3a5191e 100644 --- a/server/src/test/kotlin/delta/codecharacter/server/match/MatchServiceTest.kt +++ b/server/src/test/kotlin/delta/codecharacter/server/match/MatchServiceTest.kt @@ -66,6 +66,7 @@ internal class MatchServiceTest { private lateinit var mapValidator: MapValidator private lateinit var matchService: MatchService private lateinit var autoMatchRepository: AutoMatchRepository + private lateinit var pvPAutoMatchRepository: PvPAutoMatchRepository private lateinit var pvPMatchRepository: PvPMatchRepository private lateinit var gameRepository: GameRepository private lateinit var pvPGameRepository: PvPGameRepository @@ -94,6 +95,7 @@ internal class MatchServiceTest { simpMessagingTemplate = mockk(relaxed = true) mapValidator = mockk(relaxed = true) autoMatchRepository = mockk(relaxed = true) + pvPAutoMatchRepository = mockk(relaxed = true) pvPMatchRepository = mockk(relaxed = true) gameRepository = mockk(relaxed = true) pvPGameRepository = mockk(relaxed = true) @@ -120,6 +122,7 @@ internal class MatchServiceTest { simpMessagingTemplate, mapValidator, autoMatchRepository, + pvPAutoMatchRepository, statsService, pvPMatchRepository, gameRepository,