Files
CardServer/MatchMaker.ts
sunwen 75c483103e 1.修复匹配时可能会匹配到死去玩家的逻辑
2.修复匹配的随机性条件,改为完全随机
2025-10-11 16:52:39 +08:00

224 lines
6.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { Player } from "./Player";
interface PairingResult {
playerId: string;
opponentId: string;
isResting: boolean; // 是否为轮空玩家
}
class Matchmaker {
private mPlayers: Map<string, Player>;
private currentRound: number = 0;
private restingHistory: Map<string, number> = new Map(); // 记录每个玩家的轮空次数
constructor(players: Map<string, Player>) {
this.mPlayers = players;
}
/**
* 为当前轮次生成配对
* @returns 配对结果数组
*/
public generatePairings(): PairingResult[] {
const playerIds = Array.from(this.mPlayers.keys()); // 只使用当前存活的玩家
const playerCount = playerIds.length;
if (playerCount === 0) {
return [];
}
if (playerCount === 1) {
// 只有一个玩家时,与自己配对(轮空)
return [{
playerId: playerIds[0],
opponentId: playerIds[0],
isResting: true
}];
}
const pairings = this.createRandomPairings(playerIds);
this.currentRound++;
return pairings;
}
/**
* 创建随机配对 - 使用 Fisher-Yates 洗牌算法
* @param playerIds 玩家ID数组
* @returns 配对结果
*/
private createRandomPairings(playerIds: string[]): PairingResult[] {
const playerCount = playerIds.length;
const isOddCount = playerCount % 2 === 1;
// 如果是奇数,需要找一个玩家轮空
let restingPlayer: string | null = null;
let activePlayers = [...playerIds];
if (isOddCount) {
restingPlayer = this.selectRestingPlayer(playerIds);
activePlayers = playerIds.filter(id => id !== restingPlayer);
}
// Fisher-Yates 洗牌算法 - O(n) 时间复杂度
const shuffled = this.shuffleArray(activePlayers);
const pairings: PairingResult[] = [];
// 按顺序两两配对 - O(n) 时间复杂度
for (let i = 0; i < shuffled.length; i += 2) {
if (i + 1 < shuffled.length) {
const player1 = shuffled[i];
const player2 = shuffled[i + 1];
pairings.push({
playerId: player1,
opponentId: player2,
isResting: false
});
pairings.push({
playerId: player2,
opponentId: player1,
isResting: false
});
}
}
// 处理轮空玩家
if (restingPlayer) {
const ghostOpponent = this.selectGhostOpponent(restingPlayer, playerIds);
pairings.push({
playerId: restingPlayer,
opponentId: ghostOpponent,
isResting: true
});
}
return pairings;
}
/**
* Fisher-Yates 洗牌算法 - 完全随机且高效
* @param array 要洗牌的数组
* @returns 洗牌后的新数组
*/
private shuffleArray<T>(array: T[]): T[] {
const result = [...array];
for (let i = result.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[result[i], result[j]] = [result[j], result[i]];
}
return result;
}
/**
* 选择轮空玩家(轮换制,确保每个玩家都有机会轮空)
* @param playerIds 玩家ID数组当前存活的玩家
* @returns 轮空玩家ID
*/
private selectRestingPlayer(playerIds: string[]): string {
// 初始化轮空次数记录
playerIds.forEach(id => {
if (!this.restingHistory.has(id)) {
this.restingHistory.set(id, 0);
}
});
// 找到轮空次数最少的玩家
let minRestingCount = Infinity;
playerIds.forEach(id => {
const count = this.restingHistory.get(id) || 0;
if (count < minRestingCount) {
minRestingCount = count;
}
});
const candidatePlayers = playerIds.filter(id => {
const count = this.restingHistory.get(id) || 0;
return count === minRestingCount;
});
// 在轮空次数最少的玩家中随机选择
const selectedIndex = Math.floor(Math.random() * candidatePlayers.length);
const selectedPlayer = candidatePlayers[selectedIndex];
// 增加该玩家的轮空次数
this.restingHistory.set(selectedPlayer, (this.restingHistory.get(selectedPlayer) || 0) + 1);
return selectedPlayer;
}
/**
* 为轮空玩家选择一个"幽灵对手"(用于显示)
* @param restingPlayer 轮空玩家ID
* @param allPlayers 所有当前存活的玩家ID
* @returns 幽灵对手ID
*/
private selectGhostOpponent(restingPlayer: string, allPlayers: string[]): string {
// 只在当前存活的玩家中选择幽灵对手
const alivePlayers = allPlayers.filter(id => id !== restingPlayer);
if (alivePlayers.length === 0) {
return restingPlayer; // 如果没有其他存活玩家,返回自己
}
// 随机选择一个幽灵对手
const randomIndex = Math.floor(Math.random() * alivePlayers.length);
return alivePlayers[randomIndex];
}
/**
* 重置配对历史(新游戏开始时调用)
*/
public resetHistory(): void {
this.restingHistory.clear();
this.currentRound = 0;
}
/**
* 更新玩家列表(当有玩家加入或退出时调用)
* @param newPlayers 新的玩家Map
*/
public updatePlayers(newPlayers: Map<string, Player>): void {
this.mPlayers = newPlayers;
// 清理已死亡玩家的轮空历史记录
const alivePlayerIds = new Set(newPlayers.keys());
for (const playerId of this.restingHistory.keys()) {
if (!alivePlayerIds.has(playerId)) {
this.restingHistory.delete(playerId);
}
}
}
public deletePlayer(playerId: string) {
if (this.mPlayers.has(playerId)) {
this.mPlayers.delete(playerId);
}
// 清理该玩家的轮空历史
this.restingHistory.delete(playerId);
}
}
// 使用示例和辅助函数
function createMatchmaker(mPlayers: Map<string, Player>): Matchmaker {
return new Matchmaker(mPlayers);
}
// 显示配对结果的辅助函数
function displayPairings(pairings: PairingResult[]): void {
console.log(`=== 第${pairings.length > 0 ? '当前' : '0'}轮配对结果 ===`);
const processedPairs = new Set<string>();
pairings.forEach(pairing => {
const pairKey = [pairing.playerId, pairing.opponentId].sort().join('-');
if (!processedPairs.has(pairKey)) {
console.log(`${pairing.playerId} VS ${pairing.opponentId}`);
processedPairs.add(pairKey);
}
});
}
export { Matchmaker, PairingResult, createMatchmaker, displayPairings };