1206 lines
37 KiB
Markdown
1206 lines
37 KiB
Markdown
# 游戏流程概述
|
||
|
||
## 📋 文档概述
|
||
|
||
本文档详细说明进贤麻将的完整游戏流程,包括:
|
||
- **状态机设计** - 游戏阶段转换规则
|
||
- **完整流程** - 从房间创建到游戏结束
|
||
- **各阶段详解** - 准备、发牌、出牌、结算等
|
||
- **时序关系** - 操作顺序和同步机制
|
||
|
||
**文档目标**:帮助开发者理解游戏的完整运行流程,掌握各阶段的处理逻辑和状态转换规则。
|
||
|
||
---
|
||
|
||
## 🎮 游戏流程总览
|
||
|
||
### 核心流程图
|
||
|
||
```
|
||
房间创建 → 玩家准备 → 开始游戏 → 游戏进行 → 局数结束 → 游戏结束
|
||
↓ ↓ ↓ ↓ ↓ ↓
|
||
创建房间 玩家加入 开战函数 状态机 计分结算 总结算
|
||
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日
|
||
**维护者**:进贤麻将开发团队
|