Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: 퀴즈게임상태로직개선 #127

Merged
merged 9 commits into from
Dec 4, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@ data class AnswerMessageRequest(
val nickname: String,
)

fun AnswerMessageRequest.toPlayerAnswer(roomId: Long, PlayerId: Long) = PlayerAnswer(
answer,
quizId,
roomId,
roomId,
nickname
)
fun AnswerMessageRequest.toPlayerAnswer(roomId: Long, playerId: Long): PlayerAnswer {
return PlayerAnswer(
answer = answer,
quizId = quizId,
roomId = roomId,
playerId = playerId,
nickname = nickname
)
}
6 changes: 3 additions & 3 deletions src/main/kotlin/yjh/cstar/game/application/GameService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import yjh.cstar.game.domain.GameStartCommand
import yjh.cstar.member.application.MemberService
import yjh.cstar.play.application.GamePlayService
import yjh.cstar.play.application.request.QuizDto
import yjh.cstar.play.presentation.GamePlayEngine
import yjh.cstar.quiz.application.QuizService
import yjh.cstar.room.application.RoomService

Expand All @@ -15,7 +15,7 @@ class GameService(
val memberService: MemberService,
val roomService: RoomService,
val quizService: QuizService,
val gamePlayService: GamePlayService,
val gamePlayEngine: GamePlayEngine,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

제가 이해한 바로는, 다른 패키지와의 통일성을 위해 컨트롤러에서 서비스 단에 요청을 전달하여 게임 진행 로직을 처리하는 방식으로 변경된 것 같습니다. 여기서 특정 패키지의 서비스 단에서 다른 패키지의 컨트롤러 단을 주입받아 사용하는 경우, 계층 간의 방향성이 깨지거나 사이드 이펙트가 발생할 가능성은 없을까요? (계층 간의 방향성은, 컨트롤러 -> 서비스 -> 리포지토리로 이어지는 계층 구조를 의미합니다.)

제가 잘 모르는 부분이라 설명해주시면 큰 도움이 될 것 같습니다. 👍👍

Copy link
Contributor Author

@Jeongjjuna Jeongjjuna Dec 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

우선 game 패키지와 play 패키지를 서로 다른 두개의 서버라고 가정했습니다.

예를들어 game 서비스는 게임 시작 api 요청을 처리하고, 게임 실행 로직에 대한 이벤트를 외부 서버에 발급한다는 느낌이었습니다.
그래서 이 경우에는 game서버에서 play서버라는 외붕 서버에 요청하는것처럼 구현하였고, 실제 외부 서버의 역할을 하는 play패키는 컨트롤러에서 api요청을 받는방식과 같다고 생각해서 presentation 패키지에 두었습니다.
(물론 현재는 단일 서버이긴 하지만요..!)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

네네 이해했습니다. 단일 서버이지만 확장성을 고려해서 두 패키지를 서로 다른 서버라고 가정한거다! ㅎㅎ 이해했습니다. 그 부분을 우리 리드미나 프로젝트 소개할 때 적는 것도 좋을 것 같습니다. 😁😁

Copy link
Contributor Author

@Jeongjjuna Jeongjjuna Dec 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

추후에 멀티모듈로 분리해서 서버 분리 실습해봐도 재밌을 것 같아요!(학습용..)

) {
@Transactional
fun start(command: GameStartCommand) {
Expand All @@ -25,7 +25,7 @@ class GameService(

val randomQuizData: List<QuizDto> = getRandomQuizData(command)

gamePlayService.start(playerInfo, randomQuizData, command.roomId, command.quizCategoryId)
gamePlayEngine.start(playerInfo, randomQuizData, command.roomId, command.quizCategoryId)
}

private fun getRandomQuizData(command: GameStartCommand) =
Expand Down
48 changes: 41 additions & 7 deletions src/main/kotlin/yjh/cstar/play/application/GamePlayService.kt
Original file line number Diff line number Diff line change
@@ -1,21 +1,55 @@
package yjh.cstar.play.application

import org.springframework.scheduling.annotation.Async
import org.springframework.stereotype.Service
import yjh.cstar.common.BaseException
import yjh.cstar.game.application.GameResultService
import yjh.cstar.play.application.port.AnswerProvider
import yjh.cstar.play.application.port.GameNotifier
import yjh.cstar.play.application.port.RankingHandler
import yjh.cstar.play.application.request.QuizDto
import yjh.cstar.play.application.request.toModel
import yjh.cstar.play.domain.GameConfig
import yjh.cstar.play.domain.QuizGame
import yjh.cstar.play.domain.game.GameInfo
import yjh.cstar.room.application.RoomService
import yjh.cstar.util.Logger

@Service
class GamePlayService(
private val quizGameService: QuizGameService,
private val answerProvider: AnswerProvider,
private val gameNotifier: GameNotifier,
private val rankingHandler: RankingHandler,
private val gameResultService: GameResultService,
private val roomService: RoomService,
) {

@Async("GamePlayThreadPool")
fun start(players: Map<Long, String>, quizzes: List<QuizDto>, roomId: Long, categoryId: Long) {
Logger.info("[INFO] 게임 엔진 스레드 시작 - roomId : $roomId")
fun play(players: Map<Long, String>, randomQuizzes: List<QuizDto>, roomId: Long, categoryId: Long) {
try {
val gameInfo = GameInfo.of(
players = players,
quizzes = randomQuizzes.map { it.toModel() },
roomId = roomId,
categoryId = categoryId
)

quizGameService.play(players, quizzes, roomId, categoryId)
val gameConfig = GameConfig(
answerProvider,
gameNotifier,
rankingHandler,
gameResultService,
roomService
)
val quizGame = QuizGame.of(gameInfo, gameConfig)

Logger.info("[INFO] 게임 엔진 스레드 종료 - roomId : $roomId")
quizGame.initialize()
quizGame.run()
quizGame.finishGame()
} catch (e: BaseException) {
Logger.error(e.toString())
roomService.endGameAndResetRoom(roomId)
} catch (e: Exception) {
Logger.error(e.toString())
roomService.endGameAndResetRoom(roomId)
}
}
}
31 changes: 0 additions & 31 deletions src/main/kotlin/yjh/cstar/play/application/QuizGameService.kt

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import yjh.cstar.play.domain.player.PlayerAnswer

interface AnswerProvider {

fun receivePlayerAnswer(roomId: Long, quizId: Long): PlayerAnswer?
fun receivePlayerAnswer(roomId: Long, quizId: Long, awaitSecond: Long): PlayerAnswer?

fun initializePlayerAnswerToReceive(roomId: Long, quizId: Long)
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ interface RankingHandler {

fun initRankingBoard(roomId: Long, players: Players)

fun assignScoreToPlayer(roomId: Long, playerId: Long)
fun assignScoreToRoundWinner(roomId: Long, roundWinner: Long)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

확실히 의미가 명확해진 것 같습니다!

Copy link
Contributor Author

@Jeongjjuna Jeongjjuna Dec 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

의도를 명확히 전달하기위해 조금 과장해서 적은것도 있고, 실제로 조금 부자연스러워 보이는 것도 있는 것 같습니다.
그래도 이렇게 네이밍 연습하다보면 나중에는 자연스러워질 것 같아요!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

의도를 명확히 하기 위해 조금 부자연스럽더라도 길게 적는 게 더 좋아보입니다. 다른 사람의 코드를 읽는 개발자 입장에서 그렇게 하는게 이해하기 더 쉬운 것 같아요!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

넵넵 팀원들이 이해된다면 그걸로 된 것 같아요
관례나 관용 표현 있다면 추천해주세요!!


fun getWinner(roomId: Long): Long
fun getWinnerId(roomId: Long): Long

fun getRanking(roomId: Long): Ranking
}
15 changes: 15 additions & 0 deletions src/main/kotlin/yjh/cstar/play/domain/GameConfig.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package yjh.cstar.play.domain

import yjh.cstar.game.application.GameResultService
import yjh.cstar.play.application.port.AnswerProvider
import yjh.cstar.play.application.port.GameNotifier
import yjh.cstar.play.application.port.RankingHandler
import yjh.cstar.room.application.RoomService

data class GameConfig(
val answerProvider: AnswerProvider,
val gameNotifier: GameNotifier,
val rankingHandler: RankingHandler,
val gameResultService: GameResultService,
val roomService: RoomService,
)
147 changes: 82 additions & 65 deletions src/main/kotlin/yjh/cstar/play/domain/QuizGame.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,14 @@ import yjh.cstar.play.domain.game.GameInfo
import yjh.cstar.play.domain.game.GameInitializable
import yjh.cstar.play.domain.game.GameRunnable
import yjh.cstar.play.domain.player.Players
import yjh.cstar.play.domain.quiz.Quizzes
import yjh.cstar.play.domain.quiz.Quiz
import yjh.cstar.play.domain.quiz.RoomQuizSet
import yjh.cstar.play.domain.ranking.Ranking
import yjh.cstar.room.application.RoomService
import yjh.cstar.util.Logger
import yjh.cstar.util.TimeUtil.Companion.getCurrentLocalDateTime
import yjh.cstar.util.TimeUtil.Companion.getCurrentTime
import yjh.cstar.util.TimeUtil.Companion.getDuration
import java.time.LocalDateTime

class QuizGame(
Expand All @@ -26,117 +30,130 @@ class QuizGame(
private val roomService: RoomService,
) : GameInitializable, GameRunnable, GameFinalizable {

private val players = gameInfo.players
private val quizzes = gameInfo.quizzes
private val roomId = gameInfo.roomId
private val categoryId = gameInfo.categoryId
private val destination = "/topic/rooms/$roomId"
private val players: Players = gameInfo.players
private val roomQuizSet: RoomQuizSet = gameInfo.roomQuizSet
private val roomId: Long = gameInfo.roomId
private val categoryId: Long = gameInfo.categoryId
private val destination: String = "/topic/rooms/$roomId"

companion object {
const val TIME_LIMIT_MILLIS = 10000 // TODO("사용자의 입력을 받도록 개선")
const val TIME_LIMIT_DURATION = 10000

fun of(gameInfo: GameInfo, gameConfig: GameConfig): QuizGame {
return QuizGame(
gameInfo = gameInfo,
answerProvider = gameConfig.answerProvider,
gameNotifier = gameConfig.gameNotifier,
rankingHandler = gameConfig.rankingHandler,
gameResultService = gameConfig.gameResultService,
roomService = gameConfig.roomService
)
}
}

override fun initialize() {
rankingHandler.initRankingBoard(roomId, players)
}

override fun run() {
val gameStartedAt = getCurrentAt()
val gameStartedAt = getCurrentLocalDateTime()

gameNotifier.notifyGameStartComments(destination, gameInfo.roomId)

try {
for ((index, quiz) in quizzes.getQuizList().withIndex()) {
val quizNo = index + 1
val quizId = quiz.id
runGameWhile({ roomQuizSet.isRunning() }) {
val quizInfo = roomQuizSet.getNextQuizInfo()
val (quizNo, quizId, quiz) = quizInfo

answerProvider.initializePlayerAnswerToReceive(roomId, quizId)
submitQuizToPlayers(quizId, quizNo, quiz)
}

gameNotifier.notifyQuizQuestion(destination, quizNo, quiz)
val winnerId: Long = findWinnerId()
notifyWinnerInfo(winnerId)

val roundStartTime = getCurrentTime()
// TODO("while true 를 사용하지 않도록 수정할 것")
while (true) {
Logger.info("[INFO] 정답 대기중...")
val ranking = rankingHandler.getRanking(roomId)
saveGameResult(ranking, winnerId, gameStartedAt)
} catch (e: BaseException) {
Logger.error(e)
} catch (e: Exception) {
Logger.error(e.stackTraceToString())
Logger.error("[ERROR] 프로그램 내부에 문제가 생겼습니다.")
}
}

gameNotifier.notifyCountdown(destination)
override fun finishGame() {
roomService.endGameAndResetRoom(roomId)
}

private fun submitQuizToPlayers(quizId: Long, quizNo: Int, quiz: Quiz) {
answerProvider.initializePlayerAnswerToReceive(roomId, quizId)

if (isTimeOut(roundStartTime)) {
gameNotifier.notifyTimeOut(destination)
break
}
gameNotifier.notifyQuizQuestion(destination, quizNo, quiz)

/**
* 1초 동안 플레이어 응답을 대기합니다.(Blocking)
*/
val playerAnswer = answerProvider.receivePlayerAnswer(roomId, quizId)
?: continue
val roundStartTime = getCurrentTime()

if (playerAnswer.isCorrect(quiz)) {
val roundWinnerId = playerAnswer.playerId
while (true) {
Logger.info("[INFO] 정답 대기중...")

rankingHandler.assignScoreToPlayer(roomId, roundWinnerId)
notifyRanking()
gameNotifier.notifyCountdown(destination)

notifyRoundResult(roundWinnerId)
break
}
}
if (isTimeOut(roundStartTime)) {
gameNotifier.notifyTimeOut(destination)
break
}

findAndSendWinner(players)
val playerAnswer = answerProvider.receivePlayerAnswer(
roomId = roomId,
quizId = quizId,
awaitSecond = 1L
) ?: continue

recordGameResult(gameStartedAt)
} catch (e: BaseException) {
Logger.error(e)
} catch (e: Exception) {
Logger.error("[ERROR] 프로그램 내부에 문제가 생겼습니다.")
if (playerAnswer.isCorrect(quiz)) {
val roundWinnerId = playerAnswer.playerId

rankingHandler.assignScoreToRoundWinner(roomId, roundWinnerId)
notifyRanking()

notifyRoundResult(roundWinnerId)
break
}
}
}

override fun finishGame() {
roomService.endGameAndResetRoom(roomId)
private fun runGameWhile(isRunning: () -> Boolean, action: () -> Unit) {
while (isRunning()) {
action()
}
}

private fun recordGameResult(gameStartedAt: LocalDateTime) {
val ranking = rankingHandler.getRanking(roomId)
saveGameResult(ranking, quizzes, gameStartedAt)
private fun notifyWinnerInfo(winnerId: Long) {
val winnerNickname = players.getNickname(winnerId)
gameNotifier.notifyGameResult(destination, winnerId, winnerNickname)
}

private fun saveGameResult(ranking: Ranking, quizzes: Quizzes, gameStartedAt: LocalDateTime) {
val winnerId = findWinner()
private fun saveGameResult(ranking: Ranking, winnerId: Long, gameStartedAt: LocalDateTime) {
val gameResultCreateCommand = GameResultCreateCommand(
ranking.getRanking(),
roomId,
winnerId,
quizzes.getSize(),
roomQuizSet.getSize(),
categoryId,
gameStartedAt
)
gameResultService.create(gameResultCreateCommand)
}

private fun isTimeOut(startTime: Long) = getDuration(startTime) >= TIME_LIMIT_MILLIS

private fun findAndSendWinner(players: Players) {
val winnerId = findWinner()
val winnerNickname = players.getNickname(winnerId)
gameNotifier.notifyGameResult(destination, winnerId, winnerNickname)
}

private fun notifyRoundResult(roundWinnerId: Long) {
private fun notifyRoundResult(roundWinnerId: Long) =
gameNotifier.notifyRoundResult(destination, roundWinnerId, players.getNickname(roundWinnerId))
}

private fun notifyRanking() {
val ranking = rankingHandler.getRanking(roomId)
gameNotifier.notifyRanking(destination, players, ranking)
}

private fun findWinner() = rankingHandler.getWinner(roomId)

private fun getCurrentTime() = System.currentTimeMillis()

private fun getCurrentAt() = LocalDateTime.now()
private fun findWinnerId(): Long =
rankingHandler.getWinnerId(roomId)

private fun getDuration(pastTime: Long) = getCurrentTime() - pastTime
private fun isTimeOut(startTime: Long): Boolean =
getDuration(startTime) >= TIME_LIMIT_DURATION
}
Loading