From 81ff274dce1646f99121052767e3530e704c747d Mon Sep 17 00:00:00 2001 From: hwanvely <990706leo@gmail.com> Date: Mon, 1 Dec 2025 20:37:34 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20=ED=94=BC=EB=B2=84=ED=83=80=EC=9E=84=20?= =?UTF-8?q?=EB=84=A4=ED=8A=B8=EC=9B=8C=ED=81=AC=20=EC=A7=80=EC=97=B0=20?= =?UTF-8?q?=EB=AC=B8=EC=A0=9C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../game/snackgame/biz/domain/SnackgameBiz.kt | 14 +++- .../snackgame/biz/domain/SnackgameBizV2.kt | 14 +++- .../game/snackgame/core/domain/Snackgame.kt | 18 ++-- .../snackgame/core/domain/item/FeverTime.kt | 60 ++++++-------- .../snackgame/core/domain/SnackgameTest.kt | 67 +++++++++++++++ .../snackgame/core/service/FeverTimeTest.kt | 82 +++++++------------ .../core/service/SnackgameServiceTest.kt | 26 +++--- 7 files changed, 166 insertions(+), 115 deletions(-) diff --git a/src/main/java/com/snackgame/server/game/snackgame/biz/domain/SnackgameBiz.kt b/src/main/java/com/snackgame/server/game/snackgame/biz/domain/SnackgameBiz.kt index 8b1e90e..1df62f4 100644 --- a/src/main/java/com/snackgame/server/game/snackgame/biz/domain/SnackgameBiz.kt +++ b/src/main/java/com/snackgame/server/game/snackgame/biz/domain/SnackgameBiz.kt @@ -55,10 +55,7 @@ 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)) { @@ -66,6 +63,15 @@ open class SnackgameBiz( } } + 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 } diff --git a/src/main/java/com/snackgame/server/game/snackgame/biz/domain/SnackgameBizV2.kt b/src/main/java/com/snackgame/server/game/snackgame/biz/domain/SnackgameBizV2.kt index 9b2771d..8d942f1 100644 --- a/src/main/java/com/snackgame/server/game/snackgame/biz/domain/SnackgameBizV2.kt +++ b/src/main/java/com/snackgame/server/game/snackgame/biz/domain/SnackgameBizV2.kt @@ -49,10 +49,7 @@ 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)) { @@ -60,6 +57,15 @@ open class SnackgameBizV2( } } + 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 } diff --git a/src/main/java/com/snackgame/server/game/snackgame/core/domain/Snackgame.kt b/src/main/java/com/snackgame/server/game/snackgame/core/domain/Snackgame.kt index d0581e1..b44965b 100644 --- a/src/main/java/com/snackgame/server/game/snackgame/core/domain/Snackgame.kt +++ b/src/main/java/com/snackgame/server/game/snackgame/core/domain/Snackgame.kt @@ -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() @@ -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 } diff --git a/src/main/java/com/snackgame/server/game/snackgame/core/domain/item/FeverTime.kt b/src/main/java/com/snackgame/server/game/snackgame/core/domain/item/FeverTime.kt index 486c106..007a89c 100644 --- a/src/main/java/com/snackgame/server/game/snackgame/core/domain/item/FeverTime.kt +++ b/src/main/java/com/snackgame/server/game/snackgame/core/domain/item/FeverTime.kt @@ -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 ) } } -} +} \ No newline at end of file diff --git a/src/test/java/com/snackgame/server/game/snackgame/core/domain/SnackgameTest.kt b/src/test/java/com/snackgame/server/game/snackgame/core/domain/SnackgameTest.kt index 01dd812..976fcea 100644 --- a/src/test/java/com/snackgame/server/game/snackgame/core/domain/SnackgameTest.kt +++ b/src/test/java/com/snackgame/server/game/snackgame/core/domain/SnackgameTest.kt @@ -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 { @@ -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) + } } diff --git a/src/test/java/com/snackgame/server/game/snackgame/core/service/FeverTimeTest.kt b/src/test/java/com/snackgame/server/game/snackgame/core/service/FeverTimeTest.kt index 06901f4..5d7b713 100644 --- a/src/test/java/com/snackgame/server/game/snackgame/core/service/FeverTimeTest.kt +++ b/src/test/java/com/snackgame/server/game/snackgame/core/service/FeverTimeTest.kt @@ -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)) } -} +} \ No newline at end of file diff --git a/src/test/java/com/snackgame/server/game/snackgame/core/service/SnackgameServiceTest.kt b/src/test/java/com/snackgame/server/game/snackgame/core/service/SnackgameServiceTest.kt index 4f73191..8630b78 100644 --- a/src/test/java/com/snackgame/server/game/snackgame/core/service/SnackgameServiceTest.kt +++ b/src/test/java/com/snackgame/server/game/snackgame/core/service/SnackgameServiceTest.kt @@ -15,6 +15,7 @@ import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.springframework.beans.factory.annotation.Autowired +import java.time.Duration import java.time.LocalDateTime @ServiceTest @@ -40,7 +41,6 @@ class SnackgameServiceTest { CoordinateRequest(0, 0) ) - snackgameService.removeStreaks( 땡칠().id, game.sessionId, @@ -98,18 +98,22 @@ class SnackgameServiceTest { } @Test - fun `피버타임 pause 후 resume 시 남은 시간이 유지된다`() { - val game = snackgameRepository.save(Snackgame(1L, BoardFixture.TWO_BY_FOUR())) - - game.startFeverTime() - val feverTime = game.feverTime!! + fun `피버타임 pause 후 resume 시 종료 시간이 연장된다`() { + val game = snackgameRepository.save(Snackgame(땡칠().id, BoardFixture.TWO_BY_FOUR())) + snackgameService.useFeverTime(땡칠().id, game.sessionId) - Thread.sleep(1000) snackgameService.pause(game.ownerId, game.sessionId) - + Thread.sleep(1000) snackgameService.resume(game.ownerId, game.sessionId) - val activeAfterResume = feverTime.isActive(LocalDateTime.now().plusSeconds(28)) - assertThat(activeAfterResume).isTrue() + val resumedGame = snackgameRepository.findByOwnerIdAndSessionId(땡칠().id, game.sessionId)!! + val feverTime = resumedGame.feverTime!! + + val totalDuration = Duration.between(feverTime.feverStartedAt, feverTime.feverEndAt) + + assertThat(totalDuration).isGreaterThan(Duration.ofSeconds(30)) + + val slightlyLateTime = feverTime.feverStartedAt.plusSeconds(30).plusNanos(500) + assertThat(feverTime.isFeverTime(slightlyLateTime)).isTrue() } -} +} \ No newline at end of file