Skip to content

Commit 4c2cd3a

Browse files
✨ feat : 게임 시작, 진행, 타이머 기능 추가 (#94)
* 🔧 최신 dev 브랜치와 충돌 해결.. * ✨ feat : 게임 시작, 진행, 타이머 기능 추가 * chore: Java 스타일 수정 * ✨ feat : Room에 타이머 업데이트 해주기 * chore: Java 스타일 수정 * ♻️ refactor : PR 리뷰 반영 + chat으로 정답 맞혔을 시, Question_start 누락 해결 * chore: Java 스타일 수정 * 🔧 chore: 사용하지 않는 상수 삭제 * ♻️ refactor : getDestination() util 메서드로 분리 * chore: Java 스타일 수정 * 🔧 chore : PR 리뷰 반영 * chore: Java 스타일 수정 * ♻️ refactor : SystemNotice enum으로 분리 * chore: Java 스타일 수정 * chore: Java 스타일 수정 * 🔧 chore: cancelTimer 위치 변경 --------- Co-authored-by: github-actions <>
1 parent 2d0b0c0 commit 4c2cd3a

File tree

16 files changed

+846
-37
lines changed

16 files changed

+846
-37
lines changed

backend/src/main/java/io/f1/backend/domain/game/app/GameService.java

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
package io.f1.backend.domain.game.app;
22

3+
import static io.f1.backend.domain.game.mapper.RoomMapper.toQuestionStartResponse;
4+
import static io.f1.backend.domain.game.websocket.WebSocketUtils.getDestination;
35
import static io.f1.backend.domain.quiz.mapper.QuizMapper.toGameStartResponse;
46

5-
import io.f1.backend.domain.game.dto.response.GameStartResponse;
7+
import io.f1.backend.domain.game.dto.MessageType;
68
import io.f1.backend.domain.game.event.RoomUpdatedEvent;
79
import io.f1.backend.domain.game.model.Player;
810
import io.f1.backend.domain.game.model.Room;
911
import io.f1.backend.domain.game.model.RoomState;
1012
import io.f1.backend.domain.game.store.RoomRepository;
13+
import io.f1.backend.domain.game.websocket.MessageSender;
1114
import io.f1.backend.domain.question.entity.Question;
1215
import io.f1.backend.domain.quiz.app.QuizService;
1316
import io.f1.backend.domain.quiz.entity.Quiz;
@@ -29,11 +32,17 @@
2932
@RequiredArgsConstructor
3033
public class GameService {
3134

35+
private static final int START_DELAY = 5;
36+
37+
private final MessageSender messageSender;
38+
private final TimerService timerService;
3239
private final QuizService quizService;
3340
private final RoomRepository roomRepository;
3441
private final ApplicationEventPublisher eventPublisher;
3542

36-
public GameStartResponse gameStart(Long roomId, UserPrincipal principal) {
43+
public void gameStart(Long roomId, UserPrincipal principal) {
44+
45+
String destination = getDestination(roomId);
3746

3847
Room room =
3948
roomRepository
@@ -47,13 +56,18 @@ public GameStartResponse gameStart(Long roomId, UserPrincipal principal) {
4756
List<Question> questions = prepareQuestions(room, quiz);
4857

4958
room.updateQuestions(questions);
50-
51-
// 방 정보 게임 중으로 변경
59+
room.increaseCurrentRound();
5260
room.updateRoomState(RoomState.PLAYING);
5361

5462
eventPublisher.publishEvent(new RoomUpdatedEvent(room, quiz));
5563

56-
return toGameStartResponse(questions);
64+
timerService.startTimer(room, START_DELAY);
65+
66+
messageSender.send(destination, MessageType.GAME_START, toGameStartResponse(questions));
67+
messageSender.send(
68+
destination,
69+
MessageType.QUESTION_START,
70+
toQuestionStartResponse(room, START_DELAY));
5771
}
5872

5973
private boolean validateReadyStatus(Room room) {

backend/src/main/java/io/f1/backend/domain/game/app/RoomService.java

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@
55
import static io.f1.backend.domain.game.mapper.RoomMapper.toGameSettingResponse;
66
import static io.f1.backend.domain.game.mapper.RoomMapper.toPlayerListResponse;
77
import static io.f1.backend.domain.game.mapper.RoomMapper.toQuestionResultResponse;
8+
import static io.f1.backend.domain.game.mapper.RoomMapper.toQuestionStartResponse;
89
import static io.f1.backend.domain.game.mapper.RoomMapper.toRankUpdateResponse;
910
import static io.f1.backend.domain.game.mapper.RoomMapper.toRoomResponse;
1011
import static io.f1.backend.domain.game.mapper.RoomMapper.toRoomSetting;
1112
import static io.f1.backend.domain.game.mapper.RoomMapper.toRoomSettingResponse;
13+
import static io.f1.backend.domain.game.websocket.WebSocketUtils.getDestination;
1214
import static io.f1.backend.global.util.SecurityUtils.getCurrentUserId;
1315
import static io.f1.backend.global.util.SecurityUtils.getCurrentUserNickname;
1416

@@ -57,12 +59,17 @@
5759
@RequiredArgsConstructor
5860
public class RoomService {
5961

62+
private final TimerService timerService;
6063
private final QuizService quizService;
6164
private final RoomRepository roomRepository;
6265
private final AtomicLong roomIdGenerator = new AtomicLong(0);
6366
private final ApplicationEventPublisher eventPublisher;
6467
private final Map<Long, Object> roomLocks = new ConcurrentHashMap<>();
68+
6569
private final MessageSender messageSender;
70+
71+
private static final int CONTINUE_DELAY = 3;
72+
6673
private static final String PENDING_SESSION_ID = "PENDING_SESSION_ID";
6774

6875
public RoomCreateResponse saveRoom(RoomCreateRequest request) {
@@ -226,6 +233,7 @@ public RoomListResponse getAllRooms() {
226233

227234
// todo 동시성적용
228235
public void chat(Long roomId, String sessionId, ChatMessage chatMessage) {
236+
229237
Room room = findRoom(roomId);
230238

231239
String destination = getDestination(roomId);
@@ -246,12 +254,29 @@ public void chat(Long roomId, String sessionId, ChatMessage chatMessage) {
246254
messageSender.send(
247255
destination,
248256
MessageType.QUESTION_RESULT,
249-
toQuestionResultResponse(currentQuestion.getId(), chatMessage, answer));
257+
toQuestionResultResponse(chatMessage.nickname(), answer));
250258
messageSender.send(destination, MessageType.RANK_UPDATE, toRankUpdateResponse(room));
251259
messageSender.send(
252260
destination,
253261
MessageType.SYSTEM_NOTICE,
254-
ofPlayerEvent(chatMessage.nickname(), RoomEventType.ENTER));
262+
ofPlayerEvent(chatMessage.nickname(), RoomEventType.CORRECT_ANSWER));
263+
264+
timerService.cancelTimer(room);
265+
266+
// TODO : 게임 종료 로직 추가
267+
if (!timerService.validateCurrentRound(room)) {
268+
// 게임 종료 로직
269+
return;
270+
}
271+
272+
room.increaseCurrentRound();
273+
274+
// 타이머 추가하기
275+
timerService.startTimer(room, CONTINUE_DELAY);
276+
messageSender.send(
277+
destination,
278+
MessageType.QUESTION_START,
279+
toQuestionStartResponse(room, CONTINUE_DELAY));
255280
}
256281
}
257282

@@ -311,8 +336,4 @@ private void removePlayer(Room room, String sessionId, Player removePlayer) {
311336
room.removeUserId(removePlayer.getId());
312337
room.removeSessionId(sessionId);
313338
}
314-
315-
private String getDestination(Long roomId) {
316-
return "/sub/room/" + roomId;
317-
}
318339
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package io.f1.backend.domain.game.app;
2+
3+
import static io.f1.backend.domain.game.mapper.RoomMapper.ofPlayerEvent;
4+
import static io.f1.backend.domain.game.mapper.RoomMapper.toQuestionResultResponse;
5+
import static io.f1.backend.domain.game.mapper.RoomMapper.toQuestionStartResponse;
6+
import static io.f1.backend.domain.game.websocket.WebSocketUtils.getDestination;
7+
8+
import io.f1.backend.domain.game.dto.MessageType;
9+
import io.f1.backend.domain.game.dto.RoomEventType;
10+
import io.f1.backend.domain.game.model.Room;
11+
import io.f1.backend.domain.game.websocket.MessageSender;
12+
13+
import lombok.RequiredArgsConstructor;
14+
15+
import org.springframework.stereotype.Service;
16+
17+
import java.util.concurrent.ScheduledFuture;
18+
import java.util.concurrent.TimeUnit;
19+
20+
@Service
21+
@RequiredArgsConstructor
22+
public class TimerService {
23+
24+
private final MessageSender messageSender;
25+
26+
private static final String NONE_CORRECT_USER = "";
27+
private static final int CONTINUE_DELAY = 3;
28+
29+
public void startTimer(Room room, int delaySec) {
30+
cancelTimer(room);
31+
32+
ScheduledFuture<?> timer =
33+
room.getScheduler()
34+
.schedule(
35+
() -> {
36+
handleTimeout(room);
37+
},
38+
delaySec + room.getGameSetting().getTimeLimit(),
39+
TimeUnit.SECONDS);
40+
41+
room.updateTimer(timer);
42+
}
43+
44+
private void handleTimeout(Room room) {
45+
String destination = getDestination(room.getId());
46+
47+
messageSender.send(
48+
destination,
49+
MessageType.QUESTION_RESULT,
50+
toQuestionResultResponse(NONE_CORRECT_USER, room.getCurrentQuestion().getAnswer()));
51+
messageSender.send(
52+
destination,
53+
MessageType.SYSTEM_NOTICE,
54+
ofPlayerEvent(NONE_CORRECT_USER, RoomEventType.TIMEOUT));
55+
56+
// TODO : 게임 종료 로직
57+
if (!validateCurrentRound(room)) {
58+
// 게임 종료 로직
59+
// GAME_SETTING, PLAYER_LIST, GAME_RESULT, ROOM_SETTING
60+
return;
61+
}
62+
63+
// 다음 문제 출제
64+
room.increaseCurrentRound();
65+
66+
startTimer(room, CONTINUE_DELAY);
67+
messageSender.send(
68+
destination,
69+
MessageType.QUESTION_START,
70+
toQuestionStartResponse(room, CONTINUE_DELAY));
71+
}
72+
73+
public boolean validateCurrentRound(Room room) {
74+
if (room.getGameSetting().getRound() != room.getCurrentRound()) {
75+
return true;
76+
}
77+
cancelTimer(room);
78+
room.getScheduler().shutdown();
79+
return false;
80+
}
81+
82+
public boolean cancelTimer(Room room) {
83+
// 정답 맞혔어요 ~ 타이머 캔슬 부탁
84+
ScheduledFuture<?> timer = room.getTimer();
85+
if (timer != null && !timer.isDone()) {
86+
return timer.cancel(false);
87+
}
88+
return false;
89+
}
90+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package io.f1.backend.domain.game.dto;
2+
3+
public enum GameEventType {
4+
START,
5+
CONTINUE
6+
}

backend/src/main/java/io/f1/backend/domain/game/dto/MessageType.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,5 @@ public enum MessageType {
99
CHAT,
1010
QUESTION_RESULT,
1111
RANK_UPDATE,
12+
QUESTION_START
1213
}
Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,25 @@
11
package io.f1.backend.domain.game.dto;
22

33
public enum RoomEventType {
4-
ENTER,
5-
EXIT,
6-
START,
7-
END,
4+
ENTER(SystemNoticeMessage.ENTER),
5+
EXIT(SystemNoticeMessage.EXIT),
6+
START(null),
7+
END(null),
8+
CORRECT_ANSWER(SystemNoticeMessage.CORRECT_ANSWER),
9+
TIMEOUT(SystemNoticeMessage.TIMEOUT);
10+
11+
private final SystemNoticeMessage systemMessage;
12+
13+
RoomEventType(SystemNoticeMessage systemMessage) {
14+
this.systemMessage = systemMessage;
15+
}
16+
17+
public String getMessage(String nickname) {
18+
19+
if (this == TIMEOUT) {
20+
return systemMessage.getMessage();
21+
}
22+
23+
return nickname + systemMessage.getMessage();
24+
}
825
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package io.f1.backend.domain.game.dto;
2+
3+
public enum SystemNoticeMessage {
4+
ENTER(" 님이 입장하셨습니다"),
5+
EXIT(" 님이 퇴장하셨습니다"),
6+
CORRECT_ANSWER(" 님 정답입니다 !"),
7+
TIMEOUT("땡 ~ ⏰ 제한 시간 초과!");
8+
9+
private final String message;
10+
11+
SystemNoticeMessage(String message) {
12+
this.message = message;
13+
}
14+
15+
public String getMessage() {
16+
return message;
17+
}
18+
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
package io.f1.backend.domain.game.dto.response;
22

3-
public record QuestionResultResponse(Long questionId, String correctUser, String answer) {}
3+
public record QuestionResultResponse(String correctUser, String answer) {}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package io.f1.backend.domain.game.dto.response;
2+
3+
import java.time.Instant;
4+
5+
public record QuestionStartResponse(Long questionId, int round, Instant timestamp) {}

backend/src/main/java/io/f1/backend/domain/game/mapper/RoomMapper.java

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
package io.f1.backend.domain.game.mapper;
22

3-
import io.f1.backend.domain.game.dto.ChatMessage;
43
import io.f1.backend.domain.game.dto.Rank;
54
import io.f1.backend.domain.game.dto.RoomEventType;
65
import io.f1.backend.domain.game.dto.request.RoomCreateRequest;
76
import io.f1.backend.domain.game.dto.response.GameSettingResponse;
87
import io.f1.backend.domain.game.dto.response.PlayerListResponse;
98
import io.f1.backend.domain.game.dto.response.PlayerResponse;
109
import io.f1.backend.domain.game.dto.response.QuestionResultResponse;
10+
import io.f1.backend.domain.game.dto.response.QuestionStartResponse;
1111
import io.f1.backend.domain.game.dto.response.QuizResponse;
1212
import io.f1.backend.domain.game.dto.response.RankUpdateResponse;
1313
import io.f1.backend.domain.game.dto.response.RoomResponse;
@@ -87,18 +87,12 @@ public static QuizResponse toQuizResponse(Quiz quiz) {
8787
}
8888

8989
public static SystemNoticeResponse ofPlayerEvent(String nickname, RoomEventType roomEventType) {
90-
String message = "";
91-
if (roomEventType == RoomEventType.ENTER) {
92-
message = " 님이 입장하셨습니다";
93-
} else if (roomEventType == RoomEventType.EXIT) {
94-
message = " 님이 퇴장하셨습니다";
95-
}
96-
return new SystemNoticeResponse(nickname + message, Instant.now());
90+
return new SystemNoticeResponse(roomEventType.getMessage(nickname), Instant.now());
9791
}
9892

9993
public static QuestionResultResponse toQuestionResultResponse(
100-
Long questionId, ChatMessage chatMessage, String answer) {
101-
return new QuestionResultResponse(questionId, chatMessage.nickname(), answer);
94+
String correctUser, String answer) {
95+
return new QuestionResultResponse(correctUser, answer);
10296
}
10397

10498
public static RankUpdateResponse toRankUpdateResponse(Room room) {
@@ -108,4 +102,11 @@ public static RankUpdateResponse toRankUpdateResponse(Room room) {
108102
.map(player -> new Rank(player.getNickname(), player.getCorrectCount()))
109103
.toList());
110104
}
105+
106+
public static QuestionStartResponse toQuestionStartResponse(Room room, int delay) {
107+
return new QuestionStartResponse(
108+
room.getCurrentQuestion().getId(),
109+
room.getCurrentRound(),
110+
Instant.now().plusSeconds(delay));
111+
}
111112
}

0 commit comments

Comments
 (0)