import {Client, generateId, matchMaker, Room} from "colyseus"; import {BaseConst} from "../constants/BaseConst"; interface MatchmakingGroup { averageRank: number; clients: ClientStat[]; priority?: boolean; ready?: boolean; confirmed?: number; count: number; // cancelConfirmationTimeout?: Delayed; } interface ClientStat { clients: Map; waitingTime: number; options?: any; groupTag?: string; group?: MatchmakingGroup; rank: number; confirmed?: boolean; priority?: boolean; } export class RankedLobbyRoom extends Room { /** * If `allowUnmatchedGroups` is true, players inside an unmatched group (that * did not reached `numClientsToMatch`, and `maxWaitingTime` has been * reached) will be matched together. Your room should fill the remaining * spots with "bots" on this case. */ allowUnmatchedGroups: boolean = true; /** * Evaluate groups for each client at interval */ evaluateGroupsInterval = 1000; /** * Groups of players per iteration */ groups: MatchmakingGroup[] = []; /** * name of the room to create */ roomToCreate = "general_room"; /** * 最大匹配时间 */ maxWaitingTime = 15 * 1000; /** * 超过该时间, 组队玩家可匹配单排玩家 * @type {number} */ groupAddOneTime = 10 * 1000; /** * after this time, try to fit this client with a not-so-compatible group */ maxWaitingTimeForPriority?: number = 10 * 1000; /** * number of players on each match */ numClientsToMatch = 4; // /** // * after a group is ready, clients have this amount of milliseconds to confirm // * connection to the created room // */ // cancelConfirmationAfter = 5000; /** * rank and group cache per-player */ stats: ClientStat[] = []; rank_mpa = 0.2; fair_ir = 1.2; step_ir = 1.3; onCreate(options: any) { const fc = global.$cfg.get(BaseConst.FORMULA); this.rank_mpa = fc.get(70028).number / 100; this.fair_ir = fc.get(70029).number / 100 + 1; this.step_ir = fc.get(70031).number / 100 + 1; this.groupAddOneTime = fc.get(70023).number * 1000; this.maxWaitingTime = (fc.get(70024).number + fc.get(70025).number) * 1000; if (options.maxWaitingTime) { this.maxWaitingTime = options.maxWaitingTime; } if (options.numClientsToMatch) { this.numClientsToMatch = options.numClientsToMatch; } this.onMessage("bye", (client: Client, message: any) => { const stat = this.stats.find(obj => obj.clients.has(client.sessionId)); if (stat && stat.group && typeof (stat.group.confirmed) === "number") { stat.confirmed = true; stat.group.confirmed++; //stat.clients.delete(client.sessionId); client.leave(); } }) this.onMessage('gogogo', (client: Client, message: any) => { const stat = this.stats.find(obj => obj.clients.has(client.sessionId)); stat.priority = true; }) /** * Redistribute clients into groups at every interval */ this.setSimulationInterval(() => this.redistributeGroups(), this.evaluateGroupsInterval); } onJoin(client: Client, options: any) { /** * 如果请求带有group, 那么查找当前队列中group相同的记录 * 如果没有, 就插一条记录 * 如果找到的记录, clients已经查过2条, 则也插入一条记录 * 如果找到的记录, clients是1, 则把当前client添加到该记录中, 同时更新rank */ if (options.group) { let length = this.stats.length; let groupData; for (let i = 0; i < length; i++) { if (this.stats[i].groupTag == options.group) { groupData = this.stats[i]; break } } if (!groupData) { let clientMap = new Map(); clientMap.set(client.sessionId, {client, options}); groupData = { clients: clientMap, rank: options.rank, groupTag: options.group, waitingTime: 0, options } this.stats.push(groupData); } else { if (groupData.clients.size >= 2) { let clientMap = new Map(); options.group = generateId(); clientMap.set(client.sessionId, {client, options}); this.stats.push({ clients: clientMap, rank: options.rank, waitingTime: 0, options }); } else { groupData.clients.set(client.sessionId, {client, options}); groupData.rank = (groupData.rank + options.rank) / 2 * (1 + this.rank_mpa); } } client.send("clients", groupData.clients.size); } else { let clientMap = new Map(); clientMap.set(client.sessionId, {client, options}); this.stats.push({ clients: clientMap, rank: options.rank, waitingTime: 0, options }); } client.send("clients", 1); } createGroup() { let group: MatchmakingGroup = {clients: [], averageRank: 0, count: 0}; this.groups.push(group); return group; } redistributeGroups() { // re-set all groups this.groups = []; const stats = this.stats.sort((a, b) => a.rank - b.rank); let currentGroup: MatchmakingGroup = this.createGroup(); let totalRank = 0; // 先过滤一边组队的情况 for (let i = 0, l = stats.length; i < l; i++) { if (stats[i].clients.size == 1) { continue; } const stat = stats[i]; stat.waitingTime += this.clock.deltaTime; if (stat.group && stat.group.ready) { continue; } if (currentGroup.averageRank > 0) { const diff = Math.abs(stat.rank - currentGroup.averageRank); const diffRatio = (diff / currentGroup.averageRank); /** * figure out how to identify the diff ratio that makes sense */ if (diffRatio > this.step_ir) { currentGroup = this.createGroup(); totalRank = 0; } } stat.group = currentGroup; currentGroup.clients.push(stat); currentGroup.count += stat.clients.size; totalRank += stat.rank; currentGroup.averageRank = totalRank / currentGroup.clients.length; if (currentGroup.count === this.numClientsToMatch) { currentGroup.ready = true; currentGroup = this.createGroup(); totalRank = 0; } } totalRank = 0; currentGroup = this.createGroup(); for (let i = 0, l = stats.length; i < l; i++) { const stat = stats[i]; if (stats[i].clients.size == 1) { stat.waitingTime += this.clock.deltaTime; } else { if (stat.waitingTime < this.groupAddOneTime) { continue; } } /** * do not attempt to re-assign groups for clients inside "ready" groups */ if (stat.group && stat.group.ready) { continue; } if (currentGroup.count + stat.clients.size > this.maxClients) { continue; } if (stat.priority) { currentGroup.priority = true; } /** * Force this client to join a group, even if rank is incompatible */ // if ( // this.maxWaitingTimeForPriority !== undefined && // stat.waitingTime >= this.maxWaitingTimeForPriority // ) { // currentGroup.priority = true; // } if ( currentGroup.averageRank > 0 && !currentGroup.priority ) { const diff = Math.abs(stat.rank - currentGroup.averageRank); const diffRatio = (diff / currentGroup.averageRank); /** * figure out how to identify the diff ratio that makes sense */ if (diffRatio > this.fair_ir) { currentGroup = this.createGroup(); totalRank = 0; } } stat.group = currentGroup; currentGroup.clients.push(stat); currentGroup.count += stat.clients.size; totalRank += stat.rank; currentGroup.averageRank = totalRank / currentGroup.count; if ( (currentGroup.count === this.numClientsToMatch) || /** * Match long-waiting clients with bots * FIXME: peers of this group may be entered short ago */ (stat.waitingTime >= this.maxWaitingTime && this.allowUnmatchedGroups) || stat.priority ) { currentGroup.ready = true; currentGroup = this.createGroup(); totalRank = 0; } } this.checkGroupsReady(); } generateSeat(index: number): number { switch (index) { case 0: return 0; case 1: return 3; case 2: return 1; case 3: return 2; } } async checkGroupsReady() { await Promise.all( this.groups .map(async (group) => { if (group.ready) { group.confirmed = 0; let score = 0; let count = 0; group.clients.map(client => { for (let [,data] of client.clients) { score += (data.options?.score || 0); count ++; } }) let avaScore = score / count; /** * Create room instance in the server. */ const room = await matchMaker.createRoom(this.roomToCreate, { match: true, rank: group.averageRank, score: avaScore, count: this.numClientsToMatch - group.count }); // 预处理数据, 确定座次 let hasGroup = false; for (let client of group.clients) { if (client.clients.size > 1) { hasGroup = true; break; } } let seat = 0; if (hasGroup) { for (let client of group.clients) { if (client.clients.size > 1) { for (let [,sub] of client.clients) { sub.seat = this.generateSeat(seat ++); } } } for (let client of group.clients) { if (client.clients.size == 1) { for (let [,sub] of client.clients) { sub.seat = this.generateSeat(seat ++); } } } } else { group.clients.sort((a, b) => a.rank - b.rank); for (let client of group.clients) { for (let [,sub] of client.clients) { sub.seat = seat ++; } } } await Promise.all(group.clients.map(async (client) => { /** * Send room data for new WebSocket connection! */ for (let [,data] of client.clients) { let matchOpt = { seat: data.seat } Object.assign(matchOpt, data.options); const matchData = await matchMaker.reserveSeatFor(room, matchOpt); let options: any = {seat: data.seat, rank: data.options.rank}; Object.assign(options, matchData); data.client.send("match_success", options); } })); // /** // * Cancel & re-enqueue clients if some of them couldn't confirm connection. // */ // group.cancelConfirmationTimeout = this.clock.setTimeout(() => { // group.clients.forEach(stat => { // this.send(stat.client, 0); // stat.group = undefined; // stat.waitingTime = 0; // }); // }, this.cancelConfirmationAfter); } else { /** * Notify all clients within the group on how many players are in the queue */ group.clients.forEach(client => { for (let [,data] of client.clients) { data.client.send("clients", group.count); } }); } }) ); } onLeave(client: Client, consented: boolean) { const stat = this.stats.find(obj => obj.clients.has(client.sessionId)); if (stat.clients.size > 1) { stat.clients.delete(client.sessionId); let data = [...stat.clients.values()][0]; stat.rank = data.options.rank; } else { this.stats.remove(stat); } } onDispose() { } }