mm_os
Version:
MM_OS服务端架构,用于快速构建应用程序,支持网站建设、小程序后台、AI应用、物联网(IOT/AIOT)、游戏服务端等多种场景。
586 lines (506 loc) • 13.7 kB
JavaScript
const {
Drive
} = require('mm_machine');
const {
idGen
} = require('../../ulits/id_gen.js');
/**
* 匹配器
* 管理匹配池并实现基于等级和等待时间的匹配算法
*/
class Matcher extends Drive {
static config = {
name: 'matcher',
title: '匹配器',
wait_time: 120000,
lv_range_init: 5,
lv_range_max: 20,
lv_expand_int: 10000,
match_interval: 1000,
team_sizes: {
'1v1': 1,
'3v3': 3,
'5v5': 5
}
};
/**
* 构造函数
* @param {object} config 配置参数
*/
constructor(config) {
super({
...Matcher.config,
...config
});
this._channel = null;
this._match_timer = null;
this._pools = new Map();
this._player_pool = new Map();
}
}
/**
* 获取模板目录
* @returns {string} 模板目录
*/
Matcher.prototype.getTplDir = function() {
return __dirname;
};
/**
* 初始化匹配器
* @param {object} channel 游戏频道
*/
Matcher.prototype._initCore = async function (channel) {
this._channel = channel;
};
/**
* 启动匹配定时器
*/
Matcher.prototype._start = function () {
this._match_timer = setInterval(() => {
this._doMatch();
}, this.config.match_interval);
};
/**
* 停止匹配定时器
*/
Matcher.prototype._stop = function () {
if (this._match_timer) {
clearInterval(this._match_timer);
this._match_timer = null;
this.log('debug', '匹配定时器已停止');
}
};
/**
* 执行匹配
*/
Matcher.prototype._doMatch = function () {
this.updateLevelRanges();
this.cleanTimeout();
var modes = ['rank', 'casual'];
var formats = ['1v1', '3v3', '5v5'];
var team_sizes = [1, 2, 3, 5];
for (var m = 0; m < modes.length; m++) {
for (var f = 0; f < formats.length; f++) {
for (var t = 0; t < team_sizes.length; t++) {
this._matchPool(modes[m], formats[f], team_sizes[t]);
}
}
}
};
/**
* 匹配指定池
* @param {string} mode 游戏模式
* @param {string} format 比赛格式
* @param {number} team_size 队伍人数
*/
Matcher.prototype._matchPool = function (mode, format, team_size) {
var pool = this.getPool(mode, format, team_size);
if (pool.length < 2) {
return;
}
var required_count = this._getRequiredCount(format);
var matched_groups = this._findMatchGroups(pool, required_count);
for (var i = 0; i < matched_groups.length; i++) {
var group = matched_groups[i];
this._create(group, mode, format);
}
};
/**
* 获取需要的玩家数量
* @param {string} format 比赛格式
* @returns {number} 玩家数量
*/
Matcher.prototype._getRequiredCount = function (format) {
var counts = {
'1v1': 2,
'3v3': 6,
'5v5': 10
};
return counts[format] || 10;
};
/**
* 查找匹配组
* @param {Array} pool 匹配池
* @param {number} required_count 需要的玩家数量
* @returns {Array} 匹配组列表
*/
Matcher.prototype._findMatchGroups = function (pool, required_count) {
var groups = [];
var used = new Set();
var sorted_pool = this._sortByWaitTime(pool);
for (var i = 0; i < sorted_pool.length; i++) {
var entry = sorted_pool[i];
if (used.has(entry.player_id)) {
continue;
}
var group = this._buildGroup(entry, sorted_pool, used, required_count);
if (group && group.length === required_count) {
groups.push(group);
}
}
return groups;
};
/**
* 按等待时间排序(等待久的优先)
* @param {Array} pool 匹配池
* @returns {Array} 排序后的匹配池
*/
Matcher.prototype._sortByWaitTime = function (pool) {
return pool.slice().sort((a, b) => {
return a.join_time - b.join_time;
});
};
/**
* 构建匹配组
* @param {object} first_entry 第一个玩家
* @param {Array} pool 匹配池
* @param {Set} used 已使用的玩家
* @param {number} required_count 需要的玩家数量
* @returns {Array|null} 匹配组
*/
Matcher.prototype._buildGroup = function (first_entry, pool, used, required_count) {
var group = [first_entry];
used.add(first_entry.player_id);
var member_count = first_entry.team_members.length;
for (var i = 0; i < pool.length && member_count < required_count; i++) {
var entry = pool[i];
if (used.has(entry.player_id)) {
continue;
}
if (this._can(first_entry, entry)) {
if (member_count + entry.team_members.length <= required_count) {
group.push(entry);
used.add(entry.player_id);
member_count += entry.team_members.length;
}
}
}
if (member_count === required_count) {
return group;
}
for (var j = 0; j < group.length; j++) {
used.delete(group[j].player_id);
}
return null;
};
/**
* 判断两个玩家是否可以匹配
* @param {object} entry_a 玩家A
* @param {object} entry_b 玩家B
* @returns {boolean} 是否可以匹配
*/
Matcher.prototype._can = function (entry_a, entry_b) {
var level_diff = Math.abs(entry_a.level - entry_b.level);
var min_range = Math.min(entry_a.lv_range, entry_b.lv_range);
return level_diff <= min_range;
};
/**
* 创建匹配
* @param {Array} group 匹配组
* @param {string} mode 游戏模式
* @param {string} format 比赛格式
* @returns {object} 匹配结果
*/
Matcher.prototype._create = function (group, mode, format) {
var match_id = idGen.genId('match');
var all_players = [];
for (var i = 0; i < group.length; i++) {
var entry = group[i];
all_players = all_players.concat(entry.team_members);
this.leave(entry.player_id);
}
var teams = this._splitTeams(all_players, format);
var match_ret = {
match_id: match_id,
mode: mode,
format: format,
teams: teams,
players: all_players,
timestamp: Date.now()
};
this.emitEvent('match_success', match_ret);
this.log('info', `匹配成功: ${match_id}, 模式: ${mode}, 格式: ${format}, 玩家数: ${all_players.length}`);
return match_ret;
};
/**
* 分割队伍
* @param {Array} players 玩家列表
* @param {string} format 比赛格式
* @returns {object} 队伍配置
*/
Matcher.prototype._splitTeams = function (players, format) {
var counts = {
'1v1': 1,
'3v3': 3,
'5v5': 5
};
var team_size = counts[format] || 5;
return {
team_a: players.slice(0, team_size),
team_b: players.slice(team_size),
format: format
};
};
/**
* 玩家加入匹配池
* @param {string} player_id 玩家ID
* @param {object} options 匹配选项
* @returns {boolean} 是否成功加入
*/
Matcher.prototype.join = function (player_id, options = {}) {
if (typeof player_id !== 'string') {
throw new TypeError('玩家ID必须是字符串');
}
var mode = options.mode || 'rank';
var format = options.format || '5v5';
var team_size = options.team_size || 1;
var team_members = options.team_members || [player_id];
if (this._player_pool.has(player_id)) {
throw new Error(`玩家 ${player_id} 已在匹配池中`);
}
var player = this._channel.getPlayer(player_id);
if (!player) {
throw new Error(`玩家 ${player_id} 不存在`);
}
var pool_key = this._getPoolKey(mode, format, team_size);
var pool = this._getOrCreatePool(pool_key);
var entry = this._createEntry(player_id, player, team_members, team_size, mode, format);
pool.push(entry);
this._player_pool.set(player_id, pool_key);
this._emitJoinEvent(player_id, mode, format, team_size, entry.join_time);
this.log('debug', `玩家 ${player_id} 加入匹配池 ${pool_key}`);
return true;
};
/**
* 玩家离开匹配池
* @param {string} player_id 玩家ID
* @returns {boolean} 是否成功离开
*/
Matcher.prototype.leave = function (player_id) {
if (typeof player_id !== 'string') {
throw new TypeError('玩家ID必须是字符串');
}
var pool_key = this._player_pool.get(player_id);
if (!pool_key) {
return false;
}
var pool = this._pools.get(pool_key);
if (!pool) {
this._player_pool.delete(player_id);
return false;
}
for (var i = pool.length - 1; i >= 0; i--) {
if (pool[i].player_id === player_id) {
pool.splice(i, 1);
break;
}
}
this._player_pool.delete(player_id);
this.emitEvent('player_leave_pool', {
player_id: player_id,
timestamp: Date.now()
});
this.log('debug', `玩家 ${player_id} 离开匹配池`);
return true;
};
/**
* 检查玩家是否在匹配池中
* @param {string} player_id 玩家ID
* @returns {boolean} 是否在匹配池中
*/
Matcher.prototype.has = function (player_id) {
return this._player_pool.has(player_id);
};
/**
* 获取匹配池键
* @param {string} mode 游戏模式
* @param {string} format 比赛格式
* @param {number} team_size 队伍人数
* @returns {string} 匹配池键
*/
Matcher.prototype._getPoolKey = function (mode, format, team_size) {
return `${mode}_${format}_${team_size}`;
};
/**
* 获取或创建匹配池
* @param {string} pool_key 池键名
* @returns {Array} 匹配池
*/
Matcher.prototype._getOrCreatePool = function (pool_key) {
if (!this._pools.has(pool_key)) {
this._pools.set(pool_key, []);
}
return this._pools.get(pool_key);
};
/**
* 创建匹配条目
* @param {string} player_id 玩家ID
* @param {object} player 玩家对象
* @param {Array} members 队伍成员
* @param {number} team_size 队伍人数
* @param {string} mode 游戏模式
* @param {string} format 比赛格式
* @returns {object} 匹配条目
*/
Matcher.prototype._createEntry = function (player_id, player, members, team_size, mode, format) {
return {
player_id: player_id,
team_members: members,
team_size: team_size,
level: player.level || 0,
join_time: Date.now(),
lv_range: this.config.lv_range_init,
mode: mode,
format: format
};
};
/**
* 发送加入事件
* @param {string} player_id 玩家ID
* @param {string} mode 游戏模式
* @param {string} format 比赛格式
* @param {number} team_size 队伍人数
* @param {number} timestamp 时间戳
*/
Matcher.prototype._emitJoinEvent = function (player_id, mode, format, team_size, timestamp) {
this.emitEvent('player_join_pool', {
player_id: player_id,
mode: mode,
format: format,
team_size: team_size,
timestamp: timestamp
});
};
/**
* 获取指定匹配池
* @param {string} mode 游戏模式
* @param {string} format 比赛格式
* @param {number} team_size 队伍人数
* @returns {Array} 匹配池
*/
Matcher.prototype.getPool = function (mode, format, team_size) {
var pool_key = this._getPoolKey(mode, format, team_size);
return this._pools.get(pool_key) || [];
};
/**
* 获取匹配池大小
* @param {string} mode 游戏模式
* @param {string} format 比赛格式
* @param {number} team_size 队伍人数
* @returns {number} 匹配池大小
*/
Matcher.prototype.getPoolSize = function (mode, format, team_size) {
return this.getPool(mode, format, team_size).length;
};
/**
* 获取总等待人数
* @returns {number} 总等待人数
*/
Matcher.prototype.getTotalWaiting = function () {
return this._player_pool.size;
};
/**
* 更新玩家等级范围
* 根据等待时间扩大匹配范围
*/
Matcher.prototype.updateLevelRanges = function () {
var now = Date.now();
var expand_int = this.config.lv_expand_int;
var max_range = this.config.lv_range_max;
var self = this;
this._pools.forEach((pool) => {
for (var i = 0; i < pool.length; i++) {
var entry = pool[i];
var wait_time = now - entry.join_time;
var expand_count = Math.floor(wait_time / expand_int);
var new_range = self.config.lv_range_init + expand_count;
if (new_range > max_range) {
entry.lv_range = max_range;
} else {
entry.lv_range = new_range;
}
}
});
};
/**
* 清理超时玩家
*/
Matcher.prototype.cleanTimeout = function () {
var now = Date.now();
var max_wait = this.config.wait_time;
this._pools.forEach((pool, pool_key) => {
for (var i = pool.length - 1; i >= 0; i--) {
var entry = pool[i];
if (now - entry.join_time > max_wait) {
pool.splice(i, 1);
this._player_pool.delete(entry.player_id);
this.emitEvent('match_timeout', {
player_id: entry.player_id,
wait_time: max_wait,
timestamp: now
});
this.log('debug', `玩家 ${entry.player_id} 匹配超时`);
}
}
});
};
/**
* 清空匹配池
*/
Matcher.prototype.clear = function () {
this._pools.clear();
this._player_pool.clear();
this.log('info', '匹配池已清空');
};
/**
* 获取匹配状态
* @param {string} player_id 玩家ID
* @returns {object|null} 匹配状态
*/
Matcher.prototype.getMatchStatus = function (player_id) {
if (!this.has(player_id)) {
return null;
}
var entry = this._findPlayerEntry(player_id);
if (!entry) {
return null;
}
var now = Date.now();
var wait_time = now - entry.join_time;
return {
player_id: player_id,
mode: entry.mode,
format: entry.format,
team_size: entry.team_size,
level: entry.level,
lv_range: entry.lv_range,
wait_time: wait_time,
pool_size: this.getPoolSize(entry.mode, entry.format, entry.team_size)
};
};
/**
* 查找玩家匹配条目
* @param {string} player_id 玩家ID
* @returns {object|null} 匹配条目
*/
Matcher.prototype._findPlayerEntry = function (player_id) {
var found = null;
this._pools.forEach((pool) => {
for (var i = 0; i < pool.length; i++) {
if (pool[i].player_id === player_id) {
found = pool[i];
return;
}
}
});
return found;
};
/**
* 停止匹配器
*/
Matcher.prototype.stop = function () {
this._stop();
this.clear();
this.log('info', '匹配器已停止');
};
exports.Matcher = Matcher;