강동현

게임 로직 작성

...@@ -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
......
...@@ -21,3 +21,5 @@ export interface RoomInfo { ...@@ -21,3 +21,5 @@ export interface RoomInfo {
21 export interface UserData { 21 export interface UserData {
22 username: string; 22 username: string;
23 } 23 }
24 +
25 +export type Role = "drawer" | "guesser" | "winner" | "spectator";
......
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 }
......