First update.
This commit is contained in:
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
node_modules
|
||||
*.map
|
26
CreateRoom.js
Normal file
26
CreateRoom.js
Normal file
@@ -0,0 +1,26 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.generateStealthId = generateStealthId;
|
||||
exports.isStealthId = isStealthId;
|
||||
function generateStealthId(length = 32) {
|
||||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
const base = chars.length;
|
||||
const randomChars = (count) => Array.from({ length: count }, () => chars.charAt(Math.floor(Math.random() * base))).join('');
|
||||
// 生成前 length - 1 位
|
||||
const body = randomChars(length - 1);
|
||||
// 将 body 每个字符的 charCode 累加,再 mod base,作为校验位
|
||||
const sum = body.split('').reduce((acc, c) => acc + c.charCodeAt(0), 0);
|
||||
const checksum = chars.charAt(sum % base);
|
||||
return body + checksum;
|
||||
}
|
||||
function isStealthId(id) {
|
||||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
const base = chars.length;
|
||||
if (!/^[A-Za-z0-9]{32}$/.test(id))
|
||||
return false;
|
||||
const body = id.slice(0, -1);
|
||||
const expectedChecksum = id.slice(-1);
|
||||
const sum = body.split('').reduce((acc, c) => acc + c.charCodeAt(0), 0);
|
||||
const checksum = chars.charAt(sum % base);
|
||||
return checksum === expectedChecksum;
|
||||
}
|
31
CreateRoom.ts
Normal file
31
CreateRoom.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
export function generateStealthId(length: number = 32): string {
|
||||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
const base = chars.length;
|
||||
|
||||
const randomChars = (count: number): string =>
|
||||
Array.from({ length: count }, () => chars.charAt(Math.floor(Math.random() * base))).join('');
|
||||
|
||||
// 生成前 length - 1 位
|
||||
const body = randomChars(length - 1);
|
||||
|
||||
// 将 body 每个字符的 charCode 累加,再 mod base,作为校验位
|
||||
const sum = body.split('').reduce((acc, c) => acc + c.charCodeAt(0), 0);
|
||||
const checksum = chars.charAt(sum % base);
|
||||
|
||||
return body + checksum;
|
||||
}
|
||||
|
||||
export function isStealthId(id: string): boolean {
|
||||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
const base = chars.length;
|
||||
|
||||
if (!/^[A-Za-z0-9]{32}$/.test(id)) return false;
|
||||
|
||||
const body = id.slice(0, -1);
|
||||
const expectedChecksum = id.slice(-1);
|
||||
|
||||
const sum = body.split('').reduce((acc, c) => acc + c.charCodeAt(0), 0);
|
||||
const checksum = chars.charAt(sum % base);
|
||||
|
||||
return checksum === expectedChecksum;
|
||||
}
|
42
CreateRoomMain.js
Normal file
42
CreateRoomMain.js
Normal file
@@ -0,0 +1,42 @@
|
||||
|
||||
//const WebSocket = require('ws');
|
||||
|
||||
// const wss = new WebSocket.Server({ port: 3000 });
|
||||
|
||||
// wss.on('connection', function connection(ws) {
|
||||
// console.log('A new client is connected.');
|
||||
|
||||
// ws.on('message', function incoming(message) {
|
||||
// console.log('received: %s', message);
|
||||
|
||||
// // 向客户端发送消息
|
||||
// ws.send('Hello, this is a server response.');
|
||||
// });
|
||||
|
||||
// ws.on('close', function close() {
|
||||
// console.log('Client has disconnected.');
|
||||
// });
|
||||
// });
|
||||
|
||||
// console.log('WebSocket server is running on ws://localhost:8080')
|
||||
|
||||
const creator = require('./CreateRoom');
|
||||
// creator.generateStealthId();
|
||||
// //for(var i=0; i< 10; ++i)
|
||||
// {
|
||||
// var test = creator.generateStealthId(32);
|
||||
// console.debug(test);
|
||||
// console.info(creator.isStealthId("E5bFajEiLgQRx5eNV2VH6sFuXKSjFWsf"));
|
||||
// }
|
||||
|
||||
const io = require('socket.io-client');
|
||||
const socket = io("ws://39.185.226.199:2999");
|
||||
socket.on("connect", () => {
|
||||
console.log("已连接 SocketIO,id:", socket.id);
|
||||
const id = "huYP8TZpx30QYS0GeXYH8q28TWW5tqCN";
|
||||
socket.emit("createRoom",{ roomId: id});
|
||||
});
|
||||
|
||||
socket.on("createRoomResult",({result})=>{
|
||||
console.log("createRoomResult:", result);
|
||||
});
|
4
GameDefine.js
Normal file
4
GameDefine.js
Normal file
@@ -0,0 +1,4 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.ALL_PLAYER_NUM = void 0;
|
||||
exports.ALL_PLAYER_NUM = 6;
|
1
GameDefine.ts
Normal file
1
GameDefine.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const ALL_PLAYER_NUM:number = 6;
|
122
GameManager.js
Normal file
122
GameManager.js
Normal file
@@ -0,0 +1,122 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.GameManager = void 0;
|
||||
const GameDefine_1 = require("./GameDefine");
|
||||
class GameManager {
|
||||
constructor() {
|
||||
this.mRound = 0;
|
||||
this.mGameFightPlayer = new Map;
|
||||
this.mAlivePlayerCount = GameDefine_1.ALL_PLAYER_NUM;
|
||||
this.mCharacterNames = ["飞鸟仓介", "雾岛林汐", "飞鸟千寻", "小林琉璃", "小林莲夜"];
|
||||
}
|
||||
clearGameRoundPlayer() {
|
||||
this.mGameFightPlayer.clear();
|
||||
}
|
||||
isGameRoundPrepared() {
|
||||
if (this.mGameFightPlayer.size != this.mAlivePlayerCount) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
//返回值:是否所有人准备完毕
|
||||
setGameRoundInfo(aPlayerID, aPlayer) {
|
||||
if (aPlayer.mHp <= 0) {
|
||||
return false;
|
||||
}
|
||||
this.mGameFightPlayer.set(aPlayerID, aPlayer);
|
||||
let result = this.isGameRoundPrepared();
|
||||
if (result) {
|
||||
this.calcuteGameFightResult();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
claculatePlayerHp(aAttack, aDefender) {
|
||||
if (aAttack.mAttack > aDefender.mDefener) {
|
||||
aDefender.mHp -= aAttack.mAttack - aDefender.mDefener;
|
||||
}
|
||||
}
|
||||
claculateRanking(aPlayers) {
|
||||
let sortPlayers = [];
|
||||
for (const player of aPlayers.values()) {
|
||||
if (player.mHp <= 0) {
|
||||
sortPlayers.push(player);
|
||||
this.mAlivePlayerCount--;
|
||||
}
|
||||
}
|
||||
if (sortPlayers.length > 0) {
|
||||
sortPlayers.sort((a, b) => a.mAgile - b.mAgile);
|
||||
let playerNum = aPlayers.size;
|
||||
for (let i = 0; i < sortPlayers.length; ++i) {
|
||||
sortPlayers[i].mRank = playerNum - i;
|
||||
if (sortPlayers[i].mCharacterName != "") {
|
||||
continue;
|
||||
}
|
||||
let characterName = this.mCharacterNames.pop();
|
||||
sortPlayers[i].mCharacterName = characterName;
|
||||
if (characterName == "飞鸟千寻") {
|
||||
let player = aPlayers.get(sortPlayers[i].mCurrentEnemy);
|
||||
player.mCharacterName = "高桥林佑";
|
||||
}
|
||||
}
|
||||
}
|
||||
//计算最后一个存活玩家的排名
|
||||
if (this.mAlivePlayerCount == 1) {
|
||||
for (const player of aPlayers.values()) {
|
||||
if (player.mHp > 0) {
|
||||
player.mRank = 1;
|
||||
if (this.mCharacterNames.length > 0) {
|
||||
player.mCharacterName = this.mCharacterNames.pop();
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
calcuteGameFightResult() {
|
||||
let temp = new Map(this.mGameFightPlayer);
|
||||
let restRound = null;
|
||||
while (temp.size > 0) {
|
||||
const iterator = temp.keys();
|
||||
const playerID = iterator.next().value;
|
||||
let player = temp.get(playerID);
|
||||
if (!player) {
|
||||
return;
|
||||
}
|
||||
if (player.mIsResting) {
|
||||
restRound = [player, this.mGameFightPlayer.get(player.mCurrentEnemy)],
|
||||
temp.delete(playerID);
|
||||
continue;
|
||||
}
|
||||
else {
|
||||
let enemy = temp.get(player.mCurrentEnemy);
|
||||
if (player.mAgile >= enemy.mAgile) //palyer 敏捷高
|
||||
{
|
||||
this.claculatePlayerHp(player, enemy);
|
||||
if (enemy.mHp > 0) {
|
||||
this.claculatePlayerHp(enemy, player);
|
||||
}
|
||||
}
|
||||
else //enemy敏捷高
|
||||
{
|
||||
this.claculatePlayerHp(enemy, player);
|
||||
if (player.mHp > 0) {
|
||||
this.claculatePlayerHp(player, enemy);
|
||||
}
|
||||
}
|
||||
temp.delete(playerID);
|
||||
temp.delete(player.mCurrentEnemy);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (restRound != null) {
|
||||
let restPlayer = restRound[0];
|
||||
let enemy = restRound[1];
|
||||
if (enemy.mHp > 0) {
|
||||
this.claculatePlayerHp(enemy, restPlayer);
|
||||
}
|
||||
}
|
||||
this.claculateRanking(this.mGameFightPlayer);
|
||||
}
|
||||
}
|
||||
exports.GameManager = GameManager;
|
||||
;
|
152
GameManager.ts
Normal file
152
GameManager.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
import { ALL_PLAYER_NUM } from "./GameDefine";
|
||||
import { Player } from "./Player";
|
||||
|
||||
export class GameManager
|
||||
{
|
||||
mRound:number = 0;
|
||||
mGameFightPlayer:Map<string, Player>= new Map<string, Player>;
|
||||
mAlivePlayerCount:number = ALL_PLAYER_NUM;
|
||||
mCharacterNames:string[]= ["飞鸟仓介", "雾岛林汐", "飞鸟千寻", "小林琉璃", "小林莲夜"]
|
||||
|
||||
clearGameRoundPlayer()
|
||||
{
|
||||
this.mGameFightPlayer.clear();
|
||||
}
|
||||
|
||||
isGameRoundPrepared():boolean
|
||||
{
|
||||
if(this.mGameFightPlayer.size != this.mAlivePlayerCount)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
//返回值:是否所有人准备完毕
|
||||
setGameRoundInfo(aPlayerID:string, aPlayer:Player):boolean
|
||||
{
|
||||
if(aPlayer.mHp <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
this.mGameFightPlayer.set(aPlayerID, aPlayer);
|
||||
let result = this.isGameRoundPrepared();
|
||||
if(result)
|
||||
{
|
||||
this.calcuteGameFightResult();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
claculatePlayerHp(aAttack:Player, aDefender:Player)
|
||||
{
|
||||
if(aAttack.mAttack > aDefender.mDefener)
|
||||
{
|
||||
aDefender.mHp -= aAttack.mAttack - aDefender.mDefener;
|
||||
}
|
||||
}
|
||||
|
||||
claculateRanking(aPlayers:Map<string, Player>)
|
||||
{
|
||||
let sortPlayers:Player[] = [];
|
||||
for(const player of aPlayers.values())
|
||||
{
|
||||
if(player.mHp<=0)
|
||||
{
|
||||
sortPlayers.push(player);
|
||||
this.mAlivePlayerCount--;
|
||||
}
|
||||
}
|
||||
if(sortPlayers.length > 0)
|
||||
{
|
||||
sortPlayers.sort((a,b)=>a.mAgile - b.mAgile);
|
||||
let playerNum = aPlayers.size;
|
||||
for(let i=0; i<sortPlayers.length;++i)
|
||||
{
|
||||
sortPlayers[i].mRank = playerNum - i;
|
||||
if(sortPlayers[i].mCharacterName != "")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let characterName = this.mCharacterNames.pop()!;
|
||||
sortPlayers[i].mCharacterName = characterName;
|
||||
if(characterName == "飞鸟千寻")
|
||||
{
|
||||
let player = aPlayers.get(sortPlayers[i].mCurrentEnemy)!;
|
||||
player.mCharacterName = "高桥林佑";
|
||||
}
|
||||
}
|
||||
}
|
||||
//计算最后一个存活玩家的排名
|
||||
if(this.mAlivePlayerCount == 1)
|
||||
{
|
||||
for(const player of aPlayers.values())
|
||||
{
|
||||
if(player.mHp > 0)
|
||||
{
|
||||
player.mRank = 1;
|
||||
if(this.mCharacterNames.length > 0)
|
||||
{
|
||||
player.mCharacterName = this.mCharacterNames.pop()!;
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
calcuteGameFightResult()
|
||||
{
|
||||
let temp = new Map<string, Player>(this.mGameFightPlayer);
|
||||
let restRound:[Player,Player] | null = null;
|
||||
while (temp.size > 0)
|
||||
{
|
||||
const iterator = temp.keys();
|
||||
const playerID = iterator.next().value!;
|
||||
let player = temp.get(playerID);
|
||||
if(!player)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if(player.mIsResting)
|
||||
{
|
||||
restRound = [player, this.mGameFightPlayer.get(player.mCurrentEnemy)!],
|
||||
temp.delete(playerID);
|
||||
continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
let enemy = temp.get(player.mCurrentEnemy)!;
|
||||
if(player.mAgile >= enemy.mAgile)//palyer 敏捷高
|
||||
{
|
||||
this.claculatePlayerHp(player, enemy);
|
||||
if(enemy.mHp > 0)
|
||||
{
|
||||
this.claculatePlayerHp(enemy, player);
|
||||
}
|
||||
}
|
||||
else//enemy敏捷高
|
||||
{
|
||||
this.claculatePlayerHp(enemy, player);
|
||||
if(player.mHp > 0)
|
||||
{
|
||||
this.claculatePlayerHp(player, enemy);
|
||||
}
|
||||
}
|
||||
temp.delete(playerID);
|
||||
temp.delete(player.mCurrentEnemy);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if(restRound != null)
|
||||
{
|
||||
let restPlayer = restRound[0];
|
||||
let enemy = restRound[1];
|
||||
if(enemy.mHp > 0)
|
||||
{
|
||||
this.claculatePlayerHp(enemy, restPlayer);
|
||||
}
|
||||
}
|
||||
this.claculateRanking(this.mGameFightPlayer);
|
||||
}
|
||||
};
|
8
GetLocalTime.js
Normal file
8
GetLocalTime.js
Normal file
@@ -0,0 +1,8 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.getLocalTime = getLocalTime;
|
||||
function getLocalTime() {
|
||||
const now = new Date();
|
||||
now.setHours(now.getHours() + 8);
|
||||
return now;
|
||||
}
|
6
GetLocalTime.ts
Normal file
6
GetLocalTime.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export function getLocalTime():Date
|
||||
{
|
||||
const now = new Date();
|
||||
now.setHours(now.getHours() + 8);
|
||||
return now;
|
||||
}
|
220
MatchMaker.js
Normal file
220
MatchMaker.js
Normal file
@@ -0,0 +1,220 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.Matchmaker = void 0;
|
||||
exports.createMatchmaker = createMatchmaker;
|
||||
exports.displayPairings = displayPairings;
|
||||
class Matchmaker {
|
||||
constructor(players) {
|
||||
this.roundHistory = []; // 记录每轮的配对历史
|
||||
this.currentRound = 0;
|
||||
this.mPlayers = players;
|
||||
}
|
||||
/**
|
||||
* 为当前轮次生成配对
|
||||
* @returns 配对结果数组
|
||||
*/
|
||||
generatePairings() {
|
||||
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 配对结果
|
||||
*/
|
||||
createOptimalPairings(playerIds) {
|
||||
const playerCount = playerIds.length;
|
||||
const isOddCount = playerCount % 2 === 1;
|
||||
// 如果是奇数,需要找一个玩家轮空
|
||||
let restingPlayer = null;
|
||||
let activePlayers = [...playerIds];
|
||||
if (isOddCount) {
|
||||
restingPlayer = this.selectRestingPlayer(playerIds);
|
||||
activePlayers = playerIds.filter(id => id !== restingPlayer);
|
||||
}
|
||||
// 对剩余玩家进行配对
|
||||
const pairings = [];
|
||||
const usedPlayers = new Set();
|
||||
// 使用轮转算法确保配对的多样性
|
||||
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
|
||||
*/
|
||||
selectRestingPlayer(playerIds) {
|
||||
const restingCounts = new Map();
|
||||
// 统计每个玩家的轮空次数
|
||||
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
|
||||
*/
|
||||
selectGhostOpponent(restingPlayer, allPlayers) {
|
||||
// 选择与轮空玩家对战次数最少的玩家作为幽灵对手
|
||||
const opponentCounts = new Map();
|
||||
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 旋转后的玩家数组
|
||||
*/
|
||||
rotatePlayerOrder(playerIds) {
|
||||
if (playerIds.length <= 2) {
|
||||
return [...playerIds];
|
||||
}
|
||||
const rotateAmount = this.currentRound % playerIds.length;
|
||||
return [
|
||||
...playerIds.slice(rotateAmount),
|
||||
...playerIds.slice(0, rotateAmount)
|
||||
];
|
||||
}
|
||||
/**
|
||||
* 获取配对历史
|
||||
* @returns 历史配对记录
|
||||
*/
|
||||
getPairingHistory() {
|
||||
return [...this.roundHistory];
|
||||
}
|
||||
/**
|
||||
* 重置配对历史(新游戏开始时调用)
|
||||
*/
|
||||
resetHistory() {
|
||||
this.roundHistory = [];
|
||||
this.currentRound = 0;
|
||||
}
|
||||
/**
|
||||
* 更新玩家列表(当有玩家加入或退出时调用)
|
||||
* @param newPlayers 新的玩家Map
|
||||
*/
|
||||
updatePlayers(newPlayers) {
|
||||
this.mPlayers = newPlayers;
|
||||
}
|
||||
deletePlayer(playerId) {
|
||||
if (this.mPlayers.has(playerId)) {
|
||||
this.mPlayers.delete(playerId);
|
||||
}
|
||||
}
|
||||
}
|
||||
exports.Matchmaker = Matchmaker;
|
||||
// 使用示例和辅助函数
|
||||
function createMatchmaker(mPlayers) {
|
||||
return new Matchmaker(mPlayers);
|
||||
}
|
||||
// 显示配对结果的辅助函数
|
||||
function displayPairings(pairings) {
|
||||
console.log(`=== 第${pairings.length > 0 ? '当前' : '0'}轮配对结果 ===`);
|
||||
const processedPairs = new Set();
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
267
MatchMaker.ts
Normal file
267
MatchMaker.ts
Normal file
@@ -0,0 +1,267 @@
|
||||
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 };
|
33
Player.js
Normal file
33
Player.js
Normal file
@@ -0,0 +1,33 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.Player = void 0;
|
||||
const DEFAULT_HP = 100;
|
||||
class Player {
|
||||
constructor(aName, aSocketId) {
|
||||
this.mCards = [];
|
||||
this.mStrength = 0;
|
||||
this.mStamina = 0;
|
||||
this.mAgile = 0;
|
||||
this.mAttack = 0;
|
||||
this.mDefener = 0;
|
||||
this.mHp = DEFAULT_HP;
|
||||
this.mPrepared = false;
|
||||
this.mCurrentEnemy = "";
|
||||
this.mIsResting = false;
|
||||
this.mRank = -1;
|
||||
this.mCharacterName = "";
|
||||
this.mPlayerName = aName;
|
||||
this.mSocketId = aSocketId;
|
||||
}
|
||||
setPlayerInfo(aStrength, aStamina, aAgile, aCards) {
|
||||
this.mStrength = aStrength;
|
||||
this.mStamina = aStamina;
|
||||
this.mAgile = aAgile;
|
||||
this.mCards = aCards;
|
||||
this.mPrepared = true;
|
||||
}
|
||||
setPlayerSocketId(aId) {
|
||||
this.mSocketId = aId;
|
||||
}
|
||||
}
|
||||
exports.Player = Player;
|
40
Player.ts
Normal file
40
Player.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { Socket } from "socket.io";
|
||||
|
||||
const DEFAULT_HP = 100;
|
||||
|
||||
export class Player
|
||||
{
|
||||
mPlayerName:string;
|
||||
mSocketId:string;
|
||||
mCards:number[] = [];
|
||||
mStrength:number = 0;
|
||||
mStamina:number = 0;
|
||||
mAgile:number = 0;
|
||||
mAttack:number = 0;
|
||||
mDefener:number =0;
|
||||
mHp:number = DEFAULT_HP;
|
||||
mPrepared:boolean = false;
|
||||
mCurrentEnemy:string = "";
|
||||
mIsResting = false;
|
||||
mRank:number = -1;
|
||||
mCharacterName:string = "";
|
||||
constructor(aName:string, aSocketId:string)
|
||||
{
|
||||
this.mPlayerName = aName;
|
||||
this.mSocketId = aSocketId;
|
||||
}
|
||||
|
||||
setPlayerInfo(aStrength:number, aStamina:number, aAgile:number, aCards:number[])
|
||||
{
|
||||
this.mStrength = aStrength;
|
||||
this.mStamina = aStamina;
|
||||
this.mAgile = aAgile;
|
||||
this.mCards = aCards;
|
||||
this.mPrepared = true;
|
||||
}
|
||||
|
||||
setPlayerSocketId(aId:string)
|
||||
{
|
||||
this.mSocketId = aId;
|
||||
}
|
||||
}
|
194
RoomManager.js
Normal file
194
RoomManager.js
Normal file
@@ -0,0 +1,194 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.RoomManager = void 0;
|
||||
const Player_1 = require("./Player");
|
||||
const MatchMaker_1 = require("./MatchMaker");
|
||||
const GameDefine_1 = require("./GameDefine");
|
||||
const GameManager_1 = require("./GameManager");
|
||||
const GetLocalTime_1 = require("./GetLocalTime");
|
||||
;
|
||||
class Room {
|
||||
constructor() {
|
||||
this.mPlayers = new Map;
|
||||
this.mStatus = 0; //0:房间开启;1:所有人准备,进入fight;2:fight结束
|
||||
this.mMatchMaker = (0, MatchMaker_1.createMatchmaker)(this.mPlayers);
|
||||
this.mGameManager = new GameManager_1.GameManager();
|
||||
this.mCreatedTime = (0, GetLocalTime_1.getLocalTime)();
|
||||
}
|
||||
isRoomPrepared() {
|
||||
if (this.mPlayers.size != GameDefine_1.ALL_PLAYER_NUM) {
|
||||
return false;
|
||||
}
|
||||
for (const value of this.mPlayers.values()) {
|
||||
if (value.mPrepared == false) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
setRoomPrepared() {
|
||||
this.generatePairings();
|
||||
this.mStatus = 1;
|
||||
}
|
||||
generatePairings() {
|
||||
let tempPlayers = new Map;
|
||||
for (const key of this.mPlayers.keys()) {
|
||||
if (this.mPlayers.get(key).mHp > 0) {
|
||||
tempPlayers.set(key, this.mPlayers.get(key));
|
||||
}
|
||||
}
|
||||
this.mMatchMaker.updatePlayers(tempPlayers);
|
||||
let result = this.mMatchMaker.generatePairings();
|
||||
for (let i = 0; i < result.length; ++i) {
|
||||
let player = this.mPlayers.get(result[i].playerId);
|
||||
if (player) {
|
||||
player.mCurrentEnemy = result[i].opponentId;
|
||||
player.mIsResting = result[i].isResting;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
getGameInfos() {
|
||||
let gameInfo = [];
|
||||
for (const player of this.mPlayers.values()) {
|
||||
gameInfo.push({ playerId: player.mPlayerName,
|
||||
opponentId: player.mCurrentEnemy,
|
||||
socketID: player.mSocketId,
|
||||
rank: player.mRank,
|
||||
character: player.mCharacterName,
|
||||
hp: player.mHp });
|
||||
}
|
||||
return gameInfo;
|
||||
}
|
||||
getGameRoundInfos() {
|
||||
let gameInfo = [];
|
||||
for (const player of this.mGameManager.mGameFightPlayer.values()) {
|
||||
gameInfo.push({ playerId: player.mPlayerName,
|
||||
opponentId: player.mCurrentEnemy,
|
||||
socketID: player.mSocketId,
|
||||
rank: player.mRank,
|
||||
character: player.mCharacterName,
|
||||
hp: player.mHp });
|
||||
}
|
||||
return gameInfo;
|
||||
}
|
||||
getGameInfo(aPlayerID) {
|
||||
let player = this.mPlayers.get(aPlayerID);
|
||||
if (player) {
|
||||
return { playerId: player.mPlayerName, opponentId: player.mCurrentEnemy, socketID: player.mSocketId, rank: player.mRank, character: player.mCharacterName, hp: player.mHp };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
setGameFight(aPlayerID, aAttack, aDefender) {
|
||||
let player = this.mPlayers.get(aPlayerID);
|
||||
if (!player) {
|
||||
return false;
|
||||
}
|
||||
player.mAttack = aAttack;
|
||||
player.mDefener = aDefender;
|
||||
return this.mGameManager.setGameRoundInfo(aPlayerID, player);
|
||||
}
|
||||
}
|
||||
class RoomManager {
|
||||
constructor() {
|
||||
this.mRooms = new Map;
|
||||
}
|
||||
static getInstance() {
|
||||
if (this.mInstance == null) {
|
||||
this.mInstance = new RoomManager();
|
||||
}
|
||||
return this.mInstance;
|
||||
}
|
||||
join(aRoomID, aName, aSocket) {
|
||||
if (!this.mRooms.has(aRoomID)) {
|
||||
return [false, "No room Id", null, null];
|
||||
}
|
||||
let room = this.mRooms.get(aRoomID);
|
||||
if (room.mPlayers.has(aName)) {
|
||||
let player = room.mPlayers.get(aName);
|
||||
player.setPlayerSocketId(aSocket.id);
|
||||
aSocket.join(aRoomID);
|
||||
return [true, "", room.mStatus, player];
|
||||
}
|
||||
if (room.mPlayers.size < GameDefine_1.ALL_PLAYER_NUM) {
|
||||
let player = new Player_1.Player(aName, aSocket.id);
|
||||
aSocket.join(aRoomID);
|
||||
room.mPlayers.set(aName, player);
|
||||
return [true, "", 0, null];
|
||||
}
|
||||
return [false, "room full", null, null];
|
||||
}
|
||||
setPlayerInfo(roomId, playerId, strength, stamina, agile, cards) {
|
||||
let room = this.mRooms.get(roomId);
|
||||
if (!room) {
|
||||
return false;
|
||||
}
|
||||
let player = room.mPlayers.get(playerId);
|
||||
if (!player) {
|
||||
return false;
|
||||
}
|
||||
player.setPlayerInfo(strength, stamina, agile, cards);
|
||||
if (room.isRoomPrepared() == true) {
|
||||
room.setRoomPrepared();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
createRoom(aRoomID) {
|
||||
this.mRooms.set(aRoomID, new Room());
|
||||
}
|
||||
getPlayersByRoomID(aRoomID) {
|
||||
let room = this.mRooms.get(aRoomID);
|
||||
let result = [];
|
||||
if (room === null || room === void 0 ? void 0 : room.mPlayers) {
|
||||
for (const value of room.mPlayers.values()) {
|
||||
result.push({ playerName: value.mPlayerName, playerPrepared: value.mPrepared });
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
getGameInfos(aRoomID) {
|
||||
let room = this.mRooms.get(aRoomID);
|
||||
if (room) {
|
||||
return room.getGameInfos();
|
||||
}
|
||||
return [];
|
||||
}
|
||||
getGameInfo(aRoomID, aPlayerID) {
|
||||
let room = this.mRooms.get(aRoomID);
|
||||
if (room) {
|
||||
return room.getGameInfo(aPlayerID);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
getGameRoundInfos(aRoomID, aPlayerID) {
|
||||
let room = this.mRooms.get(aRoomID);
|
||||
if (room) {
|
||||
return room.getGameRoundInfos();
|
||||
}
|
||||
return [];
|
||||
}
|
||||
//返回值是否所有人都准备完毕
|
||||
setGameFight(aRoomID, aPlayerID, aAttack, aDefender) {
|
||||
let room = this.mRooms.get(aRoomID);
|
||||
if (!room) {
|
||||
return false;
|
||||
}
|
||||
let result = room.setGameFight(aPlayerID, aAttack, aDefender);
|
||||
if (result) {
|
||||
room.generatePairings();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
clearGameRoundPlayer(aRoomID) {
|
||||
let room = this.mRooms.get(aRoomID);
|
||||
if (!room) {
|
||||
return;
|
||||
}
|
||||
room.mGameManager.clearGameRoundPlayer();
|
||||
}
|
||||
isRoomExist(aRoomID) {
|
||||
return this.mRooms.has(aRoomID);
|
||||
}
|
||||
}
|
||||
exports.RoomManager = RoomManager;
|
276
RoomManager.ts
Normal file
276
RoomManager.ts
Normal file
@@ -0,0 +1,276 @@
|
||||
import { Socket } from "socket.io";
|
||||
import { Player } from "./Player";
|
||||
import { Json } from "sequelize/types/utils";
|
||||
import { Matchmaker, createMatchmaker, PairingResult } from "./MatchMaker";
|
||||
import { ALL_PLAYER_NUM } from "./GameDefine";
|
||||
import { GameManager } from "./GameManager";
|
||||
import { getLocalTime } from "./GetLocalTime";
|
||||
|
||||
|
||||
type RoomPlayerInfo = {
|
||||
playerName: string;
|
||||
playerPrepared: boolean;
|
||||
};
|
||||
|
||||
interface GameInfo {
|
||||
playerId: string;
|
||||
opponentId: string;
|
||||
socketID: string;
|
||||
rank: number;
|
||||
character: string;
|
||||
hp: number;
|
||||
};
|
||||
|
||||
class Room
|
||||
{
|
||||
mPlayers:Map<string, Player> = new Map<string, Player>
|
||||
mStatus:number = 0;//0:房间开启;1:所有人准备,进入fight;2:fight结束
|
||||
mMatchMaker:Matchmaker = createMatchmaker(this.mPlayers);
|
||||
mGameManager:GameManager = new GameManager();
|
||||
mCreatedTime:Date = getLocalTime();
|
||||
|
||||
isRoomPrepared():boolean
|
||||
{
|
||||
if(this.mPlayers.size != ALL_PLAYER_NUM)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for(const value of this.mPlayers.values())
|
||||
{
|
||||
if(value.mPrepared == false)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
setRoomPrepared()
|
||||
{
|
||||
this.generatePairings();
|
||||
this.mStatus = 1;
|
||||
}
|
||||
|
||||
generatePairings():PairingResult[]
|
||||
{
|
||||
let tempPlayers:Map<string, Player> = new Map<string, Player>;
|
||||
for(const key of this.mPlayers.keys())
|
||||
{
|
||||
if(this.mPlayers.get(key)!.mHp > 0)
|
||||
{
|
||||
tempPlayers.set(key, this.mPlayers.get(key)!);
|
||||
}
|
||||
}
|
||||
this.mMatchMaker.updatePlayers(tempPlayers);
|
||||
let result = this.mMatchMaker.generatePairings();
|
||||
for(let i=0; i<result.length; ++i)
|
||||
{
|
||||
let player = this.mPlayers.get(result[i].playerId);
|
||||
if(player)
|
||||
{
|
||||
player.mCurrentEnemy = result[i].opponentId;
|
||||
player.mIsResting = result[i].isResting;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
getGameInfos():GameInfo[]
|
||||
{
|
||||
let gameInfo: GameInfo[] = [];
|
||||
for(const player of this.mPlayers.values())
|
||||
{
|
||||
gameInfo.push({playerId:player.mPlayerName,
|
||||
opponentId:player.mCurrentEnemy,
|
||||
socketID: player.mSocketId,
|
||||
rank: player.mRank,
|
||||
character: player.mCharacterName,
|
||||
hp: player.mHp});
|
||||
}
|
||||
return gameInfo;
|
||||
}
|
||||
|
||||
getGameRoundInfos():GameInfo[]
|
||||
{
|
||||
let gameInfo: GameInfo[] = [];
|
||||
for(const player of this.mGameManager.mGameFightPlayer.values())
|
||||
{
|
||||
gameInfo.push({playerId:player.mPlayerName,
|
||||
opponentId:player.mCurrentEnemy,
|
||||
socketID: player.mSocketId,
|
||||
rank: player.mRank,
|
||||
character: player.mCharacterName,
|
||||
hp: player.mHp});
|
||||
}
|
||||
return gameInfo;
|
||||
}
|
||||
|
||||
getGameInfo(aPlayerID:string):GameInfo|null
|
||||
{
|
||||
let player = this.mPlayers.get(aPlayerID);
|
||||
if(player)
|
||||
{
|
||||
return {playerId:player.mPlayerName,opponentId:player.mCurrentEnemy,socketID:player.mSocketId,rank:player.mRank,character: player.mCharacterName, hp:player.mHp}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
setGameFight(aPlayerID:string, aAttack:number, aDefender:number):boolean
|
||||
{
|
||||
let player = this.mPlayers.get(aPlayerID);
|
||||
if(!player)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
player.mAttack = aAttack;
|
||||
player.mDefener = aDefender;
|
||||
return this.mGameManager.setGameRoundInfo(aPlayerID, player);
|
||||
}
|
||||
}
|
||||
|
||||
export class RoomManager
|
||||
{
|
||||
static mInstance : RoomManager;
|
||||
static getInstance(): RoomManager
|
||||
{
|
||||
if(this.mInstance == null)
|
||||
{
|
||||
this.mInstance = new RoomManager();
|
||||
}
|
||||
return this.mInstance;
|
||||
}
|
||||
|
||||
mRooms = new Map<string, Room>;
|
||||
|
||||
join(aRoomID:string, aName:string, aSocket:Socket):[boolean, string, number | null, Player | null]
|
||||
{
|
||||
if(!this.mRooms.has(aRoomID))
|
||||
{
|
||||
return [false, "No room Id", null, null];
|
||||
}
|
||||
|
||||
let room = this.mRooms.get(aRoomID)!;
|
||||
if(room.mPlayers.has(aName))
|
||||
{
|
||||
let player = room.mPlayers.get(aName);
|
||||
player!.setPlayerSocketId(aSocket.id);
|
||||
aSocket.join(aRoomID);
|
||||
return [true, "", room.mStatus, player!];
|
||||
}
|
||||
|
||||
if(room.mPlayers.size < ALL_PLAYER_NUM)
|
||||
{
|
||||
let player = new Player(aName, aSocket.id);
|
||||
aSocket.join(aRoomID);
|
||||
room.mPlayers.set(aName, player);
|
||||
return [true,"", 0, null];
|
||||
}
|
||||
|
||||
return [false, "room full", null, null];
|
||||
}
|
||||
|
||||
setPlayerInfo(roomId:string, playerId:string, strength:number, stamina:number, agile:number, cards:number[])
|
||||
: boolean //返回值是是否所有玩家都准备完毕
|
||||
{
|
||||
let room = this.mRooms.get(roomId);
|
||||
if(!room)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
let player = room.mPlayers.get(playerId);
|
||||
if(!player)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
player.setPlayerInfo(strength, stamina, agile, cards);
|
||||
if(room.isRoomPrepared() == true )
|
||||
{
|
||||
room.setRoomPrepared();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
createRoom(aRoomID:string)
|
||||
{
|
||||
this.mRooms.set(aRoomID, new Room());
|
||||
}
|
||||
|
||||
getPlayersByRoomID(aRoomID:string):RoomPlayerInfo[]
|
||||
{
|
||||
let room = this.mRooms.get(aRoomID);
|
||||
let result:RoomPlayerInfo[] = [];
|
||||
if(room?.mPlayers)
|
||||
{
|
||||
for(const value of room.mPlayers.values())
|
||||
{
|
||||
result.push({playerName:value.mPlayerName,playerPrepared:value.mPrepared});
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
getGameInfos(aRoomID:string):GameInfo[]
|
||||
{
|
||||
let room = this.mRooms.get(aRoomID);
|
||||
if(room)
|
||||
{
|
||||
return room.getGameInfos();
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
getGameInfo(aRoomID:string, aPlayerID:string):GameInfo|null
|
||||
{
|
||||
let room = this.mRooms.get(aRoomID);
|
||||
if(room)
|
||||
{
|
||||
return room.getGameInfo(aPlayerID);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
getGameRoundInfos(aRoomID:string, aPlayerID:string):GameInfo[]
|
||||
{
|
||||
let room = this.mRooms.get(aRoomID);
|
||||
if(room)
|
||||
{
|
||||
return room.getGameRoundInfos();
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
//返回值是否所有人都准备完毕
|
||||
setGameFight(aRoomID:string, aPlayerID:string, aAttack:number, aDefender:number):boolean
|
||||
{
|
||||
let room = this.mRooms.get(aRoomID);
|
||||
if(!room)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
let result = room.setGameFight(aPlayerID, aAttack, aDefender);
|
||||
if(result)
|
||||
{
|
||||
room.generatePairings();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
clearGameRoundPlayer(aRoomID:string)
|
||||
{
|
||||
let room = this.mRooms.get(aRoomID);
|
||||
if(!room)
|
||||
{
|
||||
return;
|
||||
}
|
||||
room.mGameManager.clearGameRoundPlayer();
|
||||
}
|
||||
|
||||
isRoomExist(aRoomID:string):boolean
|
||||
{
|
||||
return this.mRooms.has(aRoomID);
|
||||
}
|
||||
}
|
153
Server.js
Normal file
153
Server.js
Normal file
@@ -0,0 +1,153 @@
|
||||
|
||||
const http = require('http');
|
||||
const logger = require('./logger')
|
||||
const { Server } = require('socket.io');
|
||||
const { getConnection, isRoomAvailable } = require('./db');
|
||||
const { isStealthId } = require('./CreateRoom')
|
||||
const {RoomManager} = require('./RoomManager');
|
||||
const server = http.createServer();
|
||||
const io = new Server(server, {
|
||||
cors: {
|
||||
origin: "*"
|
||||
}
|
||||
});
|
||||
|
||||
const RoomConnectionCount = new Map();
|
||||
|
||||
io.use((socket, next) => {
|
||||
const ip = socket.handshake.address;
|
||||
const roomId = socket.handshake.query.roomId;
|
||||
if(roomId == undefined)
|
||||
{
|
||||
logger.info("room id is empty", ip);
|
||||
return next(new Error("room id is empty:"));
|
||||
}
|
||||
if(!isStealthId(roomId) /*|| !RoomManager.getInstance().isRoomExist(roomId)*/)
|
||||
{
|
||||
logger.info("illegal roomId", ip, roomId);
|
||||
return next(new Error("illegal room id"));
|
||||
}
|
||||
|
||||
// 限制:最大并发数
|
||||
const count = RoomConnectionCount.get(roomId) || 0;
|
||||
if (count >= 9) {
|
||||
logger.info("Room max connection", ip);
|
||||
return next(new Error("Too many connections from this room"));
|
||||
}
|
||||
|
||||
// 记录连接
|
||||
RoomConnectionCount.set(roomId, count + 1);
|
||||
logger.info('Client connected', ip, roomId, count + 1);
|
||||
next();
|
||||
});
|
||||
|
||||
|
||||
io.on('connection', (socket) => {
|
||||
socket.on('createRoom', async ({ roomId }) => {
|
||||
(async () => {
|
||||
const conn = await getConnection();
|
||||
if (!conn) return;
|
||||
|
||||
try {
|
||||
const isAvailable = await isRoomAvailable(conn, roomId);
|
||||
if(isAvailable)
|
||||
{
|
||||
RoomManager.getInstance().createRoom(roomId);
|
||||
}
|
||||
socket.emit('createRoomResult', {result:isAvailable});
|
||||
logger.info('createRoom', isAvailable, roomId);
|
||||
} catch (err) {
|
||||
socket.emit('createRoomResult', {result:false});
|
||||
logger.info('createRoom failed', roomId);
|
||||
} finally {
|
||||
conn.release();
|
||||
}
|
||||
})();
|
||||
});
|
||||
|
||||
socket.on('joinRoom', async ({ roomId, playerId }) => {
|
||||
let result = RoomManager.getInstance().join(roomId, playerId, socket);
|
||||
socket.emit('joinRoomResult', {result:result[0],message:result[1],roomStatus:result[2], playerInfo: result[3]});
|
||||
logger.info('joinRoom', result[0], result[1], "roomId:", roomId, "PlayerId:", playerId);
|
||||
if(result[0] == true && result[3] == null)//第一次加入房间
|
||||
{
|
||||
io.to(roomId).emit('updateRoomPlayerName', RoomManager.getInstance().getPlayersByRoomID(roomId));
|
||||
}
|
||||
else if(result[3] != null)//角色创建完后,断线后再次加入房间
|
||||
{
|
||||
socket.emit('updateRoomPlayerName', RoomManager.getInstance().getPlayersByRoomID(roomId));
|
||||
let gameInfo = RoomManager.getInstance().getGameInfo(roomId, playerId);
|
||||
if(gameInfo != null)
|
||||
{
|
||||
socket.emit('updateGameInfo', {enemyName:gameInfo.opponentId, hp:gameInfo.hp, rank:gameInfo.rank, character:gameInfo.character});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('leaveRoom', async ({ roomId, playerId }) => {
|
||||
// roomManager.leaveRoom(roomId, playerId);
|
||||
// socket.leave(roomId);
|
||||
// await db.execute('DELETE FROM room_players WHERE room_id = ? AND player_id = ?', [roomId, playerId]);
|
||||
// io.to(roomId).emit('playerLeft', { playerId });
|
||||
});
|
||||
|
||||
socket.on('getRoomInfo', async ({ roomId }) => {
|
||||
let room = RoomManager.getInstance().mRooms.get(roomId);
|
||||
socket.emit('getRoomInfoResult', {result:room});
|
||||
});
|
||||
|
||||
socket.on("setPlayerInfo", async ({roomId, playerId, strength, stamina, agile, cards})=>
|
||||
{
|
||||
let isRoomReady = RoomManager.getInstance().setPlayerInfo(roomId, playerId, strength, stamina, agile, cards);
|
||||
socket.emit('setPlayerInfoResult', {result:true,message:""});
|
||||
io.to(roomId).emit('updateRoomPlayerPrepare', {playerName:playerId});
|
||||
if (isRoomReady)
|
||||
{
|
||||
io.to(roomId).emit('roomReady', {roomStatus:1});
|
||||
let gameInfos = RoomManager.getInstance().getGameInfos(roomId);
|
||||
for(let i=0; i<gameInfos.length; ++i)
|
||||
{
|
||||
let gameInfo = gameInfos[i];
|
||||
//更新第一次进入fight场景时对手和hp
|
||||
io.to(gameInfo.socketID).emit('updateGameInfo', {enemyName:gameInfo.opponentId, hp:gameInfo.hp, rank:gameInfo.rank, character:gameInfo.character});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('setFightInfo', async ({roomId, playerId, attack, defender})=>
|
||||
{
|
||||
let isFightReady = RoomManager.getInstance().setGameFight(roomId, playerId, attack, defender);
|
||||
socket.emit('setFightInfoResult');
|
||||
io.to(roomId).emit('updateRoomPlayerFightStatus', {playerName:playerId});
|
||||
if(isFightReady)
|
||||
{
|
||||
let gameInfos = RoomManager.getInstance().getGameRoundInfos(roomId);
|
||||
for(let i=0; i<gameInfos.length; ++i)
|
||||
{
|
||||
let gameInfo = gameInfos[i];
|
||||
io.to(gameInfo.socketID).emit('endGameRound', {enemyName:gameInfo.opponentId, rank:gameInfo.rank, character:gameInfo.character, hp:gameInfo.hp});
|
||||
if(gameInfo.hp <= 0)
|
||||
{
|
||||
io.to(roomId).emit('setPlayerDead', gameInfo.playerId);
|
||||
}
|
||||
}
|
||||
RoomManager.getInstance().clearGameRoundPlayer(roomId);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('disconnect', async () => {
|
||||
const ip = socket.handshake.address;
|
||||
const roomId = socket.handshake.query.roomId;
|
||||
const count = (RoomConnectionCount.get(roomId) || 1) - 1;
|
||||
if (count <= 0) {
|
||||
RoomConnectionCount.delete(roomId);
|
||||
} else {
|
||||
RoomConnectionCount.set(roomId, count);
|
||||
}
|
||||
logger.info('Client disconnected', ip, roomId, count);
|
||||
});
|
||||
});
|
||||
|
||||
server.listen(2888, () => {
|
||||
logger.info('Socket.IO server running');
|
||||
});
|
68
db.js
Normal file
68
db.js
Normal file
@@ -0,0 +1,68 @@
|
||||
const mariadb = require('mariadb');
|
||||
|
||||
// 创建一个连接池
|
||||
const pool = mariadb.createPool({
|
||||
host: 'localhost',
|
||||
user: 'rc',
|
||||
password: '1q3e@W$R',
|
||||
database: 'Card'
|
||||
});
|
||||
|
||||
// 获取连接
|
||||
async function getConnection() {
|
||||
let conn;
|
||||
try {
|
||||
conn = await pool.getConnection();
|
||||
console.log("Connected to the database!");
|
||||
} catch (err) {
|
||||
console.error("Error connecting to the database:", err);
|
||||
}
|
||||
return conn;
|
||||
}
|
||||
|
||||
// async function insertData(conn) {
|
||||
// const query = "INSERT INTO your_table (column1, column2) VALUES (?, ?)";
|
||||
// const values = ['value1', 'value2'];
|
||||
// try {
|
||||
// const result = await conn.query(query, values);
|
||||
// console.log(`Inserted with ID: ${result.insertId}`);
|
||||
// } catch (err) {
|
||||
// console.error("Error inserting data:", err);
|
||||
// }
|
||||
// }
|
||||
|
||||
async function fetchData(conn) {
|
||||
const query = "SELECT * FROM your_table";
|
||||
try {
|
||||
const rows = await conn.query(query);
|
||||
console.log(rows);
|
||||
} catch (err) {
|
||||
console.error("Error fetching data:", err);
|
||||
}
|
||||
}
|
||||
|
||||
async function isRoomAvailable(conn, roomId) {
|
||||
const query = "SELECT 1 FROM Room WHERE RoomID = ? AND Status = 0 LIMIT 1";
|
||||
try {
|
||||
const rows = await conn.query(query, [roomId]);
|
||||
if (rows.length === 0) {
|
||||
// 不存在或已被占用
|
||||
return false;
|
||||
}
|
||||
|
||||
// 执行更新
|
||||
const updateQuery = "UPDATE Room SET Status = 1 WHERE RoomID = ?";
|
||||
const result = await conn.query(updateQuery, [roomId]);
|
||||
|
||||
// 检查是否更新了1行
|
||||
return Number(result.affectedRows) === 1;
|
||||
} catch (err) {
|
||||
console.error("Error checking room availability:", err);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getConnection,
|
||||
isRoomAvailable
|
||||
};
|
33
logger.js
Normal file
33
logger.js
Normal file
@@ -0,0 +1,33 @@
|
||||
const { createLogger, format, transports } = require('winston');
|
||||
const path = require('path');
|
||||
|
||||
// 定义日志格式:时间戳 + 日志级别 + 消息
|
||||
const logFormat = format.combine(
|
||||
format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
|
||||
format.printf(info => {
|
||||
// 把额外的参数取出来
|
||||
const splat = info[Symbol.for('splat')] || [];
|
||||
const msg = [info.message, ...splat].map(v =>
|
||||
typeof v === 'object' ? util.inspect(v, { depth: null }) : v
|
||||
).join(' ');
|
||||
|
||||
return `[${info.timestamp}] ${msg}`;
|
||||
})
|
||||
);
|
||||
|
||||
// 创建 logger
|
||||
const logger = createLogger({
|
||||
level: 'info', // 默认日志等级
|
||||
format: logFormat,
|
||||
transports: [
|
||||
new transports.File({
|
||||
filename: path.join(__dirname, 'log/app.log'), // 日志文件名
|
||||
maxsize: 5 * 1024 * 1024, // 每个文件最大 5MB
|
||||
maxFiles: 99, // 最多保留 5 个旧日志文件
|
||||
tailable: true // 让文件轮转时顺序保持最新在最后
|
||||
}),
|
||||
new transports.Console() // 同时输出到控制台(可选)
|
||||
]
|
||||
});
|
||||
|
||||
module.exports = logger;
|
2328
package-lock.json
generated
Normal file
2328
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
20
package.json
Normal file
20
package.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"mariadb": "^3.4.5",
|
||||
"sequelize": "^6.37.7",
|
||||
"socket.io": "^4.8.1",
|
||||
"winston": "^3.17.0",
|
||||
"ws": "^8.18.3",
|
||||
"wx": "^0.0.36"
|
||||
},
|
||||
"name": "node",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"description": ""
|
||||
}
|
10
tsconfig.json
Normal file
10
tsconfig.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2015", // 或 ES2020、ESNext 都可以
|
||||
"module": "commonjs", // 根据你的 Node.js 版本和需求设置
|
||||
"strict": true, // 可选但推荐
|
||||
"esModuleInterop": true, // 可选
|
||||
"downlevelIteration": true // 如果仍用 ES5 作为 target 就加这个
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user