card_svr/src/rooms/RankedLobbyRoom.ts
2021-01-19 14:04:35 +08:00

431 lines
12 KiB
TypeScript

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<string, {client: Client, options: any, seat?: number}>;
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() {
}
}