Overnap

Merge branch 'develop' of http://khuhub.khu.ac.kr/2020105578/nodejs-game into feature/room

......@@ -30,22 +30,83 @@
- `updated`: 기존 유저의 정보가 업데이트되었습니다. 해당 유저의 방장, 준비 여부가 변경되면 수신됩니다.
- `removed`: 유저가 방에서 퇴장하였습니다.
유저가 채팅을 입력했다면 `chat`을 보내면 됩니다. 다른 사람이 채팅을 입력한 경우에도 `chat`이 수신됩니다(`ServerInboundMessage<"chat">``ServerOutboundMessage<"chat">`이 다르게 정의되었다는 점에 유의하세요). 자신이 보낸 채팅에 대해서는 수신되지 않으므로 이 경우 오프라인으로 메세지를 추가하여야 합니다.
유저가 채팅을 입력했다면 `chat`을 보내면 됩니다. 다른 사람이 채팅을 입력한 경우에도 `chat`이 수신됩니다(`ServerInboundMessage<"chat">``ServerOutboundMessage<"chat">`이 다르게 정의되었다는 점에 유의하세요). 자신이 보낸 채팅에 대해서는 수신되지 않으므로 이 경우 오프라인으로 메세지를 추가하여야 합니다. 채팅 문자열의 양 끝 공백을 제거했을 때 문자열이 빈 문자열이거나, 채팅 문자열의 길이가 300을 초과하는 경우 요청이 실패 처리됩니다.
방을 나가고 싶으면 `leaveRoom`을 보내면 됩니다.
### 방장
방을 생성한 유저가 방장 권한을 가지게 됩니다. 방장 여부는 `joinRoom`의 반환값 또는 `updateRoomUser`을 통해 수신됩니다. 방장은 바뀔 수 있음에 유의하세요. 예를 들어 원래 방장이 방에서 나가는 경우, 다른 랜덤한 유저에게 방장 권한이 넘어갑니다. 이 경우 `updateRoomUser`가 수신됩니다.
방장이 원하는 유저에게 권한을 넘길 수 있는 기능은 아직 구현되지 않았습니다.
## 게임
### 요약
#### 게임 전
1. 방장을 제외한 모든 유저가 `ready`를 보내 준비한다.
2. 방장이 `startRound`를 보내 게임 시작을 요청한다.
#### 라운드 1 - 단어 선택 시간
(서버는 방장의 게임 시작 요청이 성공했을 경우 진입)
(클라이언트는 `startRound`를 수신받은 경우 진입)
1. 모든 유저는 `startRound`를 수신받고, 게임 화면으로 전환된다. 해당 메세지에는 모든 유저의 역할이 포함되어 있다. 이들 중 특별히 `drawer` 역할을 배정받았다면, 캔버스에 그릴 수 있도록 준비한다.
2. 만약 본인이 `drawer`라면, `wordSet` 메세지를 수신받게 되는데 여기에는 3가지 선택할 수 있는 단어가 포함되어 있다. 팝업을 띄워서 유저가 단어를 선택한다.
3. 만약 본인이 `drawer`가 아니라면, 아직 라운드의 시간이 흘러가지 않고 대기한다.
4. `drawer``chooseWord`를 통해 자신이 선택하고자 하는 단어를 서버에 보낸다.
#### 라운드 1 - 라운드 진행 (60초)
(서버는 `drawer``chooseWord`를 보낸 경우 진입)
(클라이언트는 `wordChosen`을 수신받은 경우 진입)
1. 모든 유저는 `wordChosen`을 수신받는데, 여기에는 정답 단어의 글자 수 만이 포함되어 있다. 따라서 정답 단어를 밑줄 개수만으로 표시한다.
2. 모든 유저는 라운드의 타이머가 시작되었다는 `timer` 메세지를 수신받는다. 이때 타이머를 동작하고 남은 시간을 동기화한다.
3. `guesser`들은 단어를 채팅에 쳐서 맞춰볼 수 있다.
4. `drawer`는 캔버스에 그림을 그릴 수 있다.
5. 만약 `guesser`가 채팅으로 정답을 보냈다면 해당 유저는 `answerAccepted` 메세지를 수신받게 되고 여기에 이번 라운드의 정답이 포함되어 있다. 그리고 `role`을 통해 해당 유저의 역할이 `winner`로 변경된다.
#### 라운드 1 - 라운드 종료 및 다음 라운드 시작 대기 (5초)
(서버는 남은 시간이 0으로 떨어지거나, `drawer`가 퇴장하거나, 모두가 답을 맞춰 남은 `guesser`가 0명이 된 경우 진입)
(클라이언트는 `finishRound`를 수신받은 경우 진입)
1. 모든 유저는 `finishRound`를 통해 이번 라운드의 정답을 알게 된다.
#### 라운드 2 - 단어 선택 시간
(위와 동일하므로 생략)
...(생략)...
#### 라운드 5 - 라운드 종료 (5초)
(위와 동일하므로 생략)
#### 게임 종료
(서버는 다음 라운드가 없으면 진입, 게임 도중 인원이 2명 미만이 되는 경우 즉시 진입)
(클라이언트는 `finishGame`을 수신받은 경우 진입)
1. 방에 접속 중인 모든 유저는 `finishGame`를 수신받는다. 이 경우, 게임이 종료되었으므로 게임 화면에서 다시 준비 화면으로 전환된다.
### 준비
방장을 제외한 모든 플레이어는 준비를 해야 게임이 시작될 수 있습니다. 서버에 `ready` 메세지를 보내서 준비 상태를 설정할 수 있습니다. 준비 상태로 설정하려면 `ready` 속성을 참, 그렇지 않으면 거짓으로 담아 보내야 합니다. 누군가 `ready`를 하면 `updateRoomUser`를 통해 해당 유저의 준비 상태가 변경됩니다. 방장에게는 준비할 수 있는 버튼 대신에 게임을 시작할 수 있는 버튼이 주어집니다. 모든 플레이어가 준비해야만 버튼이 활성화 되어야 합니다. 방장이 게임 시작 버튼을 누르면 서버에 `startGame`가 전송됩니다. 만약 게임 시작에 실패하면 Response의 `reason`값으로 실패 사유가 전달되므로 이를 유저에게 보여줄 수도 있습니다. 성공적으로 게임이 시작되면 모든 유저에게 `startRound`가 전달됩니다.
방장을 제외한 모든 플레이어는 준비를 해야 게임이 시작될 수 있습니다. 서버에 `ready` 메세지를 보내서 준비 상태를 설정할 수 있습니다. 준비 상태로 설정하려면 `ready` 속성을 참, 그렇지 않으면 거짓으로 담아 보내야 합니다. 누군가 `ready`를 하면 `updateRoomUser`를 통해 해당 유저의 준비 상태가 변경됩니다. 방장에게는 준비할 수 있는 버튼 대신에 게임을 시작할 수 있는 버튼이 주어집니다. 방에 2명 이상의 인원이 접속한 상태에서, 모든 플레이어가 준비해야만 버튼이 활성화 되어야 합니다. 방장이 게임 시작 버튼을 누르면 서버에 `startGame`가 전송됩니다. 만약 게임 시작에 실패하면 Response의 `reason`값으로 실패 사유가 전달되므로 이를 유저에게 보여줄 수도 있습니다. 성공적으로 게임이 시작되면 모든 유저에게 `startRound`가 전달됩니다.
### 라운드 진행
준비 화면에서 라운드가 시작되면 `startRound`가 수신됩니다. `round`는 현재 라운드 넘버 (1부터 시작), `duration`은 현재 라운드의 길이를 초 단위로 나타냅니다. `roles`는 각 플레이어가 이번 라운드에서 맡게 된 역할입니다(후술). 항상 라운드가 시작되면 타이머를 라운드의 길이로 맞춘 뒤 타이머를 정지해주세요. 이때 그림을 그리는 사람이 단어를 선택하게 됩니다. 단어 선택이 끝나면 타이머의 시간이 흐르게 됩니다.
준비 화면에서 라운드가 시작되면 `startRound`가 수신됩니다. 즉, `startRound`를 수신하면 게임 화면으로 전환되어야 합니다. `round`는 현재 라운드 넘버 (1부터 시작), `duration`은 현재 라운드의 길이를 초 단위로 나타냅니다. `roles`는 각 플레이어가 이번 라운드에서 맡게 된 역할입니다(후술). 이제 그림을 그리는 사람이 단어를 선택하게 됩니다. 단어 선택이 끝나면 서버에서 `timer` 메세지를 수신받고 타이머의 시간이 흐르게 됩니다.
서버는 클라이언트 타이머의 상태를 `timer`를 보내서 동기화합니다. `state``started`이면 메세지를 수신한 즉시 타이머를 동작시키고, `stopped`이면 타이머를 일시 정지합니다. 이때 `time`에 남은 시간이 초 단위로 포함되므로, 항상 이 메세지를 수신할 때마다 타이머의 남은 시간을 `time`값으로 동기화해주세요. 일반적으로 이 메세지는 단어 선택이 완료되어 라운드의 시간이 흐르기 시작하는 시점과 라운드가 종료되는 시점에 전송됩니다. 라운드가 종료되면 `state: stopped``timer`가 수신됩니다.
모든 플레이어가 단어를 맞추거나, 타이머의 시간이 0으로 떨어지면 라운드가 종료되면서 `finishRound`가 수신됩니다. 이 메세지는 이번 라운드의 정답을 포함하고 있습니다. 만약 진행할 라운드가 더 남았다면 몇 초 뒤에 다시 `startRound`가 수신될 것입니다. 그러나 이번 라운드가 마지막이었다면 `finishGame`가 수신됩니다. 이는 게임이 정상적으로 종료되었다는 의미이며, 다시 준비 화면으로 전환해주시면 됩니다.
예외적인 케이스로, 이전 라운드가 비정상적으로 종료되었을 때 `finishRound`를 수신받지 않고 `startRound`를 수신받게 될 수 있습니다. 이때 `startRound``round` 넘버가 이전 라운드와 동일한 값으로 수신받게 될 수도 있습니다. 예를 들면 `drawer`가 단어를 선택하지 않고 방에서 나가는 경우 해당 상황이 발생하게 됩니다.
또한 라운드를 진행 도중 누군가 퇴장하여 인원이 모자르게 된 경우, 즉시 `finishGame`을 수신받고 게임이 종료될 수 있습니다.
### 역할
가능한 역할은 `drawer`, `guesser`, `winner`, `spectator`로 구분됩니다. 이는 `startRound`와 함께 수신됩니다. 만약 라운드 진행 중에 역할이 바뀌게 된다면 `role`가 수신됩니다. 이는 단순히 플레이어 목록 UI를 업데이트 하기 위해서 사용되며, 따로 고려할 게임 로직은 없습니다.
......@@ -78,6 +139,7 @@
`guesser`는 정답을 채팅에 입력할 수 있습니다. 만약 정답이라면 채팅이 서버에서 무시되고 역할이 `winner`로 변경되는 `role`이 수신되고, 정답을 담고 있는 `answerAccepted`가 수신됩니다.
만약 답을 맞추지 못했다면 일반 채팅으로 전달됩니다.
시간이 지나 라운드가 종료되고 다음 라운드를 기다리는 도중 답을 채팅에 입력하면 이는 무시되어 정답 처리되지 않습니다.
### winner, spectator
......
<h1 align="center">
스케치퀴즈
</h1>
<p align="center">
<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>
<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>
</p>
......
......@@ -8,6 +8,7 @@ import {
Record,
Union,
Static,
Optional,
} from "runtypes";
import {
Role,
......@@ -47,6 +48,14 @@ export class ServerInboundMessageRecordMap {
ready: Boolean,
});
// 방장이 게임을 시작합니다.
// TODO: 주의! 아래 필드는 디버그 용도로만 사용됩니다. 추후에 준비 화면에서 공개적으로 설정하는 것으로 구현해야 합니다.
startGame = Record({
maxRound: Optional(Number),
roundDuration: Optional(Number),
roundTerm: Optional(Number),
});
// drawer가 단어를 선택합니다.
chooseWord = Record({
word: String,
......
......@@ -25,6 +25,7 @@ export class Connection {
this.socket = socket;
this.roomManager = roomManager;
socket.setHandler((raw) => this.handleRaw(raw));
socket.setDisconnectHandler(() => this.handleDisconnect());
}
public send<T extends ServerOutboundMessageKey>(
......@@ -54,6 +55,14 @@ export class Connection {
}
// Game > Room > User 순으로 전달
if (this.user?.room?.game) {
const response = this.user.room.game.handler.handle(
type,
this.user,
message
);
if (response) return response;
}
if (this.user?.room) {
const response = this.user.room.handler.handle(type, this.user, message);
if (response) return response;
......@@ -73,4 +82,8 @@ export class Connection {
return { ok: true };
}
public handleDisconnect(): void {
this.user?.disconnected();
}
}
......
......@@ -3,6 +3,7 @@ import { RawMessage, ServerResponse } from "../../common";
export interface SocketWrapper {
setHandler: (listener: (raw: RawMessage) => ServerResponse<any>) => void;
setDisconnectHandler: (listener: () => void) => void;
send: (raw: RawMessage) => void;
}
......@@ -19,6 +20,12 @@ export class SocketIoWrapper implements SocketWrapper {
});
}
public setDisconnectHandler(listener: () => void) {
this.socketIo.on("disconnect", () => {
listener();
});
}
public send(raw: RawMessage) {
this.socketIo.emit("msg", raw);
}
......
import { Role } from "../../common/dataType";
import { MessageHandler } from "../message/MessageHandler";
import { Room } from "../room/Room";
import { User } from "../user/User";
export interface Game {
join(user: User): void;
leave(user: User): void;
export class Game {
room: Room;
maxRound: number;
round: number = 0;
roundState: "choosing" | "running" | "done" = "choosing";
roundDuration: number;
readonly roundTerm: number = 5; // 다음 라운드 시작까지 기다리는 시간
wordCandidates: string[] = [];
word?: string;
timer: {
startTimeMillis: number;
timeLeftMillis: number;
running: boolean;
} = { startTimeMillis: 0, timeLeftMillis: 0, running: false };
timeoutTimerId?: NodeJS.Timeout;
nextRoundTimerId?: NodeJS.Timeout;
brush: {
size: number;
color: string;
drawing: boolean;
x: number;
y: number;
} = {
size: 24,
color: "000000",
drawing: false,
x: 0,
y: 0,
};
handler: MessageHandler;
roles: Map<User, Role>;
drawer?: User;
constructor(
room: Room,
maxRound: number,
roundDuration: number,
roundTerm: number
) {
this.room = room;
// TODO: 방장이 설정
this.maxRound = maxRound;
this.roundDuration = roundDuration;
this.roundTerm = roundTerm;
this.handler = new MessageHandler({
chooseWord: (user, message) => {
if (user !== this.drawer || this.roundState !== "choosing") {
return { ok: false };
}
const chosen = message.word;
if (this.wordCandidates.includes(chosen)) {
this.wordSelected(chosen);
return { ok: true };
}
return { ok: false };
},
chat: (user, message) => {
const text = message.message.trim();
if (
this.roles.get(user) === "guesser" &&
this.roundState === "running" &&
text === this.word
) {
this.acceptAnswer(user);
} else {
this.room.sendChat(user, text);
}
return { ok: true };
},
setBrush: (user, message) => {
if (user !== this.drawer || !/^[0-9a-f]{6}$/.test(message.color)) {
return { ok: false };
}
this.brush.size = Math.max(Math.min(message.size, 64), 1);
this.brush.color = message.color;
this.brush.drawing = message.drawing;
this.room.broadcast(
"setBrush",
{
size: this.brush.size,
color: this.brush.color,
drawing: this.brush.drawing,
},
user
);
return { ok: true };
},
moveBrush: (user, message) => {
if (user !== this.drawer) {
return { ok: false };
}
this.brush.x = Math.max(Math.min(message.x, 1), 0);
this.brush.y = Math.max(Math.min(message.y, 1), 0);
this.room.broadcast(
"moveBrush",
{
x: this.brush.x,
y: this.brush.y,
},
user
);
return { ok: true };
},
});
this.roles = new Map<User, Role>();
this.startNextRound();
}
private startNextRound(): void {
this.roundState = "choosing";
this.word = undefined;
this.round++;
this.roles.clear();
this.drawer = this.pickDrawer();
this.room.users.forEach((user) => this.roles.set(user, "guesser"));
this.roles.set(this.drawer, "drawer");
this.room.broadcast("startRound", {
round: this.round,
duration: this.roundDuration,
roles: this.makeRoleArray(),
});
this.wordCandidates = this.pickWords();
this.drawer.connection.send("wordSet", { words: this.wordCandidates });
}
private wordSelected(word: string): void {
this.word = word;
this.roundState = "running";
this.room.broadcast("wordChosen", { length: word.length });
this.startTimer(this.roundDuration * 1000);
this.timeoutTimerId = setTimeout(
() => this.finishRound(),
this.roundDuration * 1000
);
}
public finishRound(): void {
if (this.timeoutTimerId) {
clearTimeout(this.timeoutTimerId);
this.timeoutTimerId = undefined;
}
this.roundState = "done";
this.stopTimer();
if (this.word) {
this.room.broadcast("finishRound", { answer: this.word });
}
this.prepareNextRound();
}
private prepareNextRound(): void {
this.nextRoundTimerId = setTimeout(() => {
if (this.round == this.maxRound) {
this.finishGame();
} else {
this.startNextRound();
}
}, this.roundTerm * 1000);
}
private finishGame(): void {
this.room.broadcast("finishGame", {});
this.room.finishGame();
}
private forceFinishGame() {
if (this.timeoutTimerId) {
clearTimeout(this.timeoutTimerId);
}
if (this.nextRoundTimerId) {
clearTimeout(this.nextRoundTimerId);
}
if (this.word) {
this.room.broadcast("finishRound", { answer: this.word });
}
this.finishGame();
}
private acceptAnswer(user: User): void {
user.connection.send("answerAccepted", { answer: this.word! });
this.changeRole(user, "winner");
let noGuesser = true;
this.roles.forEach((role, user) => {
if (role === "guesser") {
noGuesser = false;
}
});
if (noGuesser) {
this.finishRound();
}
}
private pickDrawer(): User {
const candidates = this.room.users.filter((user) => user !== this.drawer);
return candidates[Math.floor(Math.random() * candidates.length)];
}
private pickWords(): string[] {
return ["장난감", "백화점", "파티"];
}
private startTimer(timeLeftMillis: number): void {
this.timer = {
startTimeMillis: Date.now(),
timeLeftMillis,
running: true,
};
this.room.users.forEach((user) => this.sendTimer(user));
}
private stopTimer(): void {
this.timer = {
...this.timer,
running: false,
};
this.room.users.forEach((user) => this.sendTimer(user));
}
private sendTimer(user: User): void {
user.connection.send("timer", {
state: this.timer.running ? "started" : "stopped",
time: Math.max(
(this.timer.startTimeMillis + this.timer.timeLeftMillis - Date.now()) /
1000,
0
),
});
}
private makeRoleArray(): { username: string; role: Role }[] {
let roleArray: {
username: string;
role: Role;
}[] = [];
this.roles.forEach((role, user) =>
roleArray.push({ username: user.username, role: role })
);
return roleArray;
}
private changeRole(user: User, role: Role) {
this.roles.set(user, role);
this.room.broadcast("role", { username: user.username, role });
}
joined(user: User): void {
this.changeRole(user, "spectator");
this.sendTimer(user);
user.connection.send("startRound", {
round: this.round,
duration: this.roundDuration,
roles: this.makeRoleArray(),
});
if (this.roundState === "done" && this.word) {
user.connection.send("finishRound", {
answer: this.word,
});
}
user.connection.send("setBrush", {
size: this.brush.size,
color: this.brush.color,
drawing: this.brush.drawing,
});
user.connection.send("moveBrush", {
x: this.brush.x,
y: this.brush.y,
});
}
left(user: User): void {
if (this.room.users.length < 2) {
this.forceFinishGame();
return;
}
this.roles.delete(user);
if (user === this.drawer) {
if (this.roundState === "choosing") {
this.round--; // 이번 라운드를 다시 시작
this.startNextRound();
} else if (this.roundState === "running") {
this.finishRound();
}
} else {
let guesserCount = 0;
this.roles.forEach((role, user) => {
if (role === "guesser") {
guesserCount++;
}
});
if (guesserCount < 1) {
if (this.roundState === "choosing") {
this.round--;
this.startNextRound();
} else if (this.roundState === "running") {
this.finishRound();
}
}
}
}
}
......
import { Role } from "../../common/dataType";
import { MessageHandler } from "../message/MessageHandler";
import { Room } from "../room/Room";
import { User } from "../user/User";
import { Game } from "./Game";
export class WorldGuessingGame implements Game {
room: Room;
maxRound: number;
round: number = 0;
roundState: "choosing" | "running" | "done" = "choosing";
roundDuration: number;
readonly roundTerm: number = 5; // 다음 라운드 시작까지 기다리는 시간
wordCandidates: string[] = [];
word: string = "";
timer: {
startTimeMillis: number;
timeLeftMillis: number;
running: boolean;
} = { startTimeMillis: 0, timeLeftMillis: 0, running: false };
timeoutTimerId?: NodeJS.Timeout;
nextRoundTimerId?: NodeJS.Timeout;
brush: {
size: number;
color: string;
drawing: boolean;
x: number;
y: number;
} = {
size: 24,
color: "000000",
drawing: false,
x: 0,
y: 0,
};
handler: MessageHandler;
roles: Map<User, Role>;
drawer?: User;
constructor(room: Room) {
this.room = room;
// TODO: 방장이 설정
this.maxRound = 5;
this.roundDuration = 60;
this.handler = new MessageHandler({
chooseWord: (user, message) => {
if (user !== this.drawer || this.roundState === "choosing") {
return { ok: false };
}
const chosen = message.word;
if (this.wordCandidates.includes(chosen)) {
this.wordSelected(chosen);
return { ok: true };
}
return { ok: false };
},
chat: (user, message) => {
const text = message.message.trim();
if (this.roles.get(user) === "guesser" && text === this.word) {
this.acceptAnswer(user);
} else {
this.room.sendChat(user, text);
}
return { ok: true };
},
setBrush: (user, message) => {
if (user !== this.drawer || !/^[0-9a-f]{6}$/.test(message.color)) {
return { ok: false };
}
this.brush.size = Math.max(Math.min(message.size, 64), 1);
this.brush.color = message.color;
this.brush.drawing = message.drawing;
this.room.broadcast(
"setBrush",
{
size: this.brush.size,
color: this.brush.color,
drawing: this.brush.drawing,
},
user
);
return { ok: true };
},
moveBrush: (user, message) => {
if (user !== this.drawer) {
return { ok: false };
}
this.brush.x = Math.max(Math.min(message.x, 1), 0);
this.brush.y = Math.max(Math.min(message.y, 1), 0);
this.room.broadcast(
"moveBrush",
{
x: this.brush.x,
y: this.brush.y,
},
user
);
return { ok: true };
},
});
this.roles = new Map<User, Role>();
this.startNextRound();
}
private startNextRound(): void {
this.roundState = "choosing";
this.round++;
this.roles.clear();
this.drawer = this.pickDrawer();
this.room.users.forEach((user) => this.roles.set(user, "guesser"));
this.roles.set(this.drawer, "drawer");
this.room.broadcast("startRound", {
round: this.round,
duration: this.roundDuration,
roles: this.makeRoleArray(),
});
this.wordCandidates = this.pickWords();
this.drawer.connection.send("wordSet", { words: this.wordCandidates });
}
private wordSelected(word: string): void {
this.word = word;
this.roundState = "running";
this.room.broadcast("wordChosen", { length: word.length });
this.startTimer(this.roundDuration * 1000);
this.timeoutTimerId = setTimeout(
this.finishRound,
this.roundDuration * 1000
);
}
private finishRound(): void {
if (this.timeoutTimerId) {
clearTimeout(this.timeoutTimerId);
this.timeoutTimerId = undefined;
}
this.roundState = "done";
this.stopTimer();
this.room.broadcast("finishRound", { answer: this.word });
this.prepareNextRound();
}
private prepareNextRound(): void {
this.nextRoundTimerId = setTimeout(() => {
if (this.round == this.maxRound) {
this.finishGame();
} else {
this.startNextRound();
}
}, this.roundTerm * 1000);
}
private finishGame(): void {
this.room.broadcast("finishGame", {});
// TODO
}
private forceFinishGame() {
if (this.timeoutTimerId) {
clearTimeout(this.timeoutTimerId);
}
if (this.nextRoundTimerId) {
clearTimeout(this.nextRoundTimerId);
}
this.room.broadcast("finishRound", { answer: this.word });
this.finishGame();
}
private acceptAnswer(user: User): void {
user.connection.send("answerAccepted", { answer: this.word });
this.changeRole(user, "winner");
}
private pickDrawer(): User {
const candidates = this.room.users.filter((user) => user !== this.drawer);
return candidates[Math.floor(Math.random() * candidates.length)];
}
private pickWords(): string[] {
return ["장난감", "백화점", "파티"];
}
private startTimer(timeLeftMillis: number): void {
this.timer = {
startTimeMillis: Date.now(),
timeLeftMillis,
running: true,
};
}
private stopTimer(): void {
this.timer = {
...this.timer,
running: false,
};
this.room.users.forEach((user) => this.sendTimer(user));
}
private sendTimer(user: User): void {
user.connection.send("timer", {
state: this.timer.running ? "started" : "stopped",
time: Math.max(
(this.timer.startTimeMillis + this.timer.timeLeftMillis - Date.now()) /
1000,
0
),
});
}
private makeRoleArray(): { username: string; role: Role }[] {
let roleArray: {
username: string;
role: Role;
}[] = [];
this.roles.forEach((role, user) =>
roleArray.push({ username: user.username, role: role })
);
return roleArray;
}
private changeRole(user: User, role: Role) {
this.roles.set(user, role);
this.room.broadcast("role", { username: user.username, role });
}
join(user: User): void {
this.changeRole(user, "spectator");
this.sendTimer(user);
user.connection.send("startRound", {
round: this.round,
duration: this.roundDuration,
roles: this.makeRoleArray(),
});
if (this.roundState === "done") {
user.connection.send("finishRound", {
answer: this.word,
});
}
user.connection.send("setBrush", {
size: this.brush.size,
color: this.brush.color,
drawing: this.brush.drawing,
});
user.connection.send("moveBrush", {
x: this.brush.x,
y: this.brush.y,
});
}
leave(user: User): void {
if (this.room.users.length < 2) {
this.forceFinishGame();
return;
}
this.roles.delete(user);
if (user === this.drawer) {
if (this.roundState === "choosing") {
this.round--; // 이번 라운드를 다시 시작
this.startNextRound();
} else if (this.roundState === "running") {
this.finishRound();
}
} else {
let guesserCount = 0;
this.roles.forEach((role, user) => {
if (role === "guesser") {
guesserCount++;
}
});
if (guesserCount < 1) {
if (this.roundState === "choosing") {
this.round--;
this.startNextRound();
} else if (this.roundState === "running") {
this.finishRound();
}
}
}
}
}
......@@ -16,7 +16,7 @@
},
"scripts": {
"start": "nodemon index.ts",
"test": "nyc mocha -r ts-node/register ./**/*.test.ts",
"test": "nyc mocha -r ts-node/register --timeout 8000 ./**/*.test.ts",
"build": "tsc -b -v"
},
"devDependencies": {
......
......@@ -9,6 +9,7 @@ import {
} from "../../common";
import { RoomDescription, RoomInfo, UserData } from "../../common/dataType";
import { RoomManager } from "./RoomManager";
import { Game } from "../game/Game";
export class Room {
public readonly uuid: string;
......@@ -22,6 +23,8 @@ export class Room {
public usersReady: User[] = [];
public admin?: User;
public game?: Game;
public closed: boolean = false;
public handler: MessageHandler;
......@@ -60,6 +63,33 @@ export class Room {
this.setReady(user, message.ready);
return { ok: true };
},
startGame: (user, message) => {
if (user !== this.admin) {
return { ok: false };
}
const result = this.canStart();
if (!result.ok) {
return result;
}
// TODO: 방장이 따로 메세지를 보내 설정할 수 있도록 수정해주세요.
const settings = message;
if (!settings.maxRound) {
settings.maxRound = 5;
}
if (!settings.roundDuration) {
settings.roundDuration = 60;
}
if (!settings.roundTerm) {
settings.roundTerm = 5;
}
this.startGame(
settings.maxRound,
settings.roundDuration,
settings.roundTerm
);
return { ok: true };
},
});
if (this.admin) {
......@@ -92,6 +122,8 @@ export class Room {
this.usersReady = this.usersReady.filter((u) => u !== user);
user.room = undefined;
this.game?.left(user);
this.broadcast("updateRoomUser", {
state: "removed",
user: {
......@@ -149,16 +181,35 @@ export class Room {
return this.usersReady.includes(user);
}
public canStart(): boolean {
public canStart(): { ok: boolean; reason?: string } {
if (this.isPlayingGame()) {
return { ok: false, reason: "이미 게임이 진행 중입니다." };
}
if (this.users.length < 2) {
return false;
return { ok: false, reason: "최소 2명의 플레이어가 필요합니다." };
}
for (let i = 0; i < this.users.length; i++) {
if (!this.isAdmin(this.users[i]) && !this.isReady(this.users[i])) {
return false;
return { ok: false, reason: "모든 플레이어가 준비해야 합니다." };
}
}
return true;
return { ok: true };
}
private startGame(
maxRound: number,
roundDuration: number,
roundTerm: number
): void {
this.game = new Game(this, maxRound, roundDuration, roundTerm);
}
public finishGame(): void {
this.game = undefined;
}
public isPlayingGame(): boolean {
return this.game !== undefined;
}
public sendChat(user: User, message: string): void {
......
import { expect } from "chai";
import { prepareGame } from "./util/prepare";
describe("라운드 단어 선택", () => {
it("drawer가 단어를 선택하면 wordChosen과 timer를 받습니다", () => {
const { drawerSocket, guesserSockets } = prepareGame(2);
const word = drawerSocket.socket.received("wordSet").words[0];
drawerSocket.testOk("chooseWord", { word });
expect(drawerSocket.socket.received("wordChosen").length).eq(word.length);
drawerSocket.socket.received("timer");
expect(guesserSockets[0].socket.received("wordChosen").length).eq(
word.length
);
guesserSockets[0].socket.received("timer");
});
it("drawer가 아닌 다른 사람들은 단어를 선택할 수 없습니다", () => {
const { drawerSocket, guesserSockets } = prepareGame(2);
const word = drawerSocket.socket.received("wordSet").words[0];
guesserSockets[0].testNotOk("chooseWord", { word });
});
it("단어를 이미 고른 상태에서 다시 고를 수 없습니다", () => {
const { drawerSocket } = prepareGame(2);
const word = drawerSocket.socket.received("wordSet").words[0];
drawerSocket.testOk("chooseWord", { word });
drawerSocket.testNotOk("chooseWord", { word });
});
it("목록에 없는 단어를 고를 수 없습니다", () => {
const { drawerSocket } = prepareGame(2);
drawerSocket.testNotOk("chooseWord", { word: "Nope!" });
});
});
import { expect } from "chai";
import { prepareGame } from "./util/prepare";
describe("라운드 브러시 이동", () => {
it("drawer가 브러시를 이동하면 다른 사람들이 설정을 받습니다", () => {
const { drawerSocket, guesserSockets } = prepareGame(2);
const brushCoord = { x: 0, y: 0 };
drawerSocket.testOk("moveBrush", brushCoord);
expect(guesserSockets[0].socket.received("moveBrush")).deep.eq(brushCoord);
});
it("영역을 벗어난 좌표는 Clamp 처리됩니다", () => {
const { drawerSocket, guesserSockets } = prepareGame(2);
drawerSocket.testOk("moveBrush", { x: -1, y: 2 });
expect(guesserSockets[0].socket.received("moveBrush")).deep.eq({
x: 0,
y: 1,
});
});
it("drawer가 아닌 다른 사람들은 브러시를 이동할 수 없습니다", () => {
const { guesserSockets } = prepareGame(2);
const brushCoord = { x: 0, y: 0 };
guesserSockets[0].testNotOk("moveBrush", brushCoord);
});
});
......@@ -23,10 +23,28 @@ describe("준비", () => {
} = prepareJoinedRoom(1, 2, true);
expect(room.isReady(user)).eq(false);
socket.testOk("ready", { ready: true });
expect(room.isReady(user)).eq(true);
expect(socket.socket.received("updateRoomUser")).deep.eq({
state: "updated",
user: {
username: user.username,
admin: false,
ready: true,
},
});
socket.testOk("ready", { ready: false });
expect(room.isReady(user)).eq(false);
expect(socket.socket.received("updateRoomUser")).deep.eq({
state: "updated",
user: {
username: user.username,
admin: false,
ready: false,
},
});
});
it("방장은 준비할 수 없습니다", () => {
const {
......@@ -53,7 +71,7 @@ describe("준비", () => {
it("혼자 있는 방에서는 게임을 시작할 수 없습니다", () => {
const { room } = prepareJoinedRoom(1);
expect(room.canStart()).eq(false);
expect(room.canStart().ok).eq(false);
});
it("모두가 준비해야 게임을 시작할 수 있습니다", () => {
const {
......@@ -62,7 +80,7 @@ describe("준비", () => {
} = prepareJoinedRoom(3);
// 2, 3 모두 준비 안함
expect(room.canStart()).eq(false);
expect(room.canStart().ok).eq(false);
// 2만 준비
expect(socket2.test("ready", { ready: true }).ok).eq(true);
......@@ -70,10 +88,10 @@ describe("준비", () => {
// 3만 준비
expect(socket2.test("ready", { ready: false }).ok).eq(true);
expect(socket3.test("ready", { ready: true }).ok).eq(true);
expect(room.canStart()).eq(false);
expect(room.canStart().ok).eq(false);
// 2, 3 모두 준비
expect(socket2.test("ready", { ready: true }).ok).eq(true);
expect(room.canStart()).eq(true);
expect(room.canStart().ok).eq(true);
});
});
......
import { expect } from "chai";
import { prepareGame } from "./util/prepare";
describe("라운드", () => {
it("첫 라운드가 시작되면 startRound와 wordSet을 받습니다", () => {
const {
sockets: [socket1, socket2],
drawerSocket,
} = prepareGame(2);
expect(socket1.socket.received("startRound").round).eq(1);
expect(socket2.socket.received("startRound").round).eq(1);
// drawer는 wordSet을 받습니다.
expect(drawerSocket.socket.received("wordSet").words.length).eq(3);
});
it("drawer가 단어를 선택하면 모두가 wordChosen과 timer를 받습니다", () => {
const { drawerSocket, guesserSockets } = prepareGame(2);
const word = drawerSocket.socket.received("wordSet").words[0];
drawerSocket.testOk("chooseWord", { word });
expect(drawerSocket.socket.received("wordChosen").length).eq(word.length);
expect(guesserSockets[0].socket.received("wordChosen").length).eq(
word.length
);
let timerSettings = drawerSocket.socket.received("timer");
expect(timerSettings.state).eq(timerSettings.state);
expect(timerSettings.time).greaterThan(59);
});
it("drawer가 단어를 선택하지 않으면 라운드가 진행되지 않습니다", (done) => {
const { drawerSocket, guesserSockets } = prepareGame(2, 5, 0.1);
// 0.2초 뒤에도 라운드가 종료되지 않습니다.
setTimeout(() => {
drawerSocket.socket.notReceived("finishRound");
guesserSockets[0].socket.notReceived("finishRound");
done();
}, 200);
});
it("아무도 단어를 맞추지 못하고 시간이 지나면 라운드가 종료됩니다", (done) => {
const { drawerSocket, guesserSockets } = prepareGame(2, 5, 0.2);
const word = drawerSocket.socket.received("wordSet").words[0];
drawerSocket.testOk("chooseWord", { word });
// 0.1초 뒤에는 라운드가 종료되지 않습니다.
setTimeout(() => {
drawerSocket.socket.notReceived("finishRound");
guesserSockets[0].socket.notReceived("finishRound");
}, 100);
// 0.3초 뒤에는 라운드가 종료됩니다.
setTimeout(() => {
expect(drawerSocket.socket.received("finishRound").answer).eq(word);
expect(guesserSockets[0].socket.received("finishRound").answer).eq(word);
done();
}, 300);
});
it("모든 guesser가 단어를 맞추면 라운드가 종료됩니다", (done) => {
const { drawerSocket, guesserSockets } = prepareGame(3, 5, 0.5);
const word = drawerSocket.socket.received("wordSet").words[0];
drawerSocket.testOk("chooseWord", { word });
// 0.1초 뒤에는 라운드가 종료되지 않습니다.
setTimeout(() => {
drawerSocket.socket.notReceived("finishRound");
// 첫번째 guesser가 단어를 맞춥니다.
guesserSockets[0].testOk("chat", { message: word });
expect(guesserSockets[0].socket.received("answerAccepted").answer).eq(
word
);
}, 100);
// 0.2초 뒤에도 라운드가 종료되지 않습니다.
setTimeout(() => {
drawerSocket.socket.notReceived("finishRound");
// 두번째 guesser가 단어를 맞춥니다.
guesserSockets[1].testOk("chat", { message: word });
expect(guesserSockets[1].socket.received("answerAccepted").answer).eq(
word
);
}, 200);
// 0.3초 뒤에는 라운드가 종료됩니다.
setTimeout(() => {
drawerSocket.socket.received("finishRound");
done();
}, 300);
});
it("drawer가 단어를 선택하지 않고 나가면 즉시 라운드가 다시 시작됩니다", () => {
const { drawerSocket, guesserSockets } = prepareGame(3);
guesserSockets[0].socket.received("startRound");
guesserSockets[0].socket.notReceived("startRound");
drawerSocket.disconnect();
expect(guesserSockets[0].socket.received("startRound").round).eq(1);
});
it("drawer가 단어를 선택하지 않고 모든 guesser가 나가면 인원이 부족하므로 게임이 종료됩니다", () => {
const { drawerSocket, guesserSockets } = prepareGame(3);
drawerSocket.socket.notReceived("finishRound");
guesserSockets[0].disconnect();
drawerSocket.socket.notReceived("finishRound");
guesserSockets[1].disconnect();
// 단어가 선택되지 않았으므로 finishRound가 수신되지 않습니다.
drawerSocket.socket.received("finishGame");
});
it("drawer가 단어를 선택하고 모든 guesser가 나가면 인원이 부족하므로 게임이 종료됩니다", () => {
const { drawerSocket, guesserSockets } = prepareGame(3);
const word = drawerSocket.socket.received("wordSet").words[0];
drawerSocket.testOk("chooseWord", { word });
drawerSocket.socket.notReceived("finishRound");
guesserSockets[0].disconnect();
drawerSocket.socket.notReceived("finishRound");
guesserSockets[1].disconnect();
drawerSocket.socket.received("finishRound");
drawerSocket.socket.received("finishGame");
});
it("drawer가 단어를 선택하고 나가면 라운드가 종료됩니다", () => {
const { drawerSocket, guesserSockets } = prepareGame(3);
const word = drawerSocket.socket.received("wordSet").words[0];
drawerSocket.testOk("chooseWord", { word });
guesserSockets[0].socket.notReceived("finishRound");
drawerSocket.disconnect();
guesserSockets[0].socket.received("finishRound");
guesserSockets[0].socket.notReceived("finishGame");
});
it("라운드가 종료되고 다음 라운드를 기다리는 동안 drawer가 나가도 다음 라운드가 시작됩니다", (done) => {
const { drawerSocket, guesserSockets } = prepareGame(3, 5, 5, 0.1);
guesserSockets[0].socket.received("startRound");
const word = drawerSocket.socket.received("wordSet").words[0];
drawerSocket.testOk("chooseWord", { word });
guesserSockets[0].testOk("chat", { message: word });
guesserSockets[1].testOk("chat", { message: word });
guesserSockets[0].socket.received("finishRound");
guesserSockets[0].socket.notReceived("startRound");
drawerSocket.disconnect();
setTimeout(() => {
expect(guesserSockets[0].socket.received("startRound").round).eq(2);
done();
}, 200);
});
it("라운드가 종료되고 다음 라운드를 기다리는 동안 인원이 부족해지면 게임이 즉시 종료됩니다", () => {
const { drawerSocket, guesserSockets } = prepareGame(2, 5, 5, 0.1);
guesserSockets[0].socket.received("startRound");
const word = drawerSocket.socket.received("wordSet").words[0];
drawerSocket.testOk("chooseWord", { word });
guesserSockets[0].testOk("chat", { message: word });
drawerSocket.socket.received("finishRound");
guesserSockets[0].disconnect();
drawerSocket.socket.received("finishGame");
});
it("라운드가 종료되면 다음 라운드가 시작됩니다", (done) => {
const { drawerSocket, guesserSockets } = prepareGame(2, 5, 0.2, 0.2);
drawerSocket.socket.received("startRound");
guesserSockets[0].socket.received("startRound");
const word = drawerSocket.socket.received("wordSet").words[0];
drawerSocket.testOk("chooseWord", { word });
// 0.1초 뒤에는 라운드가 종료되지 않습니다.
setTimeout(() => {
drawerSocket.socket.notReceived("finishRound");
guesserSockets[0].socket.notReceived("finishRound");
}, 100);
// 0.3초 뒤에는 라운드가 종료됩니다.
setTimeout(() => {
expect(drawerSocket.socket.received("finishRound").answer).eq(word);
expect(guesserSockets[0].socket.received("finishRound").answer).eq(word);
drawerSocket.socket.notReceived("startRound");
}, 300);
// 0.5초 뒤에는 다음 라운드가 시작됩니다.
setTimeout(() => {
expect(drawerSocket.socket.received("startRound").round).eq(2);
expect(guesserSockets[0].socket.received("startRound").round).eq(2);
done();
}, 500);
});
it("마지막 라운드가 종료되면 게임이 종료됩니다", (done) => {
const { drawerSocket } = prepareGame(2, 1, 0.1, 0.2);
const word = drawerSocket.socket.received("wordSet").words[0];
drawerSocket.testOk("chooseWord", { word });
setTimeout(() => {
drawerSocket.socket.received("finishRound");
drawerSocket.socket.notReceived("finishGame");
}, 200);
setTimeout(() => {
drawerSocket.socket.received("finishGame");
done();
}, 400);
});
});
import { expect } from "chai";
import { prepareGame } from "./util/prepare";
describe("라운드 채팅", () => {
it("guesser가 정답을 채팅으로 보내면 정답 처리되고 다른 사람들에게 채팅이 보이지 않습니다", () => {
const { drawerSocket, guesserSockets } = prepareGame(3);
const word = drawerSocket.socket.received("wordSet").words[0];
drawerSocket.testOk("chooseWord", { word });
guesserSockets[0].testOk("chat", { message: "Not Answer" });
guesserSockets[0].socket.notReceived("answerAccepted");
guesserSockets[1].socket.received("chat");
guesserSockets[0].testOk("chat", { message: word });
expect(guesserSockets[0].socket.received("answerAccepted").answer).eq(word);
guesserSockets[1].socket.notReceived("chat");
});
it("guesser가 정답을 채팅으로 보내면 역할이 winner로 변경됩니다", () => {
const { drawerSocket, guesserSockets } = prepareGame(2);
const word = drawerSocket.socket.received("wordSet").words[0];
drawerSocket.testOk("chooseWord", { word });
guesserSockets[0].testOk("chat", { message: word });
expect(guesserSockets[0].socket.received("role")).deep.eq({
username: guesserSockets[0].connection.user?.username,
role: "winner",
});
expect(drawerSocket.socket.received("role")).deep.eq({
username: guesserSockets[0].connection.user?.username,
role: "winner",
});
});
it("라운드가 끝나고 다음 라운드를 준비하는 시간에 답을 채팅으로 보내도 정답 처리되지 않습니다", (done) => {
const { drawerSocket, guesserSockets } = prepareGame(2, 5, 0.1, 0.3);
const word = drawerSocket.socket.received("wordSet").words[0];
drawerSocket.testOk("chooseWord", { word });
guesserSockets[0].socket.notReceived("finishRound");
setTimeout(() => {
guesserSockets[0].socket.received("finishRound");
guesserSockets[0].testOk("chat", { message: word });
guesserSockets[0].socket.notReceived("answerAccepted");
guesserSockets[0].socket.notReceived("role");
done();
}, 200);
});
it("다음 라운드의 단어가 선택되지 않았을 때 이전 라운드의 답을 채팅으로 보내도 정답 처리되지 않습니다", (done) => {
const { drawerSocket, guesserSockets, game } = prepareGame(2, 5, 0.2, 0.1);
const word = drawerSocket.socket.received("wordSet").words[0];
drawerSocket.testOk("chooseWord", { word });
expect(guesserSockets[0].socket.received("startRound").round).eq(1);
setTimeout(() => {
expect(guesserSockets[0].socket.received("startRound").round).eq(2);
if (game.drawer === drawerSocket.connection.user) {
guesserSockets[0].testOk("chat", { message: word });
guesserSockets[0].socket.notReceived("answerAccepted");
} else if (game.drawer === guesserSockets[0].connection.user) {
drawerSocket.testOk("chat", { message: word });
drawerSocket.socket.notReceived("answerAccepted");
} else {
throw new Error("There is no drawer!");
}
done();
}, 400);
});
});
import { expect } from "chai";
import { prepareGame } from "./util/prepare";
describe("라운드 브러시 설정", () => {
it("drawer가 브러시를 설정하면 다른 사람들이 설정을 받습니다", () => {
const { drawerSocket, guesserSockets } = prepareGame(2);
const brushSettings = {
size: 1,
color: "000000",
drawing: true,
};
drawerSocket.testOk("setBrush", brushSettings);
expect(guesserSockets[0].socket.received("setBrush")).deep.eq(
brushSettings
);
});
it("올바르지 않은 브러시 색상은 허용되지 않습니다", () => {
const { drawerSocket } = prepareGame(2);
drawerSocket.testNotOk("setBrush", {
size: 1,
color: "000",
drawing: true,
});
drawerSocket.testNotOk("setBrush", {
size: 1,
color: "asdf01",
drawing: true,
});
});
it("올바르지 않은 브러시 사이즈는 Clamp 됩니다", () => {
const { drawerSocket, guesserSockets } = prepareGame(2);
drawerSocket.testOk("setBrush", {
size: 0,
color: "000000",
drawing: true,
});
expect(guesserSockets[0].socket.received("setBrush").size).eq(1);
drawerSocket.testOk("setBrush", {
size: 100,
color: "000000",
drawing: true,
});
expect(guesserSockets[0].socket.received("setBrush").size).eq(64);
});
it("drawer가 아닌 다른 사람들은 브러시를 설정할 수 없습니다", () => {
const { guesserSockets } = prepareGame(2);
const brushSettings = {
size: 1,
color: "000000",
drawing: true,
};
guesserSockets[0].testNotOk("setBrush", brushSettings);
});
});
import { expect } from "chai";
import { prepareJoinedRoom, prepareUsersEmptyRooms } from "./util/prepare";
describe("게임 시작", () => {
it("방장만 게임 시작을 요청할 수 있습니다.", () => {
const {
sockets: [socket1, socket2],
room,
} = prepareJoinedRoom(2);
expect(room.admin).eq(socket1.connection.user);
expect(socket2.testOk("ready", { ready: true }));
expect(room.canStart().ok).eq(true);
expect(socket2.testNotOk("startGame", {}));
expect(socket1.testOk("startGame", {}));
});
it("인원이 충분해야 게임을 시작할 수 있습니다.", () => {
const {
sockets: [socket1],
} = prepareJoinedRoom(1);
expect(socket1.testNotOk("startGame", {}));
});
it("게임이 시작되면 startRound를 받습니다.", () => {
const {
sockets: [socket1, socket2],
} = prepareJoinedRoom(2);
expect(socket2.testOk("ready", { ready: true }));
expect(socket1.testOk("startGame", {}));
expect(socket1.socket.received("startRound"));
expect(socket2.socket.received("startRound"));
});
});
......@@ -9,11 +9,12 @@ import { SocketWrapper } from "../../connection/SocketWrapper";
export class DummySocket implements SocketWrapper {
public handler?: (raw: RawMessage) => ServerResponse<any>;
public disconnectHandler?: () => void;
public receivedMessages: RawMessage[] = [];
public setHandler(handler: (raw: RawMessage) => ServerResponse<any>) {
this.handler = handler;
}
public setHandler(handler: (raw: RawMessage) => ServerResponse<any>) {}
public setDisconnectHandler(handler: () => void) {}
public send(raw: RawMessage): void {
this.receivedMessages.push(raw);
......
......@@ -53,4 +53,8 @@ export class SocketTester {
this.testOk("login", { username });
expect(this.connection.user !== undefined).eq(true);
}
public disconnect(): void {
this.connection.handleDisconnect();
}
}
......
import { Game } from "../../game/Game";
import { Room } from "../../room/Room";
import { RoomManager } from "../../room/RoomManager";
import { User } from "../../user/User";
......@@ -92,3 +93,58 @@ export function prepareJoinedRoom(
}
return { sockets, users, room };
}
export function prepareGame(
userCount: number,
maxRound: number = 5,
roundDuration: number = 60,
roundTerm: number = 5,
roomMaxConnections: number = 2
): {
sockets: SocketTester[];
users: User[];
room: Room;
game: Game;
drawerSocket: SocketTester;
guesserSockets: SocketTester[];
} {
const { sockets, users, room } = prepareJoinedRoom(
userCount,
roomMaxConnections
);
for (let i = 1; i < userCount; i++) {
sockets[i].testOk("ready", { ready: true });
}
sockets[0].testOk("startGame", { maxRound, roundDuration, roundTerm });
if (!room.game) {
throw new Error("Game is not initialized.");
}
let drawerSocket = undefined;
let guesserSockets: SocketTester[] = [];
sockets.forEach((socket) => {
if (socket.connection.user === room.game?.drawer) {
drawerSocket = socket;
} else {
guesserSockets.push(socket);
}
});
if (!drawerSocket) {
throw new Error("There is no drawer!");
}
if (guesserSockets.length == 0) {
throw new Error("There is no guesser!");
}
return {
sockets,
users,
room,
game: room.game,
drawerSocket,
guesserSockets,
};
}
......
......@@ -34,4 +34,8 @@ export class User {
},
});
}
public disconnected(): void {
this.room?.disconnect(this);
}
}
......