강동현

Game 파일 이동 및 버그 수정

......@@ -8,6 +8,7 @@ import {
Record,
Union,
Static,
Optional,
} from "runtypes";
import {
Role,
......@@ -48,7 +49,12 @@ export class ServerInboundMessageRecordMap {
});
// 방장이 게임을 시작합니다.
startGame = Record({});
// TODO: 주의! 아래 필드는 디버그 용도로만 사용됩니다. 추후에 준비 화면에서 공개적으로 설정하는 것으로 구현해야 합니다.
startGame = Record({
maxRound: Optional(Number),
roundDuration: Optional(Number),
roundTerm: Optional(Number),
});
// drawer가 단어를 선택합니다.
chooseWord = Record({
......
......@@ -54,6 +54,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;
......
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" && 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
);
}
public 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", {});
this.room.finishGame();
}
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");
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 });
}
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();
}
}
}
}
}
......
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", {});
this.room.finishGame();
}
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": {
......
......@@ -10,7 +10,6 @@ import {
import { RoomDescription, RoomInfo, UserData } from "../../common/dataType";
import { RoomManager } from "./RoomManager";
import { Game } from "../game/Game";
import { WorldGuessingGame } from "../game/WordGuessingGame";
export class Room {
public readonly uuid: string;
......@@ -72,7 +71,23 @@ export class Room {
if (!result.ok) {
return result;
}
this.startGame();
// 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 };
},
});
......@@ -179,8 +194,12 @@ export class Room {
return { ok: true };
}
private startGame(): void {
this.game = new WorldGuessingGame(this);
private startGame(
maxRound: number,
roundDuration: number,
roundTerm: number
): void {
this.game = new Game(this, maxRound, roundDuration, roundTerm);
}
public finishGame(): void {
......