강동현

게임 로직에 대한 프로토콜 문서 작성

...@@ -27,3 +27,62 @@ ...@@ -27,3 +27,62 @@
27 유저가 채팅을 입력했다면 `RoomChatMessage`를 보내면 됩니다. 이 경우 오직 `message` 필드만 채워 보내면 됩니다. 다른 사람이 채팅을 입력한 경우, `RoomChatMessage`가 수신됩니다. 자신이 보낸 채팅에 대해서는 `RoomChatMessage`가 수신되지 않으므로 이 경우 오프라인으로 메세지를 추가하여야 합니다. 27 유저가 채팅을 입력했다면 `RoomChatMessage`를 보내면 됩니다. 이 경우 오직 `message` 필드만 채워 보내면 됩니다. 다른 사람이 채팅을 입력한 경우, `RoomChatMessage`가 수신됩니다. 자신이 보낸 채팅에 대해서는 `RoomChatMessage`가 수신되지 않으므로 이 경우 오프라인으로 메세지를 추가하여야 합니다.
28 28
29 방을 나가고 싶으면 `RoomLeaveMessage`를 보내면 됩니다. 29 방을 나가고 싶으면 `RoomLeaveMessage`를 보내면 됩니다.
30 +
31 +## 게임
32 +
33 +### 루프
34 +
35 +준비 화면에서 라운드가 시작되면 `RoundStartMessage`가 수신됩니다. `round`는 현재 라운드 넘버 (1부터 시작), `duration`은 현재 라운드의 길이를 초 단위로 나타냅니다. `roles`는 각 플레이어가 이번 라운드에서 맡게 된 역할입니다(후술). 항상 라운드가 시작되면 타이머를 라운드의 길이로 맞춘 뒤 타이머를 정지해주세요. 이때 그림을 그리는 사람이 단어를 선택하게 됩니다. 단어 선택이 끝나면 타이머의 시간이 흐르게 됩니다.
36 +서버는 클라이언트 타이머의 상태를 `RoundTimerMessage`를 보내서 동기화합니다. `state``started`이면 메세지를 수신한 즉시 타이머를 동작시키고, `stopped`이면 타이머를 일시 정지합니다. 이때 `time`에 남은 시간이 초 단위로 포함되므로, 항상 이 메세지를 수신할 때마다 타이머의 남은 시간을 `time`값으로 동기화해주세요. 일반적으로 이 메세지는 단어 선택이 완료되어 라운드의 시간이 흐르기 시작하는 시점에 한 번만 전송됩니다. 라운드가 종료되면 `state: stopped``RoundTimerMessage`가 수신됩니다.
37 +모든 플레이어가 단어를 맞추거나, 타이머의 시간이 0으로 떨어지면 라운드가 종료되면서 `RoundFinishMessage`가 수신됩니다. 이 메세지는 이번 라운드의 정답을 포함하고 있습니다. 만약 진행할 라운드가 더 남았다면 몇 초 뒤에 다시 `RoundStartMessage`가 수신될 것입니다. 그러나 이번 라운드가 마지막이었다면 `GameFinishMessage`가 수신됩니다. 이는 게임이 정상적으로 종료되었다는 의미이며, 다시 준비 화면으로 전환해주시면 됩니다.
38 +
39 +### 역할
40 +
41 +가능한 역할은 `drawer`, `guesser`, `winner`, `spectator`로 구분됩니다. 이는 `RoundStartMessage`와 함께 수신됩니다. 만약 라운드 진행 중에 역할이 바뀌게 된다면 `RoundRoleMessage`가 수신됩니다. 이는 단순히 플레이어 목록 UI를 업데이트 하기 위해서 사용되며, 따로 고려할 게임 로직은 없습니다.
42 +
43 +- `drawer`: 그림을 그리는 사람입니다.
44 +- `guesser`: 그림을 보고 단어를 맞추는 사람입니다. 아직 단어를 맞추지 못한 상태입니다.
45 +- `winner`: 그림을 보고 단어를 맞춘 사람입니다.
46 +- `spectator`: 중도 입장한 관전자입니다. 다음 라운드부터 참여됩니다. 이 값을 역할의 기본 값으로 취급할 수 있습니다.
47 +
48 +### drawer
49 +
50 +`drawer`는 라운드가 시작된 뒤 바로 `RoundWordSetMessage`를 통해 선택 가능한 단어들을 수신받습니다. 수신받는 단어 수는 3개입니다. `drawer`는 이 중 하나를 선택해서 그림을 그릴 수 있습니다. 단어를 선택하면 해당 단어를 `RoundChooseWordMessage`에 담아 서버로 송신합니다. 서버는 이를 확인하고 타이머를 동작시킵니다. 나머지 참가자들은 오직 정답의 글자수만을 담고 있는 `RoundWordChosenMessage`를 수신받게 됩니다.
51 +그림은 `drawer`의 브러시 움직임을 그대로 시뮬레이션하여 만들어집니다. `drawer`가 색깔, 굵기를 바꾸면 `PaintBrushMessage`가 서버로 전송되고, 나머지 플레이어들은 이를 수신받게 됩니다. `size`는 브러시의 지름을 픽셀 단위로 나타내고, `color`는 브러시의 색상을 6자리 소문자 16진수로 나타냅니다. 이 메세지는 `drawing` 필드도 담고 있는데, 이는 마우스가 눌린 상태인지, 즉 브러시로 칠을 하는 상태인지를 나타냅니다. 중요한 것은 `drawer`가 캔버스 위에 마우스를 누르는 순간 `drawing``true`로 설정된 메세지가, 캔버스에서 마우스를 떼는 순간 `false`로 설정된 메세지가 서버로 전송되어야 한다는 점입니다.
52 +만약 `drawer`가 캔버스 위에서 그림을 그리는 중이라면 실시간으로 `PaintMoveMessage`가 서버로 전송되어야 합니다. `x``y`는 캔버스 오른쪽 아래 지점을 (0, 0)로 설정했을 때의 마우스의 좌표를 픽셀 단위로 나타냅니다.
53 +다른 플레이어들은 `PaintBrushMessage`를 통해 `drawing``true`로 설정된 시점부터, 다시 `drawing``false`로 설정되는 시점까지, 마우스가 움직이는 모든 위치에 대해 점이 찍히게 됩니다. 정확히는 마우스의 좌표가 업데이트 될 때 이전 지점과 현재 지점을 선으로 이어 칠해주는 방식으로 보간을 해야 할 것입니다.
54 +
55 +전체 과정을 정리하자면 다음과 같습니다.
56 +
57 +- 굵기, 색상을 변경하면 `PaintBrushMessage` 전송.
58 +- 캔버스 위에서 마우스를 누르는 시점에 현재 마우스 위치를 `PaintMoveMessage`**먼저** 전송한 뒤에 `drawing``true`로 한 `PaintBrushMessage` 전송. (만약 Move가 먼저 전송되지 않는다면 마지막으로 브러시를 뗀 위치에서 현재 위치까지 불필요한 선이 그려질 것입니다.)
59 +- 캔버스 위에서 마우스를 움직이는 동안 `PaintMoveMessage` 전송.
60 +- 마우스를 떼는 시점에 `drawing``false`로 한 `PaintBrushMessage` 전송.
61 +
62 +이 정보들을 가지고 캔버스를 칠하는 컴포넌트를 만들어서, 이를 `drawer`의 클라이언트에도 동일하게 사용하는 방식으로 구현하여 `drawer`와 다른 플레이어의 캔버스가 동일하게 보이도록 해야 할 것입니다.
63 +
64 +캔버스의 크기: 512x384 (4:3) (추후 변경 가능)
65 +
66 +### guesser
67 +
68 +`guesser`는 정답을 채팅에 입력할 수 있습니다. 만약 정답이라면 채팅이 서버에서 무시되고 역할이 `winner`로 변경되는 `RoundRoleMessage`가 수신되고, 정답을 담고 있는 `AnswerAcceptedMessage`가 수신됩니다.
69 +만약 답을 맞추지 못했다면 일반 채팅으로 전달됩니다.
70 +
71 +### winner, spectator
72 +
73 +채팅만 이용할 수 있습니다.
74 +
75 +### 중도 입장
76 +
77 +게임 도중 입장한 유저에게는 다음과 같은 메세지들이 모두 전송됩니다.
78 +
79 +1. 준비 상태에서 자신이 방에 접속했을 때 전달 받는 모든 메세지들
80 +2. 현재 라운드에 대한 정보를 담은 `RoundStartMessage`
81 +3. 현재 라운드 타이머와 동기화할 수 있는 `RoundTimerMessage`
82 +4. 마지막으로 서버상으로 기록된 브러시 정보를 담은 `PaintBrushMessage`
83 +5. 마지막으로 서버상으로 기록된 브러시 위치를 담은 `PaintMoveMessage`
84 +
85 +다른 플레이어에게는 다음과 같은 메세지들이 모두 전송됩니다.
86 +
87 +1. 준비 상태에서 해당 유저가 방에 접속했을 때 전달 받는 모든 메세지들
88 +2. 신규 유저를 관전자로 설정하는 `RoundRoleMessage` 메세지
......
...@@ -85,6 +85,136 @@ export class RoomChatMessage implements Message { ...@@ -85,6 +85,136 @@ export class RoomChatMessage implements Message {
85 constructor(public message: string, public sender?: string) {} 85 constructor(public message: string, public sender?: string) {}
86 } 86 }
87 87
88 +/**
89 + * 클라 <- 서버
90 + * 라운드가 시작되었음을 알립니다.
91 + * @param round 현재 라운드 넘버입니다. (1부터 시작)
92 + * @param duration 초 단위의 라운드 시간입니다.
93 + * @param roles 모든 방 접속 인원의 역할입니다.
94 + */
95 +export class RoundStartMessage implements Message {
96 + readonly type = MessageType.ROUND_START;
97 + constructor(
98 + public round: number,
99 + public duration: number,
100 + public roles: {
101 + username: string;
102 + role: "drawer" | "guesser" | "winner" | "spectator";
103 + }[]
104 + ) {}
105 +}
106 +
107 +/**
108 + * 클라 <- 서버
109 + * 라운드 시작시에 오직 drawer에게만 전송되는 메세지로, drawer가 선택할 수 있는 단어들입니다.
110 + */
111 +export class RoundWordSetMessage implements Message {
112 + readonly type = MessageType.ROUND_WORD_SET;
113 + constructor(public words: string[]) {}
114 +}
115 +
116 +/**
117 + * 클라 -> 서버
118 + * drawer가 단어를 선택하면 해당 메세지가 서버로 전송됩니다.
119 + * @param word RoundWordSetMessage에서 수신받은 단어 중 drawer가 선택한 단어입니다.
120 + */
121 +export class RoundChooseWordMessage implements Message {
122 + readonly type = MessageType.ROUND_CHOOSE_WORD;
123 + constructor(public word: string) {}
124 +}
125 +
126 +/**
127 + * 클라 <- 서버
128 + * drawer가 단어를 선택하였음을 알립니다.
129 + * @param length 정답 단어의 길이입니다.
130 + */
131 +export class RoundWordChosenMessage implements Message {
132 + readonly type = MessageType.ROUND_WORD_CHOSEN;
133 + constructor(public length: number) {}
134 +}
135 +
136 +/**
137 + * 클라 <- 서버
138 + * 서버가 클라이언트의 타이머를 동기화하기 위해 사용됩니다. 라운드가 시작하면 타이머는 초기화되며 기본 상태는 stopped입니다.
139 + * @param state 타이머의 동작, 정지 여부입니다.
140 + * @param time 타이머의 남은 시간입니다. 초 단위로 주어집니다.
141 + */
142 +export class RoundTimerMessage implements Message {
143 + readonly type = MessageType.ROUND_TIMER;
144 + constructor(public state: "started" | "stopped", public time: number) {}
145 +}
146 +
147 +/**
148 + * 클라 <- 서버
149 + * 라운드가 종료되었음을 알립니다.
150 + * @param answer 이번 라운드의 정답입니다.
151 + */
152 +export class RoundFinishMessage implements Message {
153 + readonly type = MessageType.ROUND_FINISH;
154 + constructor(public answer: string) {}
155 +}
156 +
157 +/**
158 + * 클라 <- 서버
159 + * 플레이어의 역할이 바뀌었음을 알립니다.
160 + * @param username 대상 유저의 username입니다.
161 + * @param role 대상 유저의 새로운 역할입니다.
162 + */
163 +export class RoundRoleMessage implements Message {
164 + readonly type = MessageType.ROUND_ROLE;
165 + constructor(
166 + public username: string,
167 + public role: "drawer" | "guesser" | "winner" | "spectator"
168 + ) {}
169 +}
170 +
171 +/**
172 + * 클라 <- 서버
173 + * 플레이어가 정답을 맞췄음을 알립니다.
174 + * @param answer 이번 라운드의 정답입니다.
175 + */
176 +export class AnswerAcceptedMessage implements Message {
177 + readonly type = MessageType.ANSWER_ACCEPTED;
178 + constructor(public answer: string) {}
179 +}
180 +
181 +/**
182 + * 클라 <- 서버
183 + * 게임이 종료되었음을 알립니다. 다시 준비 화면으로 돌아갑니다.
184 + */
185 +export class GameFinishMessage implements Message {
186 + readonly type = MessageType.GAME_FINISH;
187 + constructor() {}
188 +}
189 +
190 +/**
191 + * 클라 <-> 서버
192 + * 브러시 설정을 동기화합니다. drawer는 메세지를 서버에 보내고, 나머지 플레이어들은 서버에서 수신받습니다.
193 + * @param size 픽셀 단위의 브러시 지름입니다.
194 + * @param color 6자리 소문자 16진수로 표현된 브러시 색상입니다.
195 + * @param drawing 현재 브러시가 캔버스에 닿은 상태인지를 나타냅니다.
196 + */
197 +export class PaintBrushMessage implements Message {
198 + readonly type = MessageType.PAINT_BRUSH;
199 + constructor(
200 + public size: number,
201 + public color: string,
202 + public drawing: boolean
203 + ) {}
204 +}
205 +
206 +/**
207 + * 클라 <-> 서버
208 + * 브러시 위치를 동기화합니다. drawer는 메세지를 서버에 보내고, 나머지 플레이어들은 서버에서 수신받습니다.
209 + * 왼쪽 하단이 원점입니다.
210 + * @param x 픽셀 단위의 가로 위치입니다.
211 + * @param y 픽셀 단위의 세로 위치입니다.
212 + */
213 +export class PaintMoveMessage implements Message {
214 + readonly type = MessageType.PAINT_MOVE;
215 + constructor(public x: number, public y: number) {}
216 +}
217 +
88 export class MessageType { 218 export class MessageType {
89 static readonly LOGIN = "login"; 219 static readonly LOGIN = "login";
90 static readonly ROOM_LIST_REQUEST = "room_list_request"; 220 static readonly ROOM_LIST_REQUEST = "room_list_request";
...@@ -92,4 +222,15 @@ export class MessageType { ...@@ -92,4 +222,15 @@ export class MessageType {
92 static readonly ROOM_LEAVE = "room_leave"; 222 static readonly ROOM_LEAVE = "room_leave";
93 static readonly ROOM_USER_UPDATE = "room_user_update"; 223 static readonly ROOM_USER_UPDATE = "room_user_update";
94 static readonly ROOM_CHAT = "room_chat"; 224 static readonly ROOM_CHAT = "room_chat";
225 + static readonly ROUND_START = "round_start";
226 + static readonly ROUND_TIMER = "round_timer";
227 + static readonly ROUND_FINISH = "round_finish";
228 + static readonly ROUND_ROLE = "round_role";
229 + static readonly ANSWER_ACCEPTED = "answer_accepted";
230 + static readonly GAME_FINISH = "game_finish";
231 + static readonly ROUND_WORD_SET = "round_word_set";
232 + static readonly ROUND_CHOOSE_WORD = "round_choose_word";
233 + static readonly ROUND_WORD_CHOSEN = "round_word_chosen";
234 + static readonly PAINT_BRUSH = "paint_brush";
235 + static readonly PAINT_MOVE = "paint_move";
95 } 236 }
......