26 KiB
26 KiB
RPC处理机制详解
文档目标:详细说明进贤麻将的RPC请求处理流程,包括RpcHandler、OperationEnumerator、AIRpcHandler的功能和使用方法。
📚 目录
- RPC机制概述
- RpcHandler - RPC请求处理器
- OperationEnumerator - 操作列举器
- AIRpcHandler - AI玩家处理器
- RPC处理标准流程
- 数据包构建规范
- 实现示例
1. RPC机制概述
1.1 什么是RPC
RPC (Remote Procedure Call) 是远程过程调用,允许客户端通过网络调用服务端的方法。
在友乐游戏框架中:
客户端(浏览器) 服务端(Node.js)
│ │
│ WebSocket/HTTP │
│ ────────────────────────> │
│ {app, route, rpc, data} │
│ │
│ packet.js
│ ↓
│ youle_app
│ ↓
│ mod_jinxianmahjong
│ ↓
│ mod_jinxianmahjong.player_discard(pack)
│ ↓
│ 处理业务逻辑
│ ↓
│ <────────────────────────│
│ 响应数据包 │
1.2 三层路由机制
【第1层】packet.js - 网络层
↓ 根据app字段路由
【第2层】youle_app - 应用层
↓ 根据route字段路由
【第3层】mod_jinxianmahjong - 模块层
↓ 根据rpc字段调用
【执行】RPC方法执行
示例数据包:
{
"app": "youle", // 应用标识 → 路由到youle_app
"route": "jinxianmahjong", // 模块标识 → 路由到mod_jinxianmahjong
"rpc": "player_discard", // 方法名 → 调用mod_jinxianmahjong.player_discard()
"data": { // 业务数据
"agentid": "agent001",
"playerid": 12345,
"gameid": "jinxianmahjong",
"roomcode": 100001,
"seat": 0,
"cardUniqueId": 45
}
}
1.3 进贤麻将RPC架构
┌────────────────────────────────────────┐
│ mod.js (RPC定义) │
│ - 定义所有RPC方法 │
│ - mod_jinxianmahjong.player_discard │
│ - mod_jinxianmahjong.player_peng │
│ - mod_jinxianmahjong.player_gang │
│ - ... │
└───────────────┬────────────────────────┘
│ 调用
┌───────────────▼────────────────────────┐
│ RpcHandler.js │
│ - 处理RPC请求 │
│ - 参数提取和验证 │
│ - 调用业戏控制器 │
│ - 构建响应数据包 │
└───────────────┬────────────────────────┘
│ 委托
┌───────────────▼────────────────────────┐
│ GameController / OperationManager │
│ - 游戏逻辑控制 │
│ - 状态管理 │
│ - 操作验证 │
└───────────────┬────────────────────────┘
│ 发包
┌───────────────▼────────────────────────┐
│ o_room.method.sendpack_* │
│ - sendpack_toall() │
│ - sendpack_toseat() │
│ - sendpack_toother() │
└────────────────────────────────────────┘
2. RpcHandler - RPC请求处理器
2.1 RpcHandler的职责
RpcHandler是进贤麻将的核心RPC处理模块,负责:
- 接收和解析RPC请求:提取参数、验证格式
- 玩家身份验证:调用
check_player验证玩家 - 委托业务逻辑:调用GameController或OperationManager
- 构建响应数据:按"一包多信息"原则构建响应
- 发送响应:调用发包接口推送给客户端
- 错误处理:统一的错误处理和日志记录
2.2 主要RPC方法
进贤麻将实现的核心RPC方法:
| RPC方法 | 对应操作 | 说明 |
|---|---|---|
handlePlayCard |
出牌 | 玩家打出一张手牌 |
handleDeclarePeng |
碰牌 | 玩家碰别人打出的牌 |
handleDeclareGang |
杠牌 | 玩家杠牌(明杠/暗杠/加杠) |
handleDeclareHu |
胡牌 | 玩家胡牌(自摸/点炮) |
handlePass |
过牌 | 玩家放弃吃碰杠胡机会 |
handleReady |
准备 | 玩家准备开始游戏 |
getGameState |
获取状态 | 获取当前游戏状态 |
2.3 RPC方法标准结构
每个RPC方法遵循统一的处理流程:
handlePlayCard: function(pack) {
try {
// ===== 第1步:提取参数 =====
var agentid = pack.data.agentid;
var playerid = parseInt(pack.data.playerid);
var gameid = pack.data.gameid;
var roomcode = pack.data.roomcode;
var seat = parseInt(pack.data.seat);
var cardUniqueId = parseInt(pack.data.cardUniqueId);
console.log('[RpcHandler.playCard] 开始处理出牌请求:', {
playerid, seat, cardUniqueId
});
// ===== 第2步:验证玩家 =====
var o_room = mod_jinxianmahjong.import.check_player(
agentid, gameid, roomcode, seat, playerid,
pack.conmode, pack.fromid
);
if (!o_room) {
console.error('[RpcHandler.playCard] 玩家验证失败');
return { success: false, error: "玩家验证失败" };
}
// ===== 第3步:获取游戏对象 =====
var o_desk = o_room.o_desk;
if (!o_desk) {
console.error('[RpcHandler.playCard] 游戏桌不存在');
this.sendErrorResponse(o_room, seat, 500, "游戏桌不存在");
return { success: false, error: "游戏桌不存在" };
}
// ===== 第4步:记录收包(调试) =====
if (o_desk.debug && typeof o_desk.debug.save_receivepack === 'function') {
o_desk.debug.save_receivepack(pack, seat, playerid);
}
// ===== 第5步:委托业务逻辑 =====
var operationRequest = {
operation: "discard_card",
playerSeat: seat,
uniqueId: cardUniqueId,
requestId: this._generateRequestId(),
timestamp: Date.now()
};
var operationResult = OperationManager.handleOperation(o_room, operationRequest);
// ===== 第6步:处理结果 =====
if (operationResult.success) {
// 构建响应数据包
var responseData = this._buildPlayCardResponse(o_room, seat, cardUniqueId, operationResult);
// 发送响应
this._sendResponse(o_room, seat, responseData);
return { success: true };
} else {
// 操作失败,发送错误
this.sendErrorResponse(o_room, seat, 400, operationResult.error);
return { success: false, error: operationResult.error };
}
} catch (error) {
console.error('[RpcHandler.playCard] 处理失败:', error);
this.sendErrorResponse(o_room, seat, 500, "服务器内部错误");
return { success: false, error: error.message };
}
}
2.4 "一包多信息"原则
RpcHandler遵循友乐平台的**"一包多信息"设计原则**:
原则说明:
- 单个响应包包含完整的状态更新信息
- 减少网络请求次数
- 确保客户端状态同步
示例:出牌响应包包含
{
status: 200,
seat: 0, // 出牌玩家
discardedCard: {...}, // 打出的牌
gameState: { // 游戏状态更新
currentPlayer: 1,
remainingCards: 52,
phase: "playing"
},
playerActions: { // 其他玩家可执行操作
1: {
availableActions: ["peng", "gang", "hu"],
pengResult: {...}, // 碰牌详情
gangResult: {...}, // 杠牌详情
huResult: {...}, // 胡牌详情
timeout: 10000
},
2: {
availableActions: ["hu"],
huResult: {...},
timeout: 10000
}
},
autoDrawCard: null, // 是否自动摸牌
waitingForResponse: true // 是否等待响应
}
2.5 分层推送机制
RpcHandler实现分层推送,为不同玩家定制不同的信息:
// 1. 给操作玩家:包含完整信息
var dataForPlayer = {
status: 200,
seat: 0,
myHandCards: [...], // 我的手牌(完整)
discardedCard: {...},
gameState: {...}
};
o_room.method.sendpack_toseat(dataForPlayer, 0);
// 2. 给其他玩家:隐藏私密信息
var dataForOthers = {
status: 200,
seat: 0,
handCardCount: 13, // 只显示数量,不显示具体牌
discardedCard: {...},
gameState: {...}
};
o_room.method.sendpack_toother(dataForOthers, 0);
3. OperationEnumerator - 操作列举器
3.1 OperationEnumerator的职责
OperationEnumerator负责生成玩家可执行的所有操作选项:
- 枚举所有可能操作:出牌、吃、碰、杠、胡、过
- 计算操作参数:每个操作需要的牌、组合等
- 生成choiceIndex:为每个操作分配索引
- 验证操作合法性:确保操作符合规则
3.2 操作类型
var operationTypes = {
discard: [], // 出牌操作
chi: [], // 吃牌操作
peng: [], // 碰牌操作
gang: [], // 杠牌操作(明杠、暗杠、加杠)
hu: [], // 胡牌操作
pass: [] // 过牌操作
};
3.3 核心方法
generateAvailableOperations
生成完整的可执行操作列表:
/**
* 为指定玩家生成完整的可执行操作列表
* @param {Object} gameState - 当前游戏状态
* @param {number} seat - 玩家座位号
* @param {Object} context - 上下文信息
* @returns {Object} 完整的操作列表
*/
generateAvailableOperations(gameState, seat, context = {}) {
var operations = {
discard: [],
chi: [],
peng: [],
gang: [],
hu: [],
pass: []
};
try {
// 1. 生成出牌操作
operations.discard = this.generateDiscardActions(gameState, seat);
// 2. 如果有刚出的牌,检查吃碰杠胡操作
if (context.lastDiscardCard) {
operations.chi = this.generateChiActions(gameState, seat, context.lastDiscardCard);
operations.peng = this.generatePengActions(gameState, seat, context.lastDiscardCard);
operations.gang = this.generateMingGangActions(gameState, seat, context.lastDiscardCard);
operations.hu = this.generateHuActions(gameState, seat, context.lastDiscardCard, "discard");
}
// 3. 检查自摸杠牌和胡牌
if (context.justDrawCard) {
operations.gang = operations.gang.concat(this.generateAnGangActions(gameState, seat));
operations.gang = operations.gang.concat(this.generateJiaGangActions(gameState, seat));
operations.hu = operations.hu.concat(this.generateHuActions(gameState, seat, context.justDrawCard, "draw"));
}
// 4. 生成过牌操作
if (this.hasNonDiscardOperations(operations)) {
operations.pass = this.generatePassAction();
}
} catch (error) {
console.error('[OperationEnumerator] 生成操作列表时出错:', error);
}
return operations;
}
generateDiscardActions
生成出牌操作:
generateDiscardActions(gameState, seat) {
if (!gameState.playerHands || !gameState.playerHands[seat]) {
return [];
}
var handCards = gameState.playerHands[seat];
var allowedCards = [];
// 获取所有手牌的uniqueId
for (var i = 0; i < handCards.length; i++) {
if (handCards[i] && handCards[i].uniqueId) {
allowedCards.push(handCards[i].uniqueId);
}
}
return [{
choiceIndex: 0,
operationType: "discard",
allowedCards: allowedCards,
description: "出牌操作"
}];
}
generatePengActions
生成碰牌操作:
generatePengActions(gameState, seat, sourceCard, fromSeat) {
var pengActions = [];
if (!gameState.playerHands || !gameState.playerHands[seat] || !sourceCard) {
return pengActions;
}
var handCards = gameState.playerHands[seat];
var sourceCode = sourceCard.code || sourceCard;
// 检查手牌中是否有至少2张相同的牌
var sameCards = this.findSameCards(handCards, sourceCode, 2);
if (sameCards.length >= 2) {
pengActions.push({
choiceIndex: 0,
operationType: "peng",
sourceCard: {
uniqueId: sourceCard.uniqueId,
code: sourceCode,
fromSeat: fromSeat
},
requiredCards: sameCards.slice(0, 2),
description: "碰" + this.getCardName(sourceCode)
});
}
return pengActions;
}
3.4 choiceIndex机制
choiceIndex是操作选择的索引,用于客户端选择和服务端执行:
// 服务端生成操作列表
var operations = OperationEnumerator.generateAvailableOperations(gameState, seat, context);
// 示例输出
{
peng: [
{ choiceIndex: 0, operationType: "peng", ... }, // 碰1万
],
gang: [
{ choiceIndex: 0, operationType: "gang", gangType: "angang", ... }, // 暗杠2万
{ choiceIndex: 1, operationType: "gang", gangType: "jiagang", ... } // 加杠3万
],
hu: [
{ choiceIndex: 0, operationType: "hu", huType: "zimo", ... }
]
}
// 客户端选择操作,发送choiceIndex
// 例如:选择加杠操作(choiceIndex=1)
var request = {
operation: "gang",
choiceIndex: 1, // 选择第2个杠牌操作
// ...
};
// 服务端根据choiceIndex执行对应操作
var selectedOperation = operations.gang[choiceIndex];
4. AIRpcHandler - AI玩家处理器
4.1 AIRpcHandler的职责
AIRpcHandler处理AI玩家的自动操作:
- AI决策:根据游戏状态做出决策
- 自动操作:自动出牌、吃碰杠胡
- 延时模拟:模拟人类玩家的思考时间
- 策略选择:根据难度选择不同策略
4.2 AI决策流程
// AI玩家轮到操作
AIRpcHandler.handleAITurn(o_room, aiSeat) {
// 1. 获取可执行操作
var operations = OperationEnumerator.generateAvailableOperations(
gameState, aiSeat, context
);
// 2. AI决策
var decision = AIStrategy.makeDecision(gameState, aiSeat, operations);
// 3. 延时模拟思考
setTimeout(function() {
// 4. 执行AI操作
if (decision.operation === "discard") {
AIRpcHandler.executeAIDiscard(o_room, aiSeat, decision.cardUniqueId);
} else if (decision.operation === "peng") {
AIRpcHandler.executeAIPeng(o_room, aiSeat, decision);
}
// ...
}, decision.thinkingTime);
}
4.3 AI策略
// 简单策略示例
AIStrategy.makeDecision = function(gameState, seat, operations) {
// 优先级:胡 > 杠 > 碰 > 吃 > 出牌
if (operations.hu.length > 0) {
return { operation: "hu", choiceIndex: 0 };
}
if (operations.gang.length > 0) {
return { operation: "gang", choiceIndex: 0 };
}
if (operations.peng.length > 0) {
return { operation: "peng", choiceIndex: 0 };
}
// 默认出牌:出最不需要的牌
var cardToDiscard = this.selectCardToDiscard(gameState, seat);
return {
operation: "discard",
cardUniqueId: cardToDiscard.uniqueId
};
};
5. RPC处理标准流程
5.1 完整RPC处理流程图
客户端发送请求
↓
【1】packet.js接收
↓
【2】youle_app路由
↓
【3】mod_jinxianmahjong路由
↓
【4】RPC方法(如player_discard)
↓
【5】RpcHandler.handlePlayCard
├─ 提取参数
├─ check_player验证
├─ 记录收包
└─ 委托OperationManager
↓
【6】OperationManager.handleOperation
├─ 验证操作合法性
├─ 执行游戏逻辑
├─ 更新游戏状态
└─ 返回结果
↓
【7】RpcHandler构建响应
├─ 基础响应数据
├─ 检查其他玩家操作机会
├─ 添加操作提示
└─ 构建完整响应包
↓
【8】发送响应
├─ sendpack_toseat(给操作玩家)
├─ sendpack_toother(给其他玩家)
└─ sendpack_toall(广播给所有人)
↓
客户端接收响应
├─ 更新本地状态
├─ 播放动画
└─ 显示界面
5.2 标准RPC方法实现模板
// 在mod.js中定义RPC方法
mod_jinxianmahjong.player_discard = function(pack) {
return RpcHandler.handlePlayCard(pack);
};
mod_jinxianmahjong.player_peng = function(pack) {
return RpcHandler.handleDeclarePeng(pack);
};
mod_jinxianmahjong.player_gang = function(pack) {
return RpcHandler.handleDeclareGang(pack);
};
mod_jinxianmahjong.player_hu = function(pack) {
return RpcHandler.handleDeclareHu(pack);
};
mod_jinxianmahjong.player_pass = function(pack) {
return RpcHandler.handlePass(pack);
};
6. 数据包构建规范
6.1 响应包标准结构
{
status: 200, // HTTP状态码风格
message: "操作成功", // 消息说明
data: { // 业务数据
// 具体业务数据
},
timestamp: 1234567890 // 时间戳
}
6.2 错误响应结构
{
status: 400, // 错误状态码
error: "操作失败", // 错误消息
code: "INVALID_OPERATION", // 错误码
details: "详细错误信息", // 详细说明
timestamp: 1234567890
}
6.3 状态码规范
| 状态码 | 含义 | 使用场景 |
|---|---|---|
| 200 | 成功 | 操作执行成功 |
| 400 | 请求错误 | 参数错误、操作不合法 |
| 401 | 未授权 | 玩家验证失败 |
| 403 | 禁止操作 | 不是当前玩家的回合 |
| 404 | 未找到 | 房间或玩家不存在 |
| 500 | 服务器错误 | 内部错误 |
7. 实现示例
7.1 完整的出牌RPC实现
// mod.js - 定义RPC方法
mod_jinxianmahjong.player_discard = function(pack) {
return RpcHandler.handlePlayCard(pack);
};
// RpcHandler.js - 实现处理逻辑
RpcHandler.handlePlayCard = function(pack) {
try {
// 1. 提取和验证参数
var params = this._extractParams(pack);
if (!params.valid) {
return { success: false, error: params.error };
}
// 2. 验证玩家
var o_room = mod_jinxianmahjong.import.check_player(
params.agentid, params.gameid, params.roomcode,
params.seat, params.playerid, pack.conmode, pack.fromid
);
if (!o_room) {
return { success: false, error: "玩家验证失败" };
}
// 3. 执行出牌操作
var result = OperationManager.handleOperation(o_room, {
operation: "discard_card",
playerSeat: params.seat,
uniqueId: params.cardUniqueId
});
if (!result.success) {
this.sendErrorResponse(o_room, params.seat, 400, result.error);
return result;
}
// 4. 构建响应数据
var responseData = this._buildPlayCardResponse(o_room, params.seat, result);
// 5. 发送响应
this._sendLayeredResponse(o_room, params.seat, responseData);
return { success: true };
} catch (error) {
console.error('[RpcHandler.playCard] 错误:', error);
return { success: false, error: error.message };
}
};
// 辅助方法:构建响应数据
RpcHandler._buildPlayCardResponse = function(o_room, seat, result) {
var gameState = o_room.o_desk.gameState;
return {
status: 200,
seat: seat,
discardedCard: this._serializeCard(result.discardedCard),
gameState: {
phase: gameState.phase,
currentPlayer: gameState.currentPlayer,
remainingCards: gameState.gameData.deck.length
},
playerActions: this._buildPlayerActions(o_room, result),
waitingForResponse: result.hasResponse,
timestamp: Date.now()
};
};
// 辅助方法:分层发送响应
RpcHandler._sendLayeredResponse = function(o_room, seat, responseData) {
// 给操作玩家:包含完整信息
var dataForPlayer = Object.assign({}, responseData, {
myHandCards: this._serializeHandCards(o_room, seat)
});
o_room.method.sendpack_toseat(dataForPlayer, seat);
// 给其他玩家:隐藏私密信息
var dataForOthers = Object.assign({}, responseData, {
handCardCount: this._getHandCardCount(o_room, seat)
});
o_room.method.sendpack_toother(dataForOthers, seat);
};
7.2 操作枚举示例
// 使用OperationEnumerator
var operations = OperationEnumerator.generateAvailableOperations(
gameState,
seat,
{
lastDiscardCard: { uniqueId: 45, code: 13 }, // 刚出的3万
fromSeat: 1
}
);
// 输出示例
{
discard: [],
chi: [],
peng: [
{
choiceIndex: 0,
operationType: "peng",
sourceCard: { uniqueId: 45, code: 13, fromSeat: 1 },
requiredCards: [
{ uniqueId: 12, code: 13 },
{ uniqueId: 78, code: 13 }
],
description: "碰3万"
}
],
gang: [],
hu: [
{
choiceIndex: 0,
operationType: "hu",
huType: "dianpao",
winCards: [...],
score: 8,
description: "胡牌 - 8分"
}
],
pass: [
{
choiceIndex: 0,
operationType: "pass",
description: "过"
}
]
}
8. 最佳实践
8.1 参数提取
// ✅ 统一的参数提取方法
_extractParams: function(pack) {
try {
return {
valid: true,
agentid: pack.data.agentid,
playerid: parseInt(pack.data.playerid),
gameid: pack.data.gameid,
roomcode: pack.data.roomcode,
seat: parseInt(pack.data.seat),
cardUniqueId: parseInt(pack.data.cardUniqueId)
};
} catch (error) {
return {
valid: false,
error: "参数解析失败: " + error.message
};
}
}
8.2 错误处理
// ✅ 统一的错误响应
sendErrorResponse: function(o_room, seat, statusCode, message) {
var errorMsg = {
app: "youle",
route: "jinxianmahjong",
rpc: "error",
data: {
status: statusCode,
error: message,
timestamp: Date.now()
}
};
o_room.method.sendpack_toseat(errorMsg, seat);
}
8.3 日志记录
// ✅ 详细的日志记录
console.log('[RpcHandler.playCard] 开始处理:', {
playerid: params.playerid,
seat: params.seat,
cardUniqueId: params.cardUniqueId
});
console.log('[RpcHandler.playCard] 操作结果:', {
success: result.success,
error: result.error
});
9. 常见问题
Q1: RpcHandler和OperationManager的区别?
A:
- RpcHandler:处理RPC请求,负责参数提取、验证、响应构建
- OperationManager:执行游戏逻辑,负责状态管理、规则验证
Q2: 为什么需要OperationEnumerator?
A: 因为需要提前告诉客户端有哪些操作可以执行,客户端根据choiceIndex选择操作。
Q3: choiceIndex有什么用?
A: choiceIndex是操作选择的索引:
- 服务端生成所有可能操作并分配索引
- 客户端选择后发送choiceIndex
- 服务端根据索引执行对应操作
Q4: 如何实现分层推送?
A: 使用不同的发包接口:
sendpack_toseat:发给操作玩家(完整信息)sendpack_toother:发给其他玩家(隐藏私密信息)
Q5: AI玩家如何处理?
A: AI玩家通过AIRpcHandler自动处理:
- 监听游戏状态变化
- 自动调用AI决策
- 模拟延时后执行操作
10. 下一步
阅读以下文档继续学习:
相关代码文件:
server/games2/jinxianmahjong/mod.js- RPC方法定义server/games2/jinxianmahjong/rpc/RpcHandler.js- RPC处理器server/games2/jinxianmahjong/rpc/OperationEnumerator.js- 操作列举器server/games2/jinxianmahjong/rpc/AIRpcHandler.js- AI处理器