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

Merge branch 'feature/test-game' into develop

This diff is collapsed. Click to expand it.
<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 { ok: true };
}
private startGame(
maxRound: number,
roundDuration: number,
roundTerm: number
): void {
this.game = new Game(this, maxRound, roundDuration, roundTerm);
}
return true;
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
);
expect(drawerSocket.socket.received("timer")).deep.eq({
state: "started",
time: 60,
});
expect(guesserSockets[0].socket.received("timer")).deep.eq({
state: "started",
time: 60,
});
});
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);
}
}
......