# 游戏流程概述 ## 📋 文档概述 本文档详细说明进贤麻将的完整游戏流程,包括: - **状态机设计** - 游戏阶段转换规则 - **完整流程** - 从房间创建到游戏结束 - **各阶段详解** - 准备、发牌、出牌、结算等 - **时序关系** - 操作顺序和同步机制 **文档目标**:帮助开发者理解游戏的完整运行流程,掌握各阶段的处理逻辑和状态转换规则。 --- ## 🎮 游戏流程总览 ### 核心流程图 ``` 房间创建 → 玩家准备 → 开始游戏 → 游戏进行 → 局数结束 → 游戏结束 ↓ ↓ ↓ ↓ ↓ ↓ 创建房间 玩家加入 开战函数 状态机 计分结算 总结算 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 | 即时 | --- ## 🔄 游戏状态机 ### 状态定义 进贤麻将使用严格的状态机控制游戏流程,确保操作的合法性和顺序性。 ```javascript // 游戏状态定义(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 │ 游戏结束、总结算 │ 游戏结束 │ └──────────┘ ``` ### 状态转换函数 ```javascript /** * 状态转换核心方法 * @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); } ``` --- ## 📊 完整游戏流程 ### 第一步:房间创建 **触发时机**:玩家在客户端点击"创建房间" **执行流程**: ```javascript // 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` **相关文档**: - [01-Export接口说明.md](../framework/01-Export接口说明.md#4-makewar-开战函数) - makewar接口详解 - [06-规则配置系统.md](../core/06-规则配置系统.md) - roomtype解析 --- ### 第二步:准备阶段(WAITING) **状态特征**:`phase = 'waiting'` **主要任务**: 1. 等待玩家加入房间 2. 玩家点击准备 3. 全员准备后开始游戏 #### 2.1 玩家加入 ```javascript // 玩家加入房间 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 玩家准备 ```javascript // 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 开始游戏 ```javascript 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 洗牌流程 ```javascript // 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)**: - 支持特定规则的洗牌干预 - 确保精牌分布的合理性 - 详见 [06-规则配置系统.md](../core/06-规则配置系统.md#洗牌增强系统) #### 3.2 发牌流程 ```javascript // 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 开精流程 ```javascript // 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 出牌流程 ```javascript // 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. **吃牌**(最低优先级,仅下家可吃) ```javascript // 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 胡牌检测 ```javascript // 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) }; }; ``` **胡牌检测算法详解**: - 详见 [05-共享代码模块.md](../core/05-共享代码模块.md#windetectionfactory-胡牌检测工厂) - 精牌万能牌处理:[进贤麻将规则手册.md](../../../../docs/important/game/进贤麻将规则手册.md#万能牌技术实现) #### 4.4 操作超时处理 ```javascript // 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 胡牌结算 ```javascript // 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 分数分配 ```javascript // 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; }; ``` **计分规则详解**: - 详见 [进贤麻将规则手册.md](../../../../docs/important/game/进贤麻将规则手册.md#胡牌计分) - 不同身份、不同胡牌方式的计分差异 #### 5.3 局结束处理 ```javascript // 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 游戏总结算 ```javascript // 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. 状态同步机制 ```javascript // 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. 断线重连 ```javascript // 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. 操作队列管理 ```javascript // 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)); }; ``` --- ## 📚 相关文档链接 - **上一篇**:[07-工具模块.md](../development/07-工具模块.md) - 日志和错误处理 - **下一篇**:[09-代码框架总结.md](09-代码框架总结.md) - 整体架构总结 - **参考**: - [01-Export接口说明.md](../framework/01-Export接口说明.md) - Export接口详解 - [03-RPC处理机制.md](../framework/03-RPC处理机制.md) - RPC路由机制 - [04-游戏核心服务.md](../core/04-游戏核心服务.md) - 核心服务类 - [05-共享代码模块.md](../core/05-共享代码模块.md) - 算法详解 - [进贤麻将规则手册.md](../../../../docs/important/game/进贤麻将规则手册.md) - 游戏规则 --- ## 📝 附录 ### A. 游戏阶段常量 ```javascript // 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. 操作类型常量 ```javascript // GameConstants.js OPERATION_TYPES: { DISCARD: 'discard', // 出牌 DRAW: 'draw', // 摸牌 CHOW: 'chow', // 吃牌 PONG: 'pong', // 碰牌 KONG: 'kong', // 杠牌 WIN: 'win', // 胡牌 PASS: 'pass' // 过 } ``` ### C. 超时设置 ```javascript // 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日 **维护者**:进贤麻将开发团队