Showing
3 changed files
with
295 additions
and
9 deletions
... | @@ -68,6 +68,7 @@ | ... | @@ -68,6 +68,7 @@ |
68 | 이 정보들을 가지고 캔버스를 칠하는 컴포넌트를 만들어서, 이를 `drawer`의 클라이언트에도 동일하게 사용하는 방식으로 구현하여 `drawer`와 다른 플레이어의 캔버스가 동일하게 보이도록 해야 할 것입니다. | 68 | 이 정보들을 가지고 캔버스를 칠하는 컴포넌트를 만들어서, 이를 `drawer`의 클라이언트에도 동일하게 사용하는 방식으로 구현하여 `drawer`와 다른 플레이어의 캔버스가 동일하게 보이도록 해야 할 것입니다. |
69 | 69 | ||
70 | 캔버스의 크기: 512x384 (4:3) (추후 변경 가능) | 70 | 캔버스의 크기: 512x384 (4:3) (추후 변경 가능) |
71 | +브러시 사이즈: 1 ~ 64px | ||
71 | 72 | ||
72 | ### guesser | 73 | ### guesser |
73 | 74 | ||
... | @@ -83,10 +84,12 @@ | ... | @@ -83,10 +84,12 @@ |
83 | 게임 도중 입장한 유저에게는 다음과 같은 메세지들이 모두 전송됩니다. | 84 | 게임 도중 입장한 유저에게는 다음과 같은 메세지들이 모두 전송됩니다. |
84 | 85 | ||
85 | 1. 준비 상태에서 자신이 방에 접속했을 때 전달 받는 모든 메세지들 | 86 | 1. 준비 상태에서 자신이 방에 접속했을 때 전달 받는 모든 메세지들 |
86 | -2. 현재 라운드에 대한 정보를 담은 `startRound` | 87 | +2. 현재 라운드 타이머와 동기화할 수 있는 `timer` |
87 | -3. 현재 라운드 타이머와 동기화할 수 있는 `timer` | 88 | +3. 현재 라운드에 대한 정보를 담은 `startRound` |
88 | -4. 마지막으로 서버상으로 기록된 브러시 정보를 담은 `setBrush` | 89 | +4. 현재 라운드가 종료되었고, 다음 라운드를 기다리고 있는 중이라면 이번 라운드의 답을 담은 `finishRound` |
89 | -5. 마지막으로 서버상으로 기록된 브러시 위치를 담은 `moveBrush` | 90 | +5. 마지막으로 서버상으로 기록된 브러시 정보를 담은 `setBrush` |
91 | +6. 마지막으로 서버상으로 기록된 브러시 위치를 담은 `moveBrush` | ||
92 | + // TODO: 중도 입장 유저에게는 비트맵을 전송하는 방식 고려해보기 | ||
90 | 93 | ||
91 | 다른 플레이어에게는 다음과 같은 메세지들이 모두 전송됩니다. | 94 | 다른 플레이어에게는 다음과 같은 메세지들이 모두 전송됩니다. |
92 | 95 | ... | ... |
1 | -import { roomChatHandler } from "../message/handler/roomChatHandler"; | 1 | +import { Role } from "../../common/dataType"; |
2 | +import { MessageHandler } from "../message/MessageHandler"; | ||
2 | import { Room } from "../room/Room"; | 3 | import { Room } from "../room/Room"; |
3 | import { User } from "../user/User"; | 4 | import { User } from "../user/User"; |
4 | import { Game } from "./Game"; | 5 | import { Game } from "./Game"; |
... | @@ -6,7 +7,37 @@ import { Game } from "./Game"; | ... | @@ -6,7 +7,37 @@ import { Game } from "./Game"; |
6 | export class WorldGuessingGame implements Game { | 7 | export class WorldGuessingGame implements Game { |
7 | room: Room; | 8 | room: Room; |
8 | maxRound: number; | 9 | maxRound: number; |
9 | - round: number; | 10 | + round: number = 0; |
11 | + roundState: "choosing" | "running" | "done" = "choosing"; | ||
12 | + roundDuration: number; | ||
13 | + readonly roundTerm: number = 5; // 다음 라운드 시작까지 기다리는 시간 | ||
14 | + wordCandidates: string[] = []; | ||
15 | + word: string = ""; | ||
16 | + timer: { | ||
17 | + startTimeMillis: number; | ||
18 | + timeLeftMillis: number; | ||
19 | + running: boolean; | ||
20 | + } = { startTimeMillis: 0, timeLeftMillis: 0, running: false }; | ||
21 | + timeoutTimerId?: NodeJS.Timeout; | ||
22 | + nextRoundTimerId?: NodeJS.Timeout; | ||
23 | + | ||
24 | + brush: { | ||
25 | + size: number; | ||
26 | + color: string; | ||
27 | + drawing: boolean; | ||
28 | + x: number; | ||
29 | + y: number; | ||
30 | + } = { | ||
31 | + size: 24, | ||
32 | + color: "000000", | ||
33 | + drawing: false, | ||
34 | + x: 0, | ||
35 | + y: 0, | ||
36 | + }; | ||
37 | + | ||
38 | + handler: MessageHandler; | ||
39 | + roles: Map<User, Role>; | ||
40 | + drawer?: User; | ||
10 | 41 | ||
11 | constructor(room: Room) { | 42 | constructor(room: Room) { |
12 | this.room = room; | 43 | this.room = room; |
... | @@ -16,14 +47,264 @@ export class WorldGuessingGame implements Game { | ... | @@ -16,14 +47,264 @@ export class WorldGuessingGame implements Game { |
16 | 47 | ||
17 | // TODO: 방장이 설정 | 48 | // TODO: 방장이 설정 |
18 | this.maxRound = 5; | 49 | this.maxRound = 5; |
19 | - this.round = 1; | 50 | + this.roundDuration = 60; |
51 | + | ||
52 | + this.handler = new MessageHandler({ | ||
53 | + chooseWord: (user, message) => { | ||
54 | + if (user !== this.drawer || this.roundState === "choosing") { | ||
55 | + return { ok: false }; | ||
56 | + } | ||
57 | + | ||
58 | + const chosen = message.word; | ||
59 | + if (this.wordCandidates.includes(chosen)) { | ||
60 | + this.wordSelected(chosen); | ||
61 | + return { ok: true }; | ||
62 | + } | ||
63 | + return { ok: false }; | ||
64 | + }, | ||
65 | + chat: (user, message) => { | ||
66 | + const text = message.message.trim(); | ||
67 | + if (this.roles.get(user) === "guesser" && text === this.word) { | ||
68 | + this.acceptAnswer(user); | ||
69 | + } else { | ||
70 | + this.room.sendChat(user, text); | ||
71 | + } | ||
72 | + return { ok: true }; | ||
73 | + }, | ||
74 | + setBrush: (user, message) => { | ||
75 | + if (user !== this.drawer || !/^[0-9a-f]{6}$/.test(message.color)) { | ||
76 | + return { ok: false }; | ||
77 | + } | ||
78 | + | ||
79 | + this.brush.size = Math.max(Math.min(message.size, 64), 1); | ||
80 | + this.brush.color = message.color; | ||
81 | + this.brush.drawing = message.drawing; | ||
82 | + | ||
83 | + this.room.broadcast( | ||
84 | + "setBrush", | ||
85 | + { | ||
86 | + size: this.brush.size, | ||
87 | + color: this.brush.color, | ||
88 | + drawing: this.brush.drawing, | ||
89 | + }, | ||
90 | + user | ||
91 | + ); | ||
92 | + | ||
93 | + return { ok: true }; | ||
94 | + }, | ||
95 | + moveBrush: (user, message) => { | ||
96 | + if (user !== this.drawer) { | ||
97 | + return { ok: false }; | ||
98 | + } | ||
99 | + | ||
100 | + this.brush.x = Math.max(Math.min(message.x, 1), 0); | ||
101 | + this.brush.y = Math.max(Math.min(message.y, 1), 0); | ||
102 | + | ||
103 | + this.room.broadcast( | ||
104 | + "moveBrush", | ||
105 | + { | ||
106 | + x: this.brush.x, | ||
107 | + y: this.brush.y, | ||
108 | + }, | ||
109 | + user | ||
110 | + ); | ||
111 | + | ||
112 | + return { ok: true }; | ||
113 | + }, | ||
114 | + }); | ||
115 | + | ||
116 | + this.roles = new Map<User, Role>(); | ||
117 | + | ||
118 | + this.startNextRound(); | ||
119 | + } | ||
120 | + | ||
121 | + private startNextRound(): void { | ||
122 | + this.roundState = "choosing"; | ||
123 | + this.round++; | ||
124 | + | ||
125 | + this.roles.clear(); | ||
126 | + | ||
127 | + this.drawer = this.pickDrawer(); | ||
128 | + this.room.users.forEach((user) => this.roles.set(user, "guesser")); | ||
129 | + this.roles.set(this.drawer, "drawer"); | ||
130 | + | ||
131 | + this.room.broadcast("startRound", { | ||
132 | + round: this.round, | ||
133 | + duration: this.roundDuration, | ||
134 | + roles: this.makeRoleArray(), | ||
135 | + }); | ||
136 | + | ||
137 | + this.wordCandidates = this.pickWords(); | ||
138 | + this.drawer.connection.send("wordSet", { words: this.wordCandidates }); | ||
139 | + } | ||
140 | + | ||
141 | + private wordSelected(word: string): void { | ||
142 | + this.word = word; | ||
143 | + this.roundState = "running"; | ||
144 | + | ||
145 | + this.room.broadcast("wordChosen", { length: word.length }); | ||
146 | + | ||
147 | + this.startTimer(this.roundDuration * 1000); | ||
148 | + | ||
149 | + this.timeoutTimerId = setTimeout( | ||
150 | + this.finishRound, | ||
151 | + this.roundDuration * 1000 | ||
152 | + ); | ||
153 | + } | ||
154 | + | ||
155 | + private finishRound(): void { | ||
156 | + if (this.timeoutTimerId) { | ||
157 | + clearTimeout(this.timeoutTimerId); | ||
158 | + this.timeoutTimerId = undefined; | ||
159 | + } | ||
160 | + | ||
161 | + this.roundState = "done"; | ||
162 | + | ||
163 | + this.stopTimer(); | ||
164 | + | ||
165 | + this.room.broadcast("finishRound", { answer: this.word }); | ||
166 | + | ||
167 | + this.prepareNextRound(); | ||
168 | + } | ||
169 | + | ||
170 | + private prepareNextRound(): void { | ||
171 | + this.nextRoundTimerId = setTimeout(() => { | ||
172 | + if (this.round == this.maxRound) { | ||
173 | + this.finishGame(); | ||
174 | + } else { | ||
175 | + this.startNextRound(); | ||
176 | + } | ||
177 | + }, this.roundTerm * 1000); | ||
178 | + } | ||
179 | + | ||
180 | + private finishGame(): void { | ||
181 | + this.room.broadcast("finishGame", {}); | ||
182 | + | ||
183 | + // TODO | ||
184 | + } | ||
185 | + | ||
186 | + private forceFinishGame() { | ||
187 | + if (this.timeoutTimerId) { | ||
188 | + clearTimeout(this.timeoutTimerId); | ||
189 | + } | ||
190 | + if (this.nextRoundTimerId) { | ||
191 | + clearTimeout(this.nextRoundTimerId); | ||
192 | + } | ||
193 | + this.room.broadcast("finishRound", { answer: this.word }); | ||
194 | + this.finishGame(); | ||
195 | + } | ||
196 | + | ||
197 | + private acceptAnswer(user: User): void { | ||
198 | + user.connection.send("answerAccepted", { answer: this.word }); | ||
199 | + this.changeRole(user, "winner"); | ||
200 | + } | ||
201 | + | ||
202 | + private pickDrawer(): User { | ||
203 | + const candidates = this.room.users.filter((user) => user !== this.drawer); | ||
204 | + return candidates[Math.floor(Math.random() * candidates.length)]; | ||
205 | + } | ||
206 | + | ||
207 | + private pickWords(): string[] { | ||
208 | + return ["장난감", "백화점", "파티"]; | ||
209 | + } | ||
210 | + | ||
211 | + private startTimer(timeLeftMillis: number): void { | ||
212 | + this.timer = { | ||
213 | + startTimeMillis: Date.now(), | ||
214 | + timeLeftMillis, | ||
215 | + running: true, | ||
216 | + }; | ||
217 | + } | ||
218 | + | ||
219 | + private stopTimer(): void { | ||
220 | + this.timer = { | ||
221 | + ...this.timer, | ||
222 | + running: false, | ||
223 | + }; | ||
224 | + this.room.users.forEach((user) => this.sendTimer(user)); | ||
225 | + } | ||
226 | + | ||
227 | + private sendTimer(user: User): void { | ||
228 | + user.connection.send("timer", { | ||
229 | + state: this.timer.running ? "started" : "stopped", | ||
230 | + time: Math.max( | ||
231 | + (this.timer.startTimeMillis + this.timer.timeLeftMillis - Date.now()) / | ||
232 | + 1000, | ||
233 | + 0 | ||
234 | + ), | ||
235 | + }); | ||
236 | + } | ||
237 | + | ||
238 | + private makeRoleArray(): { username: string; role: Role }[] { | ||
239 | + let roleArray: { | ||
240 | + username: string; | ||
241 | + role: Role; | ||
242 | + }[] = []; | ||
243 | + this.roles.forEach((role, user) => | ||
244 | + roleArray.push({ username: user.username, role: role }) | ||
245 | + ); | ||
246 | + return roleArray; | ||
247 | + } | ||
248 | + | ||
249 | + private changeRole(user: User, role: Role) { | ||
250 | + this.roles.set(user, role); | ||
251 | + this.room.broadcast("role", { username: user.username, role }); | ||
20 | } | 252 | } |
21 | 253 | ||
22 | join(user: User): void { | 254 | join(user: User): void { |
23 | - throw new Error("Method not implemented."); | 255 | + this.changeRole(user, "spectator"); |
256 | + this.sendTimer(user); | ||
257 | + user.connection.send("startRound", { | ||
258 | + round: this.round, | ||
259 | + duration: this.roundDuration, | ||
260 | + roles: this.makeRoleArray(), | ||
261 | + }); | ||
262 | + if (this.roundState === "done") { | ||
263 | + user.connection.send("finishRound", { | ||
264 | + answer: this.word, | ||
265 | + }); | ||
266 | + } | ||
267 | + user.connection.send("setBrush", { | ||
268 | + size: this.brush.size, | ||
269 | + color: this.brush.color, | ||
270 | + drawing: this.brush.drawing, | ||
271 | + }); | ||
272 | + user.connection.send("moveBrush", { | ||
273 | + x: this.brush.x, | ||
274 | + y: this.brush.y, | ||
275 | + }); | ||
24 | } | 276 | } |
25 | 277 | ||
26 | leave(user: User): void { | 278 | leave(user: User): void { |
27 | - throw new Error("Method not implemented."); | 279 | + if (this.room.users.length < 2) { |
280 | + this.forceFinishGame(); | ||
281 | + return; | ||
282 | + } | ||
283 | + | ||
284 | + this.roles.delete(user); | ||
285 | + | ||
286 | + if (user === this.drawer) { | ||
287 | + if (this.roundState === "choosing") { | ||
288 | + this.round--; // 이번 라운드를 다시 시작 | ||
289 | + this.startNextRound(); | ||
290 | + } else if (this.roundState === "running") { | ||
291 | + this.finishRound(); | ||
292 | + } | ||
293 | + } else { | ||
294 | + let guesserCount = 0; | ||
295 | + this.roles.forEach((role, user) => { | ||
296 | + if (role === "guesser") { | ||
297 | + guesserCount++; | ||
298 | + } | ||
299 | + }); | ||
300 | + if (guesserCount < 1) { | ||
301 | + if (this.roundState === "choosing") { | ||
302 | + this.round--; | ||
303 | + this.startNextRound(); | ||
304 | + } else if (this.roundState === "running") { | ||
305 | + this.finishRound(); | ||
306 | + } | ||
307 | + } | ||
308 | + } | ||
28 | } | 309 | } |
29 | } | 310 | } | ... | ... |
-
Please register or login to post a comment