UNPKG

mm_os

Version:

MM_OS服务端架构,用于快速构建应用程序,支持网站建设、小程序后台、AI应用、物联网(IOT/AIOT)、游戏服务端等多种场景。

586 lines (506 loc) 13.7 kB
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;