강동현
Builds for 1 pipeline failed in 2 minutes 48 seconds

Merge branch 'feature/test-game' into develop

...@@ -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 }
......
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: "모든 플레이어가 준비해야 합니다." };
194 + }
195 + }
196 + return { ok: true };
159 } 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);
160 } 205 }
161 - return true; 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 {
......
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 +});
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 });
......
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 + expect(drawerSocket.socket.received("timer")).deep.eq({
29 + state: "started",
30 + time: 60,
31 + });
32 + expect(guesserSockets[0].socket.received("timer")).deep.eq({
33 + state: "started",
34 + time: 60,
35 + });
36 + });
37 + it("drawer가 단어를 선택하지 않으면 라운드가 진행되지 않습니다", (done) => {
38 + const { drawerSocket, guesserSockets } = prepareGame(2, 5, 0.1);
39 +
40 + // 0.2초 뒤에도 라운드가 종료되지 않습니다.
41 + setTimeout(() => {
42 + drawerSocket.socket.notReceived("finishRound");
43 + guesserSockets[0].socket.notReceived("finishRound");
44 + done();
45 + }, 200);
46 + });
47 + it("아무도 단어를 맞추지 못하고 시간이 지나면 라운드가 종료됩니다", (done) => {
48 + const { drawerSocket, guesserSockets } = prepareGame(2, 5, 0.2);
49 +
50 + const word = drawerSocket.socket.received("wordSet").words[0];
51 + drawerSocket.testOk("chooseWord", { word });
52 +
53 + // 0.1초 뒤에는 라운드가 종료되지 않습니다.
54 + setTimeout(() => {
55 + drawerSocket.socket.notReceived("finishRound");
56 + guesserSockets[0].socket.notReceived("finishRound");
57 + }, 100);
58 + // 0.3초 뒤에는 라운드가 종료됩니다.
59 + setTimeout(() => {
60 + expect(drawerSocket.socket.received("finishRound").answer).eq(word);
61 + expect(guesserSockets[0].socket.received("finishRound").answer).eq(word);
62 + done();
63 + }, 300);
64 + });
65 + it("모든 guesser가 단어를 맞추면 라운드가 종료됩니다", (done) => {
66 + const { drawerSocket, guesserSockets } = prepareGame(3, 5, 0.5);
67 +
68 + const word = drawerSocket.socket.received("wordSet").words[0];
69 + drawerSocket.testOk("chooseWord", { word });
70 +
71 + // 0.1초 뒤에는 라운드가 종료되지 않습니다.
72 + setTimeout(() => {
73 + drawerSocket.socket.notReceived("finishRound");
74 +
75 + // 첫번째 guesser가 단어를 맞춥니다.
76 + guesserSockets[0].testOk("chat", { message: word });
77 + expect(guesserSockets[0].socket.received("answerAccepted").answer).eq(
78 + word
79 + );
80 + }, 100);
81 + // 0.2초 뒤에도 라운드가 종료되지 않습니다.
82 + setTimeout(() => {
83 + drawerSocket.socket.notReceived("finishRound");
84 +
85 + // 두번째 guesser가 단어를 맞춥니다.
86 + guesserSockets[1].testOk("chat", { message: word });
87 + expect(guesserSockets[1].socket.received("answerAccepted").answer).eq(
88 + word
89 + );
90 + }, 200);
91 + // 0.3초 뒤에는 라운드가 종료됩니다.
92 + setTimeout(() => {
93 + drawerSocket.socket.received("finishRound");
94 + done();
95 + }, 300);
96 + });
97 + it("drawer가 단어를 선택하지 않고 나가면 즉시 라운드가 다시 시작됩니다", () => {
98 + const { drawerSocket, guesserSockets } = prepareGame(3);
99 + guesserSockets[0].socket.received("startRound");
100 +
101 + guesserSockets[0].socket.notReceived("startRound");
102 + drawerSocket.disconnect();
103 + expect(guesserSockets[0].socket.received("startRound").round).eq(1);
104 + });
105 + it("drawer가 단어를 선택하지 않고 모든 guesser가 나가면 인원이 부족하므로 게임이 종료됩니다", () => {
106 + const { drawerSocket, guesserSockets } = prepareGame(3);
107 +
108 + drawerSocket.socket.notReceived("finishRound");
109 + guesserSockets[0].disconnect();
110 + drawerSocket.socket.notReceived("finishRound");
111 + guesserSockets[1].disconnect();
112 + // 단어가 선택되지 않았으므로 finishRound가 수신되지 않습니다.
113 + drawerSocket.socket.received("finishGame");
114 + });
115 + it("drawer가 단어를 선택하고 모든 guesser가 나가면 인원이 부족하므로 게임이 종료됩니다", () => {
116 + const { drawerSocket, guesserSockets } = prepareGame(3);
117 +
118 + const word = drawerSocket.socket.received("wordSet").words[0];
119 + drawerSocket.testOk("chooseWord", { word });
120 +
121 + drawerSocket.socket.notReceived("finishRound");
122 + guesserSockets[0].disconnect();
123 + drawerSocket.socket.notReceived("finishRound");
124 + guesserSockets[1].disconnect();
125 + drawerSocket.socket.received("finishRound");
126 + drawerSocket.socket.received("finishGame");
127 + });
128 + it("drawer가 단어를 선택하고 나가면 라운드가 종료됩니다", () => {
129 + const { drawerSocket, guesserSockets } = prepareGame(3);
130 +
131 + const word = drawerSocket.socket.received("wordSet").words[0];
132 + drawerSocket.testOk("chooseWord", { word });
133 +
134 + guesserSockets[0].socket.notReceived("finishRound");
135 + drawerSocket.disconnect();
136 + guesserSockets[0].socket.received("finishRound");
137 + guesserSockets[0].socket.notReceived("finishGame");
138 + });
139 + it("라운드가 종료되고 다음 라운드를 기다리는 동안 drawer가 나가도 다음 라운드가 시작됩니다", (done) => {
140 + const { drawerSocket, guesserSockets } = prepareGame(3, 5, 5, 0.1);
141 + guesserSockets[0].socket.received("startRound");
142 +
143 + const word = drawerSocket.socket.received("wordSet").words[0];
144 + drawerSocket.testOk("chooseWord", { word });
145 + guesserSockets[0].testOk("chat", { message: word });
146 + guesserSockets[1].testOk("chat", { message: word });
147 +
148 + guesserSockets[0].socket.received("finishRound");
149 + guesserSockets[0].socket.notReceived("startRound");
150 +
151 + drawerSocket.disconnect();
152 +
153 + setTimeout(() => {
154 + expect(guesserSockets[0].socket.received("startRound").round).eq(2);
155 + done();
156 + }, 200);
157 + });
158 + it("라운드가 종료되고 다음 라운드를 기다리는 동안 인원이 부족해지면 게임이 즉시 종료됩니다", () => {
159 + const { drawerSocket, guesserSockets } = prepareGame(2, 5, 5, 0.1);
160 + guesserSockets[0].socket.received("startRound");
161 +
162 + const word = drawerSocket.socket.received("wordSet").words[0];
163 + drawerSocket.testOk("chooseWord", { word });
164 + guesserSockets[0].testOk("chat", { message: word });
165 +
166 + drawerSocket.socket.received("finishRound");
167 +
168 + guesserSockets[0].disconnect();
169 +
170 + drawerSocket.socket.received("finishGame");
171 + });
172 + it("라운드가 종료되면 다음 라운드가 시작됩니다", (done) => {
173 + const { drawerSocket, guesserSockets } = prepareGame(2, 5, 0.2, 0.2);
174 +
175 + drawerSocket.socket.received("startRound");
176 + guesserSockets[0].socket.received("startRound");
177 +
178 + const word = drawerSocket.socket.received("wordSet").words[0];
179 + drawerSocket.testOk("chooseWord", { word });
180 +
181 + // 0.1초 뒤에는 라운드가 종료되지 않습니다.
182 + setTimeout(() => {
183 + drawerSocket.socket.notReceived("finishRound");
184 + guesserSockets[0].socket.notReceived("finishRound");
185 + }, 100);
186 + // 0.3초 뒤에는 라운드가 종료됩니다.
187 + setTimeout(() => {
188 + expect(drawerSocket.socket.received("finishRound").answer).eq(word);
189 + expect(guesserSockets[0].socket.received("finishRound").answer).eq(word);
190 + drawerSocket.socket.notReceived("startRound");
191 + }, 300);
192 + // 0.5초 뒤에는 다음 라운드가 시작됩니다.
193 + setTimeout(() => {
194 + expect(drawerSocket.socket.received("startRound").round).eq(2);
195 + expect(guesserSockets[0].socket.received("startRound").round).eq(2);
196 + done();
197 + }, 500);
198 + });
199 + it("마지막 라운드가 종료되면 게임이 종료됩니다", (done) => {
200 + const { drawerSocket } = prepareGame(2, 1, 0.1, 0.2);
201 +
202 + const word = drawerSocket.socket.received("wordSet").words[0];
203 + drawerSocket.testOk("chooseWord", { word });
204 +
205 + setTimeout(() => {
206 + drawerSocket.socket.received("finishRound");
207 + drawerSocket.socket.notReceived("finishGame");
208 + }, 200);
209 + setTimeout(() => {
210 + drawerSocket.socket.received("finishGame");
211 + done();
212 + }, 400);
213 + });
214 +});
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 +});
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 +});
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 +}
......
...@@ -34,4 +34,8 @@ export class User { ...@@ -34,4 +34,8 @@ export class User {
34 }, 34 },
35 }); 35 });
36 } 36 }
37 +
38 + public disconnected(): void {
39 + this.room?.disconnect(this);
40 + }
37 } 41 }
......