Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,7 @@ open class SnackgameBiz(
}

private fun increaseScore(earn: Int) {
val multiplier = if (feverTime?.isActive() == true) FEVER_MULTIPLIER else NORMAL_MULTIPLIER
this.score += earn * multiplier
this.score += earn
}

override val metadata = SNACK_GAME_BIZ
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,7 @@ open class SnackgameBizV2(
}

private fun increaseScore(earn: Int) {
val multiplier = if (feverTime?.isActive() == true) FEVER_MULTIPLIER else NORMAL_MULTIPLIER
this.score += earn * multiplier
this.score += earn
}

override val metadata = SNACK_GAME_BIZ_V2
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,12 +64,16 @@ open class Snackgame(
}

private fun isFever(streakWithFever: StreakWithFever): Int {
val serverIsFever = feverTime?.isActive(streakWithFever.occurredAt) == true
val isValid = streakWithFever.clientIsFever && serverIsFever
val multiplier = if (isValid) FEVER_MULTIPLIER else NORMAL_MULTIPLIER
return multiplier
val serverFever = feverTime
if (serverFever == null) return NORMAL_MULTIPLIER
val serverIsActive = serverFever.isActive(streakWithFever.occurredAt)
val feverStreakValidate = serverFever.validateFeverStreakOccurredAt(streakWithFever.occurredAt)
val isValid = streakWithFever.clientIsFever && serverIsActive && feverStreakValidate

return if (isValid) FEVER_MULTIPLIER else NORMAL_MULTIPLIER
}


private fun increaseScore(earn: Int) {
this.score += earn
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,40 +1,66 @@
package com.snackgame.server.game.snackgame.core.domain.item

import com.snackgame.server.game.snackgame.exception.InvalidStreakTimeException
import java.time.Duration
import java.time.LocalDateTime
import javax.persistence.Embeddable

@Embeddable
class FeverTime(
private var feverStartedAt: LocalDateTime? = null,
private var feverPausedAt: LocalDateTime? = null
private var feverRemains: Duration,
private var lastResumedAt: LocalDateTime? = null,
private var paused: Boolean? = false,
) {
fun isActive(now: LocalDateTime = LocalDateTime.now()): Boolean {
if (feverStartedAt == null) return false
val effectiveStart = feverPausedAt?.let { feverStartedAt!!.plus(Duration.between(it, now)) } ?: feverStartedAt
return Duration.between(effectiveStart, now) < FEVER_TIME_PERIOD

fun isActive(at: LocalDateTime): Boolean {
if (paused!! || feverRemains <= Duration.ZERO) return false

val feverUsed = Duration.between(lastResumedAt, at)
return (feverRemains - feverUsed) > Duration.ZERO
}

fun pause() {
if (feverStartedAt != null && feverPausedAt == null) {
feverPausedAt = LocalDateTime.now()
fun pause(at: LocalDateTime) {
if (!paused!! && lastResumedAt != null) {
val feverUsed = Duration.between(lastResumedAt, at)
feverRemains = (feverRemains - feverUsed).coerceAtLeast(Duration.ZERO)
paused = true
lastResumedAt = null
}
}

fun resume() {
if (feverStartedAt != null && feverPausedAt != null) {
val now = LocalDateTime.now()
val pausedDuration = Duration.between(feverPausedAt, now)
feverStartedAt = feverStartedAt!!.plus(pausedDuration)
feverPausedAt = null
fun resume(at: LocalDateTime) {
if (paused!! && feverRemains > Duration.ZERO) {
paused = false
lastResumedAt = at
}
}

fun validateFeverStreakOccurredAt(streakOccurredAt: LocalDateTime): Boolean {
val feverEndAt = calculateFeverEnd(streakOccurredAt)
if (streakOccurredAt.isBefore(feverStartedAt) || streakOccurredAt.isAfter(feverEndAt))
throw InvalidStreakTimeException()
return true
}

private fun calculateFeverEnd(at: LocalDateTime): LocalDateTime {
val progressed = (!paused!! && lastResumedAt != null)
.takeIf { it }?.let { Duration.between(lastResumedAt, at) } ?: Duration.ZERO

val remaining = (feverRemains - progressed).coerceAtLeast(Duration.ZERO)
return at.minus(progressed).plus(remaining)
}

companion object {
private val FEVER_TIME_PERIOD: Duration = Duration.ofSeconds(30)

fun start(now: LocalDateTime = LocalDateTime.now()): FeverTime {
return FeverTime(now)
return FeverTime(
feverStartedAt = now,
feverRemains = FEVER_TIME_PERIOD,
lastResumedAt = now,
paused = false
)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,12 @@ class FeverTimeListener(
@TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT)
fun onSessionPaused(event: SessionPauseEvent) {
val game = snackgameRepository.getBy(event.ownerId, event.sessionId)
game.feverTime?.pause()
game.feverTime?.pause(event.occurredAt)
}

@TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT)
fun onSessionResumed(event: SessionResumeEvent) {
val game = snackgameRepository.getBy(event.ownerId, event.sessionId)
game.feverTime?.resume()
game.feverTime?.resume(event.occurredAt)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,37 +3,27 @@ package com.snackgame.server.game.snackgame.core.service.dto
import com.fasterxml.jackson.annotation.JsonCreator
import com.snackgame.server.game.snackgame.core.domain.Coordinate
import com.snackgame.server.game.snackgame.core.domain.Streak
import com.snackgame.server.game.snackgame.exception.InvalidStreakTimeException
import java.time.Duration
import java.time.LocalDateTime

data class StreaksRequest @JsonCreator constructor(
val streaks: List<StreakWithMeta>
) {
fun toStreaks(now: LocalDateTime = LocalDateTime.now()): List<StreakWithFever> =
streaks.map { it.toDomain(now) }
fun toStreaks(): List<StreakWithFever> =
streaks.map { it.toDomain() }
}

data class StreakWithMeta(
val coordinates: List<CoordinateRequest>,
val isFever: Boolean,
val occurredAt: LocalDateTime,
) {
fun toDomain(now: LocalDateTime): StreakWithFever {
if (Duration.between(occurredAt, now).abs() > REQUEST_TIME_TOLERANCE) {
throw InvalidStreakTimeException()
}

fun toDomain(): StreakWithFever {
return StreakWithFever(
streak = Streak.of(coordinates.map { Coordinate(it.y, it.x) }),
clientIsFever = isFever,
occurredAt = occurredAt
)
}

companion object {
private val REQUEST_TIME_TOLERANCE: Duration = Duration.ofSeconds(2)
}
}

data class StreakWithFever(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
@file:Suppress("NonAsciiCharacters")

package com.snackgame.server.game.snackgame.core.service

import com.snackgame.server.game.snackgame.core.domain.item.FeverTime
import com.snackgame.server.game.snackgame.exception.InvalidStreakTimeException
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertThrows
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
import java.time.LocalDateTime

class FeverTimeTest {

@Test
fun `일시정지가 되면 피버타임이 비활성화 된다`() {
val startTime = LocalDateTime.now()
val pauseTime = startTime.plusSeconds(10)
val fever = FeverTime.start(startTime)

fever.pause(pauseTime)

assertFalse(fever.isActive(pauseTime))
}

@Test
fun `피버타임을 재개하면 다시 시간을 재야한다`() {
val startTime = LocalDateTime.now()
val pauseTime = startTime.plusSeconds(10)
val resumeTime = pauseTime.plusSeconds(5)
val fever = FeverTime.start(startTime)

fever.pause(pauseTime)
fever.resume(resumeTime)

assertTrue(fever.isActive(resumeTime))
}

@Test
fun `isActive should return false after fever ends`() {
val startTime = LocalDateTime.of(2025, 9, 8, 8, 0, 0)
val endTime = startTime.plusSeconds(31)
val fever = FeverTime.start(startTime)

assertFalse(fever.isActive(endTime))
}

@Test
fun `validateFeverStreakOccurredAt should correctly validate client occurrence`() {
val startTime = LocalDateTime.of(2025, 9, 8, 8, 0, 0)
val fever = FeverTime.start(startTime)

val validTime = startTime.plusSeconds(10)
val invalidTime = startTime.plusSeconds(40)

assertTrue(fever.validateFeverStreakOccurredAt(validTime))
assertThrows(InvalidStreakTimeException::class.java) { fever.validateFeverStreakOccurredAt(invalidTime) }
}

@Test
fun `pause and resume multiple times should correctly update remaining time`() {
val startTime = LocalDateTime.of(2025, 9, 8, 8, 0, 0)
val fever = FeverTime.start(startTime)

val pause1 = startTime.plusSeconds(10)
val resume1 = pause1.plusSeconds(5)
val pause2 = resume1.plusSeconds(5)
val resume2 = pause2.plusSeconds(3)

fever.pause(pause1)
fever.resume(resume1)
fever.pause(pause2)
fever.resume(resume2)

val checkTime = resume2.plusSeconds(10)
assertTrue(fever.isActive(checkTime)) // 남은 시간 고려
}
}
Loading