WordGuessingGame.ts 7.43 KB
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();
        }
      }
    }
  }
}