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 @@ -55,17 +55,23 @@ open class SnackgameBiz(
val streak = streakWithFever.streak
val removedSnacks = board.removeSnacksIn(streak)

val serverIsFever = feverTime?.isActive(streakWithFever.occurredAt) == true
val isValid = streakWithFever.clientIsFever && serverIsFever

val multiplier = if (isValid) FEVER_MULTIPLIER else NORMAL_MULTIPLIER
val multiplier = calculateMultiplier(streakWithFever)
increaseScore(streak.length * multiplier)

if (removedSnacks.any(Snack::isGolden)) {
this.board = board.reset()
}
}

private fun calculateMultiplier(streakWithFever: StreakWithFever): Int {
val serverFever = feverTime ?: return NORMAL_MULTIPLIER

if (streakWithFever.clientIsFever && serverFever.isFeverTime(streakWithFever.occurredAt)) {
return FEVER_MULTIPLIER
}
return NORMAL_MULTIPLIER
}

private fun increaseScore(earn: Int) {
this.score += earn
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,17 +49,23 @@ open class SnackgameBizV2(
val streak = streakWithFever.streak
val removedSnacks = board.removeSnacksIn(streak)

val serverIsFever = feverTime?.isActive(streakWithFever.occurredAt) == true
val isValid = streakWithFever.clientIsFever && serverIsFever

val multiplier = if (isValid) FEVER_MULTIPLIER else NORMAL_MULTIPLIER
val multiplier = calculateMultiplier(streakWithFever)
increaseScore(streak.length * multiplier)

if (removedSnacks.any(Snack::isGolden)) {
this.board = board.reset()
}
}

private fun calculateMultiplier(streakWithFever: StreakWithFever): Int {
val serverFever = feverTime ?: return NORMAL_MULTIPLIER

if (streakWithFever.clientIsFever && serverFever.isFeverTime(streakWithFever.occurredAt)) {
return FEVER_MULTIPLIER
}
return NORMAL_MULTIPLIER
}

private fun increaseScore(earn: Int) {
this.score += earn
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,10 @@ open class Snackgame(

fun remove(streakWithFever: StreakWithFever) {
val streak = streakWithFever.streak
val removedSnacks = board.removeSnacksIn(streakWithFever.streak)
val removedSnacks = board.removeSnacksIn(streak)

increaseScore(streak.length * isFever(streakWithFever))
val multiplier = calculateMultiplier(streakWithFever)
increaseScore(streak.length * multiplier)

if (removedSnacks.any(Snack::isGolden)) {
this.board = board.reset()
Expand All @@ -63,14 +64,13 @@ open class Snackgame(
}
}

private fun isFever(streakWithFever: StreakWithFever): Int {
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
private fun calculateMultiplier(streakWithFever: StreakWithFever): Int {
val serverFever = feverTime ?: return NORMAL_MULTIPLIER

return if (isValid) FEVER_MULTIPLIER else NORMAL_MULTIPLIER
if (streakWithFever.clientIsFever && serverFever.isFeverTime(streakWithFever.occurredAt)) {
return FEVER_MULTIPLIER
}
return NORMAL_MULTIPLIER
}


Expand Down
Original file line number Diff line number Diff line change
@@ -1,66 +1,54 @@
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 feverRemains: Duration,
private var lastResumedAt: LocalDateTime? = null,
private var paused: Boolean? = false,
val feverStartedAt: LocalDateTime,
var feverEndAt: LocalDateTime,
var lastPausedAt: LocalDateTime? = null,
var paused: Boolean? = false
) {

fun isActive(at: LocalDateTime): Boolean {
if (paused!! || feverRemains <= Duration.ZERO) return false
fun isFeverTime(occurredAt: LocalDateTime): Boolean {
val validStartedAt = feverStartedAt.minus(BUFFER_DURATION)

val feverUsed = Duration.between(lastResumedAt, at)
return (feverRemains - feverUsed) > Duration.ZERO
if (occurredAt.isBefore(validStartedAt) || occurredAt.isAfter(feverEndAt)) {
return false
}
return true
}

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

fun resume(at: LocalDateTime) {
if (paused!! && feverRemains > Duration.ZERO) {
paused = false
lastResumedAt = at
}
}
if (paused == true && lastPausedAt != null) {
val pausedDuration = Duration.between(lastPausedAt, at)
// 쉬었던 만큼 종료 시간을 뒤로 미룸
this.feverEndAt = this.feverEndAt.plus(pausedDuration)

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)
this.paused = false
this.lastPausedAt = null
}
}

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

fun start(now: LocalDateTime = LocalDateTime.now()): FeverTime {
return FeverTime(
feverStartedAt = now,
feverRemains = FEVER_TIME_PERIOD,
lastResumedAt = now,
feverEndAt = now.plus(FEVER_DURATION),
paused = false
)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package com.snackgame.server.game.snackgame.core.domain


import com.snackgame.server.game.snackgame.core.service.dto.StreakWithFever
import com.snackgame.server.game.snackgame.fixture.BoardFixture
import com.snackgame.server.game.snackgame.fixture.BoardFixture.TWO_BY_TWO_WITH_GOLDEN_SNACK
import com.snackgame.server.member.fixture.MemberFixture.땡칠
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import java.time.LocalDateTime

class SnackgameTest {

Expand Down Expand Up @@ -60,4 +62,69 @@ class SnackgameTest {

assertThat(game.board).isNotEqualTo(TWO_BY_TWO_WITH_GOLDEN_SNACK())
}

@Test
fun `피버타임_중에_발생한_스트릭은_점수가_2배가_된다`() {
val game = Snackgame(땡칠().id, BoardFixture.TWO_BY_FOUR())
game.startFeverTime()
val streak = Streak.of(arrayListOf(Coordinate(0, 0), Coordinate(1, 0)))

val now = LocalDateTime.now()
val request = StreakWithFever(streak, clientIsFever = true, occurredAt = now)

game.remove(request)

assertThat(game.score).isEqualTo(4)
}

@Test
fun `피버타임이_아닐_때_발생한_스트릭은_클라이언트가_우겨도_점수가_2배가_되지_않는다`() {
val game = Snackgame(땡칠().id, BoardFixture.TWO_BY_FOUR())
game.startFeverTime()
val streak = Streak.of(arrayListOf(Coordinate(0, 0), Coordinate(1, 0)))

val past = LocalDateTime.now().minusSeconds(10)
val request = StreakWithFever(streak, clientIsFever = true, occurredAt = past)

game.remove(request)

assertThat(game.score).isEqualTo(2)
}

@Test
fun `네트워크_지연으로_요청이_늦게_와도_발생_시각이_피버타임_내라면_2배_적용된다`() {
val game = Snackgame(땡칠().id, BoardFixture.TWO_BY_FOUR())

game.startFeverTime()


val streak = Streak.of(arrayListOf(Coordinate(0, 0), Coordinate(1, 0)))
val occurredAt = LocalDateTime.now().plusSeconds(5)

val request = StreakWithFever(streak, clientIsFever = true, occurredAt = occurredAt)

game.remove(request)

assertThat(game.score).isEqualTo(4)
}

@Test
fun `일시정지_후_재개하면_피버타임도_연장되어_점수_2배가_적용된다`() {
val game = Snackgame(땡칠().id, BoardFixture.TWO_BY_FOUR())
game.startFeverTime()

game.feverTime!!.pause(LocalDateTime.now().plusSeconds(10))

game.feverTime!!.resume(LocalDateTime.now().plusHours(1))

val streak = Streak.of(arrayListOf(Coordinate(0, 0), Coordinate(1, 0)))


val occurredAt = LocalDateTime.now().plusHours(1).plusSeconds(5)
val request = StreakWithFever(streak, clientIsFever = true, occurredAt = occurredAt)

game.remove(request)

assertThat(game.score).isEqualTo(4)
}
}
Original file line number Diff line number Diff line change
@@ -1,78 +1,58 @@
@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.assertj.core.api.Assertions.assertThat
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)
fun `피버타임은_시작_시점과_종료_시점_사이에_발생한_이벤트만_인정한다`() {
val start = LocalDateTime.of(2025, 11, 30, 12, 0, 0)
val end = start.plusSeconds(30)
val feverTime = FeverTime(start, end)


fever.pause(pauseTime)
assertThat(feverTime.isFeverTime(start)).isTrue
assertThat(feverTime.isFeverTime(end)).isTrue
assertThat(feverTime.isFeverTime(start.plusSeconds(15))).isTrue

assertFalse(fever.isActive(pauseTime))

assertThat(feverTime.isFeverTime(start.minusSeconds(2))).isFalse
assertThat(feverTime.isFeverTime(end.plusSeconds(2))).isFalse
}

@Test
fun `피버타임을 재개하면 다시 시간을 재야한다`() {
val startTime = LocalDateTime.now()
val pauseTime = startTime.plusSeconds(10)
val resumeTime = pauseTime.plusSeconds(5)
val fever = FeverTime.start(startTime)
fun `일시정지했다가_재개하면_피버_종료_시간이_정지했던_만큼_늘어난다`() {
val start = LocalDateTime.of(2025, 11, 30, 12, 0, 0)
val initialEnd = start.plusSeconds(30)
val feverTime = FeverTime(start, initialEnd)

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

assertTrue(fever.isActive(resumeTime))
}
val pausedAt = start.plusSeconds(10)
feverTime.pause(pausedAt)


val resumedAt = start.plusSeconds(20)
feverTime.resume(resumedAt)

@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))
assertThat(feverTime.feverEndAt).isEqualTo(initialEnd.plusSeconds(10))
}

@Test
fun `validateFeverStreakOccurredAt should correctly validate client occurrence`() {
val startTime = LocalDateTime.of(2025, 9, 8, 8, 0, 0)
val fever = FeverTime.start(startTime)
fun `일시정지_상태에서는_시간이_연장되지_않는다_재개해야_반영됨`() {
val start = LocalDateTime.of(2025, 11, 30, 12, 0, 0)
val feverTime = FeverTime(start, start.plusSeconds(30))

val validTime = startTime.plusSeconds(10)
val invalidTime = startTime.plusSeconds(40)
feverTime.pause(start.plusSeconds(10))

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)) // 남은 시간 고려
assertThat(feverTime.feverEndAt).isEqualTo(start.plusSeconds(30))
}
}
}
Loading
Loading