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