강동현

Game 파일 이동 및 버그 수정

...@@ -8,6 +8,7 @@ import { ...@@ -8,6 +8,7 @@ import {
8 Record, 8 Record,
9 Union, 9 Union,
10 Static, 10 Static,
11 + Optional,
11 } from "runtypes"; 12 } from "runtypes";
12 import { 13 import {
13 Role, 14 Role,
...@@ -48,7 +49,12 @@ export class ServerInboundMessageRecordMap { ...@@ -48,7 +49,12 @@ export class ServerInboundMessageRecordMap {
48 }); 49 });
49 50
50 // 방장이 게임을 시작합니다. 51 // 방장이 게임을 시작합니다.
51 - startGame = Record({}); 52 + // TODO: 주의! 아래 필드는 디버그 용도로만 사용됩니다. 추후에 준비 화면에서 공개적으로 설정하는 것으로 구현해야 합니다.
53 + startGame = Record({
54 + maxRound: Optional(Number),
55 + roundDuration: Optional(Number),
56 + roundTerm: Optional(Number),
57 + });
52 58
53 // drawer가 단어를 선택합니다. 59 // drawer가 단어를 선택합니다.
54 chooseWord = Record({ 60 chooseWord = Record({
......
...@@ -54,6 +54,14 @@ export class Connection { ...@@ -54,6 +54,14 @@ export class Connection {
54 } 54 }
55 55
56 // Game > Room > User 순으로 전달 56 // Game > Room > User 순으로 전달
57 + if (this.user?.room?.game) {
58 + const response = this.user.room.game.handler.handle(
59 + type,
60 + this.user,
61 + message
62 + );
63 + if (response) return response;
64 + }
57 if (this.user?.room) { 65 if (this.user?.room) {
58 const response = this.user.room.handler.handle(type, this.user, message); 66 const response = this.user.room.handler.handle(type, this.user, message);
59 if (response) return response; 67 if (response) return response;
......
1 +import { Role } from "../../common/dataType";
2 +import { MessageHandler } from "../message/MessageHandler";
3 +import { Room } from "../room/Room";
1 import { User } from "../user/User"; 4 import { User } from "../user/User";
2 5
3 -export interface Game { 6 +export class Game {
4 - join(user: User): void; 7 + room: Room;
5 - leave(user: User): void; 8 + maxRound: number;
9 + round: number = 0;
10 + roundState: "choosing" | "running" | "done" = "choosing";
11 + roundDuration: number;
12 + readonly roundTerm: number = 5; // 다음 라운드 시작까지 기다리는 시간
13 + wordCandidates: string[] = [];
14 + word: string = "";
15 + timer: {
16 + startTimeMillis: number;
17 + timeLeftMillis: number;
18 + running: boolean;
19 + } = { startTimeMillis: 0, timeLeftMillis: 0, running: false };
20 + timeoutTimerId?: NodeJS.Timeout;
21 + nextRoundTimerId?: NodeJS.Timeout;
22 +
23 + brush: {
24 + size: number;
25 + color: string;
26 + drawing: boolean;
27 + x: number;
28 + y: number;
29 + } = {
30 + size: 24,
31 + color: "000000",
32 + drawing: false,
33 + x: 0,
34 + y: 0,
35 + };
36 +
37 + handler: MessageHandler;
38 + roles: Map<User, Role>;
39 + drawer?: User;
40 +
41 + constructor(
42 + room: Room,
43 + maxRound: number,
44 + roundDuration: number,
45 + roundTerm: number
46 + ) {
47 + this.room = room;
48 +
49 + // TODO: 방장이 설정
50 + this.maxRound = maxRound;
51 + this.roundDuration = roundDuration;
52 + this.roundTerm = roundTerm;
53 +
54 + this.handler = new MessageHandler({
55 + chooseWord: (user, message) => {
56 + if (user !== this.drawer || this.roundState !== "choosing") {
57 + return { ok: false };
58 + }
59 +
60 + const chosen = message.word;
61 + if (this.wordCandidates.includes(chosen)) {
62 + this.wordSelected(chosen);
63 + return { ok: true };
64 + }
65 + return { ok: false };
66 + },
67 + chat: (user, message) => {
68 + const text = message.message.trim();
69 + if (this.roles.get(user) === "guesser" && text === this.word) {
70 + this.acceptAnswer(user);
71 + } else {
72 + this.room.sendChat(user, text);
73 + }
74 + return { ok: true };
75 + },
76 + setBrush: (user, message) => {
77 + if (user !== this.drawer || !/^[0-9a-f]{6}$/.test(message.color)) {
78 + return { ok: false };
79 + }
80 +
81 + this.brush.size = Math.max(Math.min(message.size, 64), 1);
82 + this.brush.color = message.color;
83 + this.brush.drawing = message.drawing;
84 +
85 + this.room.broadcast(
86 + "setBrush",
87 + {
88 + size: this.brush.size,
89 + color: this.brush.color,
90 + drawing: this.brush.drawing,
91 + },
92 + user
93 + );
94 +
95 + return { ok: true };
96 + },
97 + moveBrush: (user, message) => {
98 + if (user !== this.drawer) {
99 + return { ok: false };
100 + }
101 +
102 + this.brush.x = Math.max(Math.min(message.x, 1), 0);
103 + this.brush.y = Math.max(Math.min(message.y, 1), 0);
104 +
105 + this.room.broadcast(
106 + "moveBrush",
107 + {
108 + x: this.brush.x,
109 + y: this.brush.y,
110 + },
111 + user
112 + );
113 +
114 + return { ok: true };
115 + },
116 + });
117 +
118 + this.roles = new Map<User, Role>();
119 +
120 + this.startNextRound();
121 + }
122 +
123 + private startNextRound(): void {
124 + this.roundState = "choosing";
125 + this.round++;
126 +
127 + this.roles.clear();
128 +
129 + this.drawer = this.pickDrawer();
130 + this.room.users.forEach((user) => this.roles.set(user, "guesser"));
131 + this.roles.set(this.drawer, "drawer");
132 +
133 + this.room.broadcast("startRound", {
134 + round: this.round,
135 + duration: this.roundDuration,
136 + roles: this.makeRoleArray(),
137 + });
138 +
139 + this.wordCandidates = this.pickWords();
140 + this.drawer.connection.send("wordSet", { words: this.wordCandidates });
141 + }
142 +
143 + private wordSelected(word: string): void {
144 + this.word = word;
145 + this.roundState = "running";
146 +
147 + this.room.broadcast("wordChosen", { length: word.length });
148 +
149 + this.startTimer(this.roundDuration * 1000);
150 +
151 + this.timeoutTimerId = setTimeout(
152 + () => this.finishRound(),
153 + this.roundDuration * 1000
154 + );
155 + }
156 +
157 + public finishRound(): void {
158 + if (this.timeoutTimerId) {
159 + clearTimeout(this.timeoutTimerId);
160 + this.timeoutTimerId = undefined;
161 + }
162 +
163 + this.roundState = "done";
164 +
165 + this.stopTimer();
166 +
167 + this.room.broadcast("finishRound", { answer: this.word });
168 +
169 + this.prepareNextRound();
170 + }
171 +
172 + private prepareNextRound(): void {
173 + this.nextRoundTimerId = setTimeout(() => {
174 + if (this.round == this.maxRound) {
175 + this.finishGame();
176 + } else {
177 + this.startNextRound();
178 + }
179 + }, this.roundTerm * 1000);
180 + }
181 +
182 + private finishGame(): void {
183 + this.room.broadcast("finishGame", {});
184 +
185 + this.room.finishGame();
186 + }
187 +
188 + private forceFinishGame() {
189 + if (this.timeoutTimerId) {
190 + clearTimeout(this.timeoutTimerId);
191 + }
192 + if (this.nextRoundTimerId) {
193 + clearTimeout(this.nextRoundTimerId);
194 + }
195 + this.room.broadcast("finishRound", { answer: this.word });
196 + this.finishGame();
197 + }
198 +
199 + private acceptAnswer(user: User): void {
200 + user.connection.send("answerAccepted", { answer: this.word });
201 + this.changeRole(user, "winner");
202 +
203 + let noGuesser = true;
204 + this.roles.forEach((role, user) => {
205 + if (role === "guesser") {
206 + noGuesser = false;
207 + }
208 + });
209 +
210 + if (noGuesser) {
211 + this.finishRound();
212 + }
213 + }
214 +
215 + private pickDrawer(): User {
216 + const candidates = this.room.users.filter((user) => user !== this.drawer);
217 + return candidates[Math.floor(Math.random() * candidates.length)];
218 + }
219 +
220 + private pickWords(): string[] {
221 + return ["장난감", "백화점", "파티"];
222 + }
223 +
224 + private startTimer(timeLeftMillis: number): void {
225 + this.timer = {
226 + startTimeMillis: Date.now(),
227 + timeLeftMillis,
228 + running: true,
229 + };
230 + this.room.users.forEach((user) => this.sendTimer(user));
231 + }
232 +
233 + private stopTimer(): void {
234 + this.timer = {
235 + ...this.timer,
236 + running: false,
237 + };
238 + this.room.users.forEach((user) => this.sendTimer(user));
239 + }
240 +
241 + private sendTimer(user: User): void {
242 + user.connection.send("timer", {
243 + state: this.timer.running ? "started" : "stopped",
244 + time: Math.max(
245 + (this.timer.startTimeMillis + this.timer.timeLeftMillis - Date.now()) /
246 + 1000,
247 + 0
248 + ),
249 + });
250 + }
251 +
252 + private makeRoleArray(): { username: string; role: Role }[] {
253 + let roleArray: {
254 + username: string;
255 + role: Role;
256 + }[] = [];
257 + this.roles.forEach((role, user) =>
258 + roleArray.push({ username: user.username, role: role })
259 + );
260 + return roleArray;
261 + }
262 +
263 + private changeRole(user: User, role: Role) {
264 + this.roles.set(user, role);
265 + this.room.broadcast("role", { username: user.username, role });
266 + }
267 +
268 + join(user: User): void {
269 + this.changeRole(user, "spectator");
270 + this.sendTimer(user);
271 + user.connection.send("startRound", {
272 + round: this.round,
273 + duration: this.roundDuration,
274 + roles: this.makeRoleArray(),
275 + });
276 + if (this.roundState === "done") {
277 + user.connection.send("finishRound", {
278 + answer: this.word,
279 + });
280 + }
281 + user.connection.send("setBrush", {
282 + size: this.brush.size,
283 + color: this.brush.color,
284 + drawing: this.brush.drawing,
285 + });
286 + user.connection.send("moveBrush", {
287 + x: this.brush.x,
288 + y: this.brush.y,
289 + });
290 + }
291 +
292 + leave(user: User): void {
293 + if (this.room.users.length < 2) {
294 + this.forceFinishGame();
295 + return;
296 + }
297 +
298 + this.roles.delete(user);
299 +
300 + if (user === this.drawer) {
301 + if (this.roundState === "choosing") {
302 + this.round--; // 이번 라운드를 다시 시작
303 + this.startNextRound();
304 + } else if (this.roundState === "running") {
305 + this.finishRound();
306 + }
307 + } else {
308 + let guesserCount = 0;
309 + this.roles.forEach((role, user) => {
310 + if (role === "guesser") {
311 + guesserCount++;
312 + }
313 + });
314 + if (guesserCount < 1) {
315 + if (this.roundState === "choosing") {
316 + this.round--;
317 + this.startNextRound();
318 + } else if (this.roundState === "running") {
319 + this.finishRound();
320 + }
321 + }
322 + }
323 + }
6 } 324 }
......
1 -import { Role } from "../../common/dataType";
2 -import { MessageHandler } from "../message/MessageHandler";
3 -import { Room } from "../room/Room";
4 -import { User } from "../user/User";
5 -import { Game } from "./Game";
6 -
7 -export class WorldGuessingGame implements Game {
8 - room: Room;
9 - maxRound: 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;
41 -
42 - constructor(room: Room) {
43 - this.room = room;
44 -
45 - // TODO: 방장이 설정
46 - this.maxRound = 5;
47 - this.roundDuration = 60;
48 -
49 - this.handler = new MessageHandler({
50 - chooseWord: (user, message) => {
51 - if (user !== this.drawer || this.roundState === "choosing") {
52 - return { ok: false };
53 - }
54 -
55 - const chosen = message.word;
56 - if (this.wordCandidates.includes(chosen)) {
57 - this.wordSelected(chosen);
58 - return { ok: true };
59 - }
60 - return { ok: false };
61 - },
62 - chat: (user, message) => {
63 - const text = message.message.trim();
64 - if (this.roles.get(user) === "guesser" && text === this.word) {
65 - this.acceptAnswer(user);
66 - } else {
67 - this.room.sendChat(user, text);
68 - }
69 - return { ok: true };
70 - },
71 - setBrush: (user, message) => {
72 - if (user !== this.drawer || !/^[0-9a-f]{6}$/.test(message.color)) {
73 - return { ok: false };
74 - }
75 -
76 - this.brush.size = Math.max(Math.min(message.size, 64), 1);
77 - this.brush.color = message.color;
78 - this.brush.drawing = message.drawing;
79 -
80 - this.room.broadcast(
81 - "setBrush",
82 - {
83 - size: this.brush.size,
84 - color: this.brush.color,
85 - drawing: this.brush.drawing,
86 - },
87 - user
88 - );
89 -
90 - return { ok: true };
91 - },
92 - moveBrush: (user, message) => {
93 - if (user !== this.drawer) {
94 - return { ok: false };
95 - }
96 -
97 - this.brush.x = Math.max(Math.min(message.x, 1), 0);
98 - this.brush.y = Math.max(Math.min(message.y, 1), 0);
99 -
100 - this.room.broadcast(
101 - "moveBrush",
102 - {
103 - x: this.brush.x,
104 - y: this.brush.y,
105 - },
106 - user
107 - );
108 -
109 - return { ok: true };
110 - },
111 - });
112 -
113 - this.roles = new Map<User, Role>();
114 -
115 - this.startNextRound();
116 - }
117 -
118 - private startNextRound(): void {
119 - this.roundState = "choosing";
120 - this.round++;
121 -
122 - this.roles.clear();
123 -
124 - this.drawer = this.pickDrawer();
125 - this.room.users.forEach((user) => this.roles.set(user, "guesser"));
126 - this.roles.set(this.drawer, "drawer");
127 -
128 - this.room.broadcast("startRound", {
129 - round: this.round,
130 - duration: this.roundDuration,
131 - roles: this.makeRoleArray(),
132 - });
133 -
134 - this.wordCandidates = this.pickWords();
135 - this.drawer.connection.send("wordSet", { words: this.wordCandidates });
136 - }
137 -
138 - private wordSelected(word: string): void {
139 - this.word = word;
140 - this.roundState = "running";
141 -
142 - this.room.broadcast("wordChosen", { length: word.length });
143 -
144 - this.startTimer(this.roundDuration * 1000);
145 -
146 - this.timeoutTimerId = setTimeout(
147 - this.finishRound,
148 - this.roundDuration * 1000
149 - );
150 - }
151 -
152 - private finishRound(): void {
153 - if (this.timeoutTimerId) {
154 - clearTimeout(this.timeoutTimerId);
155 - this.timeoutTimerId = undefined;
156 - }
157 -
158 - this.roundState = "done";
159 -
160 - this.stopTimer();
161 -
162 - this.room.broadcast("finishRound", { answer: this.word });
163 -
164 - this.prepareNextRound();
165 - }
166 -
167 - private prepareNextRound(): void {
168 - this.nextRoundTimerId = setTimeout(() => {
169 - if (this.round == this.maxRound) {
170 - this.finishGame();
171 - } else {
172 - this.startNextRound();
173 - }
174 - }, this.roundTerm * 1000);
175 - }
176 -
177 - private finishGame(): void {
178 - this.room.broadcast("finishGame", {});
179 -
180 - this.room.finishGame();
181 - }
182 -
183 - private forceFinishGame() {
184 - if (this.timeoutTimerId) {
185 - clearTimeout(this.timeoutTimerId);
186 - }
187 - if (this.nextRoundTimerId) {
188 - clearTimeout(this.nextRoundTimerId);
189 - }
190 - this.room.broadcast("finishRound", { answer: this.word });
191 - this.finishGame();
192 - }
193 -
194 - private acceptAnswer(user: User): void {
195 - user.connection.send("answerAccepted", { answer: this.word });
196 - this.changeRole(user, "winner");
197 - }
198 -
199 - private pickDrawer(): User {
200 - const candidates = this.room.users.filter((user) => user !== this.drawer);
201 - return candidates[Math.floor(Math.random() * candidates.length)];
202 - }
203 -
204 - private pickWords(): string[] {
205 - return ["장난감", "백화점", "파티"];
206 - }
207 -
208 - private startTimer(timeLeftMillis: number): void {
209 - this.timer = {
210 - startTimeMillis: Date.now(),
211 - timeLeftMillis,
212 - running: true,
213 - };
214 - }
215 -
216 - private stopTimer(): void {
217 - this.timer = {
218 - ...this.timer,
219 - running: false,
220 - };
221 - this.room.users.forEach((user) => this.sendTimer(user));
222 - }
223 -
224 - private sendTimer(user: User): void {
225 - user.connection.send("timer", {
226 - state: this.timer.running ? "started" : "stopped",
227 - time: Math.max(
228 - (this.timer.startTimeMillis + this.timer.timeLeftMillis - Date.now()) /
229 - 1000,
230 - 0
231 - ),
232 - });
233 - }
234 -
235 - private makeRoleArray(): { username: string; role: Role }[] {
236 - let roleArray: {
237 - username: string;
238 - role: Role;
239 - }[] = [];
240 - this.roles.forEach((role, user) =>
241 - roleArray.push({ username: user.username, role: role })
242 - );
243 - return roleArray;
244 - }
245 -
246 - private changeRole(user: User, role: Role) {
247 - this.roles.set(user, role);
248 - this.room.broadcast("role", { username: user.username, role });
249 - }
250 -
251 - join(user: User): void {
252 - this.changeRole(user, "spectator");
253 - this.sendTimer(user);
254 - user.connection.send("startRound", {
255 - round: this.round,
256 - duration: this.roundDuration,
257 - roles: this.makeRoleArray(),
258 - });
259 - if (this.roundState === "done") {
260 - user.connection.send("finishRound", {
261 - answer: this.word,
262 - });
263 - }
264 - user.connection.send("setBrush", {
265 - size: this.brush.size,
266 - color: this.brush.color,
267 - drawing: this.brush.drawing,
268 - });
269 - user.connection.send("moveBrush", {
270 - x: this.brush.x,
271 - y: this.brush.y,
272 - });
273 - }
274 -
275 - leave(user: User): void {
276 - if (this.room.users.length < 2) {
277 - this.forceFinishGame();
278 - return;
279 - }
280 -
281 - this.roles.delete(user);
282 -
283 - if (user === this.drawer) {
284 - if (this.roundState === "choosing") {
285 - this.round--; // 이번 라운드를 다시 시작
286 - this.startNextRound();
287 - } else if (this.roundState === "running") {
288 - this.finishRound();
289 - }
290 - } else {
291 - let guesserCount = 0;
292 - this.roles.forEach((role, user) => {
293 - if (role === "guesser") {
294 - guesserCount++;
295 - }
296 - });
297 - if (guesserCount < 1) {
298 - if (this.roundState === "choosing") {
299 - this.round--;
300 - this.startNextRound();
301 - } else if (this.roundState === "running") {
302 - this.finishRound();
303 - }
304 - }
305 - }
306 - }
307 -}
...@@ -16,7 +16,7 @@ ...@@ -16,7 +16,7 @@
16 }, 16 },
17 "scripts": { 17 "scripts": {
18 "start": "nodemon index.ts", 18 "start": "nodemon index.ts",
19 - "test": "nyc mocha -r ts-node/register ./**/*.test.ts", 19 + "test": "nyc mocha -r ts-node/register --timeout 8000 ./**/*.test.ts",
20 "build": "tsc -b -v" 20 "build": "tsc -b -v"
21 }, 21 },
22 "devDependencies": { 22 "devDependencies": {
......
...@@ -10,7 +10,6 @@ import { ...@@ -10,7 +10,6 @@ import {
10 import { RoomDescription, RoomInfo, UserData } from "../../common/dataType"; 10 import { RoomDescription, RoomInfo, UserData } from "../../common/dataType";
11 import { RoomManager } from "./RoomManager"; 11 import { RoomManager } from "./RoomManager";
12 import { Game } from "../game/Game"; 12 import { Game } from "../game/Game";
13 -import { WorldGuessingGame } from "../game/WordGuessingGame";
14 13
15 export class Room { 14 export class Room {
16 public readonly uuid: string; 15 public readonly uuid: string;
...@@ -72,7 +71,23 @@ export class Room { ...@@ -72,7 +71,23 @@ export class Room {
72 if (!result.ok) { 71 if (!result.ok) {
73 return result; 72 return result;
74 } 73 }
75 - this.startGame(); 74 +
75 + // TODO: 방장이 따로 메세지를 보내 설정할 수 있도록 수정해주세요.
76 + const settings = message;
77 + if (!settings.maxRound) {
78 + settings.maxRound = 5;
79 + }
80 + if (!settings.roundDuration) {
81 + settings.roundDuration = 60;
82 + }
83 + if (!settings.roundTerm) {
84 + settings.roundTerm = 5;
85 + }
86 + this.startGame(
87 + settings.maxRound,
88 + settings.roundDuration,
89 + settings.roundTerm
90 + );
76 return { ok: true }; 91 return { ok: true };
77 }, 92 },
78 }); 93 });
...@@ -179,8 +194,12 @@ export class Room { ...@@ -179,8 +194,12 @@ export class Room {
179 return { ok: true }; 194 return { ok: true };
180 } 195 }
181 196
182 - private startGame(): void { 197 + private startGame(
183 - this.game = new WorldGuessingGame(this); 198 + maxRound: number,
199 + roundDuration: number,
200 + roundTerm: number
201 + ): void {
202 + this.game = new Game(this, maxRound, roundDuration, roundTerm);
184 } 203 }
185 204
186 public finishGame(): void { 205 public finishGame(): void {
......