werewolf_nodejs/server/waiting_room.js
2025-02-01 13:09:28 +08:00

293 lines
8.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

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

// 这里实现一个房间的等待逻辑并且维护房间和玩家的状态。服务器只允许一个房间房间最大8个玩家默认情况房间是关闭的如果有第一个玩家加入进来则房间状态更新为开启此后每有一个玩家加入房间所有房间内的用户都会收到ws消息。每有一个玩家退出连接所有房间内的用户也会收到ws消息。如果房间内已有8名在线玩家则房间状态需要更新为锁定并且给所有客户端发送游戏即将开始的ws消息。
// 你要实现一个接口,供前端点击“加入房间”按钮时调用,然后检查当前的房间状态。如果房间锁定了,需要返回消息给前端提示其等待。否则相应地更新房间的状态。
// waiting_room.js
const express = require('express');
const pool = require('./db');
const { verifyToken } = require('./jwt');
const { broadcastMessage, sendMessageToUser, getClients } = require('./websocket'); // 只单向依赖
const router = express.Router();
const { startGame } = require('./game');
const { room: roominfo } = require('./room'); // 从 room.js 导入
// 房间状态
const room = roominfo;
// 定时器
let statusCheckInterval;
const clients = getClients();
/**
* 从房间中移除指定 userId 的玩家并广播
* @param {string|number} userId
*/
function removePlayerFromRoom(userId) {
const index = room.players.findIndex(player => player.userId === userId);
if (index !== -1) {
const [removedPlayer] = room.players.splice(index, 1);
updateRoomStatus();
broadcastMessage('系统', `${removedPlayer.username} 离开了房间,当前玩家数: ${room.players.length}/${room.maxPlayers}`);
broadcastPlayerStatus(); // 广播最新玩家列表
}
}
/**
* 当 WebSocket 真的断线时,会通过回调方式调用此函数
* @param {string|number} userId
*/
function onUserDisconnected(userId) {
removePlayerFromRoom(userId);
}
/**
* 检查房间状态
*/
function updateRoomStatus() {
if (room.players.length === 0) {
room.status = 'closed';
clearInterval(statusCheckInterval); // 如果房间没人,停止状态检查
statusCheckInterval = null; // 清空定时器变量
} else if (room.players.length < room.maxPlayers) {
room.status = 'open';
startStatusCheck(); // 确保定时器重新启动
} else {
room.status = 'locked';
broadcastMessage('系统', '房间已满游戏将在5秒后开始');
// 设置 5 秒后启动游戏
setTimeout(() => {
startGame(room);
}, 5000); // 5000 毫秒 = 5 秒
}
}
/**
* 广播所有玩家状态
*/
function broadcastPlayerStatus() {
const playerStates = room.players.map(player => ({
userId: player.userId,
username: player.username,
status: player.status,
}));
broadcastMessage('系统', {
type: 'player_status',
players: playerStates,
});
}
/**
* 清理状态异常的玩家
*/
function cleanErrorPlayers() {
const initialPlayerCount = room.players.length;
room.players = room.players.filter(player => player.status !== 'error');
const cleanedPlayerCount = room.players.length;
if (cleanedPlayerCount < initialPlayerCount) {
console.log(`清理了 ${initialPlayerCount - cleanedPlayerCount} 名异常玩家`);
updateRoomStatus(); // 更新房间状态
broadcastPlayerStatus(); // 广播状态更新
}
}
/**
* 定时检查 WebSocket 连接稳定性
*/
function startStatusCheck() {
if (statusCheckInterval) {
console.log('状态检查已在运行');
return; // 如果定时器已存在,直接返回
}
console.log('启动状态检查定时器');
statusCheckInterval = setInterval(() => {
room.players.forEach(player => {
// 发送 ping 消息
try {
if (player.ws.readyState === player.ws.OPEN) {
player.ws.send(
JSON.stringify({
type: 'ping',
})
);
// 如果 pong 消息未及时收到,标记为异常
const timeout = setTimeout(() => {
if (player.status !== 'active') {
player.status = 'error';
broadcastPlayerStatus(); // 广播状态更新
}
}, 5000); // 等待 5 秒
// 设置 pong 消息的监听
player.ws.once('message', (message) => {
try {
const data = JSON.parse(message);
if (data.type === 'pong') {
player.status = 'active'; // 接收到 pong连接正常
clearTimeout(timeout); // 清除超时检测
}
} catch (error) {
console.error('解析消息失败:', error.message);
}
});
}
} catch (error) {
console.error(`检测 userId=${player.userId} 的连接时出错:`, error.message);
player.status = 'error';
}
});
// 清理异常玩家
cleanErrorPlayers();
// 广播所有玩家状态
broadcastPlayerStatus();
}, 10000); // 每 10 秒检测一次
}
/**
* 加入房间接口
*/
router.post('/join', async (req, res) => {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ message: '未提供有效的 Authorization Header' });
}
const token = authHeader.split(' ')[1];
try {
const result = verifyToken(token); // 验证 Token
// 检查 Token 是否包含错误
if (result.error) {
return res.status(401).json({ message: result.error });
}
const userId = result.userId; // 提取 userId
const [users] = await pool.execute('SELECT id, username FROM users WHERE id = ?', [userId]);
if (users.length === 0) {
return res.status(404).json({ message: '用户不存在' });
}
const { username } = users[0];
// 检查房间状态
if (room.status === 'locked') {
return res.status(403).json({ message: '房间已满,请等待游戏结束后再加入' });
}
// 从 WebSocket 服务中获取用户的 ws 对象
const ws = clients.get(userId);
if (!ws) {
return res.status(500).json({ message: 'WebSocket 未找到,无法加入房间' });
}
// 检查用户是否已经在房间
const existingPlayer = room.players.find(player => player.userId === userId);
if (existingPlayer) {
// 如果用户已经在房间,更新其状态和 WebSocket
existingPlayer.status = 'active';
existingPlayer.ws = ws;
existingPlayer.username = username; // 更新用户名(如果可能更改)
existingPlayer.lastPing = Date.now(); // 更新心跳时间
console.log(`用户 ${username} (ID: ${userId}) 状态已更新`);
} else {
// 用户不在房间时,添加新玩家
room.players.push({
userId,
username,
status: 'active',
ws,
lastPing: Date.now(),
});
updateRoomStatus();
// 通知房间内的所有玩家
broadcastMessage('系统', `${username} 加入了房间,当前玩家数: ${room.players.length}/8`);
// 立即广播所有玩家的状态
broadcastPlayerStatus();
// 开始状态检查
startStatusCheck();
}
// 返回房间状态
return res.status(200).json({
message: '成功加入房间',
roomStatus: room.status,
players: room.players.map(player => ({
userId: player.userId,
username: player.username,
status: player.status,
})),
});
} catch (error) {
console.error('加入房间时出错:', error.message);
return res.status(500).json({ message: '服务器错误' });
}
});
/**
* 退出房间接口
*/
router.post('/leave', async (req, res) => {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ message: '未提供有效的 Authorization Header' });
}
const token = authHeader.split(' ')[1];
try {
const result = verifyToken(token);
if (result.error) {
return res.status(401).json({ message: result.error });
}
const userId = result.userId;
// 从房间中移除玩家
removePlayerFromRoom(userId);
return res.status(200).json({ message: '成功退出房间' });
} catch (error) {
console.error('退出房间时出错:', error.message);
return res.status(500).json({ message: '服务器错误' });
}
});
module.exports = {
router,
onUserDisconnected, // 供外部websocket.js 初始化时)设置断线回调
};
// 前端需确保 WebSocket 客户端正确响应 ping 消息:
// ws.onmessage = (event) => {
// const data = JSON.parse(event.data);
// if (data.type === 'ping') {
// ws.send(
// JSON.stringify({
// type: 'pong',
// })
// );
// } else if (data.type === 'player_status') {
// console.log('房间内玩家状态:', data.players);
// }
// };