Room.ts 7.28 KB
import { Connection } from "../connection/Connection";
import { v4 as uuidv4 } from "uuid";
import { User } from "../user/User";
import { MessageHandler } from "../message/MessageHandler";
import {
  ServerInboundMessage,
  ServerOutboundMessage,
  ServerOutboundMessageKey,
} 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;

  public name: string;
  public readonly maxUsers: number;

  public readonly roomManager: RoomManager;

  public users: User[] = [];
  public usersReady: User[] = [];
  public admin?: User;

  public game?: Game;

  public closed: boolean = false;

  public handler: MessageHandler;

  public pingTimeout: NodeJS.Timeout;

  constructor(
    roomManager: RoomManager,
    name: string,
    admin?: User,
    maxUsers: number = 8
  ) {
    this.uuid = uuidv4();
    this.name = name;
    this.maxUsers = maxUsers;
    this.roomManager = roomManager;
    this.admin = admin;

    this.handler = new MessageHandler({
      chat: (user, message) => {
        if (
          message.message.length > 300 ||
          message.message.trim().length == 0
        ) {
          return { ok: false };
        }
        this.sendChat(user, message.message);
        return { ok: true };
      },
      leaveRoom: (user, message) => {
        this.disconnect(user);
        return { ok: true };
      },
      ready: (user, message) => {
        if (user === this.admin) {
          return { ok: false };
        }
        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) {
      this.connect(this.admin);
    }

    this.pingTimeout = setInterval(() => this.ping(), 1000);
  }

  public connect(user: User): void {
    if (
      this.users.includes(user) ||
      this.users.length >= this.maxUsers ||
      this.game
    ) {
      return;
    }

    this.broadcast("updateRoomUser", {
      state: "added",
      user: {
        username: user.username,
        nickname: user.nickname,
        admin: user === this.admin,
        ready: this.usersReady.includes(user),
      },
    });

    this.users.push(user);
    user.room = this;

    user.lastPong = Date.now();
  }

  public disconnect(user: User): void {
    const index = this.users.indexOf(user);
    if (index > -1) {
      this.users.splice(index, 1);
      this.usersReady = this.usersReady.filter((u) => u !== user);
      user.room = undefined;

      this.game?.left(user);

      this.broadcast("updateRoomUser", {
        state: "removed",
        user: {
          username: user.username,
          nickname: user.nickname,
          admin: user === this.admin,
          ready: this.usersReady.includes(user),
        },
      });

      if (this.users.length === 0) {
        this.close();
      } else {
        this.setNextAdmin();
      }
    }
  }

  public ping(): void {
    this.users.forEach((u) => {
      u.connection.sendPing();
      if (Date.now() - u.lastPong > 2000) {
        this.disconnect(u);
      }
    });
  }

  public pong(user: User) {
    if (!this.users.includes(user)) {
      return { ok: false };
    }
    user.lastPong = Date.now();
    return { ok: true };
  }

  public setAdmin(user: User) {
    if (this.users.includes(user)) {
      const prevAdmin = this.admin;
      this.admin = user;
      this.usersReady = this.usersReady.filter((u) => u !== user);
      if (prevAdmin) {
        this.updateUserStatus(prevAdmin);
      }
      this.updateUserStatus(user);
    }
  }

  private setNextAdmin(): void {
    const nextAdmin = this.users[Math.floor(Math.random() * this.users.length)];
    this.setAdmin(nextAdmin);
  }

  public isAdmin(user: User): boolean {
    return this.admin === user;
  }

  public clearReady(): void {
    this.usersReady = [];
  }

  public setReady(user: User, ready: boolean) {
    if (this.users.includes(user) && this.admin !== user) {
      if (ready && !this.usersReady.includes(user)) {
        this.usersReady.push(user);
      } else if (!ready && this.usersReady.includes(user)) {
        this.usersReady.splice(this.usersReady.indexOf(user), 1);
      }
      this.updateUserStatus(user);
    }
  }

  public isReady(user: User): boolean {
    return this.usersReady.includes(user);
  }

  public canStart(): { ok: boolean; reason?: string } {
    if (this.isPlayingGame()) {
      return { ok: false, reason: "이미 게임이 진행 중입니다." };
    }
    if (this.users.length < 2) {
      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 { ok: false, reason: "모든 플레이어가 준비해야 합니다." };
      }
    }
    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 {
    this.broadcast("chat", { sender: user.nickname, message: message });
  }

  private updateUserStatus(user: User) {
    this.broadcast("updateRoomUser", {
      state: "updated",
      user: {
        username: user.username,
        nickname: user.nickname,
        admin: this.isAdmin(user),
        ready: this.isReady(user),
      },
    });
  }

  public getDescription(): RoomDescription {
    return {
      uuid: this.uuid,
      name: this.name,
      currentUsers: this.users.length,
      maxUsers: this.maxUsers,
    };
  }

  public getInfo(): RoomInfo {
    return {
      uuid: this.uuid,
      name: this.name,
      maxUsers: this.maxUsers,
      users: this.users.map((u) => {
        return {
          username: u.username,
          nickname: u.nickname,
          admin: this.isAdmin(u),
          ready: this.usersReady.includes(u),
        };
      }),
    };
  }

  public broadcast<T extends ServerOutboundMessageKey>(
    type: T,
    message: ServerOutboundMessage<T>,
    except?: User
  ): void {
    this.users.forEach((u) => {
      if (u !== except) {
        u.connection.send(type, message);
      }
    });
  }

  public close(): void {
    if (!this.closed) {
      this.users.forEach((u) => this.disconnect(u));
      this.closed = true;
      this.roomManager.delete(this.uuid);
      clearInterval(this.pingTimeout);
    }
  }
}