You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Node.js/Socket.io黑杰克游戏:实现多客户端共享同一牌组

黑杰克游戏多客户端牌组不一致问题解决

我正在用Socket.io、Node.js和React开发一款简易黑杰克游戏,当前遇到的问题是:当房间内凑齐2名玩家与1名庄家后,游戏开始阶段每个客户端获取的牌组各不相同。需要实现仅构建一次牌组,并将同一牌组发送给所有客户端。

原服务端代码:

const express = require('express');
const app = express();
const http = require('http');
const cors = require('cors');
const { Server } = require('socket.io');
const buildDeck = require('./helpers/buildDeck');

require('dotenv').config();

const server = http.createServer(app);
const PORT = process.env.PORT || 3001;

let deck;
let size;
let users = ['dealer'];
let playCards = [];

app.use(cors());

const io = new Server(server, {
    cors: {
        origin: 'http://localhost:3000',
    },
});

io.on('connection', (socket) => {
    console.log(`${socket.id} has been connected`);

    socket.on('joinRoom', (data) => {
        size = io.sockets.adapter.rooms.get(data.room)?.size;
        if (size > 1) {
            io.to(socket.id).emit('reject');
            socket.disconnect();
        } else {
            socket.join(data.room);
            users.push(data.user);
            console.log(users);
        }
    });

    socket.on('checkPlayers', (data) => {
        size = io.sockets.adapter.rooms.get(data.room)?.size;
        if (size === 2) {
            io.to(socket.id).emit('joinedRoom', size);
            deck = buildDeck();
        }
        socket.to(data.room).emit('joinedRoom', size);
    });

    socket.on('initialize', (room) => {
        for (let i = 0; i < users.length; i++) {
            playCards[i] = {
                user: users[i],
                cards: [deck.pop(), deck.pop()],
            };
        }
        console.log(playCards);
        socket.to(room).emit('initialize', { deck, playCards });
    });
});

server.listen(PORT, () => {
    console.log('Server is running on port ' + PORT);
});

问题根源

  1. 全局变量跨房间干扰:deck、users、playCards是全局变量,多个房间同时运行时数据会互相覆盖,导致不同房间客户端拿到错误牌组。
  2. 初始化消息发送不完整:initialize事件仅给当前socket之外的房间成员发数据,发起初始化的客户端无法收到正确牌组信息。
  3. 用户列表未按房间隔离:全局users数组混合所有房间玩家,导致发牌逻辑混乱。

修改方案

  1. 按房间存储游戏状态:创建roomGames对象,每个房间ID对应独立的deck、users、playCards,避免跨房间污染。
  2. 统一发送初始化消息:使用io.to(room).emit给房间内所有客户端发送初始化数据,包括发起请求的socket。
  3. 隔离房间用户数据:每个房间维护自己的用户列表,不再依赖全局变量。

修改后的完整代码

const express = require('express');
const app = express();
const http = require('http');
const cors = require('cors');
const { Server } = require('socket.io');
const buildDeck = require('./helpers/buildDeck');

require('dotenv').config();

const server = http.createServer(app);
const PORT = process.env.PORT || 3001;

// 按房间存储游戏状态,key为房间ID,value为该房间的游戏数据
const roomGames = {};

app.use(cors());

const io = new Server(server, {
    cors: {
        origin: 'http://localhost:3000',
    },
});

io.on('connection', (socket) => {
    console.log(`${socket.id} has been connected`);

    socket.on('joinRoom', (data) => {
        const room = data.room;
        const roomSize = io.sockets.adapter.rooms.get(room)?.size || 0;
        
        if (roomSize > 1) {
            io.to(socket.id).emit('reject');
            socket.disconnect();
            return;
        }

        socket.join(room);
        // 初始化房间游戏状态(如果不存在)
        if (!roomGames[room]) {
            roomGames[room] = {
                users: ['dealer'], // 每个房间独立维护用户列表
                deck: null,
                playCards: []
            };
        }
        roomGames[room].users.push(data.user);
        console.log(`房间${room}用户列表:`, roomGames[room].users);
    });

    socket.on('checkPlayers', (data) => {
        const room = data.room;
        const roomSize = io.sockets.adapter.rooms.get(room)?.size || 0;
        
        if (roomSize === 2) {
            // 仅在房间人数满足时构建一次牌组
            roomGames[room].deck = buildDeck();
            // 给当前socket发送消息
            io.to(socket.id).emit('joinedRoom', roomSize);
        }
        // 给房间内其他成员发送消息
        socket.to(room).emit('joinedRoom', roomSize);
    });

    socket.on('initialize', (room) => {
        const gameData = roomGames[room];
        if (!gameData?.deck) return; // 确保牌组已构建

        // 重置当前房间的发牌数据
        gameData.playCards = [];
        for (let i = 0; i < gameData.users.length; i++) {
            gameData.playCards.push({
                user: gameData.users[i],
                cards: [gameData.deck.pop(), gameData.deck.pop()],
            });
        }
        console.log(`房间${room}发牌数据:`, gameData.playCards);
        
        // 给房间内所有客户端发送初始化数据,包括当前socket
        io.to(room).emit('initialize', { 
            deck: gameData.deck, 
            playCards: gameData.playCards 
        });
    });

    // 客户端断开连接时清理房间数据(可选,根据需求调整)
    socket.on('disconnecting', () => {
        const rooms = Array.from(socket.rooms);
        rooms.forEach(room => {
            if (room !== socket.id && roomGames[room]) {
                // 移除断开用户,或直接销毁房间状态
                roomGames[room].users = roomGames[room].users.filter(user => 
                    user !== socket.handshake.query.user // 假设join时把user存在handshake里,或根据实际存储方式调整
                );
                // 如果房间无玩家,删除房间数据
                if (roomGames[room].users.length <= 1) { // 只剩庄家
                    delete roomGames[room];
                }
            }
        });
    });
});

server.listen(PORT, () => {
    console.log('Server is running on port ' + PORT);
});

关键修改点说明

  • 房间状态隔离:roomGames对象为每个房间保存独立游戏数据,彻底解决全局变量的跨房间干扰问题。
  • 全房间消息推送:initialize事件使用io.to(room).emit,确保房间内所有客户端收到同一套牌组和发牌数据。
  • 断开连接清理:添加disconnecting事件处理,及时清理空房间的游戏数据,避免内存浪费。

内容的提问来源于stack exchange,提问作者Bogoe David

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.21 20:33:21