{
  "version": 3,
  "sources": ["../../src/rooms/QueueRoom.ts"],
  "sourcesContent": ["import { Room } from '../Room.ts';\nimport type { Client } from '../Transport.ts';\nimport type { IRoomCache } from '../matchmaker/driver.ts';\nimport * as matchMaker from '../MatchMaker.ts';\nimport { debugMatchMaking } from '../Debug.ts';\nimport { ServerError } from '../errors/ServerError.ts';\nimport { CloseCode, ErrorCode } from '@colyseus/shared-types';\n\nexport interface QueueOptions {\n  /**\n   * number of players on each match\n   */\n  maxPlayers?: number;\n\n  /**\n   * name of the room to create\n   */\n  matchRoomName: string;\n\n  /**\n   * after these cycles, create a match with a bot\n   */\n  maxWaitingCycles?: number;\n\n  /**\n   * after this time, try to fit this client with a not-so-compatible group\n   */\n  maxWaitingCyclesForPriority?: number;\n\n  /**\n   * If set, teams must have the same size to be matched together\n   */\n  maxTeamSize?: number;\n\n  /**\n   * If `allowIncompleteGroups` is true, players inside an unmatched group (that\n   * did not reached `maxPlayers`, and `maxWaitingCycles` has been\n   * reached) will be matched together. Your room should fill the remaining\n   * spots with \"bots\" on this case.\n   */\n  allowIncompleteGroups?: boolean;\n\n  /**\n   * Comparison function for matching clients to groups\n   * Returns true if the client is compatible with the group\n   */\n  compare?: (client: QueueClientData, matchGroup: QueueMatchGroup) => boolean;\n\n  /**\n   *\n   * When onGroupReady is set, the \"roomNameToCreate\" option is ignored.\n   */\n  onGroupReady?: (this: QueueRoom, group: QueueMatchGroup) => Promise<IRoomCache>;\n}\n\nexport interface QueueMatchGroup {\n  averageRank: number;\n  clients: Array<Client<{ userData: QueueClientData }>>,\n  ready?: boolean;\n  confirmed?: number;\n}\n\nexport interface QueueMatchTeam {\n  averageRank: number;\n  clients: Array<Client<{ userData: QueueClientData }>>,\n  teamId: string | symbol;\n}\n\nexport interface QueueClientData {\n  /**\n   * Rank of the client\n   */\n  rank: number;\n\n  /**\n   * Timestamp of when the client entered the queue\n   */\n  currentCycle?: number;\n\n  /**\n   * Optional: if matching with a team, the team ID\n   */\n  teamId?: string;\n\n  /**\n   * Additional options passed by the client when joining the room\n   */\n  options?: any;\n\n  /**\n   * Match group the client is currently in\n   */\n  group?: QueueMatchGroup;\n\n  /**\n   * Whether the client has confirmed the connection to the room\n   */\n  confirmed?: boolean;\n\n  /**\n   * Whether the client should be prioritized in the queue\n   * (e.g. for players that are waiting for a long time)\n   */\n  highPriority?: boolean;\n\n  /**\n   * The last number of clients in the queue sent to the client\n   */\n  lastQueueClientCount?: number;\n}\n\n//\n// Optional: strongly-typed client messages\n// (This is optional, but recommended for better type safety and code generation for native SDKs)\n//\ntype QueueClient = Client<{\n  userData: QueueClientData;\n  messages: {\n    clients: number;\n    seat: matchMaker.ISeatReservation;\n  }\n}>;\n\nconst DEFAULT_TEAM = Symbol(\"$default_team\");\nconst DEFAULT_COMPARE = (client: QueueClientData, matchGroup: QueueMatchGroup) => {\n  const diff = Math.abs(client.rank - matchGroup.averageRank);\n  const diffRatio = (diff / matchGroup.averageRank);\n  // If diff ratio is too high, create a new match group\n  return (diff < 10 || diffRatio <= 2);\n}\n\nexport class QueueRoom extends Room {\n  maxPlayers = 4;\n  maxTeamSize: number;\n  allowIncompleteGroups: boolean = false;\n\n  maxWaitingCycles = 15;\n  maxWaitingCyclesForPriority?: number = 10;\n\n  /**\n   * Evaluate groups for each client at interval\n   */\n  cycleTickInterval = 1000;\n\n  /**\n   * Groups of players per iteration\n   */\n  groups: QueueMatchGroup[] = [];\n  highPriorityGroups: QueueMatchGroup[] = [];\n\n  matchRoomName: string;\n\n  protected compare = DEFAULT_COMPARE;\n  protected onGroupReady = (group: QueueMatchGroup) => matchMaker.createRoom(this.matchRoomName, {});\n\n  messages = {\n    confirm: (client: Client, _: unknown) => {\n      const queueData = client.userData;\n\n      if (queueData && queueData.group && typeof (queueData.group.confirmed) === \"number\") {\n        queueData.confirmed = true;\n        queueData.group.confirmed++;\n        client.leave(CloseCode.NORMAL_CLOSURE);\n      }\n    },\n  }\n\n  onCreate(options: QueueOptions) {\n    if (typeof(options.maxWaitingCycles) === \"number\") {\n      this.maxWaitingCycles = options.maxWaitingCycles;\n    }\n\n    if (typeof(options.maxPlayers) === \"number\") {\n      this.maxPlayers = options.maxPlayers;\n    }\n\n    if (typeof(options.maxTeamSize) === \"number\") {\n      this.maxTeamSize = options.maxTeamSize;\n    }\n\n    if (typeof(options.allowIncompleteGroups) !== \"undefined\") {\n      this.allowIncompleteGroups = options.allowIncompleteGroups;\n    }\n\n    if (typeof(options.compare) === \"function\") {\n      this.compare = options.compare;\n    }\n\n    if (typeof(options.onGroupReady) === \"function\") {\n      this.onGroupReady = options.onGroupReady;\n    }\n\n    if (options.matchRoomName) {\n      this.matchRoomName = options.matchRoomName;\n\n    } else {\n      throw new ServerError(ErrorCode.APPLICATION_ERROR, \"QueueRoom: 'matchRoomName' option is required.\");\n    }\n\n    debugMatchMaking(\"QueueRoom#onCreate() maxPlayers: %d, maxWaitingCycles: %d, maxTeamSize: %d, allowIncompleteGroups: %d, roomNameToCreate: %s\", this.maxPlayers, this.maxWaitingCycles, this.maxTeamSize, this.allowIncompleteGroups, this.matchRoomName);\n\n    /**\n     * Redistribute clients into groups at every interval\n     */\n    this.setSimulationInterval(() => this.reassignMatchGroups(), this.cycleTickInterval);\n  }\n\n  onJoin(client: QueueClient, options: any, auth?: unknown) {\n    this.addToQueue(client, {\n      rank: options.rank,\n      teamId: options.teamId,\n      options,\n    });\n  }\n\n  addToQueue(client: QueueClient, queueData: QueueClientData) {\n    if (queueData.currentCycle === undefined) {\n      queueData.currentCycle = 0;\n    }\n    client.userData = queueData;\n\n    // FIXME: reassign groups upon joining [?] (without incrementing cycle count)\n    client.send(\"clients\", 1);\n  }\n\n  createMatchGroup() {\n    const group: QueueMatchGroup = { clients: [], averageRank: 0 };\n    this.groups.push(group);\n    return group;\n  }\n\n  reassignMatchGroups() {\n    // Re-set all groups\n    this.groups.length = 0;\n    this.highPriorityGroups.length = 0;\n\n    const sortedClients = (this.clients)\n      .filter((client) => {\n        // Filter out:\n        // - clients that are not in the queue\n        // - clients that are already in a \"ready\" group\n        return (\n          client.userData &&\n          client.userData.group?.ready !== true\n        );\n      })\n      .sort((a, b) => {\n        //\n        // Sort by rank ascending\n        //\n        return a.userData.rank - b.userData.rank;\n      });\n\n    //\n    // The room either distribute by teams or by clients\n    //\n    if (typeof(this.maxTeamSize) === \"number\") {\n      this.redistributeTeams(sortedClients);\n\n    } else {\n      this.redistributeClients(sortedClients);\n    }\n\n    this.evaluateHighPriorityGroups();\n    this.processGroupsReady();\n  }\n\n  redistributeTeams(sortedClients: Client<{ userData: QueueClientData }>[]) {\n    const teamsByID: { [teamId: string | symbol]: QueueMatchTeam } = {};\n\n    sortedClients.forEach((client) => {\n      const teamId = client.userData.teamId || DEFAULT_TEAM;\n\n      // Create a new team if it doesn't exist\n      if (!teamsByID[teamId]) {\n        teamsByID[teamId] = { teamId: teamId, clients: [], averageRank: 0, };\n      }\n\n      teamsByID[teamId].averageRank += client.userData.rank;\n      teamsByID[teamId].clients.push(client);\n    });\n\n    // Calculate average rank for each team\n    let teams = Object.values(teamsByID).map((team) => {\n      team.averageRank /= team.clients.length;\n      return team;\n    }).sort((a, b) => {\n      // Sort by average rank ascending\n      return a.averageRank - b.averageRank;\n    });\n\n    // Iterate over teams multiple times until all clients are assigned to a group\n    do {\n      let currentGroup: QueueMatchGroup = this.createMatchGroup();\n      teams = teams.filter((team) => {\n        // Remove clients from the team and add them to the current group\n        const totalRank = team.averageRank * team.clients.length;\n\n        // currentGroup.averageRank = (currentGroup.averageRank === undefined)\n        //   ? team.averageRank\n        //   : (currentGroup.averageRank + team.averageRank) / ;\n        currentGroup = this.redistributeClients(team.clients.splice(0, this.maxTeamSize), currentGroup, totalRank);\n\n        if (team.clients.length >= this.maxTeamSize) {\n          // team still has enough clients to form a group\n          return true;\n        }\n\n        // increment cycle count for all clients in the team\n        team.clients.forEach((client) => client.userData.currentCycle++);\n\n        return false;\n      });\n    } while (teams.length >= 2);\n  }\n\n  redistributeClients(\n    sortedClients: Client<{ userData: QueueClientData }>[],\n    currentGroup: QueueMatchGroup = this.createMatchGroup(),\n    totalRank: number = 0,\n  ) {\n    for (let i = 0, l = sortedClients.length; i < l; i++) {\n      const client = sortedClients[i];\n      const userData = client.userData;\n      const currentCycle = userData.currentCycle++;\n\n      if (currentGroup.averageRank > 0) {\n        if (\n          !this.compare(userData, currentGroup) &&\n          !userData.highPriority\n        ) {\n          currentGroup = this.createMatchGroup();\n          totalRank = 0;\n        }\n      }\n\n      userData.group = currentGroup;\n      currentGroup.clients.push(client);\n\n      totalRank += userData.rank;\n      currentGroup.averageRank = totalRank / currentGroup.clients.length;\n\n      // Enough players in the group, mark it as ready!\n      if (currentGroup.clients.length === this.maxPlayers) {\n        currentGroup.ready = true;\n        currentGroup = this.createMatchGroup();\n        totalRank = 0;\n        continue;\n      }\n\n      if (currentCycle >= this.maxWaitingCycles && this.allowIncompleteGroups) {\n        /**\n         * Match long-waiting clients with bots\n         */\n        if (this.highPriorityGroups.indexOf(currentGroup) === -1) {\n          this.highPriorityGroups.push(currentGroup);\n        }\n\n      } else if (\n        this.maxWaitingCyclesForPriority !== undefined &&\n        currentCycle >= this.maxWaitingCyclesForPriority\n      ) {\n        /**\n         * Force this client to join a group, even if rank is incompatible\n         */\n        userData.highPriority = true;\n      }\n    }\n\n    return currentGroup;\n  }\n\n  evaluateHighPriorityGroups() {\n    /**\n     * Evaluate groups with high priority clients\n     */\n    this.highPriorityGroups.forEach((group) => {\n      group.ready = group.clients.every((c) => {\n        // Give new clients another chance to join a group that is not \"high priority\"\n        return c.userData?.currentCycle > 1;\n        // return c.userData?.currentCycle >= this.maxWaitingCycles;\n      });\n    });\n  }\n\n  processGroupsReady() {\n    this.groups.forEach(async (group) => {\n      if (group.ready) {\n        group.confirmed = 0;\n\n        try {\n          /**\n           * Create room instance in the server.\n           */\n          const room = await this.onGroupReady.call(this, group);\n\n          /**\n           * Reserve a seat for each client in the group.\n           * (If one fails, force all clients to leave, re-queueing is up to the client-side logic)\n           */\n          await matchMaker.reserveMultipleSeatsFor(\n            room,\n            group.clients.map((client) => ({\n              sessionId: client.sessionId,\n              options: client.userData.options,\n              auth: client.auth,\n            })),\n          );\n\n          /**\n           * Send room data for new WebSocket connection!\n           */\n          group.clients.forEach((client, i) => {\n            client.send(\"seat\", matchMaker.buildSeatReservation(room, client.sessionId));\n          });\n\n        } catch (e: any) {\n          //\n          // If creating a room, or reserving a seat failed - fail all clients\n          // Whether the clients retry or not is up to the client-side logic\n          //\n          group.clients.forEach(client => client.leave(1011, e.message));\n        }\n\n      } else {\n        /**\n         * Notify clients within the group on how many players are in the queue\n         */\n        group.clients.forEach((client) => {\n          //\n          // avoid sending the same number of clients to the client if it hasn't changed\n          //\n          const queueClientCount = group.clients.length;\n          if (client.userData.lastQueueClientCount !== queueClientCount) {\n            client.userData.lastQueueClientCount = queueClientCount;\n            client.send(\"clients\", queueClientCount);\n          }\n        });\n      }\n    });\n  }\n\n}"],
  "mappings": "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",
  "names": []
}
