-
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 6 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 |
|---|---|---|
| @@ -1,11 +1,17 @@ | ||
| package io.f1.backend.domain.game.app; | ||
|
|
||
| import static io.f1.backend.domain.game.mapper.RoomMapper.toGameResultListResponse; | ||
| 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.toRankUpdateResponse; | ||
| import static io.f1.backend.domain.game.mapper.RoomMapper.toRoomSettingResponse; | ||
| 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.MessageType; | ||
| import io.f1.backend.domain.game.event.RoomUpdatedEvent; | ||
| import io.f1.backend.domain.game.model.ConnectionState; | ||
| import io.f1.backend.domain.game.model.Player; | ||
| import io.f1.backend.domain.game.model.Room; | ||
| import io.f1.backend.domain.game.model.RoomState; | ||
|
|
@@ -24,6 +30,7 @@ | |
| import org.springframework.context.ApplicationEventPublisher; | ||
| import org.springframework.stereotype.Service; | ||
|
|
||
| import java.util.ArrayList; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
| import java.util.Objects; | ||
|
|
@@ -34,9 +41,10 @@ public class GameService { | |
|
|
||
| private static final int START_DELAY = 5; | ||
|
|
||
| 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; | ||
|
|
||
|
|
@@ -64,12 +72,51 @@ 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)); | ||
| } | ||
|
|
||
| public void gameEnd(Room room) { | ||
| room.updateRoomState(RoomState.FINISHED); | ||
|
||
|
|
||
| Long roomId = room.getId(); | ||
| String destination = getDestination(roomId); | ||
|
|
||
| Map<String, Player> playerSessionMap = room.getPlayerSessionMap(); | ||
|
|
||
| messageSender.send( | ||
| destination, | ||
| MessageType.GAME_RESULT, | ||
| toGameResultListResponse(playerSessionMap, room.getGameSetting().getRound())); | ||
|
|
||
| List<Player> disconnectedPlayers = new ArrayList<>(); | ||
|
|
||
| room.initializeRound(); | ||
| for (Player player : playerSessionMap.values()) { | ||
| if (player.getState().equals(ConnectionState.DISCONNECTED)) { | ||
|
||
| disconnectedPlayers.add(player); | ||
| } | ||
| player.initializeCorrectCount(); | ||
| player.toggleReady(); | ||
| } | ||
|
|
||
|
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 내부 로직으로 변경하면서, |
||
| for (Player player : disconnectedPlayers) { | ||
| String sessionId = room.getUserIdSessionMap().get(player.id); | ||
|
||
| roomService.exitRoomForDisconnectedPlayer(roomId, player, sessionId); | ||
| } | ||
|
|
||
| room.updateRoomState(RoomState.WAITING); | ||
| messageSender.send(destination, MessageType.PLAYER_LIST, toPlayerListResponse(room)); | ||
| messageSender.send( | ||
| destination, | ||
| MessageType.GAME_SETTING, | ||
| toGameSettingResponse(room.getGameSetting(), room.getCurrentQuestion().getQuiz())); | ||
| messageSender.send(destination, MessageType.ROOM_SETTING, toRoomSettingResponse(room)); | ||
| } | ||
|
|
||
| private boolean validateReadyStatus(Room room) { | ||
|
|
||
| Map<String, Player> playerSessionMap = room.getPlayerSessionMap(); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -59,6 +59,7 @@ | |
| @RequiredArgsConstructor | ||
| public class RoomService { | ||
|
|
||
| private final GameService gameService; | ||
| private final TimerService timerService; | ||
| private final QuizService quizService; | ||
| private final RoomRepository roomRepository; | ||
|
|
@@ -263,9 +264,8 @@ public void chat(Long roomId, String sessionId, ChatMessage chatMessage) { | |
|
|
||
| timerService.cancelTimer(room); | ||
|
|
||
| // TODO : 게임 종료 로직 추가 | ||
| if (!timerService.validateCurrentRound(room)) { | ||
| // 게임 종료 로직 | ||
| gameService.gameEnd(room); | ||
| return; | ||
| } | ||
|
|
||
|
|
@@ -336,4 +336,31 @@ private void removePlayer(Room room, String sessionId, Player removePlayer) { | |
| room.removeUserId(removePlayer.getId()); | ||
| room.removeSessionId(sessionId); | ||
| } | ||
|
|
||
| public void exitRoomForDisconnectedPlayer(Long roomId, Player player, String sessionId) { | ||
|
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. [L4-변경제안] |
||
|
|
||
| // 연결 끊긴 플레이어 exit 로직 타게 해주기 | ||
| Room room = findRoom(roomId); | ||
|
|
||
| /* 방 삭제 */ | ||
| if (isLastPlayer(room, sessionId)) { | ||
| removeRoom(room); | ||
| return; | ||
| } | ||
|
|
||
| /* 방장 변경 */ | ||
| if (room.isHost(player.getId())) { | ||
| changeHost(room, sessionId); | ||
| } | ||
|
|
||
|
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. 방 삭제와 방장 변경은 동시성이 필요해보입니다! 제가 이번 pr에서 방장 변경은 connect 상태인 유저만 대상이 될 수 있게 로직을 추가하긴했는데... 다른 유저랑 동시에 나가면 방 삭제, 방장 변경이 제대로 되지 않을 것 같습니다. |
||
| /* 플레이어 삭제 */ | ||
| removePlayer(room, sessionId, player); | ||
|
|
||
| SystemNoticeResponse systemNoticeResponse = | ||
| ofPlayerEvent(player.nickname, RoomEventType.EXIT); | ||
|
|
||
| String destination = getDestination(roomId); | ||
|
|
||
| messageSender.send(destination, MessageType.SYSTEM_NOTICE, systemNoticeResponse); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,5 +9,6 @@ public enum MessageType { | |
| CHAT, | ||
| QUESTION_RESULT, | ||
| RANK_UPDATE, | ||
| QUESTION_START | ||
| QUESTION_START, | ||
| GAME_RESULT | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| package io.f1.backend.domain.game.dto.response; | ||
|
|
||
| import java.util.List; | ||
|
|
||
| public record GameResultListResponse(List<GameResultResponse> result) {} |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| package io.f1.backend.domain.game.dto.response; | ||
|
|
||
| public record GameResultResponse(String nickname, int score, int totalCorrectCount, int rank) {} |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,6 +3,8 @@ | |
| import io.f1.backend.domain.game.dto.Rank; | ||
| import io.f1.backend.domain.game.dto.RoomEventType; | ||
| import io.f1.backend.domain.game.dto.request.RoomCreateRequest; | ||
| import io.f1.backend.domain.game.dto.response.GameResultListResponse; | ||
| import io.f1.backend.domain.game.dto.response.GameResultResponse; | ||
| import io.f1.backend.domain.game.dto.response.GameSettingResponse; | ||
| import io.f1.backend.domain.game.dto.response.PlayerListResponse; | ||
| import io.f1.backend.domain.game.dto.response.PlayerResponse; | ||
|
|
@@ -21,8 +23,10 @@ | |
| import io.f1.backend.domain.quiz.entity.Quiz; | ||
|
|
||
| import java.time.Instant; | ||
| import java.util.ArrayList; | ||
| import java.util.Comparator; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
|
|
||
| public class RoomMapper { | ||
|
|
||
|
|
@@ -109,4 +113,42 @@ public static QuestionStartResponse toQuestionStartResponse(Room room, int delay | |
| room.getCurrentRound(), | ||
| Instant.now().plusSeconds(delay)); | ||
| } | ||
|
|
||
| public static GameResultResponse toGameResultResponse( | ||
| Player player, int round, int rank, int totalPlayers) { | ||
| double correctRate = (double) player.getCorrectCount() / round; | ||
| int score = (int) (correctRate * 100) + (totalPlayers - rank) * 5; | ||
|
|
||
| return new GameResultResponse(player.nickname, score, player.getCorrectCount(), rank); | ||
| } | ||
|
|
||
| public static GameResultListResponse toGameResultListResponse( | ||
| Map<String, Player> playerSessionMap, int round) { | ||
|
|
||
| List<Player> rankedPlayers = | ||
| playerSessionMap.values().stream() | ||
| .sorted(Comparator.comparingInt(Player::getCorrectCount).reversed()) | ||
| .toList(); | ||
|
|
||
| int totalPlayers = rankedPlayers.size(); | ||
|
|
||
| int prevCorrectCnt = -1; | ||
| int rank = 0; | ||
|
|
||
| List<GameResultResponse> gameResults = new ArrayList<>(); | ||
| for (int i = 0; i < totalPlayers; i++) { | ||
|
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. 해당 반복문을 메서드로 분리하면 GameResultListResponse로 변환하는 동작이 더 잘 드러날 것 같습니다 !
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. Mapper에는 오로지 매핑 (A->B) 의 역할을 가진 메서드만 존재해야 한다고 생각하여, 구분해두지 않았는데, 가독성면에서 분리하는 것이 맞다고 생각해서 분리했습니다. 또, 해당 계산 메서드를 도메인에 빼는 것은 확실히 책임 분리가 되지 않는 것이라 생각되어 매퍼 클래스 안에서 |
||
| Player player = rankedPlayers.get(i); | ||
|
|
||
| int correctCnt = player.getCorrectCount(); | ||
|
|
||
| if (prevCorrectCnt != correctCnt) { | ||
| rank = i + 1; | ||
| } | ||
|
|
||
| gameResults.add(toGameResultResponse(player, round, rank, totalPlayers)); | ||
| prevCorrectCnt = correctCnt; | ||
| } | ||
|
|
||
| return new GameResultListResponse(gameResults); | ||
| } | ||
| } | ||
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.
[L5-참고의견]
랭킹 정보 업데이트에 대한 TODO 주석을 달아두는 것도 좋을 것 같습니다.