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