Merge branch 'develop' of http://khuhub.khu.ac.kr/2020105578/nodejs-game into feature/room
Showing
20 changed files
with
1013 additions
and
325 deletions
... | @@ -30,22 +30,83 @@ | ... | @@ -30,22 +30,83 @@ |
30 | - `updated`: 기존 유저의 정보가 업데이트되었습니다. 해당 유저의 방장, 준비 여부가 변경되면 수신됩니다. | 30 | - `updated`: 기존 유저의 정보가 업데이트되었습니다. 해당 유저의 방장, 준비 여부가 변경되면 수신됩니다. |
31 | - `removed`: 유저가 방에서 퇴장하였습니다. | 31 | - `removed`: 유저가 방에서 퇴장하였습니다. |
32 | 32 | ||
33 | -유저가 채팅을 입력했다면 `chat`을 보내면 됩니다. 다른 사람이 채팅을 입력한 경우에도 `chat`이 수신됩니다(`ServerInboundMessage<"chat">`과 `ServerOutboundMessage<"chat">`이 다르게 정의되었다는 점에 유의하세요). 자신이 보낸 채팅에 대해서는 수신되지 않으므로 이 경우 오프라인으로 메세지를 추가하여야 합니다. | 33 | +유저가 채팅을 입력했다면 `chat`을 보내면 됩니다. 다른 사람이 채팅을 입력한 경우에도 `chat`이 수신됩니다(`ServerInboundMessage<"chat">`과 `ServerOutboundMessage<"chat">`이 다르게 정의되었다는 점에 유의하세요). 자신이 보낸 채팅에 대해서는 수신되지 않으므로 이 경우 오프라인으로 메세지를 추가하여야 합니다. 채팅 문자열의 양 끝 공백을 제거했을 때 문자열이 빈 문자열이거나, 채팅 문자열의 길이가 300을 초과하는 경우 요청이 실패 처리됩니다. |
34 | 34 | ||
35 | 방을 나가고 싶으면 `leaveRoom`을 보내면 됩니다. | 35 | 방을 나가고 싶으면 `leaveRoom`을 보내면 됩니다. |
36 | 36 | ||
37 | +### 방장 | ||
38 | + | ||
39 | +방을 생성한 유저가 방장 권한을 가지게 됩니다. 방장 여부는 `joinRoom`의 반환값 또는 `updateRoomUser`을 통해 수신됩니다. 방장은 바뀔 수 있음에 유의하세요. 예를 들어 원래 방장이 방에서 나가는 경우, 다른 랜덤한 유저에게 방장 권한이 넘어갑니다. 이 경우 `updateRoomUser`가 수신됩니다. | ||
40 | + | ||
41 | +방장이 원하는 유저에게 권한을 넘길 수 있는 기능은 아직 구현되지 않았습니다. | ||
42 | + | ||
37 | ## 게임 | 43 | ## 게임 |
38 | 44 | ||
45 | +### 요약 | ||
46 | + | ||
47 | +#### 게임 전 | ||
48 | + | ||
49 | +1. 방장을 제외한 모든 유저가 `ready`를 보내 준비한다. | ||
50 | +2. 방장이 `startRound`를 보내 게임 시작을 요청한다. | ||
51 | + | ||
52 | +#### 라운드 1 - 단어 선택 시간 | ||
53 | + | ||
54 | +(서버는 방장의 게임 시작 요청이 성공했을 경우 진입) | ||
55 | +(클라이언트는 `startRound`를 수신받은 경우 진입) | ||
56 | + | ||
57 | +1. 모든 유저는 `startRound`를 수신받고, 게임 화면으로 전환된다. 해당 메세지에는 모든 유저의 역할이 포함되어 있다. 이들 중 특별히 `drawer` 역할을 배정받았다면, 캔버스에 그릴 수 있도록 준비한다. | ||
58 | +2. 만약 본인이 `drawer`라면, `wordSet` 메세지를 수신받게 되는데 여기에는 3가지 선택할 수 있는 단어가 포함되어 있다. 팝업을 띄워서 유저가 단어를 선택한다. | ||
59 | +3. 만약 본인이 `drawer`가 아니라면, 아직 라운드의 시간이 흘러가지 않고 대기한다. | ||
60 | +4. `drawer`는 `chooseWord`를 통해 자신이 선택하고자 하는 단어를 서버에 보낸다. | ||
61 | + | ||
62 | +#### 라운드 1 - 라운드 진행 (60초) | ||
63 | + | ||
64 | +(서버는 `drawer`가 `chooseWord`를 보낸 경우 진입) | ||
65 | +(클라이언트는 `wordChosen`을 수신받은 경우 진입) | ||
66 | + | ||
67 | +1. 모든 유저는 `wordChosen`을 수신받는데, 여기에는 정답 단어의 글자 수 만이 포함되어 있다. 따라서 정답 단어를 밑줄 개수만으로 표시한다. | ||
68 | +2. 모든 유저는 라운드의 타이머가 시작되었다는 `timer` 메세지를 수신받는다. 이때 타이머를 동작하고 남은 시간을 동기화한다. | ||
69 | +3. `guesser`들은 단어를 채팅에 쳐서 맞춰볼 수 있다. | ||
70 | +4. `drawer`는 캔버스에 그림을 그릴 수 있다. | ||
71 | +5. 만약 `guesser`가 채팅으로 정답을 보냈다면 해당 유저는 `answerAccepted` 메세지를 수신받게 되고 여기에 이번 라운드의 정답이 포함되어 있다. 그리고 `role`을 통해 해당 유저의 역할이 `winner`로 변경된다. | ||
72 | + | ||
73 | +#### 라운드 1 - 라운드 종료 및 다음 라운드 시작 대기 (5초) | ||
74 | + | ||
75 | +(서버는 남은 시간이 0으로 떨어지거나, `drawer`가 퇴장하거나, 모두가 답을 맞춰 남은 `guesser`가 0명이 된 경우 진입) | ||
76 | +(클라이언트는 `finishRound`를 수신받은 경우 진입) | ||
77 | + | ||
78 | +1. 모든 유저는 `finishRound`를 통해 이번 라운드의 정답을 알게 된다. | ||
79 | + | ||
80 | +#### 라운드 2 - 단어 선택 시간 | ||
81 | + | ||
82 | +(위와 동일하므로 생략) | ||
83 | + | ||
84 | +...(생략)... | ||
85 | + | ||
86 | +#### 라운드 5 - 라운드 종료 (5초) | ||
87 | + | ||
88 | +(위와 동일하므로 생략) | ||
89 | + | ||
90 | +#### 게임 종료 | ||
91 | + | ||
92 | +(서버는 다음 라운드가 없으면 진입, 게임 도중 인원이 2명 미만이 되는 경우 즉시 진입) | ||
93 | +(클라이언트는 `finishGame`을 수신받은 경우 진입) | ||
94 | + | ||
95 | +1. 방에 접속 중인 모든 유저는 `finishGame`를 수신받는다. 이 경우, 게임이 종료되었으므로 게임 화면에서 다시 준비 화면으로 전환된다. | ||
96 | + | ||
39 | ### 준비 | 97 | ### 준비 |
40 | 98 | ||
41 | -방장을 제외한 모든 플레이어는 준비를 해야 게임이 시작될 수 있습니다. 서버에 `ready` 메세지를 보내서 준비 상태를 설정할 수 있습니다. 준비 상태로 설정하려면 `ready` 속성을 참, 그렇지 않으면 거짓으로 담아 보내야 합니다. 누군가 `ready`를 하면 `updateRoomUser`를 통해 해당 유저의 준비 상태가 변경됩니다. 방장에게는 준비할 수 있는 버튼 대신에 게임을 시작할 수 있는 버튼이 주어집니다. 모든 플레이어가 준비해야만 버튼이 활성화 되어야 합니다. 방장이 게임 시작 버튼을 누르면 서버에 `startGame`가 전송됩니다. 만약 게임 시작에 실패하면 Response의 `reason`값으로 실패 사유가 전달되므로 이를 유저에게 보여줄 수도 있습니다. 성공적으로 게임이 시작되면 모든 유저에게 `startRound`가 전달됩니다. | 99 | +방장을 제외한 모든 플레이어는 준비를 해야 게임이 시작될 수 있습니다. 서버에 `ready` 메세지를 보내서 준비 상태를 설정할 수 있습니다. 준비 상태로 설정하려면 `ready` 속성을 참, 그렇지 않으면 거짓으로 담아 보내야 합니다. 누군가 `ready`를 하면 `updateRoomUser`를 통해 해당 유저의 준비 상태가 변경됩니다. 방장에게는 준비할 수 있는 버튼 대신에 게임을 시작할 수 있는 버튼이 주어집니다. 방에 2명 이상의 인원이 접속한 상태에서, 모든 플레이어가 준비해야만 버튼이 활성화 되어야 합니다. 방장이 게임 시작 버튼을 누르면 서버에 `startGame`가 전송됩니다. 만약 게임 시작에 실패하면 Response의 `reason`값으로 실패 사유가 전달되므로 이를 유저에게 보여줄 수도 있습니다. 성공적으로 게임이 시작되면 모든 유저에게 `startRound`가 전달됩니다. |
42 | 100 | ||
43 | ### 라운드 진행 | 101 | ### 라운드 진행 |
44 | 102 | ||
45 | -준비 화면에서 라운드가 시작되면 `startRound`가 수신됩니다. `round`는 현재 라운드 넘버 (1부터 시작), `duration`은 현재 라운드의 길이를 초 단위로 나타냅니다. `roles`는 각 플레이어가 이번 라운드에서 맡게 된 역할입니다(후술). 항상 라운드가 시작되면 타이머를 라운드의 길이로 맞춘 뒤 타이머를 정지해주세요. 이때 그림을 그리는 사람이 단어를 선택하게 됩니다. 단어 선택이 끝나면 타이머의 시간이 흐르게 됩니다. | 103 | +준비 화면에서 라운드가 시작되면 `startRound`가 수신됩니다. 즉, `startRound`를 수신하면 게임 화면으로 전환되어야 합니다. `round`는 현재 라운드 넘버 (1부터 시작), `duration`은 현재 라운드의 길이를 초 단위로 나타냅니다. `roles`는 각 플레이어가 이번 라운드에서 맡게 된 역할입니다(후술). 이제 그림을 그리는 사람이 단어를 선택하게 됩니다. 단어 선택이 끝나면 서버에서 `timer` 메세지를 수신받고 타이머의 시간이 흐르게 됩니다. |
46 | 서버는 클라이언트 타이머의 상태를 `timer`를 보내서 동기화합니다. `state`가 `started`이면 메세지를 수신한 즉시 타이머를 동작시키고, `stopped`이면 타이머를 일시 정지합니다. 이때 `time`에 남은 시간이 초 단위로 포함되므로, 항상 이 메세지를 수신할 때마다 타이머의 남은 시간을 `time`값으로 동기화해주세요. 일반적으로 이 메세지는 단어 선택이 완료되어 라운드의 시간이 흐르기 시작하는 시점과 라운드가 종료되는 시점에 전송됩니다. 라운드가 종료되면 `state: stopped`인 `timer`가 수신됩니다. | 104 | 서버는 클라이언트 타이머의 상태를 `timer`를 보내서 동기화합니다. `state`가 `started`이면 메세지를 수신한 즉시 타이머를 동작시키고, `stopped`이면 타이머를 일시 정지합니다. 이때 `time`에 남은 시간이 초 단위로 포함되므로, 항상 이 메세지를 수신할 때마다 타이머의 남은 시간을 `time`값으로 동기화해주세요. 일반적으로 이 메세지는 단어 선택이 완료되어 라운드의 시간이 흐르기 시작하는 시점과 라운드가 종료되는 시점에 전송됩니다. 라운드가 종료되면 `state: stopped`인 `timer`가 수신됩니다. |
47 | 모든 플레이어가 단어를 맞추거나, 타이머의 시간이 0으로 떨어지면 라운드가 종료되면서 `finishRound`가 수신됩니다. 이 메세지는 이번 라운드의 정답을 포함하고 있습니다. 만약 진행할 라운드가 더 남았다면 몇 초 뒤에 다시 `startRound`가 수신될 것입니다. 그러나 이번 라운드가 마지막이었다면 `finishGame`가 수신됩니다. 이는 게임이 정상적으로 종료되었다는 의미이며, 다시 준비 화면으로 전환해주시면 됩니다. | 105 | 모든 플레이어가 단어를 맞추거나, 타이머의 시간이 0으로 떨어지면 라운드가 종료되면서 `finishRound`가 수신됩니다. 이 메세지는 이번 라운드의 정답을 포함하고 있습니다. 만약 진행할 라운드가 더 남았다면 몇 초 뒤에 다시 `startRound`가 수신될 것입니다. 그러나 이번 라운드가 마지막이었다면 `finishGame`가 수신됩니다. 이는 게임이 정상적으로 종료되었다는 의미이며, 다시 준비 화면으로 전환해주시면 됩니다. |
48 | 106 | ||
107 | +예외적인 케이스로, 이전 라운드가 비정상적으로 종료되었을 때 `finishRound`를 수신받지 않고 `startRound`를 수신받게 될 수 있습니다. 이때 `startRound`의 `round` 넘버가 이전 라운드와 동일한 값으로 수신받게 될 수도 있습니다. 예를 들면 `drawer`가 단어를 선택하지 않고 방에서 나가는 경우 해당 상황이 발생하게 됩니다. | ||
108 | +또한 라운드를 진행 도중 누군가 퇴장하여 인원이 모자르게 된 경우, 즉시 `finishGame`을 수신받고 게임이 종료될 수 있습니다. | ||
109 | + | ||
49 | ### 역할 | 110 | ### 역할 |
50 | 111 | ||
51 | 가능한 역할은 `drawer`, `guesser`, `winner`, `spectator`로 구분됩니다. 이는 `startRound`와 함께 수신됩니다. 만약 라운드 진행 중에 역할이 바뀌게 된다면 `role`가 수신됩니다. 이는 단순히 플레이어 목록 UI를 업데이트 하기 위해서 사용되며, 따로 고려할 게임 로직은 없습니다. | 112 | 가능한 역할은 `drawer`, `guesser`, `winner`, `spectator`로 구분됩니다. 이는 `startRound`와 함께 수신됩니다. 만약 라운드 진행 중에 역할이 바뀌게 된다면 `role`가 수신됩니다. 이는 단순히 플레이어 목록 UI를 업데이트 하기 위해서 사용되며, 따로 고려할 게임 로직은 없습니다. |
... | @@ -78,6 +139,7 @@ | ... | @@ -78,6 +139,7 @@ |
78 | 139 | ||
79 | `guesser`는 정답을 채팅에 입력할 수 있습니다. 만약 정답이라면 채팅이 서버에서 무시되고 역할이 `winner`로 변경되는 `role`이 수신되고, 정답을 담고 있는 `answerAccepted`가 수신됩니다. | 140 | `guesser`는 정답을 채팅에 입력할 수 있습니다. 만약 정답이라면 채팅이 서버에서 무시되고 역할이 `winner`로 변경되는 `role`이 수신되고, 정답을 담고 있는 `answerAccepted`가 수신됩니다. |
80 | 만약 답을 맞추지 못했다면 일반 채팅으로 전달됩니다. | 141 | 만약 답을 맞추지 못했다면 일반 채팅으로 전달됩니다. |
142 | +시간이 지나 라운드가 종료되고 다음 라운드를 기다리는 도중 답을 채팅에 입력하면 이는 무시되어 정답 처리되지 않습니다. | ||
81 | 143 | ||
82 | ### winner, spectator | 144 | ### winner, spectator |
83 | 145 | ... | ... |
1 | <h1 align="center"> | 1 | <h1 align="center"> |
2 | 스케치퀴즈 | 2 | 스케치퀴즈 |
3 | </h1> | 3 | </h1> |
4 | +<p align="center"> | ||
5 | + <a href="http://khuhub.khu.ac.kr/2020105578/nodejs-game/commits/develop"><img alt="build status" src="http://khuhub.khu.ac.kr/2020105578/nodejs-game/badges/develop/build.svg" /></a> | ||
6 | + <a href="http://khuhub.khu.ac.kr/2020105578/nodejs-game/commits/develop"><img alt="coverage report" src="http://khuhub.khu.ac.kr/2020105578/nodejs-game/badges/develop/coverage.svg" /></a> | ||
7 | +</p> | ... | ... |
... | @@ -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, |
... | @@ -47,6 +48,14 @@ export class ServerInboundMessageRecordMap { | ... | @@ -47,6 +48,14 @@ export class ServerInboundMessageRecordMap { |
47 | ready: Boolean, | 48 | ready: Boolean, |
48 | }); | 49 | }); |
49 | 50 | ||
51 | + // 방장이 게임을 시작합니다. | ||
52 | + // TODO: 주의! 아래 필드는 디버그 용도로만 사용됩니다. 추후에 준비 화면에서 공개적으로 설정하는 것으로 구현해야 합니다. | ||
53 | + startGame = Record({ | ||
54 | + maxRound: Optional(Number), | ||
55 | + roundDuration: Optional(Number), | ||
56 | + roundTerm: Optional(Number), | ||
57 | + }); | ||
58 | + | ||
50 | // drawer가 단어를 선택합니다. | 59 | // drawer가 단어를 선택합니다. |
51 | chooseWord = Record({ | 60 | chooseWord = Record({ |
52 | word: String, | 61 | word: String, | ... | ... |
... | @@ -25,6 +25,7 @@ export class Connection { | ... | @@ -25,6 +25,7 @@ export class Connection { |
25 | this.socket = socket; | 25 | this.socket = socket; |
26 | this.roomManager = roomManager; | 26 | this.roomManager = roomManager; |
27 | socket.setHandler((raw) => this.handleRaw(raw)); | 27 | socket.setHandler((raw) => this.handleRaw(raw)); |
28 | + socket.setDisconnectHandler(() => this.handleDisconnect()); | ||
28 | } | 29 | } |
29 | 30 | ||
30 | public send<T extends ServerOutboundMessageKey>( | 31 | public send<T extends ServerOutboundMessageKey>( |
... | @@ -54,6 +55,14 @@ export class Connection { | ... | @@ -54,6 +55,14 @@ export class Connection { |
54 | } | 55 | } |
55 | 56 | ||
56 | // Game > Room > User 순으로 전달 | 57 | // Game > Room > User 순으로 전달 |
58 | + if (this.user?.room?.game) { | ||
59 | + const response = this.user.room.game.handler.handle( | ||
60 | + type, | ||
61 | + this.user, | ||
62 | + message | ||
63 | + ); | ||
64 | + if (response) return response; | ||
65 | + } | ||
57 | if (this.user?.room) { | 66 | if (this.user?.room) { |
58 | const response = this.user.room.handler.handle(type, this.user, message); | 67 | const response = this.user.room.handler.handle(type, this.user, message); |
59 | if (response) return response; | 68 | if (response) return response; |
... | @@ -73,4 +82,8 @@ export class Connection { | ... | @@ -73,4 +82,8 @@ export class Connection { |
73 | 82 | ||
74 | return { ok: true }; | 83 | return { ok: true }; |
75 | } | 84 | } |
85 | + | ||
86 | + public handleDisconnect(): void { | ||
87 | + this.user?.disconnected(); | ||
88 | + } | ||
76 | } | 89 | } | ... | ... |
... | @@ -3,6 +3,7 @@ import { RawMessage, ServerResponse } from "../../common"; | ... | @@ -3,6 +3,7 @@ import { RawMessage, ServerResponse } from "../../common"; |
3 | 3 | ||
4 | export interface SocketWrapper { | 4 | export interface SocketWrapper { |
5 | setHandler: (listener: (raw: RawMessage) => ServerResponse<any>) => void; | 5 | setHandler: (listener: (raw: RawMessage) => ServerResponse<any>) => void; |
6 | + setDisconnectHandler: (listener: () => void) => void; | ||
6 | send: (raw: RawMessage) => void; | 7 | send: (raw: RawMessage) => void; |
7 | } | 8 | } |
8 | 9 | ||
... | @@ -19,6 +20,12 @@ export class SocketIoWrapper implements SocketWrapper { | ... | @@ -19,6 +20,12 @@ export class SocketIoWrapper implements SocketWrapper { |
19 | }); | 20 | }); |
20 | } | 21 | } |
21 | 22 | ||
23 | + public setDisconnectHandler(listener: () => void) { | ||
24 | + this.socketIo.on("disconnect", () => { | ||
25 | + listener(); | ||
26 | + }); | ||
27 | + } | ||
28 | + | ||
22 | public send(raw: RawMessage) { | 29 | public send(raw: RawMessage) { |
23 | this.socketIo.emit("msg", raw); | 30 | this.socketIo.emit("msg", raw); |
24 | } | 31 | } | ... | ... |
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 ( | ||
70 | + this.roles.get(user) === "guesser" && | ||
71 | + this.roundState === "running" && | ||
72 | + text === this.word | ||
73 | + ) { | ||
74 | + this.acceptAnswer(user); | ||
75 | + } else { | ||
76 | + this.room.sendChat(user, text); | ||
77 | + } | ||
78 | + return { ok: true }; | ||
79 | + }, | ||
80 | + setBrush: (user, message) => { | ||
81 | + if (user !== this.drawer || !/^[0-9a-f]{6}$/.test(message.color)) { | ||
82 | + return { ok: false }; | ||
83 | + } | ||
84 | + | ||
85 | + this.brush.size = Math.max(Math.min(message.size, 64), 1); | ||
86 | + this.brush.color = message.color; | ||
87 | + this.brush.drawing = message.drawing; | ||
88 | + | ||
89 | + this.room.broadcast( | ||
90 | + "setBrush", | ||
91 | + { | ||
92 | + size: this.brush.size, | ||
93 | + color: this.brush.color, | ||
94 | + drawing: this.brush.drawing, | ||
95 | + }, | ||
96 | + user | ||
97 | + ); | ||
98 | + | ||
99 | + return { ok: true }; | ||
100 | + }, | ||
101 | + moveBrush: (user, message) => { | ||
102 | + if (user !== this.drawer) { | ||
103 | + return { ok: false }; | ||
104 | + } | ||
105 | + | ||
106 | + this.brush.x = Math.max(Math.min(message.x, 1), 0); | ||
107 | + this.brush.y = Math.max(Math.min(message.y, 1), 0); | ||
108 | + | ||
109 | + this.room.broadcast( | ||
110 | + "moveBrush", | ||
111 | + { | ||
112 | + x: this.brush.x, | ||
113 | + y: this.brush.y, | ||
114 | + }, | ||
115 | + user | ||
116 | + ); | ||
117 | + | ||
118 | + return { ok: true }; | ||
119 | + }, | ||
120 | + }); | ||
121 | + | ||
122 | + this.roles = new Map<User, Role>(); | ||
123 | + | ||
124 | + this.startNextRound(); | ||
125 | + } | ||
126 | + | ||
127 | + private startNextRound(): void { | ||
128 | + this.roundState = "choosing"; | ||
129 | + this.word = undefined; | ||
130 | + this.round++; | ||
131 | + | ||
132 | + this.roles.clear(); | ||
133 | + | ||
134 | + this.drawer = this.pickDrawer(); | ||
135 | + this.room.users.forEach((user) => this.roles.set(user, "guesser")); | ||
136 | + this.roles.set(this.drawer, "drawer"); | ||
137 | + | ||
138 | + this.room.broadcast("startRound", { | ||
139 | + round: this.round, | ||
140 | + duration: this.roundDuration, | ||
141 | + roles: this.makeRoleArray(), | ||
142 | + }); | ||
143 | + | ||
144 | + this.wordCandidates = this.pickWords(); | ||
145 | + this.drawer.connection.send("wordSet", { words: this.wordCandidates }); | ||
146 | + } | ||
147 | + | ||
148 | + private wordSelected(word: string): void { | ||
149 | + this.word = word; | ||
150 | + this.roundState = "running"; | ||
151 | + | ||
152 | + this.room.broadcast("wordChosen", { length: word.length }); | ||
153 | + | ||
154 | + this.startTimer(this.roundDuration * 1000); | ||
155 | + | ||
156 | + this.timeoutTimerId = setTimeout( | ||
157 | + () => this.finishRound(), | ||
158 | + this.roundDuration * 1000 | ||
159 | + ); | ||
160 | + } | ||
161 | + | ||
162 | + public finishRound(): void { | ||
163 | + if (this.timeoutTimerId) { | ||
164 | + clearTimeout(this.timeoutTimerId); | ||
165 | + this.timeoutTimerId = undefined; | ||
166 | + } | ||
167 | + | ||
168 | + this.roundState = "done"; | ||
169 | + | ||
170 | + this.stopTimer(); | ||
171 | + | ||
172 | + if (this.word) { | ||
173 | + this.room.broadcast("finishRound", { answer: this.word }); | ||
174 | + } | ||
175 | + | ||
176 | + this.prepareNextRound(); | ||
177 | + } | ||
178 | + | ||
179 | + private prepareNextRound(): void { | ||
180 | + this.nextRoundTimerId = setTimeout(() => { | ||
181 | + if (this.round == this.maxRound) { | ||
182 | + this.finishGame(); | ||
183 | + } else { | ||
184 | + this.startNextRound(); | ||
185 | + } | ||
186 | + }, this.roundTerm * 1000); | ||
187 | + } | ||
188 | + | ||
189 | + private finishGame(): void { | ||
190 | + this.room.broadcast("finishGame", {}); | ||
191 | + | ||
192 | + this.room.finishGame(); | ||
193 | + } | ||
194 | + | ||
195 | + private forceFinishGame() { | ||
196 | + if (this.timeoutTimerId) { | ||
197 | + clearTimeout(this.timeoutTimerId); | ||
198 | + } | ||
199 | + if (this.nextRoundTimerId) { | ||
200 | + clearTimeout(this.nextRoundTimerId); | ||
201 | + } | ||
202 | + if (this.word) { | ||
203 | + this.room.broadcast("finishRound", { answer: this.word }); | ||
204 | + } | ||
205 | + | ||
206 | + this.finishGame(); | ||
207 | + } | ||
208 | + | ||
209 | + private acceptAnswer(user: User): void { | ||
210 | + user.connection.send("answerAccepted", { answer: this.word! }); | ||
211 | + this.changeRole(user, "winner"); | ||
212 | + | ||
213 | + let noGuesser = true; | ||
214 | + this.roles.forEach((role, user) => { | ||
215 | + if (role === "guesser") { | ||
216 | + noGuesser = false; | ||
217 | + } | ||
218 | + }); | ||
219 | + | ||
220 | + if (noGuesser) { | ||
221 | + this.finishRound(); | ||
222 | + } | ||
223 | + } | ||
224 | + | ||
225 | + private pickDrawer(): User { | ||
226 | + const candidates = this.room.users.filter((user) => user !== this.drawer); | ||
227 | + return candidates[Math.floor(Math.random() * candidates.length)]; | ||
228 | + } | ||
229 | + | ||
230 | + private pickWords(): string[] { | ||
231 | + return ["장난감", "백화점", "파티"]; | ||
232 | + } | ||
233 | + | ||
234 | + private startTimer(timeLeftMillis: number): void { | ||
235 | + this.timer = { | ||
236 | + startTimeMillis: Date.now(), | ||
237 | + timeLeftMillis, | ||
238 | + running: true, | ||
239 | + }; | ||
240 | + this.room.users.forEach((user) => this.sendTimer(user)); | ||
241 | + } | ||
242 | + | ||
243 | + private stopTimer(): void { | ||
244 | + this.timer = { | ||
245 | + ...this.timer, | ||
246 | + running: false, | ||
247 | + }; | ||
248 | + this.room.users.forEach((user) => this.sendTimer(user)); | ||
249 | + } | ||
250 | + | ||
251 | + private sendTimer(user: User): void { | ||
252 | + user.connection.send("timer", { | ||
253 | + state: this.timer.running ? "started" : "stopped", | ||
254 | + time: Math.max( | ||
255 | + (this.timer.startTimeMillis + this.timer.timeLeftMillis - Date.now()) / | ||
256 | + 1000, | ||
257 | + 0 | ||
258 | + ), | ||
259 | + }); | ||
260 | + } | ||
261 | + | ||
262 | + private makeRoleArray(): { username: string; role: Role }[] { | ||
263 | + let roleArray: { | ||
264 | + username: string; | ||
265 | + role: Role; | ||
266 | + }[] = []; | ||
267 | + this.roles.forEach((role, user) => | ||
268 | + roleArray.push({ username: user.username, role: role }) | ||
269 | + ); | ||
270 | + return roleArray; | ||
271 | + } | ||
272 | + | ||
273 | + private changeRole(user: User, role: Role) { | ||
274 | + this.roles.set(user, role); | ||
275 | + this.room.broadcast("role", { username: user.username, role }); | ||
276 | + } | ||
277 | + | ||
278 | + joined(user: User): void { | ||
279 | + this.changeRole(user, "spectator"); | ||
280 | + this.sendTimer(user); | ||
281 | + user.connection.send("startRound", { | ||
282 | + round: this.round, | ||
283 | + duration: this.roundDuration, | ||
284 | + roles: this.makeRoleArray(), | ||
285 | + }); | ||
286 | + if (this.roundState === "done" && this.word) { | ||
287 | + user.connection.send("finishRound", { | ||
288 | + answer: this.word, | ||
289 | + }); | ||
290 | + } | ||
291 | + user.connection.send("setBrush", { | ||
292 | + size: this.brush.size, | ||
293 | + color: this.brush.color, | ||
294 | + drawing: this.brush.drawing, | ||
295 | + }); | ||
296 | + user.connection.send("moveBrush", { | ||
297 | + x: this.brush.x, | ||
298 | + y: this.brush.y, | ||
299 | + }); | ||
300 | + } | ||
301 | + | ||
302 | + left(user: User): void { | ||
303 | + if (this.room.users.length < 2) { | ||
304 | + this.forceFinishGame(); | ||
305 | + return; | ||
306 | + } | ||
307 | + | ||
308 | + this.roles.delete(user); | ||
309 | + | ||
310 | + if (user === this.drawer) { | ||
311 | + if (this.roundState === "choosing") { | ||
312 | + this.round--; // 이번 라운드를 다시 시작 | ||
313 | + this.startNextRound(); | ||
314 | + } else if (this.roundState === "running") { | ||
315 | + this.finishRound(); | ||
316 | + } | ||
317 | + } else { | ||
318 | + let guesserCount = 0; | ||
319 | + this.roles.forEach((role, user) => { | ||
320 | + if (role === "guesser") { | ||
321 | + guesserCount++; | ||
322 | + } | ||
323 | + }); | ||
324 | + if (guesserCount < 1) { | ||
325 | + if (this.roundState === "choosing") { | ||
326 | + this.round--; | ||
327 | + this.startNextRound(); | ||
328 | + } else if (this.roundState === "running") { | ||
329 | + this.finishRound(); | ||
330 | + } | ||
331 | + } | ||
332 | + } | ||
333 | + } | ||
6 | } | 334 | } | ... | ... |
server/game/WordGuessingGame.ts
deleted
100644 → 0
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 | - // TODO | ||
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": { | ... | ... |
... | @@ -9,6 +9,7 @@ import { | ... | @@ -9,6 +9,7 @@ import { |
9 | } from "../../common"; | 9 | } from "../../common"; |
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 | 13 | ||
13 | export class Room { | 14 | export class Room { |
14 | public readonly uuid: string; | 15 | public readonly uuid: string; |
... | @@ -22,6 +23,8 @@ export class Room { | ... | @@ -22,6 +23,8 @@ export class Room { |
22 | public usersReady: User[] = []; | 23 | public usersReady: User[] = []; |
23 | public admin?: User; | 24 | public admin?: User; |
24 | 25 | ||
26 | + public game?: Game; | ||
27 | + | ||
25 | public closed: boolean = false; | 28 | public closed: boolean = false; |
26 | 29 | ||
27 | public handler: MessageHandler; | 30 | public handler: MessageHandler; |
... | @@ -60,6 +63,33 @@ export class Room { | ... | @@ -60,6 +63,33 @@ export class Room { |
60 | this.setReady(user, message.ready); | 63 | this.setReady(user, message.ready); |
61 | return { ok: true }; | 64 | return { ok: true }; |
62 | }, | 65 | }, |
66 | + startGame: (user, message) => { | ||
67 | + if (user !== this.admin) { | ||
68 | + return { ok: false }; | ||
69 | + } | ||
70 | + const result = this.canStart(); | ||
71 | + if (!result.ok) { | ||
72 | + return result; | ||
73 | + } | ||
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 | + ); | ||
91 | + return { ok: true }; | ||
92 | + }, | ||
63 | }); | 93 | }); |
64 | 94 | ||
65 | if (this.admin) { | 95 | if (this.admin) { |
... | @@ -92,6 +122,8 @@ export class Room { | ... | @@ -92,6 +122,8 @@ export class Room { |
92 | this.usersReady = this.usersReady.filter((u) => u !== user); | 122 | this.usersReady = this.usersReady.filter((u) => u !== user); |
93 | user.room = undefined; | 123 | user.room = undefined; |
94 | 124 | ||
125 | + this.game?.left(user); | ||
126 | + | ||
95 | this.broadcast("updateRoomUser", { | 127 | this.broadcast("updateRoomUser", { |
96 | state: "removed", | 128 | state: "removed", |
97 | user: { | 129 | user: { |
... | @@ -149,16 +181,35 @@ export class Room { | ... | @@ -149,16 +181,35 @@ export class Room { |
149 | return this.usersReady.includes(user); | 181 | return this.usersReady.includes(user); |
150 | } | 182 | } |
151 | 183 | ||
152 | - public canStart(): boolean { | 184 | + public canStart(): { ok: boolean; reason?: string } { |
185 | + if (this.isPlayingGame()) { | ||
186 | + return { ok: false, reason: "이미 게임이 진행 중입니다." }; | ||
187 | + } | ||
153 | if (this.users.length < 2) { | 188 | if (this.users.length < 2) { |
154 | - return false; | 189 | + return { ok: false, reason: "최소 2명의 플레이어가 필요합니다." }; |
155 | } | 190 | } |
156 | for (let i = 0; i < this.users.length; i++) { | 191 | for (let i = 0; i < this.users.length; i++) { |
157 | if (!this.isAdmin(this.users[i]) && !this.isReady(this.users[i])) { | 192 | if (!this.isAdmin(this.users[i]) && !this.isReady(this.users[i])) { |
158 | - return false; | 193 | + return { ok: false, reason: "모든 플레이어가 준비해야 합니다." }; |
159 | } | 194 | } |
160 | } | 195 | } |
161 | - return true; | 196 | + return { ok: true }; |
197 | + } | ||
198 | + | ||
199 | + private startGame( | ||
200 | + maxRound: number, | ||
201 | + roundDuration: number, | ||
202 | + roundTerm: number | ||
203 | + ): void { | ||
204 | + this.game = new Game(this, maxRound, roundDuration, roundTerm); | ||
205 | + } | ||
206 | + | ||
207 | + public finishGame(): void { | ||
208 | + this.game = undefined; | ||
209 | + } | ||
210 | + | ||
211 | + public isPlayingGame(): boolean { | ||
212 | + return this.game !== undefined; | ||
162 | } | 213 | } |
163 | 214 | ||
164 | public sendChat(user: User, message: string): void { | 215 | public sendChat(user: User, message: string): void { | ... | ... |
server/test/chooseWord.test.ts
0 → 100644
1 | +import { expect } from "chai"; | ||
2 | +import { prepareGame } from "./util/prepare"; | ||
3 | + | ||
4 | +describe("라운드 단어 선택", () => { | ||
5 | + it("drawer가 단어를 선택하면 wordChosen과 timer를 받습니다", () => { | ||
6 | + const { drawerSocket, guesserSockets } = prepareGame(2); | ||
7 | + | ||
8 | + const word = drawerSocket.socket.received("wordSet").words[0]; | ||
9 | + drawerSocket.testOk("chooseWord", { word }); | ||
10 | + | ||
11 | + expect(drawerSocket.socket.received("wordChosen").length).eq(word.length); | ||
12 | + drawerSocket.socket.received("timer"); | ||
13 | + expect(guesserSockets[0].socket.received("wordChosen").length).eq( | ||
14 | + word.length | ||
15 | + ); | ||
16 | + guesserSockets[0].socket.received("timer"); | ||
17 | + }); | ||
18 | + it("drawer가 아닌 다른 사람들은 단어를 선택할 수 없습니다", () => { | ||
19 | + const { drawerSocket, guesserSockets } = prepareGame(2); | ||
20 | + | ||
21 | + const word = drawerSocket.socket.received("wordSet").words[0]; | ||
22 | + | ||
23 | + guesserSockets[0].testNotOk("chooseWord", { word }); | ||
24 | + }); | ||
25 | + it("단어를 이미 고른 상태에서 다시 고를 수 없습니다", () => { | ||
26 | + const { drawerSocket } = prepareGame(2); | ||
27 | + | ||
28 | + const word = drawerSocket.socket.received("wordSet").words[0]; | ||
29 | + drawerSocket.testOk("chooseWord", { word }); | ||
30 | + drawerSocket.testNotOk("chooseWord", { word }); | ||
31 | + }); | ||
32 | + it("목록에 없는 단어를 고를 수 없습니다", () => { | ||
33 | + const { drawerSocket } = prepareGame(2); | ||
34 | + | ||
35 | + drawerSocket.testNotOk("chooseWord", { word: "Nope!" }); | ||
36 | + }); | ||
37 | +}); |
server/test/moveBrush.test.ts
0 → 100644
1 | +import { expect } from "chai"; | ||
2 | +import { prepareGame } from "./util/prepare"; | ||
3 | + | ||
4 | +describe("라운드 브러시 이동", () => { | ||
5 | + it("drawer가 브러시를 이동하면 다른 사람들이 설정을 받습니다", () => { | ||
6 | + const { drawerSocket, guesserSockets } = prepareGame(2); | ||
7 | + | ||
8 | + const brushCoord = { x: 0, y: 0 }; | ||
9 | + drawerSocket.testOk("moveBrush", brushCoord); | ||
10 | + expect(guesserSockets[0].socket.received("moveBrush")).deep.eq(brushCoord); | ||
11 | + }); | ||
12 | + it("영역을 벗어난 좌표는 Clamp 처리됩니다", () => { | ||
13 | + const { drawerSocket, guesserSockets } = prepareGame(2); | ||
14 | + | ||
15 | + drawerSocket.testOk("moveBrush", { x: -1, y: 2 }); | ||
16 | + expect(guesserSockets[0].socket.received("moveBrush")).deep.eq({ | ||
17 | + x: 0, | ||
18 | + y: 1, | ||
19 | + }); | ||
20 | + }); | ||
21 | + it("drawer가 아닌 다른 사람들은 브러시를 이동할 수 없습니다", () => { | ||
22 | + const { guesserSockets } = prepareGame(2); | ||
23 | + | ||
24 | + const brushCoord = { x: 0, y: 0 }; | ||
25 | + guesserSockets[0].testNotOk("moveBrush", brushCoord); | ||
26 | + }); | ||
27 | +}); |
... | @@ -23,10 +23,28 @@ describe("준비", () => { | ... | @@ -23,10 +23,28 @@ describe("준비", () => { |
23 | } = prepareJoinedRoom(1, 2, true); | 23 | } = prepareJoinedRoom(1, 2, true); |
24 | 24 | ||
25 | expect(room.isReady(user)).eq(false); | 25 | expect(room.isReady(user)).eq(false); |
26 | + | ||
26 | socket.testOk("ready", { ready: true }); | 27 | socket.testOk("ready", { ready: true }); |
27 | expect(room.isReady(user)).eq(true); | 28 | expect(room.isReady(user)).eq(true); |
29 | + expect(socket.socket.received("updateRoomUser")).deep.eq({ | ||
30 | + state: "updated", | ||
31 | + user: { | ||
32 | + username: user.username, | ||
33 | + admin: false, | ||
34 | + ready: true, | ||
35 | + }, | ||
36 | + }); | ||
37 | + | ||
28 | socket.testOk("ready", { ready: false }); | 38 | socket.testOk("ready", { ready: false }); |
29 | expect(room.isReady(user)).eq(false); | 39 | expect(room.isReady(user)).eq(false); |
40 | + expect(socket.socket.received("updateRoomUser")).deep.eq({ | ||
41 | + state: "updated", | ||
42 | + user: { | ||
43 | + username: user.username, | ||
44 | + admin: false, | ||
45 | + ready: false, | ||
46 | + }, | ||
47 | + }); | ||
30 | }); | 48 | }); |
31 | it("방장은 준비할 수 없습니다", () => { | 49 | it("방장은 준비할 수 없습니다", () => { |
32 | const { | 50 | const { |
... | @@ -53,7 +71,7 @@ describe("준비", () => { | ... | @@ -53,7 +71,7 @@ describe("준비", () => { |
53 | it("혼자 있는 방에서는 게임을 시작할 수 없습니다", () => { | 71 | it("혼자 있는 방에서는 게임을 시작할 수 없습니다", () => { |
54 | const { room } = prepareJoinedRoom(1); | 72 | const { room } = prepareJoinedRoom(1); |
55 | 73 | ||
56 | - expect(room.canStart()).eq(false); | 74 | + expect(room.canStart().ok).eq(false); |
57 | }); | 75 | }); |
58 | it("모두가 준비해야 게임을 시작할 수 있습니다", () => { | 76 | it("모두가 준비해야 게임을 시작할 수 있습니다", () => { |
59 | const { | 77 | const { |
... | @@ -62,7 +80,7 @@ describe("준비", () => { | ... | @@ -62,7 +80,7 @@ describe("준비", () => { |
62 | } = prepareJoinedRoom(3); | 80 | } = prepareJoinedRoom(3); |
63 | 81 | ||
64 | // 2, 3 모두 준비 안함 | 82 | // 2, 3 모두 준비 안함 |
65 | - expect(room.canStart()).eq(false); | 83 | + expect(room.canStart().ok).eq(false); |
66 | 84 | ||
67 | // 2만 준비 | 85 | // 2만 준비 |
68 | expect(socket2.test("ready", { ready: true }).ok).eq(true); | 86 | expect(socket2.test("ready", { ready: true }).ok).eq(true); |
... | @@ -70,10 +88,10 @@ describe("준비", () => { | ... | @@ -70,10 +88,10 @@ describe("준비", () => { |
70 | // 3만 준비 | 88 | // 3만 준비 |
71 | expect(socket2.test("ready", { ready: false }).ok).eq(true); | 89 | expect(socket2.test("ready", { ready: false }).ok).eq(true); |
72 | expect(socket3.test("ready", { ready: true }).ok).eq(true); | 90 | expect(socket3.test("ready", { ready: true }).ok).eq(true); |
73 | - expect(room.canStart()).eq(false); | 91 | + expect(room.canStart().ok).eq(false); |
74 | 92 | ||
75 | // 2, 3 모두 준비 | 93 | // 2, 3 모두 준비 |
76 | expect(socket2.test("ready", { ready: true }).ok).eq(true); | 94 | expect(socket2.test("ready", { ready: true }).ok).eq(true); |
77 | - expect(room.canStart()).eq(true); | 95 | + expect(room.canStart().ok).eq(true); |
78 | }); | 96 | }); |
79 | }); | 97 | }); | ... | ... |
server/test/round.test.ts
0 → 100644
1 | +import { expect } from "chai"; | ||
2 | +import { prepareGame } from "./util/prepare"; | ||
3 | + | ||
4 | +describe("라운드", () => { | ||
5 | + it("첫 라운드가 시작되면 startRound와 wordSet을 받습니다", () => { | ||
6 | + const { | ||
7 | + sockets: [socket1, socket2], | ||
8 | + drawerSocket, | ||
9 | + } = prepareGame(2); | ||
10 | + | ||
11 | + expect(socket1.socket.received("startRound").round).eq(1); | ||
12 | + expect(socket2.socket.received("startRound").round).eq(1); | ||
13 | + | ||
14 | + // drawer는 wordSet을 받습니다. | ||
15 | + expect(drawerSocket.socket.received("wordSet").words.length).eq(3); | ||
16 | + }); | ||
17 | + it("drawer가 단어를 선택하면 모두가 wordChosen과 timer를 받습니다", () => { | ||
18 | + const { drawerSocket, guesserSockets } = prepareGame(2); | ||
19 | + | ||
20 | + const word = drawerSocket.socket.received("wordSet").words[0]; | ||
21 | + drawerSocket.testOk("chooseWord", { word }); | ||
22 | + | ||
23 | + expect(drawerSocket.socket.received("wordChosen").length).eq(word.length); | ||
24 | + expect(guesserSockets[0].socket.received("wordChosen").length).eq( | ||
25 | + word.length | ||
26 | + ); | ||
27 | + | ||
28 | + let timerSettings = drawerSocket.socket.received("timer"); | ||
29 | + expect(timerSettings.state).eq(timerSettings.state); | ||
30 | + expect(timerSettings.time).greaterThan(59); | ||
31 | + }); | ||
32 | + it("drawer가 단어를 선택하지 않으면 라운드가 진행되지 않습니다", (done) => { | ||
33 | + const { drawerSocket, guesserSockets } = prepareGame(2, 5, 0.1); | ||
34 | + | ||
35 | + // 0.2초 뒤에도 라운드가 종료되지 않습니다. | ||
36 | + setTimeout(() => { | ||
37 | + drawerSocket.socket.notReceived("finishRound"); | ||
38 | + guesserSockets[0].socket.notReceived("finishRound"); | ||
39 | + done(); | ||
40 | + }, 200); | ||
41 | + }); | ||
42 | + it("아무도 단어를 맞추지 못하고 시간이 지나면 라운드가 종료됩니다", (done) => { | ||
43 | + const { drawerSocket, guesserSockets } = prepareGame(2, 5, 0.2); | ||
44 | + | ||
45 | + const word = drawerSocket.socket.received("wordSet").words[0]; | ||
46 | + drawerSocket.testOk("chooseWord", { word }); | ||
47 | + | ||
48 | + // 0.1초 뒤에는 라운드가 종료되지 않습니다. | ||
49 | + setTimeout(() => { | ||
50 | + drawerSocket.socket.notReceived("finishRound"); | ||
51 | + guesserSockets[0].socket.notReceived("finishRound"); | ||
52 | + }, 100); | ||
53 | + // 0.3초 뒤에는 라운드가 종료됩니다. | ||
54 | + setTimeout(() => { | ||
55 | + expect(drawerSocket.socket.received("finishRound").answer).eq(word); | ||
56 | + expect(guesserSockets[0].socket.received("finishRound").answer).eq(word); | ||
57 | + done(); | ||
58 | + }, 300); | ||
59 | + }); | ||
60 | + it("모든 guesser가 단어를 맞추면 라운드가 종료됩니다", (done) => { | ||
61 | + const { drawerSocket, guesserSockets } = prepareGame(3, 5, 0.5); | ||
62 | + | ||
63 | + const word = drawerSocket.socket.received("wordSet").words[0]; | ||
64 | + drawerSocket.testOk("chooseWord", { word }); | ||
65 | + | ||
66 | + // 0.1초 뒤에는 라운드가 종료되지 않습니다. | ||
67 | + setTimeout(() => { | ||
68 | + drawerSocket.socket.notReceived("finishRound"); | ||
69 | + | ||
70 | + // 첫번째 guesser가 단어를 맞춥니다. | ||
71 | + guesserSockets[0].testOk("chat", { message: word }); | ||
72 | + expect(guesserSockets[0].socket.received("answerAccepted").answer).eq( | ||
73 | + word | ||
74 | + ); | ||
75 | + }, 100); | ||
76 | + // 0.2초 뒤에도 라운드가 종료되지 않습니다. | ||
77 | + setTimeout(() => { | ||
78 | + drawerSocket.socket.notReceived("finishRound"); | ||
79 | + | ||
80 | + // 두번째 guesser가 단어를 맞춥니다. | ||
81 | + guesserSockets[1].testOk("chat", { message: word }); | ||
82 | + expect(guesserSockets[1].socket.received("answerAccepted").answer).eq( | ||
83 | + word | ||
84 | + ); | ||
85 | + }, 200); | ||
86 | + // 0.3초 뒤에는 라운드가 종료됩니다. | ||
87 | + setTimeout(() => { | ||
88 | + drawerSocket.socket.received("finishRound"); | ||
89 | + done(); | ||
90 | + }, 300); | ||
91 | + }); | ||
92 | + it("drawer가 단어를 선택하지 않고 나가면 즉시 라운드가 다시 시작됩니다", () => { | ||
93 | + const { drawerSocket, guesserSockets } = prepareGame(3); | ||
94 | + guesserSockets[0].socket.received("startRound"); | ||
95 | + | ||
96 | + guesserSockets[0].socket.notReceived("startRound"); | ||
97 | + drawerSocket.disconnect(); | ||
98 | + expect(guesserSockets[0].socket.received("startRound").round).eq(1); | ||
99 | + }); | ||
100 | + it("drawer가 단어를 선택하지 않고 모든 guesser가 나가면 인원이 부족하므로 게임이 종료됩니다", () => { | ||
101 | + const { drawerSocket, guesserSockets } = prepareGame(3); | ||
102 | + | ||
103 | + drawerSocket.socket.notReceived("finishRound"); | ||
104 | + guesserSockets[0].disconnect(); | ||
105 | + drawerSocket.socket.notReceived("finishRound"); | ||
106 | + guesserSockets[1].disconnect(); | ||
107 | + // 단어가 선택되지 않았으므로 finishRound가 수신되지 않습니다. | ||
108 | + drawerSocket.socket.received("finishGame"); | ||
109 | + }); | ||
110 | + it("drawer가 단어를 선택하고 모든 guesser가 나가면 인원이 부족하므로 게임이 종료됩니다", () => { | ||
111 | + const { drawerSocket, guesserSockets } = prepareGame(3); | ||
112 | + | ||
113 | + const word = drawerSocket.socket.received("wordSet").words[0]; | ||
114 | + drawerSocket.testOk("chooseWord", { word }); | ||
115 | + | ||
116 | + drawerSocket.socket.notReceived("finishRound"); | ||
117 | + guesserSockets[0].disconnect(); | ||
118 | + drawerSocket.socket.notReceived("finishRound"); | ||
119 | + guesserSockets[1].disconnect(); | ||
120 | + drawerSocket.socket.received("finishRound"); | ||
121 | + drawerSocket.socket.received("finishGame"); | ||
122 | + }); | ||
123 | + it("drawer가 단어를 선택하고 나가면 라운드가 종료됩니다", () => { | ||
124 | + const { drawerSocket, guesserSockets } = prepareGame(3); | ||
125 | + | ||
126 | + const word = drawerSocket.socket.received("wordSet").words[0]; | ||
127 | + drawerSocket.testOk("chooseWord", { word }); | ||
128 | + | ||
129 | + guesserSockets[0].socket.notReceived("finishRound"); | ||
130 | + drawerSocket.disconnect(); | ||
131 | + guesserSockets[0].socket.received("finishRound"); | ||
132 | + guesserSockets[0].socket.notReceived("finishGame"); | ||
133 | + }); | ||
134 | + it("라운드가 종료되고 다음 라운드를 기다리는 동안 drawer가 나가도 다음 라운드가 시작됩니다", (done) => { | ||
135 | + const { drawerSocket, guesserSockets } = prepareGame(3, 5, 5, 0.1); | ||
136 | + guesserSockets[0].socket.received("startRound"); | ||
137 | + | ||
138 | + const word = drawerSocket.socket.received("wordSet").words[0]; | ||
139 | + drawerSocket.testOk("chooseWord", { word }); | ||
140 | + guesserSockets[0].testOk("chat", { message: word }); | ||
141 | + guesserSockets[1].testOk("chat", { message: word }); | ||
142 | + | ||
143 | + guesserSockets[0].socket.received("finishRound"); | ||
144 | + guesserSockets[0].socket.notReceived("startRound"); | ||
145 | + | ||
146 | + drawerSocket.disconnect(); | ||
147 | + | ||
148 | + setTimeout(() => { | ||
149 | + expect(guesserSockets[0].socket.received("startRound").round).eq(2); | ||
150 | + done(); | ||
151 | + }, 200); | ||
152 | + }); | ||
153 | + it("라운드가 종료되고 다음 라운드를 기다리는 동안 인원이 부족해지면 게임이 즉시 종료됩니다", () => { | ||
154 | + const { drawerSocket, guesserSockets } = prepareGame(2, 5, 5, 0.1); | ||
155 | + guesserSockets[0].socket.received("startRound"); | ||
156 | + | ||
157 | + const word = drawerSocket.socket.received("wordSet").words[0]; | ||
158 | + drawerSocket.testOk("chooseWord", { word }); | ||
159 | + guesserSockets[0].testOk("chat", { message: word }); | ||
160 | + | ||
161 | + drawerSocket.socket.received("finishRound"); | ||
162 | + | ||
163 | + guesserSockets[0].disconnect(); | ||
164 | + | ||
165 | + drawerSocket.socket.received("finishGame"); | ||
166 | + }); | ||
167 | + it("라운드가 종료되면 다음 라운드가 시작됩니다", (done) => { | ||
168 | + const { drawerSocket, guesserSockets } = prepareGame(2, 5, 0.2, 0.2); | ||
169 | + | ||
170 | + drawerSocket.socket.received("startRound"); | ||
171 | + guesserSockets[0].socket.received("startRound"); | ||
172 | + | ||
173 | + const word = drawerSocket.socket.received("wordSet").words[0]; | ||
174 | + drawerSocket.testOk("chooseWord", { word }); | ||
175 | + | ||
176 | + // 0.1초 뒤에는 라운드가 종료되지 않습니다. | ||
177 | + setTimeout(() => { | ||
178 | + drawerSocket.socket.notReceived("finishRound"); | ||
179 | + guesserSockets[0].socket.notReceived("finishRound"); | ||
180 | + }, 100); | ||
181 | + // 0.3초 뒤에는 라운드가 종료됩니다. | ||
182 | + setTimeout(() => { | ||
183 | + expect(drawerSocket.socket.received("finishRound").answer).eq(word); | ||
184 | + expect(guesserSockets[0].socket.received("finishRound").answer).eq(word); | ||
185 | + drawerSocket.socket.notReceived("startRound"); | ||
186 | + }, 300); | ||
187 | + // 0.5초 뒤에는 다음 라운드가 시작됩니다. | ||
188 | + setTimeout(() => { | ||
189 | + expect(drawerSocket.socket.received("startRound").round).eq(2); | ||
190 | + expect(guesserSockets[0].socket.received("startRound").round).eq(2); | ||
191 | + done(); | ||
192 | + }, 500); | ||
193 | + }); | ||
194 | + it("마지막 라운드가 종료되면 게임이 종료됩니다", (done) => { | ||
195 | + const { drawerSocket } = prepareGame(2, 1, 0.1, 0.2); | ||
196 | + | ||
197 | + const word = drawerSocket.socket.received("wordSet").words[0]; | ||
198 | + drawerSocket.testOk("chooseWord", { word }); | ||
199 | + | ||
200 | + setTimeout(() => { | ||
201 | + drawerSocket.socket.received("finishRound"); | ||
202 | + drawerSocket.socket.notReceived("finishGame"); | ||
203 | + }, 200); | ||
204 | + setTimeout(() => { | ||
205 | + drawerSocket.socket.received("finishGame"); | ||
206 | + done(); | ||
207 | + }, 400); | ||
208 | + }); | ||
209 | +}); |
server/test/roundChat.test.ts
0 → 100644
1 | +import { expect } from "chai"; | ||
2 | +import { prepareGame } from "./util/prepare"; | ||
3 | + | ||
4 | +describe("라운드 채팅", () => { | ||
5 | + it("guesser가 정답을 채팅으로 보내면 정답 처리되고 다른 사람들에게 채팅이 보이지 않습니다", () => { | ||
6 | + const { drawerSocket, guesserSockets } = prepareGame(3); | ||
7 | + | ||
8 | + const word = drawerSocket.socket.received("wordSet").words[0]; | ||
9 | + drawerSocket.testOk("chooseWord", { word }); | ||
10 | + | ||
11 | + guesserSockets[0].testOk("chat", { message: "Not Answer" }); | ||
12 | + guesserSockets[0].socket.notReceived("answerAccepted"); | ||
13 | + guesserSockets[1].socket.received("chat"); | ||
14 | + | ||
15 | + guesserSockets[0].testOk("chat", { message: word }); | ||
16 | + expect(guesserSockets[0].socket.received("answerAccepted").answer).eq(word); | ||
17 | + guesserSockets[1].socket.notReceived("chat"); | ||
18 | + }); | ||
19 | + it("guesser가 정답을 채팅으로 보내면 역할이 winner로 변경됩니다", () => { | ||
20 | + const { drawerSocket, guesserSockets } = prepareGame(2); | ||
21 | + | ||
22 | + const word = drawerSocket.socket.received("wordSet").words[0]; | ||
23 | + drawerSocket.testOk("chooseWord", { word }); | ||
24 | + | ||
25 | + guesserSockets[0].testOk("chat", { message: word }); | ||
26 | + | ||
27 | + expect(guesserSockets[0].socket.received("role")).deep.eq({ | ||
28 | + username: guesserSockets[0].connection.user?.username, | ||
29 | + role: "winner", | ||
30 | + }); | ||
31 | + expect(drawerSocket.socket.received("role")).deep.eq({ | ||
32 | + username: guesserSockets[0].connection.user?.username, | ||
33 | + role: "winner", | ||
34 | + }); | ||
35 | + }); | ||
36 | + it("라운드가 끝나고 다음 라운드를 준비하는 시간에 답을 채팅으로 보내도 정답 처리되지 않습니다", (done) => { | ||
37 | + const { drawerSocket, guesserSockets } = prepareGame(2, 5, 0.1, 0.3); | ||
38 | + | ||
39 | + const word = drawerSocket.socket.received("wordSet").words[0]; | ||
40 | + drawerSocket.testOk("chooseWord", { word }); | ||
41 | + | ||
42 | + guesserSockets[0].socket.notReceived("finishRound"); | ||
43 | + setTimeout(() => { | ||
44 | + guesserSockets[0].socket.received("finishRound"); | ||
45 | + guesserSockets[0].testOk("chat", { message: word }); | ||
46 | + guesserSockets[0].socket.notReceived("answerAccepted"); | ||
47 | + guesserSockets[0].socket.notReceived("role"); | ||
48 | + done(); | ||
49 | + }, 200); | ||
50 | + }); | ||
51 | + it("다음 라운드의 단어가 선택되지 않았을 때 이전 라운드의 답을 채팅으로 보내도 정답 처리되지 않습니다", (done) => { | ||
52 | + const { drawerSocket, guesserSockets, game } = prepareGame(2, 5, 0.2, 0.1); | ||
53 | + | ||
54 | + const word = drawerSocket.socket.received("wordSet").words[0]; | ||
55 | + drawerSocket.testOk("chooseWord", { word }); | ||
56 | + | ||
57 | + expect(guesserSockets[0].socket.received("startRound").round).eq(1); | ||
58 | + setTimeout(() => { | ||
59 | + expect(guesserSockets[0].socket.received("startRound").round).eq(2); | ||
60 | + | ||
61 | + if (game.drawer === drawerSocket.connection.user) { | ||
62 | + guesserSockets[0].testOk("chat", { message: word }); | ||
63 | + guesserSockets[0].socket.notReceived("answerAccepted"); | ||
64 | + } else if (game.drawer === guesserSockets[0].connection.user) { | ||
65 | + drawerSocket.testOk("chat", { message: word }); | ||
66 | + drawerSocket.socket.notReceived("answerAccepted"); | ||
67 | + } else { | ||
68 | + throw new Error("There is no drawer!"); | ||
69 | + } | ||
70 | + done(); | ||
71 | + }, 400); | ||
72 | + }); | ||
73 | +}); |
server/test/setBrush.test.ts
0 → 100644
1 | +import { expect } from "chai"; | ||
2 | +import { prepareGame } from "./util/prepare"; | ||
3 | + | ||
4 | +describe("라운드 브러시 설정", () => { | ||
5 | + it("drawer가 브러시를 설정하면 다른 사람들이 설정을 받습니다", () => { | ||
6 | + const { drawerSocket, guesserSockets } = prepareGame(2); | ||
7 | + | ||
8 | + const brushSettings = { | ||
9 | + size: 1, | ||
10 | + color: "000000", | ||
11 | + drawing: true, | ||
12 | + }; | ||
13 | + drawerSocket.testOk("setBrush", brushSettings); | ||
14 | + expect(guesserSockets[0].socket.received("setBrush")).deep.eq( | ||
15 | + brushSettings | ||
16 | + ); | ||
17 | + }); | ||
18 | + it("올바르지 않은 브러시 색상은 허용되지 않습니다", () => { | ||
19 | + const { drawerSocket } = prepareGame(2); | ||
20 | + drawerSocket.testNotOk("setBrush", { | ||
21 | + size: 1, | ||
22 | + color: "000", | ||
23 | + drawing: true, | ||
24 | + }); | ||
25 | + drawerSocket.testNotOk("setBrush", { | ||
26 | + size: 1, | ||
27 | + color: "asdf01", | ||
28 | + drawing: true, | ||
29 | + }); | ||
30 | + }); | ||
31 | + it("올바르지 않은 브러시 사이즈는 Clamp 됩니다", () => { | ||
32 | + const { drawerSocket, guesserSockets } = prepareGame(2); | ||
33 | + drawerSocket.testOk("setBrush", { | ||
34 | + size: 0, | ||
35 | + color: "000000", | ||
36 | + drawing: true, | ||
37 | + }); | ||
38 | + expect(guesserSockets[0].socket.received("setBrush").size).eq(1); | ||
39 | + drawerSocket.testOk("setBrush", { | ||
40 | + size: 100, | ||
41 | + color: "000000", | ||
42 | + drawing: true, | ||
43 | + }); | ||
44 | + expect(guesserSockets[0].socket.received("setBrush").size).eq(64); | ||
45 | + }); | ||
46 | + it("drawer가 아닌 다른 사람들은 브러시를 설정할 수 없습니다", () => { | ||
47 | + const { guesserSockets } = prepareGame(2); | ||
48 | + | ||
49 | + const brushSettings = { | ||
50 | + size: 1, | ||
51 | + color: "000000", | ||
52 | + drawing: true, | ||
53 | + }; | ||
54 | + guesserSockets[0].testNotOk("setBrush", brushSettings); | ||
55 | + }); | ||
56 | +}); |
server/test/startGame.test.ts
0 → 100644
1 | +import { expect } from "chai"; | ||
2 | +import { prepareJoinedRoom, prepareUsersEmptyRooms } from "./util/prepare"; | ||
3 | + | ||
4 | +describe("게임 시작", () => { | ||
5 | + it("방장만 게임 시작을 요청할 수 있습니다.", () => { | ||
6 | + const { | ||
7 | + sockets: [socket1, socket2], | ||
8 | + room, | ||
9 | + } = prepareJoinedRoom(2); | ||
10 | + | ||
11 | + expect(room.admin).eq(socket1.connection.user); | ||
12 | + expect(socket2.testOk("ready", { ready: true })); | ||
13 | + expect(room.canStart().ok).eq(true); | ||
14 | + | ||
15 | + expect(socket2.testNotOk("startGame", {})); | ||
16 | + expect(socket1.testOk("startGame", {})); | ||
17 | + }); | ||
18 | + it("인원이 충분해야 게임을 시작할 수 있습니다.", () => { | ||
19 | + const { | ||
20 | + sockets: [socket1], | ||
21 | + } = prepareJoinedRoom(1); | ||
22 | + | ||
23 | + expect(socket1.testNotOk("startGame", {})); | ||
24 | + }); | ||
25 | + it("게임이 시작되면 startRound를 받습니다.", () => { | ||
26 | + const { | ||
27 | + sockets: [socket1, socket2], | ||
28 | + } = prepareJoinedRoom(2); | ||
29 | + | ||
30 | + expect(socket2.testOk("ready", { ready: true })); | ||
31 | + expect(socket1.testOk("startGame", {})); | ||
32 | + | ||
33 | + expect(socket1.socket.received("startRound")); | ||
34 | + expect(socket2.socket.received("startRound")); | ||
35 | + }); | ||
36 | +}); |
... | @@ -9,11 +9,12 @@ import { SocketWrapper } from "../../connection/SocketWrapper"; | ... | @@ -9,11 +9,12 @@ import { SocketWrapper } from "../../connection/SocketWrapper"; |
9 | 9 | ||
10 | export class DummySocket implements SocketWrapper { | 10 | export class DummySocket implements SocketWrapper { |
11 | public handler?: (raw: RawMessage) => ServerResponse<any>; | 11 | public handler?: (raw: RawMessage) => ServerResponse<any>; |
12 | + public disconnectHandler?: () => void; | ||
12 | public receivedMessages: RawMessage[] = []; | 13 | public receivedMessages: RawMessage[] = []; |
13 | 14 | ||
14 | - public setHandler(handler: (raw: RawMessage) => ServerResponse<any>) { | 15 | + public setHandler(handler: (raw: RawMessage) => ServerResponse<any>) {} |
15 | - this.handler = handler; | 16 | + |
16 | - } | 17 | + public setDisconnectHandler(handler: () => void) {} |
17 | 18 | ||
18 | public send(raw: RawMessage): void { | 19 | public send(raw: RawMessage): void { |
19 | this.receivedMessages.push(raw); | 20 | this.receivedMessages.push(raw); | ... | ... |
... | @@ -53,4 +53,8 @@ export class SocketTester { | ... | @@ -53,4 +53,8 @@ export class SocketTester { |
53 | this.testOk("login", { username }); | 53 | this.testOk("login", { username }); |
54 | expect(this.connection.user !== undefined).eq(true); | 54 | expect(this.connection.user !== undefined).eq(true); |
55 | } | 55 | } |
56 | + | ||
57 | + public disconnect(): void { | ||
58 | + this.connection.handleDisconnect(); | ||
59 | + } | ||
56 | } | 60 | } | ... | ... |
1 | +import { Game } from "../../game/Game"; | ||
1 | import { Room } from "../../room/Room"; | 2 | import { Room } from "../../room/Room"; |
2 | import { RoomManager } from "../../room/RoomManager"; | 3 | import { RoomManager } from "../../room/RoomManager"; |
3 | import { User } from "../../user/User"; | 4 | import { User } from "../../user/User"; |
... | @@ -92,3 +93,58 @@ export function prepareJoinedRoom( | ... | @@ -92,3 +93,58 @@ export function prepareJoinedRoom( |
92 | } | 93 | } |
93 | return { sockets, users, room }; | 94 | return { sockets, users, room }; |
94 | } | 95 | } |
96 | + | ||
97 | +export function prepareGame( | ||
98 | + userCount: number, | ||
99 | + maxRound: number = 5, | ||
100 | + roundDuration: number = 60, | ||
101 | + roundTerm: number = 5, | ||
102 | + roomMaxConnections: number = 2 | ||
103 | +): { | ||
104 | + sockets: SocketTester[]; | ||
105 | + users: User[]; | ||
106 | + room: Room; | ||
107 | + game: Game; | ||
108 | + drawerSocket: SocketTester; | ||
109 | + guesserSockets: SocketTester[]; | ||
110 | +} { | ||
111 | + const { sockets, users, room } = prepareJoinedRoom( | ||
112 | + userCount, | ||
113 | + roomMaxConnections | ||
114 | + ); | ||
115 | + | ||
116 | + for (let i = 1; i < userCount; i++) { | ||
117 | + sockets[i].testOk("ready", { ready: true }); | ||
118 | + } | ||
119 | + sockets[0].testOk("startGame", { maxRound, roundDuration, roundTerm }); | ||
120 | + | ||
121 | + if (!room.game) { | ||
122 | + throw new Error("Game is not initialized."); | ||
123 | + } | ||
124 | + | ||
125 | + let drawerSocket = undefined; | ||
126 | + let guesserSockets: SocketTester[] = []; | ||
127 | + sockets.forEach((socket) => { | ||
128 | + if (socket.connection.user === room.game?.drawer) { | ||
129 | + drawerSocket = socket; | ||
130 | + } else { | ||
131 | + guesserSockets.push(socket); | ||
132 | + } | ||
133 | + }); | ||
134 | + | ||
135 | + if (!drawerSocket) { | ||
136 | + throw new Error("There is no drawer!"); | ||
137 | + } | ||
138 | + if (guesserSockets.length == 0) { | ||
139 | + throw new Error("There is no guesser!"); | ||
140 | + } | ||
141 | + | ||
142 | + return { | ||
143 | + sockets, | ||
144 | + users, | ||
145 | + room, | ||
146 | + game: room.game, | ||
147 | + drawerSocket, | ||
148 | + guesserSockets, | ||
149 | + }; | ||
150 | +} | ... | ... |
-
Please register or login to post a comment