基于Promise等同步机制解决多人回合制卡牌游戏对手行动检测问题
解决多人回合制卡牌游戏的行动同步问题
我太懂你这种卡壳的感觉了——花了四天啃Promise、协程这些异步同步概念,结果卡在多人回合制游戏的行动同步上,尤其是混合AI和人类玩家的场景,确实容易踩坑。你的游戏类似Uno还带算术计分,这个设定挺有意思的,咱们来一步步解决这个问题。
核心问题诊断
先看你现有代码的核心问题:oppoPlays是同步循环逻辑,遇到人类玩家时,只是发送了回合提醒消息,却没有暂停等待玩家出牌的异步事件(通过socket接收),就直接进入下一次循环了。这就导致游戏流程跳过人类玩家的回合,或者后续操作乱序——毕竟人类玩家的出牌是异步触发的,同步循环根本抓不到这个时机。
整体架构调整思路
我建议把回合管理改成**「状态机+异步等待」**的模式,替代原来的同步循环:
- 维护一个「当前回合玩家」的状态,而不是用for循环遍历所有玩家
- 每个玩家的回合完成后(AI出牌结束/收到人类玩家的socket出牌消息),再主动触发下一个玩家的回合
- 用Promise封装每个玩家的回合操作,让游戏流程可以按顺序「等待」每个玩家完成动作
具体代码修改方案
1. 用Promise封装单个玩家的回合
先写一个函数,专门处理单个玩家的回合,返回一个Promise——当该玩家完成出牌时,Promise就会resolve,流程自动推进到下一个玩家:
// 处理单个玩家的回合,返回Promise,出牌完成后resolve function handlePlayerTurn(playerIndex) { return new Promise((resolve) => { const playerType = oppoType[playerIndex]; const playerName = playerNames[playerIndex]; if (playerType === "AI") { // AI出牌逻辑:执行完AI的出牌动作后直接resolve // 这里替换成你实际的AI出牌代码 aiPlayCard(playerIndex); resolve(); } else { // 人类玩家:发送回合提醒,并等待socket的出牌消息 const yourTurnMsg = `It's ${playerName}'s turn`; $("#text_message").val(yourTurnMsg).trigger(jQuery.Event('keypress', { keyCode: 13, which: 13 })); // 监听socket消息,直到当前玩家出牌 function onPlayerMove(data) { const res = JSON.parse(data); if (res.action === "game_move" && res.user_id === playerName) { // 移除监听器,避免干扰后续回合 socket.off('message', onPlayerMove); // 处理玩家出牌 const pp = playerNames.indexOf(res.user_id); const cpos = res.cardno; playCard_oppo(pp, cpos); // 完成回合,推进流程 resolve(); } } socket.on('message', onPlayerMove); } }); }
2. 重构oppoPlays为异步流程
把原来的同步循环改成async/await的异步顺序执行,这样每个玩家的回合都会等待上一个完成后再开始:
async function oppoPlays() { // 只有游戏发起者才执行回合控制 if (joiner !== "") return; // 按顺序遍历每个玩家,等待每个回合完成 for (let pp = 1; pp < numberofplayers; pp++) { await handlePlayerTurn(pp); // 这里可以加回合结束后的通用逻辑,比如检查游戏是否结束、更新全局状态等 } // 所有玩家回合结束后,比如回到发起者回合,或者开启下一轮 // startInitiatorTurn(); // 示例:发起者自己的回合 }
3. 优化socket消息处理(可选的全局统一方案)
如果你想把socket消息处理统一管理,而不是每个回合单独监听,可以维护全局的「等待状态」:
// 全局变量:记录当前等待出牌的玩家,以及对应的resolve函数 let currentWaitingPlayer = null; let turnResolve = null; // 原有的socket消息处理函数修改 function checkJson(res, sttr_id, game_no) { if (res.action === "game_move") { const pp = playerNames.indexOf(res.user_id); const cpos = res.cardno; playCard_oppo(pp, cpos); // 如果当前正等待这个玩家出牌,就触发resolve推进流程 if (currentWaitingPlayer === res.user_id && turnResolve) { turnResolve(); currentWaitingPlayer = null; turnResolve = null; } } } // 对应的handlePlayerTurn修改人类玩家部分 function handlePlayerTurn(playerIndex) { return new Promise((resolve) => { const playerType = oppoType[playerIndex]; const playerName = playerNames[playerIndex]; if (playerType === "AI") { aiPlayCard(playerIndex); resolve(); } else { // 设置全局等待状态 currentWaitingPlayer = playerName; turnResolve = resolve; const yourTurnMsg = `It's ${playerName}'s turn`; $("#text_message").val(yourTurnMsg).trigger(jQuery.Event('keypress', { keyCode: 13, which: 13 })); } }); }
4. 简化playCard_oppo逻辑
现在不需要在playCard_oppo里通知oppoPlays了,因为Promise的resolve已经会自动推进流程:
function playCard_oppo(pp, cardno) { // 卡牌移动和计分逻辑保持不变 const topoc = parseInt($("#oppo_card" + cardno).css('top')); const leftoc = parseInt($("#oppo_card" + cardno).css('left')); $("#oppo_card" + cardno).css({ top: topoc, left: leftoc, opacity: "50%" }); // ... 你的其他计分、UI更新逻辑 ... // 这里不需要额外通知oppoPlays,流程由Promise自动推进 }
额外优化建议
- 状态管理:建议维护一个全局的游戏状态对象(比如
gameState = { currentPlayer: 0, gamePhase: 'playing', isGameOver: false }),替代零散的变量(比如joiner),逻辑更清晰 - 超时处理:给人类玩家的回合加超时逻辑,比如5分钟未出牌就自动跳过,避免游戏卡住——可以用
setTimeout在Promise里添加reject逻辑 - AI异步化:如果AI出牌有动画延迟,也要把AI逻辑封装成Promise,确保流程等待动画完成再推进
- 模块拆分:把socket通信、回合控制、UI更新拆成独立模块,避免耦合在一起,后续维护更方便
内容的提问来源于stack exchange,提问作者cneeds




