First update.

This commit is contained in:
2025-10-11 14:45:08 +08:00
commit 6aa8fb818a
22 changed files with 4036 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
node_modules
*.map

26
CreateRoom.js Normal file
View 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
View 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
View 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
View 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
View File

@@ -0,0 +1 @@
export const ALL_PLAYER_NUM:number = 6;

122
GameManager.js Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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所有人准备进入fight2fight结束
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
View 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所有人准备进入fight2fight结束
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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

20
package.json Normal file
View 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
View File

@@ -0,0 +1,10 @@
{
"compilerOptions": {
"target": "ES2015", // 或 ES2020、ESNext 都可以
"module": "commonjs", // 根据你的 Node.js 版本和需求设置
"strict": true, // 可选但推荐
"esModuleInterop": true, // 可选
"downlevelIteration": true // 如果仍用 ES5 作为 target 就加这个
}
}