-
Notifications
You must be signed in to change notification settings - Fork 3
[feat] GameSetting 변경 기능 추가 #107
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
Changes from 9 commits
be3f034
e1d79fc
214bc54
ccac167
9af131a
2fb0b8f
a7ead79
6981459
fdaaee7
655929e
39d9f54
80101d0
db62cbc
181f4ab
c7506d9
b6ae413
476ceea
b7852c2
2db2e95
7c6515e
482bc72
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,10 +1,12 @@ | ||
| 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.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.dto.request.GameSettingChanger; | ||
| import io.f1.backend.domain.game.event.RoomUpdatedEvent; | ||
| import io.f1.backend.domain.game.model.Player; | ||
| import io.f1.backend.domain.game.model.Room; | ||
|
|
@@ -25,7 +27,6 @@ | |
| import org.springframework.stereotype.Service; | ||
|
|
||
| import java.util.List; | ||
| import java.util.Map; | ||
| import java.util.Objects; | ||
|
|
||
| @Service | ||
|
|
@@ -70,19 +71,47 @@ public void gameStart(Long roomId, UserPrincipal principal) { | |
| toQuestionStartResponse(room, START_DELAY)); | ||
| } | ||
|
|
||
| private boolean validateReadyStatus(Room room) { | ||
| public void handlePlayerReady(Long roomId, String sessionId) { | ||
| Player player = | ||
| roomRepository | ||
| .findPlayerInRoomBySessionId(roomId, sessionId) | ||
| .orElseThrow(() -> new CustomException(RoomErrorCode.PLAYER_NOT_FOUND)); | ||
|
|
||
| Room room = findRoom(roomId); | ||
|
|
||
| toggleReadyIfPossible(room, player); | ||
|
|
||
| String destination = getDestination(roomId); | ||
|
|
||
| messageSender.send(destination, MessageType.PLAYER_LIST, toPlayerListResponse(room)); | ||
| } | ||
|
|
||
| public void changeGameSetting( | ||
| Long roomId, UserPrincipal principal, GameSettingChanger request) { | ||
| Room room = findRoom(roomId); | ||
| validateHostAndState(room, principal); | ||
|
|
||
| Map<String, Player> playerSessionMap = room.getPlayerSessionMap(); | ||
| if (!request.change(room, quizService)) { | ||
| return; | ||
| } | ||
| request.afterChange(room, messageSender); | ||
|
|
||
| broadcastGameSetting(room); | ||
|
|
||
| return playerSessionMap.values().stream().allMatch(Player::isReady); | ||
| RoomUpdatedEvent roomUpdatedEvent = | ||
| new RoomUpdatedEvent( | ||
| room, | ||
| quizService.getQuizWithQuestionsById(room.getGameSetting().getQuizId())); | ||
|
|
||
| eventPublisher.publishEvent(roomUpdatedEvent); | ||
dlsrks1021 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| private void validateRoomStart(Room room, UserPrincipal principal) { | ||
| if (!Objects.equals(principal.getUserId(), room.getHost().getId())) { | ||
| throw new CustomException(RoomErrorCode.NOT_ROOM_OWNER); | ||
| } | ||
|
|
||
| if (!validateReadyStatus(room)) { | ||
| if (!room.validateReadyStatus()) { | ||
| throw new CustomException(GameErrorCode.PLAYER_NOT_READY); | ||
| } | ||
|
|
||
|
|
@@ -97,4 +126,41 @@ private List<Question> prepareQuestions(Room room, Quiz quiz) { | |
| Integer round = room.getGameSetting().getRound(); | ||
| return quizService.getRandomQuestionsWithoutAnswer(quizId, round); | ||
| } | ||
|
|
||
| private Room findRoom(Long roomId) { | ||
| return roomRepository | ||
| .findRoom(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 (!Objects.equals(principal.getUserId(), room.getHost().getId())) { | ||
| throw new CustomException(RoomErrorCode.NOT_ROOM_OWNER); | ||
| } | ||
| if (room.isPlaying()) { | ||
| throw new CustomException(RoomErrorCode.GAME_ALREADY_PLAYING); | ||
| } | ||
| } | ||
|
|
||
| private void toggleReadyIfPossible(Room room, Player player) { | ||
| if (room.isPlaying()) { | ||
| throw new CustomException(RoomErrorCode.GAME_ALREADY_PLAYING); | ||
| } | ||
| if (!Objects.equals(player.getId(), room.getHost().getId())) { | ||
|
||
| player.toggleReady(); | ||
| } | ||
| } | ||
|
|
||
| private void broadcastGameSetting(Room room) { | ||
| String destination = getDestination(room.getId()); | ||
| Quiz quiz = quizService.getQuizWithQuestionsById(room.getGameSetting().getQuizId()); | ||
| messageSender.send( | ||
| destination, | ||
| MessageType.GAME_SETTING, | ||
| toGameSettingResponse(room.getGameSetting(), quiz)); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| package io.f1.backend.domain.game.dto.request; | ||
|
|
||
| import io.f1.backend.domain.game.model.Room; | ||
| import io.f1.backend.domain.game.websocket.MessageSender; | ||
| import io.f1.backend.domain.quiz.app.QuizService; | ||
|
|
||
| public interface GameSettingChanger { | ||
|
|
||
| boolean change(Room room, QuizService quizService); | ||
|
|
||
| void afterChange(Room room, MessageSender messageSender); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| package io.f1.backend.domain.game.dto.request; | ||
|
|
||
| import static io.f1.backend.domain.game.mapper.RoomMapper.toPlayerListResponse; | ||
|
|
||
| import io.f1.backend.domain.game.dto.MessageType; | ||
| import io.f1.backend.domain.game.dto.response.PlayerListResponse; | ||
| import io.f1.backend.domain.game.model.Room; | ||
| import io.f1.backend.domain.game.websocket.MessageSender; | ||
| import io.f1.backend.domain.quiz.app.QuizService; | ||
| import io.f1.backend.domain.quiz.entity.Quiz; | ||
|
|
||
| import java.util.Objects; | ||
|
|
||
| public record QuizChangeRequest(Long quizId) implements GameSettingChanger { | ||
dlsrks1021 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| @Override | ||
| public boolean change(Room room, QuizService quizService) { | ||
| if (Objects.equals(room.getGameSetting().getQuizId(), quizId)) { | ||
| return false; // 동일하면 무시 | ||
| } | ||
| Quiz quiz = quizService.getQuizWithQuestionsById(quizId); | ||
| int questionSize = quiz.getQuestions().size(); | ||
| room.getGameSetting().changeQuiz(quiz); | ||
| // 퀴즈의 문제 갯수로 변경 | ||
| room.getGameSetting().changeRound(questionSize, questionSize); | ||
|
||
| return true; | ||
| } | ||
|
|
||
| @Override | ||
| public void afterChange(Room room, MessageSender messageSender) { | ||
| room.resetAllPlayerReadyStates(); | ||
|
|
||
| String destination = "/sub/room/" + room.getId(); | ||
|
||
| PlayerListResponse response = toPlayerListResponse(room); | ||
|
|
||
| messageSender.send(destination, MessageType.PLAYER_LIST, response); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| package io.f1.backend.domain.game.dto.request; | ||
|
|
||
| import io.f1.backend.domain.game.model.Room; | ||
| import io.f1.backend.domain.game.websocket.MessageSender; | ||
| import io.f1.backend.domain.quiz.app.QuizService; | ||
| import io.f1.backend.domain.quiz.entity.Quiz; | ||
|
|
||
| import java.util.Objects; | ||
|
|
||
| public record RoundChangeRequest(Integer round) implements GameSettingChanger { | ||
|
|
||
| @Override | ||
| public boolean change(Room room, QuizService quizService) { | ||
| if (Objects.equals(room.getGameSetting().getRound(), round)) { | ||
| return false; // 동일하면 무시 | ||
| } | ||
|
|
||
| Quiz quiz = quizService.findQuizById(room.getGameSetting().getQuizId()); | ||
| int questionSize = quiz.getQuestions().size(); | ||
|
|
||
| room.getGameSetting().changeRound(round, questionSize); | ||
| return true; | ||
| } | ||
|
|
||
| @Override | ||
| public void afterChange(Room room, MessageSender messageSender) { | ||
| // 고유한 후처리 동작 없음 | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| package io.f1.backend.domain.game.dto.request; | ||
|
|
||
| import io.f1.backend.global.exception.CustomException; | ||
| import io.f1.backend.global.exception.errorcode.GameErrorCode; | ||
|
|
||
| import lombok.Getter; | ||
|
|
||
| import java.util.Arrays; | ||
|
|
||
| @Getter | ||
| public enum TimeLimit { | ||
| FIFTEEN(15), | ||
| THIRTY(30), | ||
| FORTY_FIVE(45), | ||
| SIXTY(60); | ||
|
|
||
| private final int value; | ||
|
|
||
| TimeLimit(int value) { | ||
| this.value = value; | ||
| } | ||
|
|
||
| public static TimeLimit from(int value) { | ||
| return Arrays.stream(values()) | ||
| .filter(t -> t.value == value) | ||
| .findFirst() | ||
| .orElseThrow(() -> new CustomException(GameErrorCode.GAME_SETTING_CONFLICT)); | ||
|
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. 게임 내에서 일반 사용자가 유효하지 않은 타이머를 보낼일은 없다고 생각해서, 비정상적인 요청에 대해서 만들어놓은 방어적 예외라 예외 내용을 구체화하지는 않았습니다! |
||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| package io.f1.backend.domain.game.dto.request; | ||
|
|
||
| import io.f1.backend.domain.game.model.Room; | ||
| import io.f1.backend.domain.game.websocket.MessageSender; | ||
| import io.f1.backend.domain.quiz.app.QuizService; | ||
|
|
||
| public record TimeLimitChangeRequest(int timeLimit) implements GameSettingChanger { | ||
|
|
||
| @Override | ||
| public boolean change(Room room, QuizService quizService) { | ||
| if (room.getGameSetting().getTimeLimit() == timeLimit) { | ||
|
||
| return false; // 동일하면 무시 | ||
| } | ||
| room.getGameSetting().changeTimeLimit(TimeLimit.from(timeLimit)); | ||
|
||
| return true; | ||
| } | ||
|
|
||
| @Override | ||
| public void afterChange(Room room, MessageSender messageSender) { | ||
| // 고유한 후처리 동작 없음 | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,19 +1,34 @@ | ||
| package io.f1.backend.domain.game.model; | ||
|
|
||
| import io.f1.backend.domain.game.dto.request.TimeLimit; | ||
| import io.f1.backend.domain.quiz.entity.Quiz; | ||
| import io.f1.backend.global.exception.CustomException; | ||
| import io.f1.backend.global.exception.errorcode.GameErrorCode; | ||
|
|
||
| import lombok.AllArgsConstructor; | ||
| import lombok.Getter; | ||
|
|
||
| import java.util.Objects; | ||
|
|
||
| @Getter | ||
| @AllArgsConstructor | ||
| public class GameSetting { | ||
|
|
||
| private Long quizId; | ||
| private Integer round; // 게임 변경 시 해당 게임의 총 문제 수로 설정 | ||
| private Integer round; | ||
| private int timeLimit; | ||
|
|
||
| public boolean validateQuizId(Long quizId) { | ||
| return Objects.equals(this.quizId, quizId); | ||
| public void changeQuiz(Quiz quiz) { | ||
| quizId = quiz.getId(); | ||
| round = quiz.getQuestions().size(); // 라운드를 바꾼 퀴즈의 문제 수로 동기화 | ||
| } | ||
|
|
||
| public void changeTimeLimit(TimeLimit timeLimit) { | ||
| this.timeLimit = timeLimit.getValue(); | ||
| } | ||
|
|
||
| public void changeRound(int round, int questionsCount) { | ||
| if (round > questionsCount) { | ||
| throw new CustomException(GameErrorCode.ROUND_EXCEEDS_QUESTION_COUNT); | ||
| } | ||
| this.round = round; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,6 +8,7 @@ | |
| import java.util.ArrayList; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
| import java.util.Objects; | ||
| import java.util.concurrent.ConcurrentHashMap; | ||
| import java.util.concurrent.Executors; | ||
| import java.util.concurrent.ScheduledExecutorService; | ||
|
|
@@ -90,4 +91,16 @@ public Boolean isPlaying() { | |
| public void increaseCurrentRound() { | ||
| currentRound++; | ||
| } | ||
|
|
||
| public boolean validateReadyStatus() { | ||
|
|
||
| return playerSessionMap.values().stream().allMatch(Player::isReady); | ||
| } | ||
|
|
||
| public void resetAllPlayerReadyStates() { | ||
| for (Player player : playerSessionMap.values()) { | ||
| if (Objects.equals(player.getId(), getHost().getId())) continue; | ||
|
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. isHost() 메서드가 아닌 Objects.equal()를 통해 직접 비교한 이유가 궁금합니다 ! 만약, 이유가 null-safe라면 isHost()를 null-safe하게 바꿔 사용하는 것이 가독성이나 일관성 측면에서 좋을 것 같습니다 !
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. Host에 대해서는 널 체크를 하지 않아도 될것 같습니다! isHost로 변경했습니다! |
||
| player.setReadyFalse(); | ||
| } | ||
| } | ||
| } | ||
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.
이렇게
player를 가져오는 코드가 있는줄은 몰랐네요!단순하게
room.PlayerSessionMap.get(sessionId)를 쓰지 않고 이렇게 가져오는 이유가 있을까요?NPE때문일까요?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.
저건 작성한지 좀 된것같은데, 세션아이디 재연결 로직이 없어서 저렇게 짠것 같습니다. 재연결 로직에서 맵에서 바로 세션을 바로 삭제하지 않으니까 Room안에서 플레이어는 null일 가능성이 없을것 같네요!소켓의 세션아이디가 널이 아니어도 룸에 플레이어로 등록되지 않은 경우도 존재할 것 같네요! null 체크하나 추가해서 수정했습니다!
수정했습니다!