diff --git a/backend/src/main/kotlin/com/retypeme/project/racing/repository/RaceRepository.kt b/backend/src/main/kotlin/com/retypeme/project/racing/repository/RaceRepository.kt index 6e58292..41288a1 100644 --- a/backend/src/main/kotlin/com/retypeme/project/racing/repository/RaceRepository.kt +++ b/backend/src/main/kotlin/com/retypeme/project/racing/repository/RaceRepository.kt @@ -5,6 +5,7 @@ import com.retypeme.project.messaging.GameEventPublisher import com.retypeme.project.racing.model.Race import com.retypeme.project.racing.controller.DriverMetrics import com.retypeme.project.racing.service.DateTimeProvider +import com.retypeme.project.statistic.service.StatisticService import org.springframework.stereotype.Component import java.time.LocalDateTime import java.time.ZoneOffset.UTC @@ -16,13 +17,13 @@ const val REGISTERED = "registered" class RaceRepository( private val dateTimeProvider: DateTimeProvider, private val gameEventPublisher: GameEventPublisher, - private val chainService: ChainService - + private val chainService: ChainService, + private val userStatisticService: StatisticService ) { private val openRaces: MutableMap = mutableMapOf() - fun createRace(id: String, chain:Int, users: Int): Race { + fun createRace(id: String, chain: Int, users: Int): Race { val now: LocalDateTime = dateTimeProvider.now() val usersList: MutableList = mutableListOf() val session = Race(id, chain, "", users, null, now, usersList) @@ -34,19 +35,19 @@ class RaceRepository( return openRaces[id] ?: throw Exception("Session not found") } - fun start(sessionId: String, text: String): Unit { + fun start(sessionId: String, text: String) { val session: Race = getSessionById(sessionId) session.startedAt = dateTimeProvider.now() session.text = text } - fun updateRegistration(sessionId: String, chain:Int, userId: String, walletId: String, state: String): List { + fun updateRegistration(sessionId: String, chain: Int, userId: String, walletId: String, state: String): List { val race: Race = getSessionById(sessionId) - if(race.chain != chain) { + if (race.chain != chain) { val expected = chainService.getChainById(race.chain).name val actual = chainService.getChainById(chain).name val msg = "Session is created for different chain, expected: $expected, got: $actual" - return mutableListOf(msg); + return mutableListOf(msg) } if (state == JOINED) { join(sessionId, userId, walletId) @@ -60,7 +61,7 @@ class RaceRepository( private fun register(race: Race, userId: String, sessionId: String) { val user: DriverMetrics = race.users.find { u -> u.userId == userId && u.state == JOINED } ?: throw Exception("User not found") - user.state = REGISTERED; + user.state = REGISTERED if (race.isReady()) { gameEventPublisher.publishRaceReady( sessionId, race.users.map { u -> u.userId }.toMutableList() @@ -68,9 +69,8 @@ class RaceRepository( } } - fun join(sessionId: String, userId: String, walletId: String): Unit { + fun join(sessionId: String, userId: String, walletId: String) { val session = getSessionById(sessionId) - if (session.users.map { u -> u.userId }.contains(userId)) { val user: DriverMetrics = session.users.find { u -> u.userId == userId } ?: throw Exception("User not found") user.walletId = walletId @@ -79,7 +79,7 @@ class RaceRepository( } } - fun updateProgress(sessionId: String, userId: String, progress: Int): Unit { + fun updateProgress(sessionId: String, userId: String, progress: Int) { val race: Race = getSessionById(sessionId) val user: DriverMetrics = race.users.find { u -> u.userId == userId } ?: throw Exception("User not found") if (user.progress < 100) { @@ -97,6 +97,12 @@ class RaceRepository( if (user.place == 1 && user.walletId.isNotEmpty() && user.walletId.startsWith("0x")) { gameEventPublisher.publishWinnerFinished(session.id, user.walletId) } + + val duelDate = session.startedAt?.toLocalDate() + if (duelDate != null) { + val won = user.place == 1 + userStatisticService.updateUserStatistic(user.walletId, user.cpm.toDouble(), won, duelDate) + } } } @@ -105,4 +111,4 @@ class RaceRepository( val time: Long = session.updatedAt.toEpochSecond(UTC) - session.startedAt!!.toEpochSecond(UTC) return ((typedChars * 60) / time).toInt() } -} \ No newline at end of file +} diff --git a/backend/src/main/kotlin/com/retypeme/project/statistic/controller/StatisticController.kt b/backend/src/main/kotlin/com/retypeme/project/statistic/controller/StatisticController.kt new file mode 100644 index 0000000..4f9e23c --- /dev/null +++ b/backend/src/main/kotlin/com/retypeme/project/statistic/controller/StatisticController.kt @@ -0,0 +1,16 @@ +package com.retypeme.project.racing.controller + +import com.retypeme.project.statistic.model.Statistic +import com.retypeme.project.statistic.service.StatisticService +import org.springframework.web.bind.annotation.* +import java.time.LocalDate + +@RestController +@RequestMapping("/statistics") +class StatisticController(private val userStatisticService: StatisticService) { + + @GetMapping("/{userId}") + fun getStatistic(@PathVariable userId: String): Statistic? { + return userStatisticService.getUserStatistic(userId) + } +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/retypeme/project/statistic/model/Statistic.kt b/backend/src/main/kotlin/com/retypeme/project/statistic/model/Statistic.kt new file mode 100644 index 0000000..c8e7c4c --- /dev/null +++ b/backend/src/main/kotlin/com/retypeme/project/statistic/model/Statistic.kt @@ -0,0 +1,14 @@ +package com.retypeme.project.statistic.model + +import java.time.LocalDate + +class Statistic ( + val userId: String, + var completedDuels: Int = 0, + var averageSpeed: Double = 0.0, + var bestSpeed: Double = 0.0, + var wins: Int = 0, + var maxSpeed: Double = 0.0, + var topSpeeds: MutableList = mutableListOf(), + var firstDuelDate: LocalDate? = null ) + diff --git a/backend/src/main/kotlin/com/retypeme/project/statistic/repository/StatisticRepository.kt b/backend/src/main/kotlin/com/retypeme/project/statistic/repository/StatisticRepository.kt new file mode 100644 index 0000000..3330b4b --- /dev/null +++ b/backend/src/main/kotlin/com/retypeme/project/statistic/repository/StatisticRepository.kt @@ -0,0 +1,29 @@ +package com.retypeme.project.statistic.repository + +import com.retypeme.project.statistic.model.Statistic +import org.springframework.stereotype.Repository +import java.util.concurrent.ConcurrentHashMap + + +interface StatisticRepository { + fun save(userStatistic: Statistic) + fun findById(userId: String): Statistic? + fun findAll(): List +} + +@Repository +class UserStatisticRepositoryImpl : StatisticRepository { + private val userStatistics = ConcurrentHashMap() + + override fun save(userStatistic: Statistic) { + userStatistics[userStatistic.userId] = userStatistic + } + + override fun findById(userId: String): Statistic? { + return userStatistics[userId] + } + + override fun findAll(): List { + return userStatistics.values.toList() + } +} diff --git a/backend/src/main/kotlin/com/retypeme/project/statistic/service/StatisticService.kt b/backend/src/main/kotlin/com/retypeme/project/statistic/service/StatisticService.kt new file mode 100644 index 0000000..bc8e21f --- /dev/null +++ b/backend/src/main/kotlin/com/retypeme/project/statistic/service/StatisticService.kt @@ -0,0 +1,38 @@ +package com.retypeme.project.statistic.service + +import com.retypeme.project.statistic.model.Statistic +import com.retypeme.project.statistic.repository.StatisticRepository +import org.springframework.stereotype.Service +import java.time.LocalDate + +@Service +class StatisticService(private val userStatisticRepository: StatisticRepository) { + + fun updateUserStatistic(userId: String, speed: Double, won: Boolean, duelDate: LocalDate) { + val userStatistic = userStatisticRepository.findById(userId) ?: Statistic(userId, firstDuelDate = duelDate) + + userStatistic.completedDuels++ + userStatistic.averageSpeed = + ((userStatistic.averageSpeed * (userStatistic.completedDuels - 1)) + speed) / userStatistic.completedDuels + if (speed > userStatistic.bestSpeed) { + userStatistic.bestSpeed = speed + } + if (speed > userStatistic.maxSpeed) { + userStatistic.maxSpeed = speed + } + if (won) { + userStatistic.wins++ + } + userStatistic.topSpeeds.add(speed) + userStatistic.topSpeeds.sortDescending() + if (userStatistic.topSpeeds.size > 10) { + userStatistic.topSpeeds = userStatistic.topSpeeds.take(10).toMutableList() + } + + userStatisticRepository.save(userStatistic) + } + + fun getUserStatistic(userId: String): Statistic? { + return userStatisticRepository.findById(userId) + } +} diff --git a/backend/src/main/resources/api-spec.yml b/backend/src/main/resources/api-spec.yml index 8a34743..04b0488 100644 --- a/backend/src/main/resources/api-spec.yml +++ b/backend/src/main/resources/api-spec.yml @@ -33,7 +33,7 @@ paths: schema: $ref: '#/components/schemas/SessionResponse' /sessions/{id}: - parameters: # Add parameters at the path level + parameters: - name: id in: path required: true @@ -52,6 +52,26 @@ paths: $ref: '#/components/schemas/SessionResponse' '404': description: Not Found + /statistics/{userId}: + get: + summary: Get user statistics by ID + operationId: getUserStatistics + parameters: + - name: userId + in: path + required: true + description: User ID + schema: + type: string + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/UserStatistic' + '404': + description: Not Found components: schemas: @@ -95,3 +115,43 @@ components: properties: userId: type: string + ProgressUpdateRequest: + type: object + properties: + userId: + type: string + progress: + type: integer + ProgressUpdateResponse: + type: object + properties: + userId: + type: string + progress: + type: integer + UserStatistic: + type: object + properties: + userId: + type: string + completedDuels: + type: integer + averageSpeed: + type: number + format: double + bestSpeed: + type: number + format: double + wins: + type: integer + maxSpeed: + type: number + format: double + topSpeeds: + type: array + items: + type: number + format: double + firstDuelDate: + type: string + format: date