You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多
文档控制台
免费开始使用

基于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

火山引擎 最新活动