diff --git a/.runConfigurations/Format.run.xml b/.runConfigurations/Format.run.xml
index 84d7259f..f043e364 100644
--- a/.runConfigurations/Format.run.xml
+++ b/.runConfigurations/Format.run.xml
@@ -20,4 +20,4 @@
false
-
\ No newline at end of file
+
diff --git a/docs/spec/CodeCharacter-API.yml b/docs/spec/CodeCharacter-API.yml
index ed6e545e..8facc446 100644
--- a/docs/spec/CodeCharacter-API.yml
+++ b/docs/spec/CodeCharacter-API.yml
@@ -303,6 +303,7 @@ paths:
description: Leaderboard Tier
description: Get leaderboard
parameters: []
+
/top-matches:
get:
summary: Get top matches
@@ -1934,6 +1935,7 @@ components:
- user
- stats
description: Leaderboard entry model
+
DailyChallengeGetRequest:
title: Get daily challenge
description: Get current-user daily challenge
diff --git a/library/src/main/kotlin/delta/codecharacter/core/DailyChallengesApi.kt b/library/src/main/kotlin/delta/codecharacter/core/DailyChallengesApi.kt
index 2950dd4e..4f3303da 100644
--- a/library/src/main/kotlin/delta/codecharacter/core/DailyChallengesApi.kt
+++ b/library/src/main/kotlin/delta/codecharacter/core/DailyChallengesApi.kt
@@ -15,19 +15,24 @@ import io.swagger.v3.oas.annotations.media.*
import io.swagger.v3.oas.annotations.responses.*
import io.swagger.v3.oas.annotations.security.*
import org.springframework.http.HttpStatus
-import org.springframework.http.MediaType
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.*
import org.springframework.validation.annotation.Validated
-import org.springframework.web.context.request.NativeWebRequest
-import org.springframework.beans.factory.annotation.Autowired
import jakarta.validation.constraints.*
import jakarta.validation.Valid
+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 java.util.Date
import kotlin.collections.List
-import kotlin.collections.Map
@Validated
@RequestMapping("\${api.base-path:}")
diff --git a/server/src/main/kotlin/delta/codecharacter/server/match/AutoMatchEntity.kt b/server/src/main/kotlin/delta/codecharacter/server/match/AutoMatchEntity.kt
new file mode 100644
index 00000000..9cfd4e4c
--- /dev/null
+++ b/server/src/main/kotlin/delta/codecharacter/server/match/AutoMatchEntity.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 = "auto_match")
+data class AutoMatchEntity(@Id val matchId: UUID, val tries: Int)
diff --git a/server/src/main/kotlin/delta/codecharacter/server/match/AutoMatchRepository.kt b/server/src/main/kotlin/delta/codecharacter/server/match/AutoMatchRepository.kt
new file mode 100644
index 00000000..6637962f
--- /dev/null
+++ b/server/src/main/kotlin/delta/codecharacter/server/match/AutoMatchRepository.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 AutoMatchRepository : MongoRepository
diff --git a/server/src/main/kotlin/delta/codecharacter/server/match/MatchRepository.kt b/server/src/main/kotlin/delta/codecharacter/server/match/MatchRepository.kt
index 124ba5b8..a5d94a55 100644
--- a/server/src/main/kotlin/delta/codecharacter/server/match/MatchRepository.kt
+++ b/server/src/main/kotlin/delta/codecharacter/server/match/MatchRepository.kt
@@ -9,4 +9,5 @@ import java.util.UUID
interface MatchRepository : MongoRepository {
fun findTop10ByOrderByTotalPointsDesc(): List
fun findByPlayer1OrderByCreatedAtDesc(player1: PublicUserEntity): List
+ fun findByIdIn(matchIds: List): List
}
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 7a2949ba..fe604707 100644
--- a/server/src/main/kotlin/delta/codecharacter/server/match/MatchService.kt
+++ b/server/src/main/kotlin/delta/codecharacter/server/match/MatchService.kt
@@ -30,6 +30,8 @@ import delta.codecharacter.server.logic.verdict.VerdictAlgorithm
import delta.codecharacter.server.notifications.NotificationService
import delta.codecharacter.server.user.public_user.PublicUserService
import delta.codecharacter.server.user.rating_history.RatingHistoryService
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
import org.springframework.amqp.rabbit.annotation.RabbitListener
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.http.HttpStatus
@@ -59,9 +61,11 @@ class MatchService(
@Autowired private val dailyChallengeMatchRepository: DailyChallengeMatchRepository,
@Autowired private val jackson2ObjectMapperBuilder: Jackson2ObjectMapperBuilder,
@Autowired private val simpMessagingTemplate: SimpMessagingTemplate,
- @Autowired private val mapValidator: MapValidator
+ @Autowired private val mapValidator: MapValidator,
+ @Autowired private val autoMatchRepository: AutoMatchRepository
) {
private var mapper: ObjectMapper = jackson2ObjectMapperBuilder.build()
+ private val logger: Logger = LoggerFactory.getLogger(MatchService::class.java)
private fun createSelfMatch(userId: UUID, codeRevisionId: UUID?, mapRevisionId: UUID?) {
val code: String
@@ -107,19 +111,13 @@ class MatchService(
gameService.sendGameRequest(game, code, LanguageEnum.valueOf(language.name), map)
}
- fun createDualMatch(userId: UUID, opponentUsername: String) {
+ fun createDualMatch(userId: UUID, opponentUsername: String, mode: MatchModeEnum): UUID {
val publicUser = publicUserService.getPublicUser(userId)
val publicOpponent = publicUserService.getPublicUserByUsername(opponentUsername)
val opponentId = publicOpponent.userId
-
if (userId == opponentId) {
throw CustomException(HttpStatus.BAD_REQUEST, "You cannot play against yourself")
}
- if (publicOpponent.tier == TierTypeDto.TIER1) {
- throw CustomException(
- HttpStatus.BAD_REQUEST, "Opponent cannot be a tier 1 player in manual match"
- )
- }
val (userLanguage, userCode) = lockedCodeService.getLockedCode(userId)
val userMap = lockedMapService.getLockedMap(userId)
@@ -135,7 +133,7 @@ class MatchService(
MatchEntity(
id = matchId,
games = listOf(game1, game2),
- mode = MatchModeEnum.MANUAL,
+ mode = mode,
verdict = MatchVerdictEnum.TIE,
createdAt = Instant.now(),
totalPoints = 0,
@@ -146,6 +144,12 @@ class MatchService(
gameService.sendGameRequest(game1, userCode, userLanguage, opponentMap)
gameService.sendGameRequest(game2, opponentCode, opponentLanguage, userMap)
+ if (mode == MatchModeEnum.AUTO) {
+ logger.info(
+ "Auto match started between ${match.player1.username} and ${match.player2.username}"
+ )
+ }
+ return matchId
}
fun createDCMatch(userId: UUID, dailyChallengeMatchRequestDto: DailyChallengeMatchRequestDto) {
@@ -198,7 +202,7 @@ class MatchService(
if (createMatchRequestDto.opponentUsername == null) {
throw CustomException(HttpStatus.BAD_REQUEST, "Opponent ID is required")
}
- createDualMatch(userId, createMatchRequestDto.opponentUsername!!)
+ createDualMatch(userId, createMatchRequestDto.opponentUsername!!, MatchModeEnum.MANUAL)
}
else -> {
throw CustomException(HttpStatus.BAD_REQUEST, "MatchMode Is Not Correct")
@@ -206,6 +210,23 @@ class MatchService(
}
}
+ fun createAutoMatch() {
+ val topNUsers = publicUserService.getTopNUsers()
+ val userIds = topNUsers.map { it.userId }
+ val usernames = topNUsers.map { it.username }
+ logger.info("Auto matches started for users: $usernames")
+ autoMatchRepository.deleteAll()
+ userIds.forEachIndexed { i, userId ->
+ run {
+ for (j in i + 1 until userIds.size) {
+ val opponentUsername = usernames[j]
+ val matchId = createDualMatch(userId, opponentUsername, MatchModeEnum.AUTO)
+ autoMatchRepository.save(AutoMatchEntity(matchId, 0))
+ }
+ }
+ }
+ }
+
private fun mapMatchEntitiesToDtos(matchEntities: List): List {
return matchEntities.map { matchEntity ->
MatchDto(
@@ -285,7 +306,10 @@ class MatchService(
fun getUserMatches(userId: UUID): List {
val publicUser = publicUserService.getPublicUser(userId)
- val matches = matchRepository.findByPlayer1OrderByCreatedAtDesc(publicUser)
+ val matches =
+ matchRepository.findByPlayer1OrderByCreatedAtDesc(publicUser).filter { match ->
+ match.mode != MatchModeEnum.AUTO
+ }
val dcMatches =
dailyChallengeMatchRepository.findByUserOrderByCreatedAtDesc(publicUser).takeWhile {
Duration.between(it.createdAt, Instant.now()).toHours() < 24 &&
@@ -318,6 +342,20 @@ class MatchService(
game.status == GameStatusEnum.EXECUTED || game.status == GameStatusEnum.EXECUTE_ERROR
}
) {
+
+ if (match.mode == MatchModeEnum.AUTO) {
+ if (match.games.any { game -> game.status == GameStatusEnum.EXECUTE_ERROR }) {
+ val autoMatch = autoMatchRepository.findById(match.id).get()
+ if (autoMatch.tries < 2) {
+ autoMatchRepository.delete(autoMatch)
+ val newMatchId =
+ createDualMatch(match.player1.userId, match.player2.username, MatchModeEnum.AUTO)
+ autoMatchRepository.save(AutoMatchEntity(newMatchId, autoMatch.tries + 1))
+ return
+ }
+ }
+ }
+
val player1Game = match.games.first()
val player2Game = match.games.last()
val verdict =
@@ -332,22 +370,21 @@ class MatchService(
val finishedMatch = match.copy(verdict = verdict)
val (newUserRating, newOpponentRating) =
ratingHistoryService.updateRating(match.player1.userId, match.player2.userId, verdict)
- if (!(match.mode == MatchModeEnum.MANUAL && (match.player1.tier == TierTypeDto.TIER1))) {
-
- publicUserService.updatePublicRating(
- userId = match.player1.userId,
- isInitiator = true,
- verdict = verdict,
- newRating = newUserRating
- )
- publicUserService.updatePublicRating(
- userId = match.player2.userId,
- isInitiator = false,
- verdict = verdict,
- newRating = newOpponentRating
- )
- }
if (match.mode == MatchModeEnum.MANUAL) {
+ if (match.player1.tier == TierTypeDto.TIER2 && match.player2.tier == TierTypeDto.TIER2) {
+ publicUserService.updatePublicRating(
+ userId = match.player1.userId,
+ isInitiator = true,
+ verdict = verdict,
+ newRating = newUserRating
+ )
+ publicUserService.updatePublicRating(
+ userId = match.player2.userId,
+ isInitiator = false,
+ verdict = verdict,
+ newRating = newOpponentRating
+ )
+ }
notificationService.sendNotification(
match.player1.userId,
"Match Result",
@@ -360,8 +397,37 @@ class MatchService(
} against ${match.player2.username}",
)
}
-
matchRepository.save(finishedMatch)
+
+ if (match.mode == MatchModeEnum.AUTO) {
+ if (autoMatchRepository.findAll().all { autoMatch ->
+ matchRepository.findById(autoMatch.matchId).get().games.all { game ->
+ game.status == GameStatusEnum.EXECUTED || game.status == GameStatusEnum.EXECUTE_ERROR
+ }
+ }
+ ) {
+ val matches =
+ matchRepository.findByIdIn(autoMatchRepository.findAll().map { it.matchId })
+ val userIds =
+ matches.map { it.player1.userId }.toSet() +
+ matches.map { it.player2.userId }.toSet()
+ val newRatings =
+ ratingHistoryService.updateAndGetAutoMatchRatings(userIds.toList(), matches)
+ newRatings.forEach { (userId, newRating) ->
+ publicUserService.updatePublicRating(
+ userId = userId,
+ isInitiator = true,
+ verdict = verdict,
+ newRating = newRating.rating
+ )
+ }
+ logger.info("LeaderBoard Tier Promotion and Demotion started")
+ publicUserService.promoteTiers()
+ }
+ logger.info(
+ "Match between ${match.player1.username} and ${match.player2.username} completed with verdict $verdict"
+ )
+ }
}
} else if (dailyChallengeMatchRepository.findById(matchId).isPresent) {
val match = dailyChallengeMatchRepository.findById(matchId).get()
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 8d6c437d..6900bba2 100644
--- a/server/src/main/kotlin/delta/codecharacter/server/schedulers/SchedulingService.kt
+++ b/server/src/main/kotlin/delta/codecharacter/server/schedulers/SchedulingService.kt
@@ -1,5 +1,6 @@
package delta.codecharacter.server.schedulers
+import delta.codecharacter.server.match.MatchService
import delta.codecharacter.server.user.public_user.PublicUserService
import org.slf4j.Logger
import org.slf4j.LoggerFactory
@@ -8,7 +9,10 @@ import org.springframework.scheduling.annotation.Scheduled
import org.springframework.stereotype.Service
@Service
-class SchedulingService(@Autowired private val publicUserService: PublicUserService) {
+class SchedulingService(
+ @Autowired private val publicUserService: PublicUserService,
+ @Autowired private val matchService: MatchService
+) {
private val logger: Logger = LoggerFactory.getLogger(SchedulingService::class.java)
@Scheduled(cron = "\${environment.registration-time}", zone = "GMT+5:30")
@@ -17,9 +21,9 @@ class SchedulingService(@Autowired private val publicUserService: PublicUserServ
publicUserService.resetRatingsAfterPracticePhase()
publicUserService.updateLeaderboardAfterPracticePhase()
}
- @Scheduled(cron = "\${environment.update-time}", zone = "GMT+5:30")
- fun promoteAndDemoteUserTiers() {
- logger.info("LeaderBoard Tier Promotion and Demotion started")
- publicUserService.promoteTiers()
+
+ @Scheduled(cron = "\${environment.promote-demote-time}", zone = "GMT+5:30")
+ fun createAutoMatch() {
+ matchService.createAutoMatch()
}
}
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 09883522..8a6d96e9 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
@@ -9,5 +9,7 @@ import java.util.UUID
interface PublicUserRepository : MongoRepository {
fun findByUsername(username: String): Optional
+ fun findTopnByOrderByRatingDesc(pageRequest: PageRequest): List
+
fun findAllByTier(tier: TierTypeDto?, pageRequest: PageRequest): 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 e2e4eec6..c1b19fdb 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
@@ -282,4 +282,9 @@ class PublicUserService(@Autowired private val publicUserRepository: PublicUserR
val updatedUser = user.copy(score = user.score + score, dailyChallengeHistory = current)
publicUserRepository.save(updatedUser)
}
+
+ fun getTopNUsers(): List {
+ val pageRequest = PageRequest.of(0, tier1Players.toInt(), Sort.by("rating"))
+ return publicUserRepository.findTopnByOrderByRatingDesc(pageRequest)
+ }
}
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 a838d5f7..7de0ada7 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
@@ -3,6 +3,7 @@ package delta.codecharacter.server.user.rating_history
import delta.codecharacter.dtos.RatingHistoryDto
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 org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Service
@@ -93,4 +94,73 @@ class RatingHistoryService(
return Pair(newUserRating.rating, newOpponentRating.rating)
}
+
+ private fun getNewRatingAfterAutoMatches(
+ userId: UUID,
+ userRatings: Map,
+ autoMatches: List
+ ): GlickoRating {
+ val userAsInitiatorMatches = autoMatches.filter { it.player1.userId == userId }
+ val userAsOpponentMatches = autoMatches.filter { it.player2.userId == userId }
+
+ val usersWeightedRatingDeviations =
+ userRatings
+ .map {
+ it.key to
+ ratingAlgorithm.getWeightedRatingDeviationSinceLastCompetition(
+ it.value.ratingDeviation, it.value.validFrom
+ )
+ }
+ .toMap()
+
+ val ratingsForUserAsPlayer1 =
+ userAsInitiatorMatches.map { match ->
+ GlickoRating(match.player2.rating, usersWeightedRatingDeviations[match.player2.userId]!!)
+ }
+ val verdictsForUserAsPlayer1 =
+ userAsInitiatorMatches.map { match -> convertVerdictToMatchResult(match.verdict) }
+
+ val ratingsForUserAsPlayer2 =
+ userAsOpponentMatches.map { match ->
+ GlickoRating(match.player1.rating, usersWeightedRatingDeviations[match.player1.userId]!!)
+ }
+ val verdictsForUserAsPlayer2 =
+ userAsOpponentMatches.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 updateAndGetAutoMatchRatings(
+ userIds: List,
+ matches: List
+ ): Map {
+ val userRatings =
+ userIds.associateWith { userId ->
+ ratingHistoryRepository.findFirstByUserIdOrderByValidFromDesc(userId)
+ }
+ val newRatings =
+ userIds.associateWith { userId ->
+ getNewRatingAfterAutoMatches(userId, userRatings, matches)
+ }
+ val currentInstant = Instant.now()
+ newRatings.forEach { (userId, rating) ->
+ ratingHistoryRepository.save(
+ RatingHistoryEntity(
+ userId = userId,
+ rating = rating.rating,
+ ratingDeviation = rating.ratingDeviation,
+ validFrom = currentInstant
+ )
+ )
+ }
+
+ return newRatings
+ }
}
diff --git a/server/src/main/resources/application.example.yml b/server/src/main/resources/application.example.yml
index 81cc765f..233c146c 100644
--- a/server/src/main/resources/application.example.yml
+++ b/server/src/main/resources/application.example.yml
@@ -31,7 +31,7 @@ environment:
registration-time: "* * * * * *"
no-of-tier-1-players: 20
no-of-players-for-promotion: 5
- update-time: "*/20 * * * * *"
+ promote-demote-time: "* * */6 * * *"
server:
compression:
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 b5c0571f..b57d784c 100644
--- a/server/src/test/kotlin/delta/codecharacter/server/match/MatchServiceTest.kt
+++ b/server/src/test/kotlin/delta/codecharacter/server/match/MatchServiceTest.kt
@@ -8,7 +8,6 @@ import delta.codecharacter.dtos.DailyChallengeMatchRequestDto
import delta.codecharacter.dtos.GameMapRevisionDto
import delta.codecharacter.dtos.LanguageDto
import delta.codecharacter.dtos.MatchModeDto
-import delta.codecharacter.dtos.TierTypeDto
import delta.codecharacter.server.TestAttributes
import delta.codecharacter.server.code.LanguageEnum
import delta.codecharacter.server.code.code_revision.CodeRevisionService
@@ -61,6 +60,7 @@ internal class MatchServiceTest {
private lateinit var simpMessagingTemplate: SimpMessagingTemplate
private lateinit var mapValidator: MapValidator
private lateinit var matchService: MatchService
+ private lateinit var autoMatchRepository: AutoMatchRepository
@BeforeEach
fun setUp() {
@@ -81,6 +81,7 @@ internal class MatchServiceTest {
jackson2ObjectMapperBuilder = Jackson2ObjectMapperBuilder()
simpMessagingTemplate = mockk(relaxed = true)
mapValidator = mockk(relaxed = true)
+ autoMatchRepository = mockk(relaxed = true)
matchService =
MatchService(
@@ -100,7 +101,8 @@ internal class MatchServiceTest {
dailyChallengeMatchRepository,
jackson2ObjectMapperBuilder,
simpMessagingTemplate,
- mapValidator
+ mapValidator,
+ autoMatchRepository
)
}
@@ -281,42 +283,6 @@ internal class MatchServiceTest {
)
}
- @Test
- @Throws(CustomException::class)
- fun `should throw bad request if the opponent player belongs to tier 1 in manual match`() {
- val playerId = UUID.randomUUID()
- val opponentId = UUID.randomUUID()
- val opponentPublicUser =
- TestAttributes.publicUser.copy(
- userId = opponentId, username = "opponent", tier = TierTypeDto.TIER1
- )
- val userCode = Pair(LanguageEnum.CPP, "user-code")
- val opponentCode = Pair(LanguageEnum.PYTHON, "opponent-code")
- val userMap = "user-map"
- val opponentMap = "opponent-map"
- every { publicUserService.getPublicUserByUsername(opponentPublicUser.username) } returns
- opponentPublicUser
- every { lockedCodeService.getLockedCode(playerId) } returns userCode
- every { lockedCodeService.getLockedCode(opponentId) } returns opponentCode
- every { lockedMapService.getLockedMap(playerId) } returns userMap
- every { lockedMapService.getLockedMap(opponentId) } returns opponentMap
- every { gameService.createGame(any()) } returns mockk()
- every { matchRepository.save(any()) } returns mockk()
- every { gameService.sendGameRequest(any(), userCode.second, userCode.first, userMap) } returns
- Unit
- every {
- gameService.sendGameRequest(any(), opponentCode.second, opponentCode.first, opponentMap)
- } returns Unit
-
- val exception =
- assertThrows {
- matchService.createDualMatch(playerId, opponentPublicUser.username)
- }
-
- assertThat(exception.status).isEqualTo(HttpStatus.BAD_REQUEST)
- assertThat(exception.message).isEqualTo("Opponent cannot be a tier 1 player in manual match")
- }
-
@Test
fun `should create auto match`() {
val userId = UUID.randomUUID()