diff --git a/src/main/java/com/snackgame/server/game/session/event/SessionEndEvent.kt b/src/main/java/com/snackgame/server/game/session/event/SessionEndEvent.kt index 9b65a590..a36f459a 100644 --- a/src/main/java/com/snackgame/server/game/session/event/SessionEndEvent.kt +++ b/src/main/java/com/snackgame/server/game/session/event/SessionEndEvent.kt @@ -5,10 +5,10 @@ import com.snackgame.server.game.session.domain.Session data class SessionEndEvent( val metadata: Metadata, - val ownerId: Long, - val sessionId: Long, + override val ownerId: Long, + override val sessionId: Long, val score: Int -) { +) : SessionStateEvent { companion object { fun of(session: Session): SessionEndEvent { diff --git a/src/main/java/com/snackgame/server/game/session/event/SessionPauseEvent.kt b/src/main/java/com/snackgame/server/game/session/event/SessionPauseEvent.kt new file mode 100644 index 00000000..0383da1a --- /dev/null +++ b/src/main/java/com/snackgame/server/game/session/event/SessionPauseEvent.kt @@ -0,0 +1,21 @@ +package com.snackgame.server.game.session.event + +import com.snackgame.server.game.session.domain.Session +import java.time.LocalDateTime + +data class SessionPauseEvent( + override val sessionId: Long, + override val ownerId: Long, + val occurredAt: LocalDateTime +) : SessionStateEvent { + + companion object { + fun of(session: Session): SessionPauseEvent { + return SessionPauseEvent( + session.sessionId, + session.ownerId, + LocalDateTime.now() + ) + } + } +} \ No newline at end of file diff --git a/src/main/java/com/snackgame/server/game/session/event/SessionResumeEvent.kt b/src/main/java/com/snackgame/server/game/session/event/SessionResumeEvent.kt new file mode 100644 index 00000000..f993a831 --- /dev/null +++ b/src/main/java/com/snackgame/server/game/session/event/SessionResumeEvent.kt @@ -0,0 +1,21 @@ +package com.snackgame.server.game.session.event + +import com.snackgame.server.game.session.domain.Session +import java.time.LocalDateTime + +data class SessionResumeEvent( + override val sessionId: Long, + override val ownerId: Long, + val occurredAt: LocalDateTime +) : SessionStateEvent { + + companion object { + fun of(session: Session): SessionResumeEvent { + return SessionResumeEvent( + session.sessionId, + session.ownerId, + LocalDateTime.now() + ) + } + } +} diff --git a/src/main/java/com/snackgame/server/game/session/event/SessionStateEvent.kt b/src/main/java/com/snackgame/server/game/session/event/SessionStateEvent.kt new file mode 100644 index 00000000..2862c5b6 --- /dev/null +++ b/src/main/java/com/snackgame/server/game/session/event/SessionStateEvent.kt @@ -0,0 +1,6 @@ +package com.snackgame.server.game.session.event + +interface SessionStateEvent { + val sessionId: Long + val ownerId: Long +} 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 3ff312d7..89ac2fad 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 @@ -6,12 +6,17 @@ import com.snackgame.server.game.snackgame.core.domain.Board import com.snackgame.server.game.snackgame.core.domain.BoardConverter import com.snackgame.server.game.snackgame.core.domain.Snackgame.Companion.DEFAULT_HEIGHT import com.snackgame.server.game.snackgame.core.domain.Snackgame.Companion.DEFAULT_WIDTH +import com.snackgame.server.game.snackgame.core.domain.Snackgame.Companion.FEVER_MULTIPLIER +import com.snackgame.server.game.snackgame.core.domain.Snackgame.Companion.NORMAL_MULTIPLIER import com.snackgame.server.game.snackgame.core.domain.Snackgame.Companion.SESSION_TIME import com.snackgame.server.game.snackgame.core.domain.Snackgame.Companion.SPARE_TIME import com.snackgame.server.game.snackgame.core.domain.Streak +import com.snackgame.server.game.snackgame.core.domain.item.FeverTime import com.snackgame.server.game.snackgame.core.domain.snack.Snack +import com.snackgame.server.game.snackgame.core.service.dto.StreakWithFever import java.time.Duration import javax.persistence.Convert +import javax.persistence.Embedded import javax.persistence.Entity import javax.persistence.Lob @@ -33,6 +38,11 @@ open class SnackgameBiz( this.score = score } + @Embedded + var feverTime: FeverTime? = null + private set + + fun remove(streak: Streak) { val removedSnacks = board.removeSnacksIn(streak) this.score += removedSnacks.size @@ -41,5 +51,25 @@ open class SnackgameBiz( } } + fun remove(streakWithFever: StreakWithFever) { + 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 + increaseScore(streak.length * multiplier) + + if (removedSnacks.any(Snack::isGolden)) { + this.board = board.reset() + } + } + + private fun increaseScore(earn: Int) { + val multiplier = if (feverTime?.isActive() == true) FEVER_MULTIPLIER else NORMAL_MULTIPLIER + this.score += earn * multiplier + } + override val metadata = SNACK_GAME_BIZ } 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 04012629..146368ab 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 @@ -6,12 +6,17 @@ import com.snackgame.server.game.snackgame.core.domain.Board import com.snackgame.server.game.snackgame.core.domain.BoardConverter import com.snackgame.server.game.snackgame.core.domain.Snackgame.Companion.DEFAULT_HEIGHT import com.snackgame.server.game.snackgame.core.domain.Snackgame.Companion.DEFAULT_WIDTH +import com.snackgame.server.game.snackgame.core.domain.Snackgame.Companion.FEVER_MULTIPLIER +import com.snackgame.server.game.snackgame.core.domain.Snackgame.Companion.NORMAL_MULTIPLIER import com.snackgame.server.game.snackgame.core.domain.Snackgame.Companion.SESSION_TIME import com.snackgame.server.game.snackgame.core.domain.Snackgame.Companion.SPARE_TIME import com.snackgame.server.game.snackgame.core.domain.Streak +import com.snackgame.server.game.snackgame.core.domain.item.FeverTime import com.snackgame.server.game.snackgame.core.domain.snack.Snack +import com.snackgame.server.game.snackgame.core.service.dto.StreakWithFever import java.time.Duration import javax.persistence.Convert +import javax.persistence.Embedded import javax.persistence.Entity import javax.persistence.Lob @@ -28,6 +33,10 @@ open class SnackgameBizV2( var board = board private set + @Embedded + var feverTime: FeverTime? = null + private set + fun remove(streak: Streak) { val removedSnacks = board.removeSnacksIn(streak) this.score += removedSnacks.size @@ -36,5 +45,25 @@ open class SnackgameBizV2( } } + fun remove(streakWithFever: StreakWithFever) { + 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 + increaseScore(streak.length * multiplier) + + if (removedSnacks.any(Snack::isGolden)) { + this.board = board.reset() + } + } + + private fun increaseScore(earn: Int) { + val multiplier = if (feverTime?.isActive() == true) FEVER_MULTIPLIER else NORMAL_MULTIPLIER + this.score += earn * multiplier + } + override val metadata = SNACK_GAME_BIZ_V2 } 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 73be78cb..f786cbe4 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 @@ -4,6 +4,7 @@ import com.snackgame.server.game.metadata.Metadata.SNACK_GAME import com.snackgame.server.game.session.domain.Session import com.snackgame.server.game.snackgame.core.domain.item.FeverTime import com.snackgame.server.game.snackgame.core.domain.snack.Snack +import com.snackgame.server.game.snackgame.core.service.dto.StreakWithFever import java.time.Duration import javax.persistence.Convert import javax.persistence.Embedded @@ -32,6 +33,7 @@ open class Snackgame( this.score = score } + //todo : 제거 예정 fun remove(streak: Streak) { val removedSnacks = board.removeSnacksIn(streak) increaseScore(streak.length) @@ -41,6 +43,21 @@ open class Snackgame( } } + fun remove(streakWithFever: StreakWithFever) { + 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 + increaseScore(streak.length * multiplier) + + if (removedSnacks.any(Snack::isGolden)) { + this.board = board.reset() + } + } + fun removeBomb(streak: Streak) { val removedSnacks = board.bombSnacksIn(streak) increaseScore(removedSnacks.size) 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 ed77a4c2..ead7eaf6 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 @@ -6,10 +6,28 @@ import javax.persistence.Embeddable @Embeddable class FeverTime( - private val feverStartedAt: LocalDateTime? = null + private var feverStartedAt: LocalDateTime? = null, + private var feverPausedAt: LocalDateTime? = null ) { fun isActive(now: LocalDateTime = LocalDateTime.now()): Boolean { - return feverStartedAt != null && Duration.between(feverStartedAt, now) < DURATION + if (feverStartedAt == null) return false + val effectiveStart = feverPausedAt?.let { feverStartedAt!!.plus(Duration.between(it, now)) } ?: feverStartedAt + return Duration.between(effectiveStart, now) < DURATION + } + + fun pause() { + if (feverStartedAt != null && feverPausedAt == null) { + feverPausedAt = LocalDateTime.now() + } + } + + fun resume() { + if (feverStartedAt != null && feverPausedAt != null) { + val now = LocalDateTime.now() + val pausedDuration = Duration.between(feverPausedAt, now) + feverStartedAt = feverStartedAt!!.plus(pausedDuration) + feverPausedAt = null + } } companion object { diff --git a/src/main/java/com/snackgame/server/game/snackgame/core/domain/item/FeverTimeListener.kt b/src/main/java/com/snackgame/server/game/snackgame/core/domain/item/FeverTimeListener.kt new file mode 100644 index 00000000..76bcab40 --- /dev/null +++ b/src/main/java/com/snackgame/server/game/snackgame/core/domain/item/FeverTimeListener.kt @@ -0,0 +1,26 @@ +package com.snackgame.server.game.snackgame.core.domain.item + +import com.snackgame.server.game.session.event.SessionPauseEvent +import com.snackgame.server.game.session.event.SessionResumeEvent +import com.snackgame.server.game.snackgame.core.domain.SnackgameRepository +import com.snackgame.server.game.snackgame.core.domain.getBy +import org.springframework.stereotype.Component +import org.springframework.transaction.event.TransactionPhase +import org.springframework.transaction.event.TransactionalEventListener + +@Component +class FeverTimeListener( + private val snackgameRepository: SnackgameRepository +) { + @TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT) + fun onSessionPaused(event: SessionPauseEvent) { + val game = snackgameRepository.getBy(event.ownerId, event.sessionId) + game.feverTime?.pause() + } + + @TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT) + fun onSessionResumed(event: SessionResumeEvent) { + val game = snackgameRepository.getBy(event.ownerId, event.sessionId) + game.feverTime?.resume() + } +} \ No newline at end of file diff --git a/src/main/java/com/snackgame/server/game/snackgame/core/service/SnackgameService.kt b/src/main/java/com/snackgame/server/game/snackgame/core/service/SnackgameService.kt index 3473a33b..c89752bc 100644 --- a/src/main/java/com/snackgame/server/game/snackgame/core/service/SnackgameService.kt +++ b/src/main/java/com/snackgame/server/game/snackgame/core/service/SnackgameService.kt @@ -2,6 +2,8 @@ package com.snackgame.server.game.snackgame.core.service import com.snackgame.server.game.session.event.SessionEndEvent +import com.snackgame.server.game.session.event.SessionPauseEvent +import com.snackgame.server.game.session.event.SessionResumeEvent import com.snackgame.server.game.snackgame.core.domain.Snackgame import com.snackgame.server.game.snackgame.core.domain.SnackgameRepository import com.snackgame.server.game.snackgame.core.domain.Streak @@ -78,6 +80,7 @@ class SnackgameService( val game = snackGameRepository.getBy(memberId, sessionId) game.pause() + eventPublisher.publishEvent(SessionPauseEvent.of(game)) return SnackgameResponse.of(game) } @@ -87,6 +90,7 @@ class SnackgameService( val game = snackGameRepository.getBy(memberId, sessionId) game.resume() + eventPublisher.publishEvent(SessionResumeEvent.of(game)) return SnackgameResponse.of(game) } diff --git a/src/main/java/com/snackgame/server/game/snackgame/core/service/dto/StreaksRequest.kt b/src/main/java/com/snackgame/server/game/snackgame/core/service/dto/StreaksRequest.kt index 51671ece..f9ff0133 100644 --- a/src/main/java/com/snackgame/server/game/snackgame/core/service/dto/StreaksRequest.kt +++ b/src/main/java/com/snackgame/server/game/snackgame/core/service/dto/StreaksRequest.kt @@ -3,12 +3,29 @@ 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 java.time.LocalDateTime data class StreaksRequest @JsonCreator constructor( - val streaks: List> + val streaks: List ) { + fun toStreaks(now: LocalDateTime = LocalDateTime.now()): List = + streaks.map { it.toDomain(now) } +} - fun toStreaks(): List = streaks.map { streak -> - Streak.of(streak.map { Coordinate(it.y, it.x) }) - } +data class StreakWithMeta( + val coordinates: List, + val isFever: Boolean +) { + fun toDomain(now: LocalDateTime): StreakWithFever = + StreakWithFever( + streak = Streak.of(coordinates.map { Coordinate(it.y, it.x) }), + clientIsFever = isFever, + occurredAt = now + ) } + +data class StreakWithFever( + val streak: Streak, + val clientIsFever: Boolean, + val occurredAt: LocalDateTime +) \ 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 2d2c0d1b..0384d49a 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 @@ -2,10 +2,10 @@ package com.snackgame.server.game.snackgame.core.service -import com.snackgame.server.fixture.SeasonFixture import com.snackgame.server.game.snackgame.core.domain.Snackgame import com.snackgame.server.game.snackgame.core.domain.SnackgameRepository import com.snackgame.server.game.snackgame.core.service.dto.CoordinateRequest +import com.snackgame.server.game.snackgame.core.service.dto.StreakWithMeta import com.snackgame.server.game.snackgame.core.service.dto.StreaksRequest import com.snackgame.server.game.snackgame.fixture.BoardFixture import com.snackgame.server.game.snackgame.fixture.ItemFixture @@ -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.LocalDateTime @ServiceTest class SnackgameServiceTest { @@ -39,7 +40,19 @@ class SnackgameServiceTest { CoordinateRequest(0, 0) ) - snackgameService.removeStreaks(땡칠().id, game.sessionId, StreaksRequest(listOf(coordinates))) + + snackgameService.removeStreaks( + 땡칠().id, + game.sessionId, + StreaksRequest( + listOf( + StreakWithMeta( + coordinates = coordinates, + isFever = false + ) + ) + ) + ) val found = snackgameRepository.findByOwnerIdAndSessionId(땡칠().id, game.sessionId)!! assertThat(found.score).isEqualTo(2) @@ -64,9 +77,37 @@ class SnackgameServiceTest { ) snackgameService.useFeverTime(땡칠().id, game.sessionId) - snackgameService.removeStreaks(땡칠().id, game.sessionId, StreaksRequest(listOf(coordinates))) + + snackgameService.removeStreaks( + 땡칠().id, + game.sessionId, + StreaksRequest( + listOf( + StreakWithMeta( + coordinates = coordinates, + isFever = false + ) + ) + ) + ) val found = snackgameRepository.findByOwnerIdAndSessionId(땡칠().id, game.sessionId)!! assertThat(found.score).isEqualTo(4) } + + @Test + fun `피버타임 pause 후 resume 시 남은 시간이 유지된다`() { + val game = snackgameRepository.save(Snackgame(1L, BoardFixture.TWO_BY_FOUR())) + + game.startFeverTime() + val feverTime = game.feverTime!! + + Thread.sleep(1000) + snackgameService.pause(game.ownerId, game.sessionId) + + snackgameService.resume(game.ownerId, game.sessionId) + + val activeAfterResume = feverTime.isActive(LocalDateTime.now().plusSeconds(28)) + assertThat(activeAfterResume).isTrue() + } }