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

1206 lines
37 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 游戏流程概述
## 📋 文档概述
本文档详细说明进贤麻将的完整游戏流程,包括:
- **状态机设计** - 游戏阶段转换规则
- **完整流程** - 从房间创建到游戏结束
- **各阶段详解** - 准备、发牌、出牌、结算等
- **时序关系** - 操作顺序和同步机制
**文档目标**:帮助开发者理解游戏的完整运行流程,掌握各阶段的处理逻辑和状态转换规则。
---
## 🎮 游戏流程总览
### 核心流程图
```
房间创建 → 玩家准备 → 开始游戏 → 游戏进行 → 局数结束 → 游戏结束
↓ ↓ ↓ ↓ ↓ ↓
创建房间 玩家加入 开战函数 状态机 计分结算 总结算
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日
**维护者**:进贤麻将开发团队