Files
youlegames/codes/games/server/docs/guides/architecture/08-游戏流程概述.md
2026-02-04 23:47:45 +08:00

37 KiB
Raw Blame History

游戏流程概述

📋 文档概述

本文档详细说明进贤麻将的完整游戏流程,包括:

  • 状态机设计 - 游戏阶段转换规则
  • 完整流程 - 从房间创建到游戏结束
  • 各阶段详解 - 准备、发牌、出牌、结算等
  • 时序关系 - 操作顺序和同步机制

文档目标:帮助开发者理解游戏的完整运行流程,掌握各阶段的处理逻辑和状态转换规则。


🎮 游戏流程总览

核心流程图

房间创建 → 玩家准备 → 开始游戏 → 游戏进行 → 局数结束 → 游戏结束
   ↓          ↓          ↓          ↓          ↓          ↓
创建房间    玩家加入    开战函数    状态机      计分结算    总结算
Export接口  玩家管理    makewar    WAITING→    单局结算    多局统计
配置解析    准备状态            DEALING→     精分计算    总分排名
                              PLAYING→
                              SETTLEMENT

流程阶段划分

阶段 主要功能 涉及模块 持续时间
房间创建 创建游戏房间,解析配置 Export.makewar, RoomAdapter 即时
准备阶段 玩家加入,等待准备 Import.prepare, GameController 最长5分钟
发牌阶段 洗牌、发牌、开精 MahjongWall, DiceService 3-5秒
出牌阶段 玩家出牌、操作响应 OperationManager, RpcHandler 每局约5-10分钟
结算阶段 胡牌计分、精分结算 ScoreCalculation, JingAlgorithm 3-5秒
游戏结束 多局统计、排名 GameController 即时

🔄 游戏状态机

状态定义

进贤麻将使用严格的状态机控制游戏流程,确保操作的合法性和顺序性。

// 游戏状态定义GameStateManager.js
GAME_PHASES: {
  WAITING: 'waiting',               // 等待状态:等待玩家准备
  DEALING: 'dealing',               // 发牌状态:洗牌、发牌、开精
  JING_DETERMINING: 'jing_determining',  // 确定精牌状态:掷骰子选精
  PLAYING: 'playing',               // 游戏进行状态:出牌、吃碰杠胡
  RESPONDING: 'responding',         // 响应状态:等待玩家操作响应
  ROUND_END: 'round_end',          // 局结束状态:单局结算
  GAME_END: 'game_end'             // 游戏结束状态:总结算
}

状态转换规则

┌─────────────────────────────────────────────────────────────┐
│                      游戏状态机流程图                         │
└─────────────────────────────────────────────────────────────┘

        ┌──────────┐
        │ WAITING  │  房间创建后的初始状态
        │  等待     │  所有玩家准备完毕→DEALING
        └────┬─────┘
             │ all_ready
             ↓
        ┌──────────┐
        │ DEALING  │  执行发牌流程
        │  发牌     │  发牌完成→JING_DETERMINING
        └────┬─────┘
             │ deal_complete
             ↓
     ┌───────────────────┐
     │JING_DETERMINING   │  掷骰子确定精牌
     │  确定精牌          │  精牌确定→PLAYING
     └────┬─────────────┘
          │ jing_determined
          ↓
        ┌──────────┐
   ┌───→│ PLAYING  │  玩家轮流出牌、操作
   │    │  游戏中   │  有玩家胡牌→ROUND_END
   │    └────┬─────┘  流局→ROUND_END
   │         │ win/draw
   │         ↓
   │    ┌──────────┐
   │    │ROUND_END │  单局结算、计分
   │    │ 局结束    │  继续下局→DEALING
   │    └────┬─────┘  所有局结束→GAME_END
   │         │
   │         ├─continue→ DEALING (下一局)
   │         │
   └─────────┘
             │ game_over
             ↓
        ┌──────────┐
        │GAME_END  │  游戏结束、总结算
        │ 游戏结束  │
        └──────────┘

状态转换函数

/**
 * 状态转换核心方法
 * @param {Object} gameState - 游戏状态对象
 * @param {string} newPhase - 目标状态
 * @param {Object} transitionData - 转换数据
 * @returns {Object} 转换结果 {success, oldPhase, newPhase, errors}
 */
GameStateManager.transitionToPhase(gameState, newPhase, transitionData)

// 示例:从等待状态转换到发牌状态
var result = GameStateManager.transitionToPhase(
  gameState, 
  'dealing', 
  { reason: 'all_ready' }
);

if (result.success) {
  console.log('状态转换成功:', result.newPhase);
} else {
  console.error('状态转换失败:', result.errors);
}

📊 完整游戏流程

第一步:房间创建

触发时机:玩家在客户端点击"创建房间"

执行流程

// 1. 客户端发送创建房间请求
// youle_app.js → packet.js → mod.js

// 2. 框架调用export.makewar()
export.makewar = function(room, roomtype) {
  // 2.1 验证参数
  if (!room || !roomtype) {
    return { error: '参数无效' };
  }
  
  // 2.2 解析roomtype配置
  var config = RoomConfigUtils.parse(roomtype);
  
  // 2.3 创建游戏状态
  var gameState = GameStateManager.createGameState(
    room.roomcode,
    roomtype,
    room.getPlayerIds()
  );
  
  // 2.4 初始化游戏控制器
  var controller = new GameController(room, gameState);
  
  // 2.5 保存到room对象
  room.gameController = controller;
  room.gameState = gameState;
  
  return { success: true };
};

关键点

  • 解析房间配置(局数、规则、超时设置)
  • 创建游戏状态对象(包含所有游戏数据)
  • 初始化玩家状态4个座位即使不足4人
  • 设置初始状态为 WAITING

相关文档


第二步准备阶段WAITING

状态特征phase = 'waiting'

主要任务

  1. 等待玩家加入房间
  2. 玩家点击准备
  3. 全员准备后开始游戏

2.1 玩家加入

// 玩家加入房间
export.player_enter = function(room, player) {
  // 1. 分配座位号0-3
  var seat = room.findEmptySeat();
  player.seat = seat;
  
  // 2. 初始化玩家游戏信息
  player.gameinfo = {
    isprepare: 0,      // 准备状态0-未准备1-已准备
    score: 0,          // 当前分数
    totalScore: 0      // 累计分数
  };
  
  // 3. 广播玩家加入消息
  import.broadcastPlayerJoin(room, player);
  
  return { success: true };
};

2.2 玩家准备

// RPC: player_ready
RpcHandler.prototype.player_ready = function(pack, room, callback) {
  var playerId = pack.playerid;
  
  // 1. 设置准备状态
  var player = room.getPlayer(playerId);
  player.gameinfo.isprepare = 1;
  
  // 2. 检查是否全员准备
  var allReady = room.checkAllReady();
  
  // 3. 广播准备消息
  import.broadcastPlayerReady(room, playerId);
  
  // 4. 如果全员准备,开始游戏
  if (allReady) {
    this._startGame(room);
  }
  
  callback({ success: true, allReady: allReady });
};

2.3 开始游戏

RpcHandler.prototype._startGame = function(room) {
  var gameState = room.gameState;
  
  // 1. 状态转换WAITING → DEALING
  var result = GameStateManager.transitionToPhase(
    gameState, 
    'dealing',
    { reason: 'all_ready' }
  );
  
  if (!result.success) {
    console.error('状态转换失败:', result.errors);
    return;
  }
  
  // 2. 确定首局庄家(通过掷骰子)
  var dealer = this._determineDealerBydice(room);
  
  // 3. 开始新局
  GameStateManager.startNewRound(gameState, dealer);
  
  // 4. 执行发牌流程
  this._dealCards(room);
};

超时处理

  • 准备超时默认5分钟300秒
  • 超时后:未准备玩家自动离开或托管

第三步发牌阶段DEALING

状态特征phase = 'dealing'

主要任务

  1. 洗牌Shuffle
  2. 配牌Deal
  3. 开精Determine Jing

3.1 洗牌流程

// MahjongWall.js - 洗牌
MahjongWall.prototype.shuffle = function() {
  // 1. 创建136张牌
  var allCards = this._createAllCards();  // [1m, 1m, 1m, 1m, 2m, ...]
  
  // 2. Fisher-Yates洗牌算法
  for (var i = allCards.length - 1; i > 0; i--) {
    var j = Math.floor(Math.random() * (i + 1));
    var temp = allCards[i];
    allCards[i] = allCards[j];
    allCards[j] = temp;
  }
  
  // 3. 转换为牌墩17墩 × 4排 = 68墩 = 136张
  this.wall = this._convertToWall(allCards);
  
  // 4. 标记洗牌完成
  this.shuffled = true;
  
  return allCards;
};

洗牌增强系统v5.0

3.2 发牌流程

// MahjongWall.js - 发牌
MahjongWall.prototype.dealCards = function(players, dealer) {
  var dealedCards = [];
  
  // 1. 确定开始位置(从庄家开始)
  var currentPlayer = dealer;
  
  // 2. 四家各发12张每次4张共3轮
  for (var round = 0; round < 3; round++) {
    for (var i = 0; i < players.length; i++) {
      var player = players[currentPlayer];
      
      // 每次发4张
      for (var j = 0; j < 4; j++) {
        var card = this._drawCard();  // 从牌墙抓牌
        player.handCards.push(card);
        dealedCards.push({
          playerId: player.id,
          card: card,
          uniqueId: card.uniqueId
        });
      }
      
      currentPlayer = (currentPlayer + 1) % players.length;
    }
  }
  
  // 3. 四家各补1张共4张
  for (var i = 0; i < players.length; i++) {
    var player = players[currentPlayer];
    var card = this._drawCard();
    player.handCards.push(card);  // 现在每人13张
    dealedCards.push({
      playerId: player.id,
      card: card,
      uniqueId: card.uniqueId
    });
    
    currentPlayer = (currentPlayer + 1) % players.length;
  }
  
  // 4. 庄家再补1张庄家14张闲家13张
  var dealerPlayer = players[dealer];
  var card = this._drawCard();
  dealerPlayer.handCards.push(card);
  dealedCards.push({
    playerId: dealerPlayer.id,
    card: card,
    uniqueId: card.uniqueId
  });
  
  return dealedCards;
};

牌源信息标记

  • 发牌时自动设置 sourceInfo.sourceType = 'dealt'
  • 记录发牌轮次和座位信息
  • 用于追踪牌的来源,支持调试和验证

3.3 开精流程

// DiceService.js - 掷骰子选精
DiceService.prototype.rollForJing = function(gameState) {
  // 1. 庄家掷两个骰子
  var dice1 = this._rollDice();  // 1-6
  var dice2 = this._rollDice();  // 1-6
  var total = dice1 + dice2;     // 2-12
  
  // 2. 从牌墙后往前数,定位精牌位置
  var jingPosition = this._locateJingPosition(total);
  
  // 3. 翻开精牌
  var jingCard = gameState.wall.getCardAt(jingPosition);
  
  // 4. 确定正精和副精
  var zhengJing = jingCard.code;          // 正精:翻开的牌
  var fuJing = this._getNextCard(zhengJing);  // 副精:正精+1
  
  // 5. 保存精牌信息
  gameState.jingInfo = {
    zhengJing: zhengJing,      // 正精(如:'5m'
    fuJing: fuJing,            // 副精(如:'6m'
    diceResult: { dice1: dice1, dice2: dice2, total: total },
    position: jingPosition,
    timestamp: Date.now()
  };
  
  // 6. 状态转换DEALING → PLAYING
  GameStateManager.transitionToPhase(
    gameState, 
    'playing',
    { reason: 'jing_determined' }
  );
  
  return gameState.jingInfo;
};

精牌规则

  • 正精翻开的牌每张计2分可当万能牌
  • 副精:正精+1的牌每张计1分可当万能牌
  • 循环规则9万→1万北风→东风白板→红中

相关文档

  • [05-共享代码模块.md](../core/05-共享代码模块.md#dicese rvice-掷骰子服务) - DiceService详解

第四步出牌阶段PLAYING

状态特征phase = 'playing'

主要任务

  1. 玩家轮流出牌
  2. 其他玩家响应(吃碰杠胡)
  3. 操作优先级处理
  4. 超时托管

4.1 出牌流程

// RPC: player_discard
RpcHandler.prototype.player_discard = function(pack, room, callback) {
  var playerId = pack.playerid;
  var tileCode = pack.data.tile;  // 出的牌(如:'3m'
  
  // 1. 验证操作合法性
  var validation = OperationManager.validateDiscard(
    room.gameState,
    playerId,
    tileCode
  );
  
  if (!validation.valid) {
    return callback({ error: validation.error });
  }
  
  // 2. 执行出牌
  var result = OperationManager.performDiscard(
    room.gameState,
    playerId,
    tileCode
  );
  
  // 3. 记录出牌操作
  GameStateManager.recordAction(room.gameState, {
    type: 'discard',
    playerId: playerId,
    tile: tileCode,
    timestamp: Date.now()
  });
  
  // 4. 广播出牌消息
  import.broadcastDiscard(room, playerId, tileCode);
  
  // 5. 检查其他玩家的响应(吃碰杠胡)
  var responses = OperationManager.checkResponses(
    room.gameState,
    tileCode
  );
  
  // 6. 如果有响应,等待操作
  if (responses.length > 0) {
    this._waitForResponses(room, responses, callback);
  } else {
    // 7. 无响应,下家摸牌
    this._nextPlayerDraw(room, callback);
  }
};

4.2 操作响应流程

操作优先级(从高到低):

  1. 胡牌(最高优先级,多人胡牌按下家优先)
  2. 杠牌(与胡牌平级,玩家可选择)
  3. 碰牌
  4. 吃牌(最低优先级,仅下家可吃)
// OperationManager.js - 检查响应
OperationManager.prototype.checkResponses = function(gameState, discardedTile) {
  var responses = [];
  var currentPlayer = gameState.currentPlayer;
  
  // 1. 检查每个玩家的可用操作
  for (var i = 0; i < gameState.players.length; i++) {
    if (i === currentPlayer) continue;  // 跳过出牌者
    
    var player = gameState.players[i];
    var availableOps = [];
    
    // 1.1 检查胡牌
    if (this._canWin(player, discardedTile, gameState)) {
      availableOps.push({
        type: 'win',
        priority: 1,  // 最高优先级
        tile: discardedTile
      });
    }
    
    // 1.2 检查杠牌
    if (this._canKong(player, discardedTile)) {
      availableOps.push({
        type: 'kong',
        priority: 1,  // 与胡牌平级
        tile: discardedTile
      });
    }
    
    // 1.3 检查碰牌
    if (this._canPong(player, discardedTile)) {
      availableOps.push({
        type: 'pong',
        priority: 2,
        tile: discardedTile
      });
    }
    
    // 1.4 检查吃牌(仅下家)
    var nextPlayer = (currentPlayer + 1) % gameState.players.length;
    if (i === nextPlayer && this._canChow(player, discardedTile)) {
      availableOps.push({
        type: 'chow',
        priority: 3,  // 最低优先级
        tile: discardedTile,
        combinations: this._getChowCombinations(player, discardedTile)
      });
    }
    
    // 1.5 添加到响应列表
    if (availableOps.length > 0) {
      responses.push({
        playerId: player.id,
        seat: i,
        operations: availableOps,
        timeout: 5000  // 5秒操作时间
      });
    }
  }
  
  // 2. 按优先级排序
  responses.sort(function(a, b) {
    var aPriority = Math.min.apply(null, a.operations.map(function(op) { return op.priority; }));
    var bPriority = Math.min.apply(null, b.operations.map(function(op) { return op.priority; }));
    return aPriority - bPriority;
  });
  
  return responses;
};

4.3 胡牌检测

// WinDetectionFactory.js - 胡牌检测
WinDetectionFactory.detectWin = function(handTiles, jingInfo, gameState) {
  // 1. 基础验证
  if (!handTiles || handTiles.length % 3 !== 2) {
    return { canWin: false, reason: '手牌数量不符合胡牌要求' };
  }
  
  // 2. 检测所有可能的胡牌牌型
  var patterns = [];
  
  // 2.1 平胡检测
  var pinghuResult = this._checkPinghu(handTiles, jingInfo);
  if (pinghuResult.canWin) {
    patterns.push({
      type: 'pinghu',
      hasJing: pinghuResult.hasJing,
      baseScore: pinghuResult.hasJing ? 4 : 8,
      combinations: pinghuResult.combinations
    });
  }
  
  // 2.2 七对检测
  var qiduiResult = this._checkQidui(handTiles, jingInfo);
  if (qiduiResult.canWin) {
    patterns.push({
      type: 'qidui',
      hasJing: qiduiResult.hasJing,
      baseScore: qiduiResult.hasJing ? 8 : 64
    });
  }
  
  // 2.3 四碰检测
  var sipengResult = this._checkSipeng(handTiles, jingInfo);
  if (sipengResult.canWin) {
    patterns.push({
      type: 'sipeng',
      hasJing: sipengResult.hasJing,
      baseScore: sipengResult.hasJing ? 32 : 64
    });
  }
  
  // 2.4 十三烂检测
  var shisanlanResult = this._checkShisanlan(handTiles, jingInfo);
  if (shisanlanResult.canWin) {
    patterns.push({
      type: 'shisanlan',
      hasJing: shisanlanResult.hasJing,
      baseScore: shisanlanResult.hasJing ? 8 : 16
    });
  }
  
  // 2.5 七星十三烂检测
  var qixingshisanlanResult = this._checkQixingShisanlan(handTiles, jingInfo);
  if (qixingshisanlanResult.canWin) {
    patterns.push({
      type: 'qixing_shisanlan',
      hasJing: qixingshisanlanResult.hasJing,
      baseScore: qixingshisanlanResult.hasJing ? 32 : 64
    });
  }
  
  // 3. 返回检测结果
  if (patterns.length === 0) {
    return { canWin: false, reason: '未组成任何胡牌牌型' };
  }
  
  return {
    canWin: true,
    patterns: patterns,
    bestPattern: this._selectBestPattern(patterns)
  };
};

胡牌检测算法详解

4.4 操作超时处理

// OperationManager.js - 操作超时
OperationManager.prototype._startOperationTimeout = function(gameState, playerId, timeout) {
  var self = this;
  var timerId = setTimeout(function() {
    // 1. 检查玩家是否已操作
    if (gameState.currentAction.playerId !== playerId) {
      return;  // 已经操作过了
    }
    
    // 2. 超时处理
    if (gameState.rules.gameplayOptions.kuaiban) {
      // 快版模式:自动托管
      self._autoPlay(gameState, playerId);
    } else {
      // 标准模式:等待玩家操作
      // 可以发送提醒消息
      import.sendTimeoutWarning(gameState.room, playerId);
    }
  }, timeout);
  
  // 3. 保存定时器ID
  gameState.currentAction.timerId = timerId;
  
  return timerId;
};

第五步结算阶段ROUND_END

状态特征phase = 'round_end'

触发条件

  1. 有玩家胡牌
  2. 流局剩余17墩牌

5.1 胡牌结算

// ScoreCalculation.js - 胡牌计分
ScoreCalculation.calculateWinScore = function(winResult, gameState) {
  var scores = {
    baseScore: 0,      // 牌型基础分
    bonusScore: 0,     // 奖励分(无精+5、精钓+5等
    jingScore: 0,      // 精分
    multiplier: 1,     // 倍数(冲关、杠开等)
    totalScore: 0      // 总分
  };
  
  // 1. 计算牌型基础分
  var pattern = winResult.bestPattern;
  scores.baseScore = pattern.baseScore;
  
  // 2. 计算奖励分
  if (!pattern.hasJing) {
    scores.bonusScore += 5;  // 无精奖励
  }
  
  if (winResult.isJingdiao) {
    scores.bonusScore += 5;  // 精钓奖励
  }
  
  if (winResult.isGangkai) {
    scores.multiplier *= 2;  // 杠开翻倍
  }
  
  // 3. 计算精分
  scores.jingScore = JingAlgorithm.calculateJingScore(
    gameState.players[winResult.winnerId].handCards,
    gameState.jingInfo
  );
  
  // 4. 检查冲关
  if (scores.jingScore >= 10) {
    var chongguanMultiplier = JingAlgorithm.calculateChongguan(scores.jingScore);
    scores.multiplier *= chongguanMultiplier;
  }
  
  // 5. 计算总分
  scores.totalScore = (scores.baseScore + scores.bonusScore) * scores.multiplier + scores.jingScore;
  
  return scores;
};

5.2 分数分配

// ScoreCalculation.js - 分数分配
ScoreCalculation.distributeScore = function(winResult, gameState) {
  var winner = winResult.winnerId;
  var loser = winResult.loserId;  // 放炮者自摸时为null
  var isDealer = (winner === gameState.dealer);
  var isSelfDraw = (loser === null);
  
  var distribution = [];
  
  if (isSelfDraw) {
    // 自摸:所有其他玩家给分
    for (var i = 0; i < gameState.players.length; i++) {
      if (i === winner) continue;
      
      var score = this._calculatePayment(
        isDealer,
        i === gameState.dealer,
        winResult.totalScore
      );
      
      distribution.push({
        from: i,
        to: winner,
        amount: score
      });
    }
  } else {
    // 放炮
    if (isDealer) {
      // 庄家胡牌
      // 放炮者给全分,其他玩家给一半
      var fullScore = winResult.totalScore;
      var halfScore = Math.floor(fullScore / 2);
      
      distribution.push({
        from: loser,
        to: winner,
        amount: fullScore
      });
      
      for (var i = 0; i < gameState.players.length; i++) {
        if (i === winner || i === loser) continue;
        distribution.push({
          from: i,
          to: winner,
          amount: halfScore
        });
      }
    } else {
      // 闲家胡牌
      // 放炮者根据身份给分
      var loserIsDealer = (loser === gameState.dealer);
      var payment = this._calculatePayment(false, loserIsDealer, winResult.totalScore);
      
      distribution.push({
        from: loser,
        to: winner,
        amount: payment
      });
    }
  }
  
  // 应用分数变化
  distribution.forEach(function(transfer) {
    gameState.players[transfer.from].score -= transfer.amount;
    gameState.players[transfer.to].score += transfer.amount;
  });
  
  return distribution;
};

计分规则详解

5.3 局结束处理

// GameController.js - 局结束
GameController.prototype.endRound = function(roundResult) {
  var gameState = this.gameState;
  
  // 1. 保存局结果
  gameState.history.rounds.push({
    roundNumber: gameState.currentRound,
    winner: roundResult.winnerId,
    pattern: roundResult.pattern,
    scores: roundResult.scores,
    distribution: roundResult.distribution,
    timestamp: Date.now()
  });
  
  // 2. 状态转换PLAYING → ROUND_END
  GameStateManager.transitionToPhase(
    gameState,
    'round_end',
    { reason: 'win', winnerId: roundResult.winnerId }
  );
  
  // 3. 广播结算消息
  import.broadcastRoundEnd(this.room, roundResult);
  
  // 4. 检查游戏是否结束
  if (gameState.currentRound >= gameState.totalRounds) {
    this.endGame();
  } else {
    // 5. 准备下一局
    this.prepareNextRound();
  }
};

第六步游戏结束GAME_END

状态特征phase = 'game_end'

触发条件:所有局数完成

6.1 游戏总结算

// GameController.js - 游戏结束
GameController.prototype.endGame = function() {
  var gameState = this.gameState;
  
  // 1. 计算总分
  var finalScores = gameState.players.map(function(player) {
    return {
      playerId: player.id,
      totalScore: player.score,
      wins: player.statistics.wins,
      losses: player.statistics.losses
    };
  });
  
  // 2. 排名
  finalScores.sort(function(a, b) {
    return b.totalScore - a.totalScore;
  });
  
  // 3. 状态转换ROUND_END → GAME_END
  GameStateManager.transitionToPhase(
    gameState,
    'game_end',
    { reason: 'all_rounds_complete' }
  );
  
  // 4. 广播游戏结束
  import.broadcastGameEnd(this.room, {
    finalScores: finalScores,
    totalRounds: gameState.totalRounds,
    duration: Date.now() - gameState.createTime
  });
  
  // 5. 保存游戏记录
  this._saveGameRecord(finalScores);
  
  // 6. 清理资源
  this._cleanup();
};

⏱️ 时序图

完整游戏时序图

客户端A    客户端B    客户端C    客户端D    服务器      GameController
   │          │          │          │          │              │
   ├─创建房间──────────────────────────────────>│              │
   │          │          │          │          ├─makewar────>│
   │          │          │          │          │<─成功───────┤
   │<─房间创建成功─────────────────────────────┤              │
   │          │          │          │          │              │
   │          ├─加入房间──────────────────────>│              │
   │          │          │          │          ├─player_enter>│
   │<─────────┴─玩家B加入─────────────────────┤              │
   │          │<─加入成功────────────────────┤              │
   │          │          │          │          │              │
   │          │          ├─加入房间──────────>│              │
   │<─────────┴──────────┴─玩家C加入─────────┤              │
   │          │          │<─加入成功──────────┤              │
   │          │          │          │          │              │
   │─准备────────────────────────────────────>│              │
   │          │─准备────────────────────────>│              │
   │          │          │─准备────────────>│              │
   │          │          │          │          │              │
   │          │          │          │          ├─全员准备────>│
   │          │          │          │          │              ├─开始游戏
   │          │          │          │          │              ├─洗牌
   │          │          │          │          │              ├─发牌
   │          │          │          │          │              ├─开精
   │<─────────┴──────────┴──────────发牌完成─┤              │
   │          │          │          │          │              │
   │─出牌(3m)────────────────────────────────>│              │
   │          │          │          │          ├─检查响应────>│
   │          │<─────────┴──────────可碰/可胡─┤              │
   │          │─碰牌────────────────────────>│              │
   │          │          │          │          │              ├─执行碰牌
   │<─────────┴──────────┴──────────玩家B碰牌┤              │
   │          │          │          │          │              │
   │          │─出牌(5m)────────────────────>│              │
   │          │          │          │          ├─检查响应────>│
   │<─────────┴──────────┴──────────无响应───┤              │
   │          │          │          │          │              ├─下家摸牌
   │          │          │<─────────轮到玩家C─┤              │
   │          │          │          │          │              │
   ...(出牌循环)...
   │          │          │          │          │              │
   │─出牌(8m)────────────────────────────────>│              │
   │          │<─────────┴──────────可胡──────┤              │
   │          │─胡牌────────────────────────>│              │
   │          │          │          │          ├─胡牌检测────>│
   │          │          │          │          ├─计算分数────>│
   │<─────────┴──────────┴──────────结算结果─┤              │
   │          │          │          │          │              │
   │<─────────┴──────────┴──────────准备下局─┤              │
   │          │          │          │          │              ├─开始新局
   ...(继续下一局)...
   │          │          │          │          │              │
   │<─────────┴──────────┴──────────游戏结束─┤              │
   │          │          │          │          │              │

单局流程时序

[准备阶段]      [发牌阶段]      [游戏阶段]      [结算阶段]
    │               │               │               │
    ├─玩家加入      │               │               │
    ├─玩家准备      │               │               │
    ├─全员就绪      │               │               │
    │               │               │               │
    │  状态转换:    │               │               │
    │  WAITING →    │               │               │
    │    DEALING    │               │               │
    │               ├─洗牌          │               │
    │               ├─发牌          │               │
    │               ├─开精          │               │
    │               │               │               │
    │               │  状态转换:    │               │
    │               │  DEALING →    │               │
    │               │    PLAYING    │               │
    │               │               ├─庄家出牌      │
    │               │               ├─其他玩家响应  │
    │               │               ├─下家摸牌      │
    │               │               ├─继续出牌      │
    │               │               ├─...循环...    │
    │               │               ├─胡牌/流局     │
    │               │               │               │
    │               │               │  状态转换:    │
    │               │               │  PLAYING →    │
    │               │               │  ROUND_END    │
    │               │               │               ├─计算分数
    │               │               │               ├─分配分数
    │               │               │               ├─广播结果
    │               │               │               ├─检查游戏结束
    │               │               │               │
    └───────────────┴───────────────┴───────────────┘

🔧 关键技术点

1. 状态同步机制

// Import.js - 广播消息
import.broadcastGameState = function(room, updateType, data) {
  // 1. 构建消息包
  var message = {
    cmd: 'game_state_update',
    type: updateType,
    data: data,
    timestamp: Date.now()
  };
  
  // 2. 发送给所有玩家
  room.players.forEach(function(player) {
    import.sendToPlayer(player, message);
  });
};

同步时机

  • 玩家加入/离开
  • 状态转换
  • 玩家操作(出牌、吃碰杠胡)
  • 分数变化
  • 局结束/游戏结束

2. 断线重连

// Export.js - 断线重连
export.get_deskinfo = function(room, player) {
  var gameState = room.gameState;
  
  // 1. 构建完整游戏信息
  var deskInfo = {
    phase: gameState.phase,
    currentRound: gameState.currentRound,
    totalRounds: gameState.totalRounds,
    currentPlayer: gameState.currentPlayer,
    dealer: gameState.dealer,
    
    // 2. 玩家信息
    players: gameState.players.map(function(p) {
      return {
        id: p.id,
        seat: p.seat,
        score: p.score,
        handCount: p.id === player.id ? p.handCards.length : null,  // 只返回自己的手牌数
        discards: p.discards.map(function(c) { return c.code; })
      };
    }),
    
    // 3. 玩家自己的手牌
    myHandCards: this._getPlayerHandCards(gameState, player.id),
    
    // 4. 精牌信息
    jingInfo: gameState.jingInfo,
    
    // 5. 当前操作
    currentAction: gameState.currentAction
  };
  
  return deskInfo;
};

3. 操作队列管理

// OperationManager.js - 操作队列
OperationManager.prototype.queueOperation = function(operation) {
  // 1. 添加到队列
  this.operationQueue.push(operation);
  
  // 2. 如果当前没有操作在执行,开始执行
  if (!this.isProcessing) {
    this.processQueue();
  }
};

OperationManager.prototype.processQueue = function() {
  if (this.operationQueue.length === 0) {
    this.isProcessing = false;
    return;
  }
  
  this.isProcessing = true;
  var operation = this.operationQueue.shift();
  
  // 执行操作
  this.executeOperation(operation, function(error, result) {
    if (error) {
      console.error('操作执行失败:', error);
    }
    
    // 继续处理队列
    this.processQueue();
  }.bind(this));
};

📚 相关文档链接


📝 附录

A. 游戏阶段常量

// GameConstants.js
GAME_PHASES: {
  WAITING: 'waiting',
  DEALING: 'dealing',
  JING_DETERMINING: 'jing_determining',
  PLAYING: 'playing',
  RESPONDING: 'responding',
  ROUND_END: 'round_end',
  GAME_END: 'game_end'
}

B. 操作类型常量

// GameConstants.js
OPERATION_TYPES: {
  DISCARD: 'discard',      // 出牌
  DRAW: 'draw',            // 摸牌
  CHOW: 'chow',            // 吃牌
  PONG: 'pong',            // 碰牌
  KONG: 'kong',            // 杠牌
  WIN: 'win',              // 胡牌
  PASS: 'pass'             // 过
}

C. 超时设置

// RoomConstants.js
TIMEOUTS: {
  READY: 300000,           // 准备超时5分钟
  DISCARD: 30000,          // 出牌超时30秒
  RESPONSE: 5000,          // 响应超时5秒
  FAST_MODE_DISCARD: 15000,// 快版出牌15秒
  FAST_MODE_RESPONSE: 3000 // 快版响应3秒
}

D. 常见问题

Q1: 游戏状态转换失败怎么处理?

A: 检查转换规则是否合法,使用 GameStateManager.validateStateTransition() 验证。

Q2: 断线重连后手牌数据不完整?

A: 确保 export.get_deskinfo() 正确返回玩家手牌信息,检查数据序列化。

Q3: 操作超时没有自动处理?

A: 检查 gameState.rules.gameplayOptions.kuaiban 配置,确保快版模式正确启用。

Q4: 多人同时胡牌如何处理?

A: 按下家优先原则,使用 OperationManager.resolveMultipleWins() 解决冲突。


文档版本v1.0
最后更新2025年10月15日
维护者:进贤麻将开发团队