-
Notifications
You must be signed in to change notification settings - Fork 3
[feat] 게임 종료 구현 #109
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
[feat] 게임 종료 구현 #109
Changes from all commits
6f3b0e7
a24dab5
a735651
343e50b
c4db37d
b804952
ba9ca4d
0670bd5
babfe49
939fee3
826e6d5
c7beb6a
b63cfb0
8cc3948
9d47481
2795ad1
6e6dcd3
77031fb
e3beace
931a184
6fcd83c
f2388ef
4a303e9
916f8c4
3846701
4ac146b
6bdfd15
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| package io.f1.backend.domain.game.app; | ||
|
|
||
| import static io.f1.backend.domain.game.websocket.WebSocketUtils.getDestination; | ||
|
|
||
| import io.f1.backend.domain.game.dto.ChatMessage; | ||
| import io.f1.backend.domain.game.dto.MessageType; | ||
| import io.f1.backend.domain.game.event.GameCorrectAnswerEvent; | ||
| import io.f1.backend.domain.game.model.Room; | ||
| import io.f1.backend.domain.game.websocket.MessageSender; | ||
| import io.f1.backend.domain.question.entity.Question; | ||
|
|
||
| import lombok.RequiredArgsConstructor; | ||
|
|
||
| import org.springframework.context.ApplicationEventPublisher; | ||
| import org.springframework.stereotype.Service; | ||
|
|
||
| @Service | ||
| @RequiredArgsConstructor | ||
| public class ChatService { | ||
|
|
||
| private final RoomService roomService; | ||
| private final TimerService timerService; | ||
| private final MessageSender messageSender; | ||
| private final ApplicationEventPublisher eventPublisher; | ||
|
|
||
| // todo 동시성적용 | ||
| public void chat(Long roomId, String sessionId, ChatMessage chatMessage) { | ||
|
|
||
| Room room = roomService.findRoom(roomId); | ||
|
|
||
| String destination = getDestination(roomId); | ||
|
|
||
| messageSender.send(destination, MessageType.CHAT, chatMessage); | ||
|
|
||
| if (!room.isPlaying()) { | ||
| return; | ||
| } | ||
|
|
||
| Question currentQuestion = room.getCurrentQuestion(); | ||
|
|
||
| String answer = currentQuestion.getAnswer(); | ||
|
|
||
| if (answer.equals(chatMessage.message())) { | ||
| eventPublisher.publishEvent( | ||
| new GameCorrectAnswerEvent(room, sessionId, chatMessage, answer)); | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,13 +1,16 @@ | ||
| package io.f1.backend.domain.game.app; | ||
|
|
||
| import static io.f1.backend.domain.game.mapper.RoomMapper.toGameSettingResponse; | ||
| import static io.f1.backend.domain.game.mapper.RoomMapper.toPlayerListResponse; | ||
| import static io.f1.backend.domain.game.mapper.RoomMapper.toQuestionStartResponse; | ||
| import static io.f1.backend.domain.game.mapper.RoomMapper.*; | ||
| import static io.f1.backend.domain.game.websocket.WebSocketUtils.getDestination; | ||
| import static io.f1.backend.domain.quiz.mapper.QuizMapper.toGameStartResponse; | ||
|
|
||
| import io.f1.backend.domain.game.dto.ChatMessage; | ||
| import io.f1.backend.domain.game.dto.MessageType; | ||
| import io.f1.backend.domain.game.dto.RoomEventType; | ||
| import io.f1.backend.domain.game.dto.request.GameSettingChanger; | ||
| import io.f1.backend.domain.game.dto.response.PlayerListResponse; | ||
| import io.f1.backend.domain.game.event.GameCorrectAnswerEvent; | ||
| import io.f1.backend.domain.game.event.GameTimeoutEvent; | ||
| import io.f1.backend.domain.game.event.RoomUpdatedEvent; | ||
| import io.f1.backend.domain.game.model.Player; | ||
| import io.f1.backend.domain.game.model.Room; | ||
|
|
@@ -26,21 +29,26 @@ | |
| import lombok.extern.slf4j.Slf4j; | ||
|
|
||
| import org.springframework.context.ApplicationEventPublisher; | ||
| import org.springframework.context.event.EventListener; | ||
| import org.springframework.stereotype.Service; | ||
|
|
||
| import java.util.List; | ||
| import java.util.Map; | ||
| import java.util.Objects; | ||
|
|
||
| @Slf4j | ||
| @Service | ||
| @RequiredArgsConstructor | ||
| public class GameService { | ||
|
|
||
| public static final int START_DELAY = 5; | ||
| private static final int START_DELAY = 5; | ||
| private static final int CONTINUE_DELAY = 3; | ||
| private static final String NONE_CORRECT_USER = ""; | ||
|
|
||
| private final MessageSender messageSender; | ||
| private final TimerService timerService; | ||
| private final QuizService quizService; | ||
| private final RoomService roomService; | ||
| private final TimerService timerService; | ||
| private final MessageSender messageSender; | ||
| private final RoomRepository roomRepository; | ||
| private final ApplicationEventPublisher eventPublisher; | ||
|
|
||
|
|
@@ -68,12 +76,109 @@ public void gameStart(Long roomId, UserPrincipal principal) { | |
| timerService.startTimer(room, START_DELAY); | ||
|
|
||
| messageSender.send(destination, MessageType.GAME_START, toGameStartResponse(questions)); | ||
| messageSender.send(destination, MessageType.RANK_UPDATE, toRankUpdateResponse(room)); | ||
| messageSender.send( | ||
| destination, | ||
| MessageType.QUESTION_START, | ||
| toQuestionStartResponse(room, START_DELAY)); | ||
| } | ||
|
|
||
| @EventListener | ||
| public void onCorrectAnswer(GameCorrectAnswerEvent event) { | ||
|
|
||
| Room room = event.room(); | ||
| String sessionId = event.sessionId(); | ||
| ChatMessage chatMessage = event.chatMessage(); | ||
| String answer = event.answer(); | ||
|
|
||
| String destination = getDestination(room.getId()); | ||
|
|
||
| room.increasePlayerCorrectCount(sessionId); | ||
|
|
||
| messageSender.send( | ||
| destination, | ||
| MessageType.QUESTION_RESULT, | ||
| toQuestionResultResponse(chatMessage.nickname(), answer)); | ||
| messageSender.send(destination, MessageType.RANK_UPDATE, toRankUpdateResponse(room)); | ||
| messageSender.send( | ||
| destination, | ||
| MessageType.SYSTEM_NOTICE, | ||
| ofPlayerEvent(chatMessage.nickname(), RoomEventType.CORRECT_ANSWER)); | ||
|
|
||
| timerService.cancelTimer(room); | ||
|
|
||
| if (!timerService.validateCurrentRound(room)) { | ||
| gameEnd(room); | ||
| return; | ||
| } | ||
|
|
||
| room.increaseCurrentRound(); | ||
|
|
||
| // 타이머 추가하기 | ||
| timerService.startTimer(room, CONTINUE_DELAY); | ||
| messageSender.send( | ||
| destination, | ||
| MessageType.QUESTION_START, | ||
| toQuestionStartResponse(room, CONTINUE_DELAY)); | ||
| } | ||
|
|
||
| @EventListener | ||
| public void onTimeout(GameTimeoutEvent event) { | ||
| Room room = event.room(); | ||
| String destination = getDestination(room.getId()); | ||
|
|
||
| messageSender.send( | ||
| destination, | ||
| MessageType.QUESTION_RESULT, | ||
| toQuestionResultResponse(NONE_CORRECT_USER, room.getCurrentQuestion().getAnswer())); | ||
| messageSender.send( | ||
| destination, | ||
| MessageType.SYSTEM_NOTICE, | ||
| ofPlayerEvent(NONE_CORRECT_USER, RoomEventType.TIMEOUT)); | ||
|
|
||
| if (!timerService.validateCurrentRound(room)) { | ||
| gameEnd(room); | ||
| return; | ||
| } | ||
|
|
||
| room.increaseCurrentRound(); | ||
|
|
||
| timerService.startTimer(room, CONTINUE_DELAY); | ||
| messageSender.send( | ||
| destination, | ||
| MessageType.QUESTION_START, | ||
| toQuestionStartResponse(room, CONTINUE_DELAY)); | ||
| } | ||
|
|
||
| public void gameEnd(Room room) { | ||
| Long roomId = room.getId(); | ||
| String destination = getDestination(roomId); | ||
|
|
||
| Map<String, Player> playerSessionMap = room.getPlayerSessionMap(); | ||
|
|
||
| // TODO : 랭킹 정보 업데이트 | ||
| messageSender.send( | ||
| destination, | ||
| MessageType.GAME_RESULT, | ||
| toGameResultListResponse(playerSessionMap, room.getGameSetting().getRound())); | ||
|
|
||
| room.initializeRound(); | ||
| room.initializePlayers(); | ||
|
|
||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. room 내부 로직으로 변경하면서, |
||
| List<Player> disconnectedPlayers = room.getDisconnectedPlayers(); | ||
| roomService.handleDisconnectedPlayers(room, disconnectedPlayers); | ||
|
|
||
| room.updateRoomState(RoomState.WAITING); | ||
|
|
||
| messageSender.send( | ||
| destination, | ||
| MessageType.GAME_SETTING, | ||
| toGameSettingResponse( | ||
| room.getGameSetting(), | ||
| quizService.getQuizWithQuestionsById(room.getGameSetting().getQuizId()))); | ||
| messageSender.send(destination, MessageType.ROOM_SETTING, toRoomSettingResponse(room)); | ||
| } | ||
|
|
||
| public void handlePlayerReady(Long roomId, String sessionId) { | ||
|
|
||
| Room room = findRoom(roomId); | ||
|
|
@@ -136,10 +241,6 @@ private Room findRoom(Long roomId) { | |
| .orElseThrow(() -> new CustomException(RoomErrorCode.ROOM_NOT_FOUND)); | ||
| } | ||
|
|
||
| private String getDestination(Long roomId) { | ||
| return "/sub/room/" + roomId; | ||
| } | ||
|
|
||
| private void validateHostAndState(Room room, UserPrincipal principal) { | ||
| if (!room.isHost(principal.getUserId())) { | ||
| throw new CustomException(RoomErrorCode.NOT_ROOM_OWNER); | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
동시성 처리가 redis로 변환될 예정이니 chat을 서비스로 분리한 점 넘 좋네욥!
정답일 경우 구현해놓은 로직은 eventPublisher로 구현하신 이유가 있을까요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
앗 위에 리뷰 확인했습니다 !
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
서비스 간의 순환 참조가 발생하면서, 게임 진행의 과정이 다른 서비스와 너무 연결되어 사용된다고 생각했습니다.
기존 코드 기준 예를 들어, 채팅 (정답) (RoomService)-> 타이머 취소 (TimerService) -> 마지막 라운드 시 게임 종료 (GameService) -> Disconnected 된 플레이어 처리 (RoomService) 이런 식으로 서비스들이 서로 깊게 연결되고, 흐름 제어가 분산되어 있어서 유지보수와 테스트가 어렵다고 생각했습니다.
그래서 정답 맞힘/타임아웃과 같은 주요 게임 이벤트를
ApplicationEventPublisher를 통해 GameService에서 받아서 처리하도록 변경했습니다. 정답을 맞혔을 때, 타임아웃이 됐을 때, 두 개의 상황에서GameService에서 서로 연결되게 호출하는 방식이 아닌 이벤트 핸들링을 통해 GameService에서 핸들링해줄 수 있도록 하기 위해 이벤트 퍼블리싱 방식으로 구현했습니다 !