Files
CardServer/MatchMaker.ts

267 lines
7.9 KiB
TypeScript
Raw Normal View History

2025-10-11 14:45:08 +08:00
import { Player } from "./Player";
interface PairingResult {
playerId: string;
opponentId: string;
// socketID: string;
isResting: boolean; // 是否为轮空玩家
}
class Matchmaker {
private mPlayers: Map<string, Player>;
private roundHistory: string[][] = []; // 记录每轮的配对历史
private currentRound: number = 0;
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],
// socketID: this.mPlayers.get(playerIds[0])!.mSocketId,
isResting: true
}];
}
const pairings = this.createOptimalPairings(playerIds);
this.roundHistory.push(pairings.map(p => `${p.playerId}-${p.opponentId}`));
this.currentRound++;
return pairings;
}
/**
*
* @param playerIds ID数组
* @returns
*/
private createOptimalPairings(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);
}
// 对剩余玩家进行配对
const pairings: PairingResult[] = [];
const usedPlayers = new Set<string>();
// 使用轮转算法确保配对的多样性
const rotatedPlayers = this.rotatePlayerOrder(activePlayers);
for (let i = 0; i < rotatedPlayers.length; i += 2) {
if (i + 1 < rotatedPlayers.length) {
const player1 = rotatedPlayers[i];
const player2 = rotatedPlayers[i + 1];
if (!usedPlayers.has(player1) && !usedPlayers.has(player2)) {
pairings.push({
playerId: player1,
opponentId: player2,
// socketID: this.mPlayers.get(player1)!.mSocketId,
isResting: false
});
pairings.push({
playerId: player2,
opponentId: player1,
// socketID: this.mPlayers.get(player2)!.mSocketId,
isResting: false
});
usedPlayers.add(player1);
usedPlayers.add(player2);
}
}
}
// 处理轮空玩家
if (restingPlayer) {
const ghostOpponent = this.selectGhostOpponent(restingPlayer, playerIds);
pairings.push({
playerId: restingPlayer,
opponentId: ghostOpponent,
// socketID: this.mPlayers.get(restingPlayer)!.mSocketId,
isResting: true
});
}
return pairings;
}
/**
*
* @param playerIds ID数组
* @returns ID
*/
private selectRestingPlayer(playerIds: string[]): string {
const restingCounts = new Map<string, number>();
// 统计每个玩家的轮空次数
playerIds.forEach(id => restingCounts.set(id, 0));
this.roundHistory.forEach(round => {
round.forEach(pairing => {
const [playerId, opponentId] = pairing.split('-');
if (playerId === opponentId) {
const count = restingCounts.get(playerId) || 0;
restingCounts.set(playerId, count + 1);
}
});
});
// 找到轮空次数最少的所有玩家
let minRestingCount = Infinity;
playerIds.forEach(id => {
const count = restingCounts.get(id) || 0;
if (count < minRestingCount) {
minRestingCount = count;
}
});
const candidatePlayers = playerIds.filter(id => {
const count = restingCounts.get(id) || 0;
return count === minRestingCount;
});
// 在轮空次数最少的玩家中轮换选择
// 使用轮次数来确定选择哪个玩家,确保轮换
const selectedIndex = this.currentRound % candidatePlayers.length;
return candidatePlayers[selectedIndex];
}
/**
* "幽灵对手"
* @param restingPlayer ID
* @param allPlayers ID
* @returns ID
*/
private selectGhostOpponent(restingPlayer: string, allPlayers: string[]): string {
// 选择与轮空玩家对战次数最少的玩家作为幽灵对手
const opponentCounts = new Map<string, number>();
allPlayers.forEach(id => {
if (id !== restingPlayer) {
opponentCounts.set(id, 0);
}
});
this.roundHistory.forEach(round => {
round.forEach(pairing => {
const [playerId, opponentId] = pairing.split('-');
if (playerId === restingPlayer && opponentId !== restingPlayer) {
const count = opponentCounts.get(opponentId) || 0;
opponentCounts.set(opponentId, count + 1);
}
});
});
let minCount = Infinity;
let ghostOpponent = allPlayers.find(id => id !== restingPlayer) || restingPlayer;
opponentCounts.forEach((count, playerId) => {
if (count < minCount) {
minCount = count;
ghostOpponent = playerId;
}
});
return ghostOpponent;
}
/**
*
* @param playerIds ID数组
* @returns
*/
private rotatePlayerOrder(playerIds: string[]): string[] {
if (playerIds.length <= 2) {
return [...playerIds];
}
const rotateAmount = this.currentRound % playerIds.length;
return [
...playerIds.slice(rotateAmount),
...playerIds.slice(0, rotateAmount)
];
}
/**
*
* @returns
*/
public getPairingHistory(): string[][] {
return [...this.roundHistory];
}
/**
*
*/
public resetHistory(): void {
this.roundHistory = [];
this.currentRound = 0;
}
/**
* 退
* @param newPlayers Map
*/
public updatePlayers(newPlayers: Map<string, Player>): void {
this.mPlayers = newPlayers;
}
public deletePlayer(playerId:string)
{
if(this.mPlayers.has(playerId))
{
this.mPlayers.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)) {
// if (pairing.isResting) {
// console.log(`${pairing.playerId} 轮空 (对手: ${pairing.opponentId})`);
// } else {
console.log(`${pairing.playerId} VS ${pairing.opponentId}`);
// }
processedPairs.add(pairKey);
}
});
}
export { Matchmaker, PairingResult, createMatchmaker, displayPairings };