diff --git a/adapter/memory/src/main/kotlin/com/xorker/draw/timer/TimerAdapter.kt b/adapter/memory/src/main/kotlin/com/xorker/draw/timer/TimerAdapter.kt index e2a9520b..bd15d878 100644 --- a/adapter/memory/src/main/kotlin/com/xorker/draw/timer/TimerAdapter.kt +++ b/adapter/memory/src/main/kotlin/com/xorker/draw/timer/TimerAdapter.kt @@ -1,7 +1,9 @@ package com.xorker.draw.timer +import com.xorker.draw.mafia.event.JobWithStartTime import java.time.Duration -import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch import org.springframework.context.ApplicationEventPublisher @@ -12,10 +14,21 @@ internal class TimerAdapter( private val eventPublisher: ApplicationEventPublisher, ) : TimerRepository { - override fun startTimer(interval: Duration, event: T) { - GlobalScope.launch { + override fun startTimer(interval: Duration, event: T): JobWithStartTime { + val job = JobWithStartTime() + CoroutineScope(Dispatchers.IO + job).launch { delay(interval.toMillis()) eventPublisher.publishEvent(event) } + return job + } + + override fun startTimer(interval: Duration, callback: () -> Unit): JobWithStartTime { + val job = JobWithStartTime() + CoroutineScope(Dispatchers.IO + job).launch { + delay(interval.toMillis()) + callback.invoke() + } + return job } } diff --git a/app/support/exception/src/main/kotlin/com/xorker/draw/exception/ExceptionButtonType.kt b/app/support/exception/src/main/kotlin/com/xorker/draw/exception/ExceptionButtonType.kt index 1c938f10..eeb8f44b 100644 --- a/app/support/exception/src/main/kotlin/com/xorker/draw/exception/ExceptionButtonType.kt +++ b/app/support/exception/src/main/kotlin/com/xorker/draw/exception/ExceptionButtonType.kt @@ -53,6 +53,8 @@ fun XorkerException.getButtons(): List { InvalidMafiaGamePlayingPhaseStatusException, NotFoundWordException, InvalidBroadcastException, + is InvalidMafiaPhaseException, + InvalidRequestOnlyMyTurnException, -> buttonOk } } diff --git a/app/websocket/src/main/kotlin/com/xorker/draw/websocket/EmptyObject.kt b/app/websocket/src/main/kotlin/com/xorker/draw/websocket/EmptyObject.kt new file mode 100644 index 00000000..44fba0af --- /dev/null +++ b/app/websocket/src/main/kotlin/com/xorker/draw/websocket/EmptyObject.kt @@ -0,0 +1,3 @@ +package com.xorker.draw.websocket + +object EmptyObject diff --git a/app/websocket/src/main/kotlin/com/xorker/draw/websocket/WebSocketRouter.kt b/app/websocket/src/main/kotlin/com/xorker/draw/websocket/WebSocketRouter.kt index 86951c9c..2aab6147 100644 --- a/app/websocket/src/main/kotlin/com/xorker/draw/websocket/WebSocketRouter.kt +++ b/app/websocket/src/main/kotlin/com/xorker/draw/websocket/WebSocketRouter.kt @@ -3,7 +3,7 @@ package com.xorker.draw.websocket import com.fasterxml.jackson.databind.ObjectMapper import com.xorker.draw.exception.InvalidRequestValueException import com.xorker.draw.mafia.MafiaGameUseCase -import com.xorker.draw.mafia.MafiaStartGameUseCase +import com.xorker.draw.mafia.MafiaPhaseUseCase import com.xorker.draw.room.RoomId import com.xorker.draw.websocket.message.request.RequestAction import com.xorker.draw.websocket.message.request.dto.StartMafiaGameRequest @@ -16,7 +16,7 @@ class WebSocketRouter( private val objectMapper: ObjectMapper, private val webSocketController: WebSocketController, private val sessionUseCase: SessionUseCase, - private val mafiaStartGameUseCase: MafiaStartGameUseCase, + private val mafiaPhaseUseCase: MafiaPhaseUseCase, private val mafiaGameUseCase: MafiaGameUseCase, ) { fun route(session: WebSocketSession, request: WebSocketRequest) { @@ -26,15 +26,17 @@ class WebSocketRouter( val requestDto = request.extractBody() val roomId = RoomId(requestDto.roomId) - mafiaStartGameUseCase.startMafiaGame(roomId) - } - RequestAction.DRAW -> { - val sessionDto = sessionUseCase.getSession(SessionId(session.id)) ?: throw InvalidRequestValueException - mafiaGameUseCase.draw(sessionDto, request.extractBody()) + mafiaPhaseUseCase.startGame(roomId) } + + RequestAction.DRAW -> mafiaGameUseCase.draw(session.getDto(), request.extractBody()) + RequestAction.END_TURN -> mafiaGameUseCase.nextTurnByUser(session.getDto()) } } + private fun WebSocketSession.getDto(): Session = + sessionUseCase.getSession(SessionId(this.id)) ?: throw InvalidRequestValueException + private inline fun WebSocketRequest.extractBody(): T { return objectMapper.readValue(this.body, T::class.java) } diff --git a/app/websocket/src/main/kotlin/com/xorker/draw/websocket/broker/SimpleSessionMessageBroker.kt b/app/websocket/src/main/kotlin/com/xorker/draw/websocket/broker/SimpleSessionMessageBroker.kt index 7f5953fb..2e6635bc 100644 --- a/app/websocket/src/main/kotlin/com/xorker/draw/websocket/broker/SimpleSessionMessageBroker.kt +++ b/app/websocket/src/main/kotlin/com/xorker/draw/websocket/broker/SimpleSessionMessageBroker.kt @@ -7,6 +7,7 @@ import com.xorker.draw.websocket.BroadcastEvent import com.xorker.draw.websocket.RespectiveBroadcastEvent import com.xorker.draw.websocket.SessionMessageBroker import com.xorker.draw.websocket.SessionUseCase +import com.xorker.draw.websocket.UnicastEvent import com.xorker.draw.websocket.parser.WebSocketResponseParser import org.springframework.context.event.EventListener import org.springframework.stereotype.Component @@ -18,6 +19,15 @@ class SimpleSessionMessageBroker( private val parser: WebSocketResponseParser, ) : SessionMessageBroker { + @EventListener + override fun unicast(event: UnicastEvent) { + val userId = event.userId + val session = sessionUseCase.getSession(userId) + val response = parser.parse(event.message) + + session?.send(response) + } + @EventListener override fun broadcast(event: BroadcastEvent) { val roomId = event.roomId diff --git a/app/websocket/src/main/kotlin/com/xorker/draw/websocket/broker/WebSocketBroadcaster.kt b/app/websocket/src/main/kotlin/com/xorker/draw/websocket/broker/WebSocketBroadcaster.kt index 15bba8f0..f9405376 100644 --- a/app/websocket/src/main/kotlin/com/xorker/draw/websocket/broker/WebSocketBroadcaster.kt +++ b/app/websocket/src/main/kotlin/com/xorker/draw/websocket/broker/WebSocketBroadcaster.kt @@ -1,24 +1,44 @@ package com.xorker.draw.websocket.broker +import com.xorker.draw.room.RoomId +import com.xorker.draw.user.UserId import com.xorker.draw.websocket.BranchedBroadcastEvent import com.xorker.draw.websocket.BroadcastEvent import com.xorker.draw.websocket.RespectiveBroadcastEvent +import com.xorker.draw.websocket.SessionMessage +import com.xorker.draw.websocket.UnicastEvent import org.springframework.context.ApplicationEventPublisher import org.springframework.stereotype.Component +interface WebSocketBroadcaster { + fun unicast(userId: UserId, message: SessionMessage) + fun broadcast(roomId: RoomId, sessionMessage: SessionMessage) + fun publishBroadcastEvent(event: BroadcastEvent) + fun publishBranchedBroadcastEvent(event: BranchedBroadcastEvent) + fun publishRespectiveBroadcastEvent(event: RespectiveBroadcastEvent) +} + @Component -class WebSocketBroadcaster( +internal class WebSocketBroadcasterSingleInstance( private val publisher: ApplicationEventPublisher, -) { - fun publishBroadcastEvent(event: BroadcastEvent) { +) : WebSocketBroadcaster { + override fun unicast(userId: UserId, message: SessionMessage) { + publisher.publishEvent(UnicastEvent(userId, message)) + } + + override fun broadcast(roomId: RoomId, sessionMessage: SessionMessage) { + publisher.publishEvent(BroadcastEvent(roomId, sessionMessage)) + } + + override fun publishBroadcastEvent(event: BroadcastEvent) { publisher.publishEvent(event) } - fun publishBranchedBroadcastEvent(event: BranchedBroadcastEvent) { + override fun publishBranchedBroadcastEvent(event: BranchedBroadcastEvent) { publisher.publishEvent(event) } - fun publishRespectiveBroadcastEvent(event: RespectiveBroadcastEvent) { + override fun publishRespectiveBroadcastEvent(event: RespectiveBroadcastEvent) { publisher.publishEvent(event) } } diff --git a/app/websocket/src/main/kotlin/com/xorker/draw/websocket/message/request/RequestAction.kt b/app/websocket/src/main/kotlin/com/xorker/draw/websocket/message/request/RequestAction.kt index 02268c7d..9b1dde21 100644 --- a/app/websocket/src/main/kotlin/com/xorker/draw/websocket/message/request/RequestAction.kt +++ b/app/websocket/src/main/kotlin/com/xorker/draw/websocket/message/request/RequestAction.kt @@ -6,4 +6,5 @@ enum class RequestAction( INIT("세션 초기화"), START_GAME("마피아 게임 시작"), DRAW("그림 그리기"), + END_TURN("턴 넘기기"), } diff --git a/app/websocket/src/main/kotlin/com/xorker/draw/websocket/message/response/MafiaGameMessengerImpl.kt b/app/websocket/src/main/kotlin/com/xorker/draw/websocket/message/response/MafiaGameMessengerImpl.kt index fba6e067..595ea9a4 100644 --- a/app/websocket/src/main/kotlin/com/xorker/draw/websocket/message/response/MafiaGameMessengerImpl.kt +++ b/app/websocket/src/main/kotlin/com/xorker/draw/websocket/message/response/MafiaGameMessengerImpl.kt @@ -4,27 +4,20 @@ import com.xorker.draw.exception.InvalidMafiaGamePlayingPhaseStatusException import com.xorker.draw.mafia.MafiaGameInfo import com.xorker.draw.mafia.MafiaGameMessenger import com.xorker.draw.mafia.MafiaPhase -import com.xorker.draw.mafia.MafiaPlayer -import com.xorker.draw.room.Room +import com.xorker.draw.mafia.MafiaPhaseWithTurnList +import com.xorker.draw.mafia.assertIs import com.xorker.draw.room.RoomId -import com.xorker.draw.user.UserId import com.xorker.draw.websocket.BranchedBroadcastEvent import com.xorker.draw.websocket.BroadcastEvent -import com.xorker.draw.websocket.RespectiveBroadcastEvent -import com.xorker.draw.websocket.SessionMessage import com.xorker.draw.websocket.broker.WebSocketBroadcaster -import com.xorker.draw.websocket.message.response.dto.MafiaGameDrawBody import com.xorker.draw.websocket.message.response.dto.MafiaGameDrawMessage -import com.xorker.draw.websocket.message.response.dto.MafiaGameInfoBody -import com.xorker.draw.websocket.message.response.dto.MafiaGameInfoMessage -import com.xorker.draw.websocket.message.response.dto.MafiaGameReadyBody -import com.xorker.draw.websocket.message.response.dto.MafiaGameReadyMessage +import com.xorker.draw.websocket.message.response.dto.MafiaGameTurnInfoBody +import com.xorker.draw.websocket.message.response.dto.MafiaGameTurnInfoMessage import com.xorker.draw.websocket.message.response.dto.MafiaPlayerListBody import com.xorker.draw.websocket.message.response.dto.MafiaPlayerListMessage import com.xorker.draw.websocket.message.response.dto.MafiaPlayerTurnListBody import com.xorker.draw.websocket.message.response.dto.MafiaPlayerTurnListMessage import com.xorker.draw.websocket.message.response.dto.toResponse -import java.time.LocalDateTime import org.springframework.stereotype.Component @Component @@ -32,13 +25,20 @@ class MafiaGameMessengerImpl( private val broadcaster: WebSocketBroadcaster, ) : MafiaGameMessenger { - override fun broadcastPlayerList(room: Room) { - val roomId = room.id + override fun broadcastPlayerList(gameInfo: MafiaGameInfo) { + val roomId = gameInfo.room.id + val phase = gameInfo.phase + + val list = + if (phase is MafiaPhaseWithTurnList) { + phase.turnList + } else { + gameInfo.room.players + } val message = MafiaPlayerListMessage( MafiaPlayerListBody( - roomId, - room.players.map { it.toResponse(room.owner) }.toList(), + list.map { it.toResponse(gameInfo.room.owner) }.toList(), ), ) @@ -47,72 +47,6 @@ class MafiaGameMessengerImpl( broadcaster.publishBroadcastEvent(event) } - override fun broadcastGameInfo(mafiaGameInfo: MafiaGameInfo) { - val roomId = mafiaGameInfo.room.id - - val phase = mafiaGameInfo.phase as? MafiaPhase.Playing ?: throw InvalidMafiaGamePlayingPhaseStatusException - - val mafia = phase.mafiaPlayer - val keyword = phase.keyword - - val gameOption = mafiaGameInfo.gameOption - - val message = MafiaGameInfoMessage( - MafiaGameInfoBody( - category = keyword.category, - answer = keyword.answer, - gameOption = gameOption.toResponse(), - ), - ) - - val branchedMessage = MafiaGameInfoMessage( - MafiaGameInfoBody( - isMafia = true, - category = keyword.category, - answer = keyword.answer, - gameOption = gameOption.toResponse(), - ), - ) - - val branched = setOf(mafia.userId) - - val event = BranchedBroadcastEvent( - roomId = roomId, - branched = branched, - message = message, - branchedMessage = branchedMessage, - ) - - broadcaster.publishBranchedBroadcastEvent(event) - } - - override fun broadcastGameReady(mafiaGameInfo: MafiaGameInfo) { - val roomId = mafiaGameInfo.room.id - - val phase = mafiaGameInfo.phase as? MafiaPhase.Playing ?: throw InvalidMafiaGamePlayingPhaseStatusException - - val turnList = phase.turnList - - val messages = mutableMapOf() - - turnList.forEachIndexed { i, player -> - val message = MafiaGameReadyMessage( - MafiaGameReadyBody( - turn = i + 1, - player = player.toResponse(mafiaGameInfo.room.owner), - ), - ) - messages[player.userId] = message - } - - val event = RespectiveBroadcastEvent( - roomId = roomId, - messages = messages, - ) - - broadcaster.publishRespectiveBroadcastEvent(event) - } - override fun broadcastPlayerTurnList(mafiaGameInfo: MafiaGameInfo) { val room = mafiaGameInfo.room val roomId = room.id @@ -155,22 +89,20 @@ class MafiaGameMessengerImpl( broadcaster.publishBranchedBroadcastEvent(event) } - override fun broadcastDraw(roomId: RoomId, phase: MafiaPhase.Playing) { - if (phase !is MafiaPhase.Playing) throw InvalidMafiaGamePlayingPhaseStatusException - - val event = BroadcastEvent( - roomId, - MafiaGameDrawMessage( - MafiaGameDrawBody( - round = phase.round, - turn = phase.turn, - startTurnTime = LocalDateTime.now(), // TOOD: 턴 시스템 도입 시 수정 - draw = phase.drawData.take(phase.drawData.size - 1).map { it.second }, - currentDraw = phase.drawData.last().second, - ), - ), - ) + override fun broadcastDraw(roomId: RoomId, data: Map) { + val message = MafiaGameDrawMessage(data) + broadcaster.broadcast(roomId, message) + } - broadcaster.publishBroadcastEvent(event) + override fun broadcastNextTurn(gameInfo: MafiaGameInfo) { + val phase = gameInfo.phase + assertIs(phase) + val body = MafiaGameTurnInfoBody( + phase.round, + phase.turn, + phase.timerJob.startTime, + phase.turnList[phase.turn].userId, + ) + broadcaster.broadcast(gameInfo.room.id, MafiaGameTurnInfoMessage(body)) } } diff --git a/app/websocket/src/main/kotlin/com/xorker/draw/websocket/message/response/MafiaPhaseMessengerImpl.kt b/app/websocket/src/main/kotlin/com/xorker/draw/websocket/message/response/MafiaPhaseMessengerImpl.kt new file mode 100644 index 00000000..72de6e7c --- /dev/null +++ b/app/websocket/src/main/kotlin/com/xorker/draw/websocket/message/response/MafiaPhaseMessengerImpl.kt @@ -0,0 +1,70 @@ +package com.xorker.draw.websocket.message.response + +import com.xorker.draw.mafia.MafiaGameInfo +import com.xorker.draw.mafia.MafiaPhase +import com.xorker.draw.mafia.MafiaPhaseMessenger +import com.xorker.draw.user.UserId +import com.xorker.draw.websocket.RespectiveBroadcastEvent +import com.xorker.draw.websocket.SessionMessage +import com.xorker.draw.websocket.broker.WebSocketBroadcaster +import com.xorker.draw.websocket.message.response.dto.MafiaGameInfoBody +import com.xorker.draw.websocket.message.response.dto.MafiaGameInfoMessage +import com.xorker.draw.websocket.message.response.dto.MafiaPhaseReadyBody +import com.xorker.draw.websocket.message.response.dto.MafiaPhaseReadyMessage +import com.xorker.draw.websocket.message.response.dto.toResponse +import org.springframework.stereotype.Component + +@Component +class MafiaPhaseMessengerImpl( + private val broadcaster: WebSocketBroadcaster, +) : MafiaPhaseMessenger { + override fun unicastPhase(userId: UserId, gameInfo: MafiaGameInfo) { + // broadcaster.unicast(userId, gameInfo.generateMessage()) + } + + override fun broadcastPhase(gameInfo: MafiaGameInfo) { + val event = RespectiveBroadcastEvent(gameInfo.room.id, gameInfo.generateMessage()) + + broadcaster.publishRespectiveBroadcastEvent(event) + } + + private fun MafiaGameInfo.generateMessage(): Map { + return when (val phase = this.phase) { + MafiaPhase.Wait -> TODO() + is MafiaPhase.Ready -> { + val messages = mutableMapOf() + val startTime = phase.job.startTime + val turnList = phase.turnList + val mafiaPlayer = phase.mafiaPlayer + val keyword = phase.keyword + + turnList.forEachIndexed { i, player -> + val message = MafiaPhaseReadyMessage( + MafiaPhaseReadyBody( + startTime = startTime, + gameInfo = MafiaGameInfoMessage( + MafiaGameInfoBody( + userId = player.userId, + turn = i, + isMafia = mafiaPlayer.userId == player.userId, + turnList = turnList, + category = keyword.category, + answer = keyword.answer, + gameOption = gameOption.toResponse(), + ), + ), + ), + ) + messages[player.userId] = message + } + + return messages + } + + is MafiaPhase.Playing -> TODO() + is MafiaPhase.Vote -> TODO() + is MafiaPhase.InferAnswer -> TODO() + is MafiaPhase.End -> TODO() + } + } +} diff --git a/app/websocket/src/main/kotlin/com/xorker/draw/websocket/message/response/dto/MafiaGameDrawMessage.kt b/app/websocket/src/main/kotlin/com/xorker/draw/websocket/message/response/dto/MafiaGameDrawMessage.kt index 8552bd64..af3d60ea 100644 --- a/app/websocket/src/main/kotlin/com/xorker/draw/websocket/message/response/dto/MafiaGameDrawMessage.kt +++ b/app/websocket/src/main/kotlin/com/xorker/draw/websocket/message/response/dto/MafiaGameDrawMessage.kt @@ -2,19 +2,16 @@ package com.xorker.draw.websocket.message.response.dto import com.xorker.draw.websocket.ResponseAction import com.xorker.draw.websocket.SessionMessage -import java.time.LocalDateTime data class MafiaGameDrawMessage( override val body: MafiaGameDrawBody, ) : SessionMessage { override val action: ResponseAction = ResponseAction.DRAW override val status: SessionMessage.Status = SessionMessage.Status.OK + + constructor(draw: Map) : this(MafiaGameDrawBody(draw)) } class MafiaGameDrawBody( - val round: Int, - val turn: Int, - val startTurnTime: LocalDateTime, - val draw: List>, - val currentDraw: Map, + val draw: Map, ) diff --git a/app/websocket/src/main/kotlin/com/xorker/draw/websocket/message/response/dto/MafiaGameInfoMessage.kt b/app/websocket/src/main/kotlin/com/xorker/draw/websocket/message/response/dto/MafiaGameInfoMessage.kt index 11b7fd7c..ece654d0 100644 --- a/app/websocket/src/main/kotlin/com/xorker/draw/websocket/message/response/dto/MafiaGameInfoMessage.kt +++ b/app/websocket/src/main/kotlin/com/xorker/draw/websocket/message/response/dto/MafiaGameInfoMessage.kt @@ -1,6 +1,8 @@ package com.xorker.draw.websocket.message.response.dto import com.xorker.draw.mafia.MafiaGameOption +import com.xorker.draw.mafia.MafiaPlayer +import com.xorker.draw.user.UserId import com.xorker.draw.websocket.ResponseAction import com.xorker.draw.websocket.SessionMessage @@ -12,18 +14,35 @@ data class MafiaGameInfoMessage( } data class MafiaGameInfoBody( + val userId: UserId, + val turn: Int, val isMafia: Boolean = false, + val turnList: List, val category: String, val answer: String, val gameOption: MafiaGameOptionResponse, ) data class MafiaGameOptionResponse( + val minimum: Int, + val maximum: Int, + val readyTime: Long, + val animationTime: Long, + val round: Int, val turnTime: Long, - val numTurn: Int, + val turnCount: Int, + val voteTime: Long, + val answerTime: Long, ) fun MafiaGameOption.toResponse(): MafiaGameOptionResponse = MafiaGameOptionResponse( - turnTime = this.turnTime.toSeconds(), - numTurn = this.numTurn, + minimum = minimum, + maximum = maximum, + readyTime = readyTime.toSeconds(), + animationTime = animationTime.toSeconds(), + round = round, + turnTime = turnTime.toSeconds(), + turnCount = turnCount, + voteTime = voteTime.toSeconds(), + answerTime = answerTime.toSeconds(), ) diff --git a/app/websocket/src/main/kotlin/com/xorker/draw/websocket/message/response/dto/MafiaGameTurnInfoMessage.kt b/app/websocket/src/main/kotlin/com/xorker/draw/websocket/message/response/dto/MafiaGameTurnInfoMessage.kt new file mode 100644 index 00000000..e94b7aae --- /dev/null +++ b/app/websocket/src/main/kotlin/com/xorker/draw/websocket/message/response/dto/MafiaGameTurnInfoMessage.kt @@ -0,0 +1,20 @@ +package com.xorker.draw.websocket.message.response.dto + +import com.xorker.draw.user.UserId +import com.xorker.draw.websocket.ResponseAction +import com.xorker.draw.websocket.SessionMessage +import java.time.LocalDateTime + +class MafiaGameTurnInfoMessage( + override val body: MafiaGameTurnInfoBody, +) : SessionMessage { + override val action: ResponseAction = ResponseAction.TURN_INFO + override val status: SessionMessage.Status = SessionMessage.Status.OK +} + +class MafiaGameTurnInfoBody( + val round: Int, + val turn: Int, + val startTurnTime: LocalDateTime, + val currentTurnUser: UserId, +) diff --git a/app/websocket/src/main/kotlin/com/xorker/draw/websocket/message/response/dto/MafiaPhasePlayingMessage.kt b/app/websocket/src/main/kotlin/com/xorker/draw/websocket/message/response/dto/MafiaPhasePlayingMessage.kt new file mode 100644 index 00000000..275a829b --- /dev/null +++ b/app/websocket/src/main/kotlin/com/xorker/draw/websocket/message/response/dto/MafiaPhasePlayingMessage.kt @@ -0,0 +1,19 @@ +package com.xorker.draw.websocket.message.response.dto + +import com.xorker.draw.websocket.ResponseAction +import com.xorker.draw.websocket.SessionMessage +import java.time.LocalDateTime + +data class MafiaPhasePlayingMessage( + override val body: MafiaPhasePlayingBody, +) : SessionMessage { + override val action: ResponseAction = ResponseAction.PHASE_PLAYING + override val status: SessionMessage.Status = SessionMessage.Status.OK +} + +class MafiaPhasePlayingBody( + val round: Int, + val turn: Int, + val startTurnTime: LocalDateTime, + val draw: List>, +) diff --git a/app/websocket/src/main/kotlin/com/xorker/draw/websocket/message/response/dto/MafiaGameReadyMessage.kt b/app/websocket/src/main/kotlin/com/xorker/draw/websocket/message/response/dto/MafiaPhaseReadyMessage.kt similarity index 57% rename from app/websocket/src/main/kotlin/com/xorker/draw/websocket/message/response/dto/MafiaGameReadyMessage.kt rename to app/websocket/src/main/kotlin/com/xorker/draw/websocket/message/response/dto/MafiaPhaseReadyMessage.kt index a9bc559f..60b0fbce 100644 --- a/app/websocket/src/main/kotlin/com/xorker/draw/websocket/message/response/dto/MafiaGameReadyMessage.kt +++ b/app/websocket/src/main/kotlin/com/xorker/draw/websocket/message/response/dto/MafiaPhaseReadyMessage.kt @@ -2,15 +2,16 @@ package com.xorker.draw.websocket.message.response.dto import com.xorker.draw.websocket.ResponseAction import com.xorker.draw.websocket.SessionMessage +import java.time.LocalDateTime -class MafiaGameReadyMessage( - override val body: MafiaGameReadyBody, +class MafiaPhaseReadyMessage( + override val body: MafiaPhaseReadyBody, ) : SessionMessage { override val action = ResponseAction.GAME_READY override val status = SessionMessage.Status.OK } -data class MafiaGameReadyBody( - val turn: Int, - val player: MafiaPlayerResponse, +data class MafiaPhaseReadyBody( + val startTime: LocalDateTime, + val gameInfo: MafiaGameInfoMessage?, ) diff --git a/app/websocket/src/main/kotlin/com/xorker/draw/websocket/message/response/dto/MafiaPhaseWaitMessage.kt b/app/websocket/src/main/kotlin/com/xorker/draw/websocket/message/response/dto/MafiaPhaseWaitMessage.kt new file mode 100644 index 00000000..644ce557 --- /dev/null +++ b/app/websocket/src/main/kotlin/com/xorker/draw/websocket/message/response/dto/MafiaPhaseWaitMessage.kt @@ -0,0 +1,17 @@ +package com.xorker.draw.websocket.message.response.dto + +import com.xorker.draw.room.RoomId +import com.xorker.draw.websocket.ResponseAction +import com.xorker.draw.websocket.SessionMessage + +data class MafiaPhaseWaitMessage( + override val body: MafiaPhaseWaitBody, +) : SessionMessage { + override val action: ResponseAction = ResponseAction.PHASE_WAIT + override val status: SessionMessage.Status = SessionMessage.Status.OK +} + +data class MafiaPhaseWaitBody( + val roomId: RoomId, + val players: List, +) diff --git a/app/websocket/src/main/kotlin/com/xorker/draw/websocket/message/response/dto/MafiaPlayerListMessage.kt b/app/websocket/src/main/kotlin/com/xorker/draw/websocket/message/response/dto/MafiaPlayerListMessage.kt index 81aacda2..e23dbb23 100644 --- a/app/websocket/src/main/kotlin/com/xorker/draw/websocket/message/response/dto/MafiaPlayerListMessage.kt +++ b/app/websocket/src/main/kotlin/com/xorker/draw/websocket/message/response/dto/MafiaPlayerListMessage.kt @@ -1,7 +1,6 @@ package com.xorker.draw.websocket.message.response.dto import com.xorker.draw.mafia.MafiaPlayer -import com.xorker.draw.room.RoomId import com.xorker.draw.user.UserId import com.xorker.draw.websocket.ResponseAction import com.xorker.draw.websocket.SessionMessage @@ -14,7 +13,6 @@ data class MafiaPlayerListMessage( } data class MafiaPlayerListBody( - val roomId: RoomId, val players: List, ) diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 00aba8c2..c305b6d6 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -10,6 +10,7 @@ dependencies { implementation(project(":adapter:oauth")) implementation(project(":adapter:rdb")) implementation(project(":support:jwt")) + implementation(project(":support:time")) implementation("org.springframework.boot:spring-boot-starter:${Versions.SPRING_BOOT}") compileOnly("org.springframework.boot:spring-boot-starter-jdbc:${Versions.SPRING_BOOT}") diff --git a/core/src/main/kotlin/com/xorker/draw/mafia/MafiaGameRoomService.kt b/core/src/main/kotlin/com/xorker/draw/mafia/MafiaGameRoomService.kt index 7fc5584c..6c724051 100644 --- a/core/src/main/kotlin/com/xorker/draw/mafia/MafiaGameRoomService.kt +++ b/core/src/main/kotlin/com/xorker/draw/mafia/MafiaGameRoomService.kt @@ -9,6 +9,7 @@ import org.springframework.stereotype.Component internal class MafiaGameRoomService( private val mafiaGameRepository: MafiaGameRepository, private val mafiaGameMessenger: MafiaGameMessenger, + private val mafiaPhaseMessenger: MafiaPhaseMessenger, ) : SessionEventListener { override fun connectSession(session: Session, nickname: String) { @@ -32,7 +33,8 @@ internal class MafiaGameRoomService( } mafiaGameRepository.saveGameInfo(gameInfo) - mafiaGameMessenger.broadcastPlayerList(gameInfo.room) + mafiaPhaseMessenger.unicastPhase(session.user.id, gameInfo) + mafiaGameMessenger.broadcastPlayerList(gameInfo) } override fun disconnectSession(session: Session) { @@ -47,7 +49,7 @@ internal class MafiaGameRoomService( player.disconnect() mafiaGameRepository.saveGameInfo(gameInfo) - mafiaGameMessenger.broadcastPlayerList(gameInfo.room) + mafiaGameMessenger.broadcastPlayerList(gameInfo) } override fun exitSession(session: Session) { @@ -61,8 +63,17 @@ internal class MafiaGameRoomService( val player = gameInfo.findPlayer(session.user.id) ?: return gameInfo.room.remove(player) + + if (gameInfo.room.players.isEmpty()) { + mafiaGameRepository.removeGameInfo(gameInfo) + return + } + + if (gameInfo.room.owner == player) { + gameInfo.room.owner = gameInfo.room.players.first() + } mafiaGameRepository.saveGameInfo(gameInfo) - mafiaGameMessenger.broadcastPlayerList(gameInfo.room) + mafiaGameMessenger.broadcastPlayerList(gameInfo) } private fun generateColor(gameInfo: MafiaGameInfo?): String { diff --git a/core/src/main/kotlin/com/xorker/draw/mafia/MafiaGameService.kt b/core/src/main/kotlin/com/xorker/draw/mafia/MafiaGameService.kt index 877fadf9..a6ca5c6a 100644 --- a/core/src/main/kotlin/com/xorker/draw/mafia/MafiaGameService.kt +++ b/core/src/main/kotlin/com/xorker/draw/mafia/MafiaGameService.kt @@ -1,20 +1,26 @@ package com.xorker.draw.mafia +import com.xorker.draw.exception.InvalidRequestOnlyMyTurnException import com.xorker.draw.exception.InvalidRequestValueException import com.xorker.draw.mafia.dto.DrawRequest +import com.xorker.draw.timer.TimerRepository +import com.xorker.draw.user.UserId import com.xorker.draw.websocket.Session import org.springframework.stereotype.Component +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.contract @Component internal class MafiaGameService( private val mafiaGameRepository: MafiaGameRepository, private val mafiaGameMessenger: MafiaGameMessenger, + private val timerRepository: TimerRepository, ) : MafiaGameUseCase { override fun draw(session: Session, request: DrawRequest) { - val gameInfo = mafiaGameRepository.getGameInfo(session.roomId) ?: throw InvalidRequestValueException + val gameInfo = session.getGameInfo() val phase = gameInfo.phase - if (phase !is MafiaPhase.Playing) throw InvalidRequestValueException + assertTurn(phase, session.user.id) val drawData = phase.drawData.lastOrNull() if (drawData != null && drawData.first == session.user.id) { @@ -23,7 +29,48 @@ internal class MafiaGameService( phase.drawData.add(Pair(session.user.id, request.drawData)) mafiaGameRepository.saveGameInfo(gameInfo) + mafiaGameMessenger.broadcastDraw(gameInfo.room.id, request.drawData) + } + + override fun nextTurnByUser(session: Session) { + val gameInfo = session.getGameInfo() + val phase = gameInfo.phase + assertTurn(phase, session.user.id) + + phase.timerJob.cancel() + + processNextTurn(gameInfo) + } + + fun processNextTurn(gameInfo: MafiaGameInfo) { + val phase = gameInfo.phase + assertIs(phase) + val gameOption = gameInfo.gameOption + + val nextTurn = phase.nextTurn(gameOption.round, gameInfo.room.size()) - mafiaGameMessenger.broadcastDraw(gameInfo.room.id, phase) + if (nextTurn == null) { + // TODO 투표로 넘기기 + return + } + + phase.turnInfo = nextTurn + + mafiaGameMessenger.broadcastNextTurn(gameInfo) + phase.timerJob = timerRepository.startTimer(gameInfo.gameOption.turnTime) { + processNextTurn(gameInfo) + } + } + + private fun Session.getGameInfo(): MafiaGameInfo = + mafiaGameRepository.getGameInfo(roomId) ?: throw InvalidRequestValueException + + @OptIn(ExperimentalContracts::class) + private fun assertTurn(phase: MafiaPhase, userId: UserId) { + contract { + returns() implies (phase is MafiaPhase.Playing) + } + if (phase !is MafiaPhase.Playing) throw InvalidRequestValueException + if (phase.getPlayerTurn(userId) == phase.turn) throw InvalidRequestOnlyMyTurnException } } diff --git a/core/src/main/kotlin/com/xorker/draw/mafia/MafiaGameUseCase.kt b/core/src/main/kotlin/com/xorker/draw/mafia/MafiaGameUseCase.kt index c0fb5b5e..3196baa9 100644 --- a/core/src/main/kotlin/com/xorker/draw/mafia/MafiaGameUseCase.kt +++ b/core/src/main/kotlin/com/xorker/draw/mafia/MafiaGameUseCase.kt @@ -5,4 +5,5 @@ import com.xorker.draw.websocket.Session interface MafiaGameUseCase { fun draw(session: Session, request: DrawRequest) + fun nextTurnByUser(session: Session) } diff --git a/core/src/main/kotlin/com/xorker/draw/mafia/MafiaPhaseService.kt b/core/src/main/kotlin/com/xorker/draw/mafia/MafiaPhaseService.kt new file mode 100644 index 00000000..73795fc7 --- /dev/null +++ b/core/src/main/kotlin/com/xorker/draw/mafia/MafiaPhaseService.kt @@ -0,0 +1,54 @@ +package com.xorker.draw.mafia + +import com.xorker.draw.exception.NotFoundRoomException +import com.xorker.draw.room.RoomId +import com.xorker.draw.timer.TimerRepository +import org.springframework.stereotype.Service + +@Service +internal class MafiaPhaseService( + private val mafiaPhaseMessenger: MafiaPhaseMessenger, + private val startGameService: MafiaStartGameService, + private val mafiaGameService: MafiaGameService, + private val mafiaGameRepository: MafiaGameRepository, + private val timerRepository: TimerRepository, +) : MafiaPhaseUseCase { + + override fun startGame(roomId: RoomId): MafiaPhase.Ready { + val gameInfo = getGameInfo(roomId) + + val phase = synchronized(gameInfo) { + assertIs(gameInfo.phase) + startGameService.startMafiaGame(gameInfo) { + playGame(roomId) + } + } + + mafiaPhaseMessenger.broadcastPhase(gameInfo) + + return phase + } + + override fun playGame(roomId: RoomId): MafiaPhase.Playing { + val gameInfo = getGameInfo(roomId) + + val phase = synchronized(gameInfo) { + val readyPhase = gameInfo.phase + assertIs(readyPhase) + val job = timerRepository.startTimer(gameInfo.gameOption.turnTime) { + mafiaGameService.processNextTurn(gameInfo) + } + val playingPhase = readyPhase.toPlaying(job) + gameInfo.phase = playingPhase + playingPhase + } + + mafiaPhaseMessenger.broadcastPhase(gameInfo) + + return phase + } + + private fun getGameInfo(roomId: RoomId): MafiaGameInfo { + return mafiaGameRepository.getGameInfo(roomId) ?: throw NotFoundRoomException + } +} diff --git a/core/src/main/kotlin/com/xorker/draw/mafia/MafiaPhaseUseCase.kt b/core/src/main/kotlin/com/xorker/draw/mafia/MafiaPhaseUseCase.kt new file mode 100644 index 00000000..91c9b664 --- /dev/null +++ b/core/src/main/kotlin/com/xorker/draw/mafia/MafiaPhaseUseCase.kt @@ -0,0 +1,15 @@ +package com.xorker.draw.mafia + +import com.xorker.draw.room.RoomId + +interface MafiaPhaseUseCase { + /** + * MafiaPhase.Wait -> MafiaPhase.Ready + */ + fun startGame(roomId: RoomId): MafiaPhase.Ready + + /** + * MafiaPhase.Ready -> MafiaPhase.Playing + */ + fun playGame(roomId: RoomId): MafiaPhase.Playing +} diff --git a/core/src/main/kotlin/com/xorker/draw/mafia/MafiaStartGameService.kt b/core/src/main/kotlin/com/xorker/draw/mafia/MafiaStartGameService.kt index 66c2e90a..890de0c1 100644 --- a/core/src/main/kotlin/com/xorker/draw/mafia/MafiaStartGameService.kt +++ b/core/src/main/kotlin/com/xorker/draw/mafia/MafiaStartGameService.kt @@ -1,49 +1,37 @@ package com.xorker.draw.mafia -import com.xorker.draw.exception.InvalidMafiaGamePlayingPhaseStatusException -import com.xorker.draw.mafia.event.MafiaReadyExpiredEvent -import com.xorker.draw.room.RoomId import com.xorker.draw.timer.TimerRepository -import java.util.Locale +import java.util.* import org.springframework.stereotype.Service import kotlin.random.Random @Service internal class MafiaStartGameService( - private val mafiaGameRepository: MafiaGameRepository, private val mafiaKeywordRepository: MafiaKeywordRepository, - private val mafiaGameMessenger: MafiaGameMessenger, private val timerRepository: TimerRepository, -) : MafiaStartGameUseCase { - - override fun startMafiaGame(roomId: RoomId) { - val gameInfo = mafiaGameRepository.getGameInfo(roomId) ?: throw InvalidMafiaGamePlayingPhaseStatusException +) { + internal fun startMafiaGame(gameInfo: MafiaGameInfo, nextStep: () -> Unit): MafiaPhase.Ready { val room = gameInfo.room val players = room.players + val gameOption = gameInfo.gameOption val turnList = generateTurnList(players) - room.clear() - room.addAll(turnList) - val mafiaIndex = Random.nextInt(0, players.size) val keyword = mafiaKeywordRepository.getRandomKeyword(Locale.KOREAN) // TODO extract room locale - gameInfo.phase = MafiaPhase.Playing( + val job = timerRepository.startTimer(gameOption.readyTime, nextStep) + + val phase = MafiaPhase.Ready( + job = job, turnList = turnList, mafiaPlayer = players[mafiaIndex], keyword = keyword, - drawData = mutableListOf(), ) + gameInfo.phase = phase - timerRepository.startTimer(gameInfo.gameOption.readyShowingTime, MafiaReadyExpiredEvent(gameInfo)) - - mafiaGameMessenger.broadcastGameInfo(gameInfo) - - mafiaGameMessenger.broadcastGameReady(gameInfo) - - mafiaGameMessenger.broadcastPlayerTurnList(gameInfo) + return phase } private fun generateTurnList(players: List): MutableList { diff --git a/core/src/main/kotlin/com/xorker/draw/mafia/MafiaStartGameUseCase.kt b/core/src/main/kotlin/com/xorker/draw/mafia/MafiaStartGameUseCase.kt deleted file mode 100644 index 70e1e3b1..00000000 --- a/core/src/main/kotlin/com/xorker/draw/mafia/MafiaStartGameUseCase.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.xorker.draw.mafia - -import com.xorker.draw.room.RoomId - -interface MafiaStartGameUseCase { - fun startMafiaGame(roomId: RoomId) -} diff --git a/core/src/main/kotlin/com/xorker/draw/mafia/MafiaTimerService.kt b/core/src/main/kotlin/com/xorker/draw/mafia/MafiaTimerService.kt index 5c4d4110..4806d319 100644 --- a/core/src/main/kotlin/com/xorker/draw/mafia/MafiaTimerService.kt +++ b/core/src/main/kotlin/com/xorker/draw/mafia/MafiaTimerService.kt @@ -26,7 +26,7 @@ internal class MafiaTimerService( val gameOption = gameInfo.gameOption // TODO broadcast ready expired event - timerRepository.startTimer(gameOption.roleShowingTime, MafiaRoleExpiredEvent(gameInfo)) + timerRepository.startTimer(gameOption.animationTime, MafiaRoleExpiredEvent(gameInfo)) } @EventListener @@ -37,7 +37,7 @@ internal class MafiaTimerService( val gameOption = gameInfo.gameOption // TODO broadcast role expired event - timerRepository.startTimer(gameOption.roundShowingTime, MafiaRoundExpiredEvent(gameInfo)) + timerRepository.startTimer(gameOption.animationTime, MafiaRoundExpiredEvent(gameInfo)) } @EventListener @@ -68,21 +68,21 @@ internal class MafiaTimerService( println("현재 라운드 = $currentRound, 현재 턴 = $currentTurn") - if (currentTurn == room.size() - 1) { - if (currentRound == gameOption.numTurn) { - // TODO broadcast turn expired event - // TODO 투표 expired timer start - println("투표 화면으로 이동") - } else { - phase.turn = nextTurn - phase.round = currentRound + 1 - // TODO broadcast turn expired event - timerRepository.startTimer(gameOption.roundShowingTime, MafiaRoundExpiredEvent(gameInfo)) - } - } else { - phase.turn = nextTurn - // TODO broadcast turn expired event - timerRepository.startTimer(gameOption.turnTime, MafiaTurnExpiredEvent(gameInfo)) - } +// if (currentTurn == room.size() - 1) { +// if (currentRound == gameOption.turnCount) { +// // TODO broadcast turn expired event +// // TODO 투표 expired timer start +// println("투표 화면으로 이동") +// } else { +// phase.turn = nextTurn +// phase.round = currentRound + 1 +// // TODO broadcast turn expired event +// timerRepository.startTimer(gameOption.animationTime, MafiaRoundExpiredEvent(gameInfo)) +// } +// } else { +// phase.turn = nextTurn +// // TODO broadcast turn expired event +// timerRepository.startTimer(gameOption.turnTime, MafiaTurnExpiredEvent(gameInfo)) +// } } } diff --git a/core/src/main/kotlin/com/xorker/draw/websocket/SessionMessageBroker.kt b/core/src/main/kotlin/com/xorker/draw/websocket/SessionMessageBroker.kt index 28a2e97b..2371e394 100644 --- a/core/src/main/kotlin/com/xorker/draw/websocket/SessionMessageBroker.kt +++ b/core/src/main/kotlin/com/xorker/draw/websocket/SessionMessageBroker.kt @@ -1,6 +1,7 @@ package com.xorker.draw.websocket interface SessionMessageBroker { + fun unicast(event: UnicastEvent) fun broadcast(event: BroadcastEvent) fun broadcast(event: BranchedBroadcastEvent) fun broadcast(event: RespectiveBroadcastEvent) diff --git a/domain/build.gradle.kts b/domain/build.gradle.kts index 14a69d32..771c3b84 100644 --- a/domain/build.gradle.kts +++ b/domain/build.gradle.kts @@ -1,5 +1,9 @@ import org.springframework.boot.gradle.tasks.bundling.BootJar +dependencies { + api("org.jetbrains.kotlinx:kotlinx-coroutines-core:${Versions.COROUTINES}") +} + tasks { withType { enabled = true } withType { enabled = false } diff --git a/domain/src/main/kotlin/com/xorker/draw/exception/XorkerException.kt b/domain/src/main/kotlin/com/xorker/draw/exception/XorkerException.kt index e01e4490..63297ec3 100644 --- a/domain/src/main/kotlin/com/xorker/draw/exception/XorkerException.kt +++ b/domain/src/main/kotlin/com/xorker/draw/exception/XorkerException.kt @@ -13,6 +13,8 @@ data object OAuthFailureException : ClientException("c002", "OAuth 인증 실패 data object NotFoundRoomException : ClientException("c003", "존재하지 않는 Room Id") { private fun readResolve(): Any = NotFoundRoomException } data object MaxRoomException : ClientException("c004", "인원 수가 가득 찬 Room Id") { private fun readResolve(): Any = MaxRoomException } data object AlreadyJoinRoomException : ClientException("c005", "이미 참여한 방") { private fun readResolve(): Any = AlreadyJoinRoomException } +data object InvalidRequestOnlyMyTurnException : ClientException("c006", "요청자의 차례가 아니라서 처리 불가능") { private fun readResolve(): Any = InvalidRequestValueException } + //endregion //region Server @@ -30,4 +32,5 @@ data object InvalidUserStatusException : CriticalException("crt002", "유효하 data object UnSupportedException : CriticalException("crt003", "정의하지 않는 행위") { private fun readResolve(): Any = UnSupportedException } data object InvalidMafiaGamePlayingPhaseStatusException : CriticalException("crt004", "마피아 게임 Playing 단계에서 유효하지 않은 상태") { private fun readResolve(): Any = InvalidMafiaGamePlayingPhaseStatusException } data object InvalidBroadcastException : CriticalException("crt005", "유효하지 않은 브로드캐스트 상태") { private fun readResolve(): Any = InvalidBroadcastException } +class InvalidMafiaPhaseException(message: String) : CriticalException("crt004", message) { private fun readResolve(): Any = InvalidMafiaGamePlayingPhaseStatusException } //endregion diff --git a/domain/src/main/kotlin/com/xorker/draw/mafia/MafiaGameMessenger.kt b/domain/src/main/kotlin/com/xorker/draw/mafia/MafiaGameMessenger.kt index f8984f16..22d4c62c 100644 --- a/domain/src/main/kotlin/com/xorker/draw/mafia/MafiaGameMessenger.kt +++ b/domain/src/main/kotlin/com/xorker/draw/mafia/MafiaGameMessenger.kt @@ -1,12 +1,10 @@ package com.xorker.draw.mafia -import com.xorker.draw.room.Room import com.xorker.draw.room.RoomId interface MafiaGameMessenger { - fun broadcastPlayerList(room: Room) - fun broadcastGameInfo(mafiaGameInfo: MafiaGameInfo) - fun broadcastGameReady(mafiaGameInfo: MafiaGameInfo) + fun broadcastPlayerList(gameInfo: MafiaGameInfo) fun broadcastPlayerTurnList(mafiaGameInfo: MafiaGameInfo) - fun broadcastDraw(roomId: RoomId, phase: MafiaPhase.Playing) + fun broadcastDraw(roomId: RoomId, data: Map) + fun broadcastNextTurn(gameInfo: MafiaGameInfo) } diff --git a/domain/src/main/kotlin/com/xorker/draw/mafia/MafiaGameOption.kt b/domain/src/main/kotlin/com/xorker/draw/mafia/MafiaGameOption.kt index 474a0c29..34632422 100644 --- a/domain/src/main/kotlin/com/xorker/draw/mafia/MafiaGameOption.kt +++ b/domain/src/main/kotlin/com/xorker/draw/mafia/MafiaGameOption.kt @@ -3,9 +3,13 @@ package com.xorker.draw.mafia import java.time.Duration data class MafiaGameOption( - var turnTime: Duration = Duration.ofSeconds(5), // 턴 당 최대 시간 - var numTurn: Int = 2, // 라운드 횟수 - val readyShowingTime: Duration = Duration.ofSeconds(7), // 준비 시간 - val roleShowingTime: Duration = Duration.ofSeconds(3), // 각자 턴 순서 시간 - val roundShowingTime: Duration = Duration.ofSeconds(2), // 라운드 시간 + val minimum: Int = 3, + val maximum: Int = 10, + val readyTime: Duration = Duration.ofSeconds(10), // 마피아 게임 준비 시간 + val animationTime: Duration = Duration.ofSeconds(2), // 애니메이션 시간 + val round: Int = 2, // 전체 라운드 수 + val turnTime: Duration = Duration.ofSeconds(5), // 턴 당 최대 시간 + val turnCount: Int = 2, // 턴 당 최대 획 수 + val voteTime: Duration = Duration.ofMinutes(10), // 투표 시간 + val answerTime: Duration = Duration.ofMinutes(10), // 정답 입력 시간 ) diff --git a/domain/src/main/kotlin/com/xorker/draw/mafia/MafiaPhase.kt b/domain/src/main/kotlin/com/xorker/draw/mafia/MafiaPhase.kt index ffade074..c0f9c654 100644 --- a/domain/src/main/kotlin/com/xorker/draw/mafia/MafiaPhase.kt +++ b/domain/src/main/kotlin/com/xorker/draw/mafia/MafiaPhase.kt @@ -1,18 +1,41 @@ package com.xorker.draw.mafia +import com.xorker.draw.exception.InvalidMafiaPhaseException +import com.xorker.draw.mafia.event.JobWithStartTime +import com.xorker.draw.mafia.turn.TurnInfo import com.xorker.draw.user.UserId +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.contract sealed class MafiaPhase { + data object Wait : MafiaPhase() + data class Ready( + val job: JobWithStartTime, + override val turnList: List, + val mafiaPlayer: MafiaPlayer, + val keyword: MafiaKeyword, + ) : MafiaPhase(), MafiaPhaseWithTurnList { + fun toPlaying(job: JobWithStartTime): Playing { + return Playing( + turnList = turnList, + mafiaPlayer = mafiaPlayer, + keyword = keyword, + drawData = mutableListOf(), + timerJob = job, + ) + } + } + class Playing( - var turn: Int = 0, - var round: Int = 1, - val turnList: List, + override val turnList: List, val mafiaPlayer: MafiaPlayer, val keyword: MafiaKeyword, + var turnInfo: TurnInfo = TurnInfo(), val drawData: MutableList>>, - ) : MafiaPhase() + var timerJob: JobWithStartTime, + ) : MafiaPhase(), MafiaPhaseWithTurnList, TurnInfo by turnInfo class Vote() : MafiaPhase() @@ -20,3 +43,25 @@ sealed class MafiaPhase { class End() : MafiaPhase() } + +interface MafiaPhaseWithTurnList { + val turnList: List + + fun getPlayerTurn(userId: UserId): Int? { + turnList.forEachIndexed { index, player -> + if (player.userId == userId) return index + } + return null + } +} + +@OptIn(ExperimentalContracts::class) +inline fun assertIs(phase: MafiaPhase) { + contract { + returns() implies (phase is T) + } + + if (phase is T) { + throw InvalidMafiaPhaseException("유효하지 않는 Phase 입니다. 기대값: ${T::class}, 요청값: $phase") + } +} diff --git a/domain/src/main/kotlin/com/xorker/draw/mafia/MafiaPhaseMessenger.kt b/domain/src/main/kotlin/com/xorker/draw/mafia/MafiaPhaseMessenger.kt new file mode 100644 index 00000000..b374696f --- /dev/null +++ b/domain/src/main/kotlin/com/xorker/draw/mafia/MafiaPhaseMessenger.kt @@ -0,0 +1,8 @@ +package com.xorker.draw.mafia + +import com.xorker.draw.user.UserId + +interface MafiaPhaseMessenger { + fun unicastPhase(userId: UserId, gameInfo: MafiaGameInfo) + fun broadcastPhase(gameInfo: MafiaGameInfo) +} diff --git a/domain/src/main/kotlin/com/xorker/draw/mafia/event/JobWithStartTime.kt b/domain/src/main/kotlin/com/xorker/draw/mafia/event/JobWithStartTime.kt new file mode 100644 index 00000000..6f379aa0 --- /dev/null +++ b/domain/src/main/kotlin/com/xorker/draw/mafia/event/JobWithStartTime.kt @@ -0,0 +1,9 @@ +package com.xorker.draw.mafia.event + +import java.time.LocalDateTime +import kotlinx.coroutines.Job + +class JobWithStartTime( + private val job: Job = Job(), + val startTime: LocalDateTime = LocalDateTime.now(), +) : Job by job diff --git a/domain/src/main/kotlin/com/xorker/draw/mafia/turn/TurnInfo.kt b/domain/src/main/kotlin/com/xorker/draw/mafia/turn/TurnInfo.kt new file mode 100644 index 00000000..ebe6b213 --- /dev/null +++ b/domain/src/main/kotlin/com/xorker/draw/mafia/turn/TurnInfo.kt @@ -0,0 +1,23 @@ +package com.xorker.draw.mafia.turn + +interface TurnInfo { + val round: Int + val turn: Int + + fun nextTurn(maxRound: Int, maxTurnPerRound: Int): TurnInfo? { + if (this.round == maxRound && this.turn == maxTurnPerRound) return null + + if (this.turn == maxTurnPerRound) { + return TurnInfo(round + 1, 0) + } + + return TurnInfo(round, turn + 1) + } +} + +fun TurnInfo(round: Int = 0, turn: Int = 0): TurnInfo = TurnInfoImpl(round, turn) + +private data class TurnInfoImpl( + override val round: Int, + override val turn: Int, +) : TurnInfo diff --git a/domain/src/main/kotlin/com/xorker/draw/timer/TimerRepository.kt b/domain/src/main/kotlin/com/xorker/draw/timer/TimerRepository.kt index 5b71f440..f0355b78 100644 --- a/domain/src/main/kotlin/com/xorker/draw/timer/TimerRepository.kt +++ b/domain/src/main/kotlin/com/xorker/draw/timer/TimerRepository.kt @@ -1,7 +1,10 @@ package com.xorker.draw.timer +import com.xorker.draw.mafia.event.JobWithStartTime import java.time.Duration interface TimerRepository { - fun startTimer(interval: Duration, event: T) + fun startTimer(interval: Duration, event: T): JobWithStartTime + + fun startTimer(interval: Duration, callback: () -> Unit): JobWithStartTime } diff --git a/domain/src/main/kotlin/com/xorker/draw/websocket/ResponseAction.kt b/domain/src/main/kotlin/com/xorker/draw/websocket/ResponseAction.kt index 0fb08db8..815ef4e4 100644 --- a/domain/src/main/kotlin/com/xorker/draw/websocket/ResponseAction.kt +++ b/domain/src/main/kotlin/com/xorker/draw/websocket/ResponseAction.kt @@ -8,4 +8,9 @@ enum class ResponseAction( GAME_READY("마피아 게임 준비"), PLAYER_TURN_LIST("플레이어 턴 순서"), DRAW("그림 데이터"), + TURN_INFO("새로운 턴 시작"), + + PHASE_WAIT("Wait Phase 시작/초기화"), + PHASE_READY("Ready Phase 시작/초기화"), + PHASE_PLAYING("Player Phase 시작/초기화"), } diff --git a/domain/src/main/kotlin/com/xorker/draw/websocket/UnicastEvent.kt b/domain/src/main/kotlin/com/xorker/draw/websocket/UnicastEvent.kt new file mode 100644 index 00000000..bff200ba --- /dev/null +++ b/domain/src/main/kotlin/com/xorker/draw/websocket/UnicastEvent.kt @@ -0,0 +1,8 @@ +package com.xorker.draw.websocket + +import com.xorker.draw.user.UserId + +data class UnicastEvent( + val userId: UserId, + val message: SessionMessage, +) diff --git a/settings.gradle.kts b/settings.gradle.kts index caa96592..974ea314 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -10,5 +10,6 @@ include( "core", "support:jwt", "support:logging", + "support:time", "support:yaml", ) diff --git a/support/time/build.gradle.kts b/support/time/build.gradle.kts new file mode 100644 index 00000000..14a69d32 --- /dev/null +++ b/support/time/build.gradle.kts @@ -0,0 +1,6 @@ +import org.springframework.boot.gradle.tasks.bundling.BootJar + +tasks { + withType { enabled = true } + withType { enabled = false } +} diff --git a/support/time/src/main/kotlin/com/xorker/draw/support/time/TimeExtensions.kt b/support/time/src/main/kotlin/com/xorker/draw/support/time/TimeExtensions.kt new file mode 100644 index 00000000..5b2bc39b --- /dev/null +++ b/support/time/src/main/kotlin/com/xorker/draw/support/time/TimeExtensions.kt @@ -0,0 +1,22 @@ +package com.xorker.draw.support.time + +import java.text.DateFormat +import java.text.SimpleDateFormat +import java.time.LocalDateTime +import java.time.ZoneId +import java.util.* + +fun generateUtc(): String { + val now = LocalDateTime.now() + + val timeZone = TimeZone.getTimeZone("UTC") + val dateFormat: DateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm.ss.SSS'Z'") + dateFormat.timeZone = timeZone + + return dateFormat.format(now.toDate()) +} + +fun LocalDateTime.toDate(): Date { + val instant = this.atZone(ZoneId.systemDefault()).toInstant() + return Date.from(instant) +}