37 KiB
37 KiB
游戏流程概述
📋 文档概述
本文档详细说明进贤麻将的完整游戏流程,包括:
- 状态机设计 - 游戏阶段转换规则
- 完整流程 - 从房间创建到游戏结束
- 各阶段详解 - 准备、发牌、出牌、结算等
- 时序关系 - 操作顺序和同步机制
文档目标:帮助开发者理解游戏的完整运行流程,掌握各阶段的处理逻辑和状态转换规则。
🎮 游戏流程总览
核心流程图
房间创建 → 玩家准备 → 开始游戏 → 游戏进行 → 局数结束 → 游戏结束
↓ ↓ ↓ ↓ ↓ ↓
创建房间 玩家加入 开战函数 状态机 计分结算 总结算
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
相关文档:
- 01-Export接口说明.md - makewar接口详解
- 06-规则配置系统.md - roomtype解析
第二步:准备阶段(WAITING)
状态特征:phase = 'waiting'
主要任务:
- 等待玩家加入房间
- 玩家点击准备
- 全员准备后开始游戏
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'
主要任务:
- 洗牌(Shuffle)
- 配牌(Deal)
- 开精(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):
- 支持特定规则的洗牌干预
- 确保精牌分布的合理性
- 详见 06-规则配置系统.md
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'
主要任务:
- 玩家轮流出牌
- 其他玩家响应(吃碰杠胡)
- 操作优先级处理
- 超时托管
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 操作响应流程
操作优先级(从高到低):
- 胡牌(最高优先级,多人胡牌按下家优先)
- 杠牌(与胡牌平级,玩家可选择)
- 碰牌
- 吃牌(最低优先级,仅下家可吃)
// 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)
};
};
胡牌检测算法详解:
- 详见 05-共享代码模块.md
- 精牌万能牌处理:进贤麻将规则手册.md
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'
触发条件:
- 有玩家胡牌
- 流局(剩余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;
};
计分规则详解:
- 详见 进贤麻将规则手册.md
- 不同身份、不同胡牌方式的计分差异
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));
};
📚 相关文档链接
- 上一篇:07-工具模块.md - 日志和错误处理
- 下一篇:09-代码框架总结.md - 整体架构总结
- 参考:
- 01-Export接口说明.md - Export接口详解
- 03-RPC处理机制.md - RPC路由机制
- 04-游戏核心服务.md - 核心服务类
- 05-共享代码模块.md - 算法详解
- 进贤麻将规则手册.md - 游戏规则
📝 附录
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日
维护者:进贤麻将开发团队