강동현

게임 로직 작성

......@@ -68,6 +68,7 @@
이 정보들을 가지고 캔버스를 칠하는 컴포넌트를 만들어서, 이를 `drawer`의 클라이언트에도 동일하게 사용하는 방식으로 구현하여 `drawer`와 다른 플레이어의 캔버스가 동일하게 보이도록 해야 할 것입니다.
캔버스의 크기: 512x384 (4:3) (추후 변경 가능)
브러시 사이즈: 1 ~ 64px
### guesser
......@@ -83,10 +84,12 @@
게임 도중 입장한 유저에게는 다음과 같은 메세지들이 모두 전송됩니다.
1. 준비 상태에서 자신이 방에 접속했을 때 전달 받는 모든 메세지들
2. 현재 라운드에 대한 정보를 담은 `startRound`
3. 현재 라운드 타이머와 동기화할 수 있는 `timer`
4. 마지막으로 서버상으로 기록된 브러시 정보를 담은 `setBrush`
5. 마지막으로 서버상으로 기록된 브러시 위치를 담은 `moveBrush`
2. 현재 라운드 타이머와 동기화할 수 있는 `timer`
3. 현재 라운드에 대한 정보를 담은 `startRound`
4. 현재 라운드가 종료되었고, 다음 라운드를 기다리고 있는 중이라면 이번 라운드의 답을 담은 `finishRound`
5. 마지막으로 서버상으로 기록된 브러시 정보를 담은 `setBrush`
6. 마지막으로 서버상으로 기록된 브러시 위치를 담은 `moveBrush`
// TODO: 중도 입장 유저에게는 비트맵을 전송하는 방식 고려해보기
다른 플레이어에게는 다음과 같은 메세지들이 모두 전송됩니다.
......
......@@ -21,3 +21,5 @@ export interface RoomInfo {
export interface UserData {
username: string;
}
export type Role = "drawer" | "guesser" | "winner" | "spectator";
......
import { roomChatHandler } from "../message/handler/roomChatHandler";
import { Role } from "../../common/dataType";
import { MessageHandler } from "../message/MessageHandler";
import { Room } from "../room/Room";
import { User } from "../user/User";
import { Game } from "./Game";
......@@ -6,7 +7,37 @@ import { Game } from "./Game";
export class WorldGuessingGame implements Game {
room: Room;
maxRound: number;
round: 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;
......@@ -16,14 +47,264 @@ export class WorldGuessingGame implements Game {
// TODO: 방장이 설정
this.maxRound = 5;
this.round = 1;
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 {
throw new Error("Method not implemented.");
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 {
throw new Error("Method not implemented.");
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();
}
}
}
}
}
......