目录结构调整
This commit is contained in:
606
codes/games/server/docs/guides/framework/00-框架基础概述.md
Normal file
606
codes/games/server/docs/guides/framework/00-框架基础概述.md
Normal file
@@ -0,0 +1,606 @@
|
||||
# 友乐游戏框架基础概述
|
||||
|
||||
> **文档目标**:帮助开发者理解友乐游戏平台的整体架构、前后端分离机制、模块化设计和基础开发规范。
|
||||
|
||||
## 📚 目录
|
||||
|
||||
1. [友乐游戏平台架构](#1-友乐游戏平台架构)
|
||||
2. [前后端分离部署](#2-前后端分离部署)
|
||||
3. [三文件架构规范](#3-三文件架构规范)
|
||||
4. [模块加载机制](#4-模块加载机制)
|
||||
5. [数据包协议基础](#5-数据包协议基础)
|
||||
6. [开发环境与兼容性](#6-开发环境与兼容性)
|
||||
|
||||
---
|
||||
|
||||
## 1. 友乐游戏平台架构
|
||||
|
||||
### 1.1 整体架构
|
||||
|
||||
友乐游戏平台采用**模块化、可扩展**的架构设计,支持多个子游戏的独立开发和部署。
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ 友乐游戏平台(Node.js) │
|
||||
├─────────────────────────────────────────────────┤
|
||||
│ 应用层(youle_app) │
|
||||
│ ├─ 房间管理(youle_room) │
|
||||
│ ├─ 玩家管理(youle_player) │
|
||||
│ ├─ 通信服务(youle_socket) │
|
||||
│ └─ 数据存储(youle_database) │
|
||||
├─────────────────────────────────────────────────┤
|
||||
│ 游戏模块层(mod_*) │
|
||||
│ ├─ mod_jinxianmahjong(进贤麻将) │
|
||||
│ ├─ mod_other_game1(其他游戏1) │
|
||||
│ └─ mod_other_game2(其他游戏2) │
|
||||
├─────────────────────────────────────────────────┤
|
||||
│ 网络层(packet.js) │
|
||||
│ ├─ WebSocket通信 │
|
||||
│ ├─ HTTP通信 │
|
||||
│ └─ RPC路由分发 │
|
||||
└─────────────────────────────────────────────────┘
|
||||
↕ WebSocket/HTTP
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ 客户端(浏览器环境) │
|
||||
│ ├─ 游戏界面渲染 │
|
||||
│ ├─ 用户交互处理 │
|
||||
│ ├─ 本地状态管理 │
|
||||
│ └─ 网络通信封装 │
|
||||
└─────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 1.2 核心组件
|
||||
|
||||
| 组件名称 | 职责说明 | 部署位置 |
|
||||
|---------|---------|---------|
|
||||
| **youle_app** | 应用级服务提供者,管理所有游戏模块 | 服务端 |
|
||||
| **youle_room** | 房间管理服务,处理房间创建、加入、解散 | 服务端 |
|
||||
| **packet.js** | 数据包路由分发器,实现RPC调用 | 服务端 |
|
||||
| **mod_*(游戏模块)** | 子游戏具体实现,独立封装游戏逻辑 | 服务端 |
|
||||
| **客户端界面** | 游戏前端界面和交互逻辑 | 浏览器 |
|
||||
|
||||
### 1.3 关键设计理念
|
||||
|
||||
1. **模块独立性**:每个游戏模块独立开发、测试、部署
|
||||
2. **接口标准化**:所有游戏模块遵循统一的接口规范
|
||||
3. **双向解耦**:框架和游戏通过export/import接口解耦
|
||||
4. **状态同步**:服务端为权威状态源,客户端被动接收
|
||||
|
||||
---
|
||||
|
||||
## 2. 前后端分离部署
|
||||
|
||||
### 2.1 部署架构
|
||||
|
||||
> ⚠️ **重要**:友乐游戏采用**真正的前后端物理分离**,而非同进程模块调用。
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────┐
|
||||
│ 用户浏览器(客户端) │
|
||||
│ 环境:Chrome/Firefox/Safari等浏览器 │
|
||||
│ 语言:JavaScript ES5(不使用Node.js) │
|
||||
│ 部署:静态HTML/JS/CSS文件 │
|
||||
│ 运行:浏览器JavaScript引擎 │
|
||||
└──────────────────────────────────────────────┘
|
||||
↕
|
||||
WebSocket/HTTP协议通信
|
||||
↕
|
||||
┌──────────────────────────────────────────────┐
|
||||
│ 游戏服务器(服务端) │
|
||||
│ 环境:Node.js运行时 │
|
||||
│ 语言:JavaScript ES5 │
|
||||
│ 部署:服务器进程(pm2/systemd等) │
|
||||
│ 运行:Node.js引擎 │
|
||||
└──────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 2.2 前后端通信特点
|
||||
|
||||
#### 客户端特点(浏览器环境)
|
||||
- **运行环境**:运行在用户浏览器中(Chrome、Firefox、Safari等)
|
||||
- **语言规范**:使用原生JavaScript ES5标准,不使用Node.js特性
|
||||
- **部署方式**:作为静态资源部署(HTML、JS、CSS文件)
|
||||
- **模块系统**:不使用npm包管理,不使用require/import
|
||||
- **依赖管理**:通过`<script>`标签按顺序加载
|
||||
|
||||
#### 服务端特点(Node.js环境)
|
||||
- **运行环境**:运行在服务器Node.js进程中
|
||||
- **语言规范**:使用JavaScript ES5,遵循Node.js模块规范
|
||||
- **部署方式**:作为Node.js应用部署
|
||||
- **模块系统**:使用Node.js的`require()`进行模块加载
|
||||
- **依赖管理**:使用npm管理依赖包
|
||||
|
||||
#### 通信机制
|
||||
|
||||
| 特性 | 说明 | 影响 |
|
||||
|-----|------|------|
|
||||
| **物理分离** | 客户端和服务端是完全独立的进程 | 不能直接调用函数,必须通过网络通信 |
|
||||
| **网络通信** | 使用WebSocket/HTTP协议 | 存在网络延迟,需要异步处理 |
|
||||
| **数据序列化** | 所有数据必须JSON序列化 | 不能传递函数、对象引用等 |
|
||||
| **异步交互** | 所有请求都是异步的 | 需要回调或事件机制处理响应 |
|
||||
| **状态同步** | 服务端主动推送状态变化 | 客户端需要维护本地状态副本 |
|
||||
|
||||
### 2.3 代码共享机制
|
||||
|
||||
由于前后端部署环境不同,代码共享通过**文件复制**方式实现:
|
||||
|
||||
```
|
||||
server/games2/jinxianmahjong/shared/
|
||||
├── core/ # 核心算法(前后端共享)
|
||||
│ ├── JingAlgorithm.js # 精牌算法
|
||||
│ ├── HandEvaluator.js # 牌型评估
|
||||
│ └── ScoringEngine.js # 计分引擎
|
||||
├── constants/ # 常量定义(前后端共享)
|
||||
├── dataStructures/ # 数据结构(前后端共享)
|
||||
└── utils/ # 工具函数(前后端共享)
|
||||
|
||||
↓ 文件复制(构建时)↓
|
||||
|
||||
client/js/shared/ # 复制到客户端目录
|
||||
├── core/
|
||||
├── constants/
|
||||
├── dataStructures/
|
||||
└── utils/
|
||||
```
|
||||
|
||||
**共享代码编写要求**:
|
||||
1. **ES5语法**:不使用任何ES6+特性(箭头函数、class、let/const等)
|
||||
2. **无副作用**:纯函数设计,不依赖全局状态
|
||||
3. **环境兼容**:同时兼容浏览器和Node.js环境
|
||||
4. **无依赖**:不依赖Node.js特定API或浏览器特定API
|
||||
|
||||
---
|
||||
|
||||
## 3. 三文件架构规范
|
||||
|
||||
每个子游戏必须遵循**三文件架构规范**:
|
||||
|
||||
### 3.1 架构概览
|
||||
|
||||
```
|
||||
server/games2/jinxianmahjong/
|
||||
├── mod.js # 【1】模块主入口
|
||||
├── export.js # 【2】输出接口(框架→子游戏)
|
||||
└── import.js # 【3】输入接口(子游戏→框架)
|
||||
```
|
||||
|
||||
### 3.2 三个核心文件
|
||||
|
||||
#### 1️⃣ mod.js - 模块主入口
|
||||
|
||||
**职责**:
|
||||
- 创建游戏模块实例
|
||||
- 按顺序加载依赖文件
|
||||
- 初始化模块状态
|
||||
- 定义RPC方法
|
||||
|
||||
**关键代码**:
|
||||
```javascript
|
||||
// 创建模块实例
|
||||
var mod_jinxianmahjong = cls_mod.new(
|
||||
"mod_jinxianmahjong", // 模块名称
|
||||
"jinxianmahjong", // 游戏ID
|
||||
youle_app // 父级应用
|
||||
);
|
||||
|
||||
// 加载依赖(按顺序)
|
||||
require('./export.js'); // 输出接口
|
||||
require('./import.js'); // 输入接口
|
||||
require('./rpc/RpcHandler.js'); // RPC处理器
|
||||
// ... 其他依赖
|
||||
|
||||
// 定义RPC方法
|
||||
mod_jinxianmahjong.player_draw = function(pack) {
|
||||
// 处理玩家摸牌请求
|
||||
};
|
||||
```
|
||||
|
||||
#### 2️⃣ export.js - 输出接口
|
||||
|
||||
**职责**:框架调用子游戏的标准接口
|
||||
|
||||
**必需的8个接口**:
|
||||
1. `get_needroomcard` - 创建房间所需房卡
|
||||
2. `get_asetcount` - 游戏局数
|
||||
3. `get_needroomcard_joinroom` - 加入房间所需房卡
|
||||
4. `makewar` - 开战(游戏开始)
|
||||
5. `get_deskinfo` - 获取牌桌信息(断线重连)
|
||||
6. `get_disbandRoom` - 解散房间
|
||||
7. `player_enter` - 玩家进入
|
||||
8. `player_leave` - 玩家离开
|
||||
|
||||
**实现模式**:
|
||||
```javascript
|
||||
var cls_jinxianmahjong_export = {
|
||||
new: function() {
|
||||
var exp = {};
|
||||
|
||||
exp.get_needroomcard = function(roomtype, o_game_config) {
|
||||
// 根据roomtype返回所需房卡数
|
||||
return 1; // 示例
|
||||
};
|
||||
|
||||
exp.makewar = function(o_room, o_game_config) {
|
||||
// 创建游戏桌对象
|
||||
var o_desk = createDesk(o_room);
|
||||
o_room.o_desk = o_desk;
|
||||
o_desk.o_room = o_room;
|
||||
|
||||
// 返回开战数据包
|
||||
return {
|
||||
app: "youle",
|
||||
route: "jinxianmahjong",
|
||||
rpc: "makewar",
|
||||
data: { /* 开战数据 */ }
|
||||
};
|
||||
};
|
||||
|
||||
// ... 其他6个接口
|
||||
|
||||
return exp;
|
||||
}
|
||||
};
|
||||
|
||||
// 挂载到模块
|
||||
mod_jinxianmahjong.export = cls_jinxianmahjong_export.new();
|
||||
```
|
||||
|
||||
#### 3️⃣ import.js - 输入接口
|
||||
|
||||
**职责**:子游戏调用框架服务
|
||||
|
||||
**核心的4个接口**:
|
||||
1. `check_player` - 验证玩家身份和位置
|
||||
2. `deduct_roomcard` - 扣除房卡
|
||||
3. `save_grade` - 保存游戏成绩
|
||||
4. `finish_gametask` - 完成游戏任务
|
||||
|
||||
**实现模式**:
|
||||
```javascript
|
||||
var cls_jinxianmahjong_import = {
|
||||
new: function() {
|
||||
var imp = {};
|
||||
|
||||
imp.check_player = function(agentid, gameid, roomcode, seat, playerid, conmode, fromid) {
|
||||
// 调用框架验证服务
|
||||
return mod_jinxianmahjong.app.youle_room.export.check_player(
|
||||
agentid, gameid, roomcode, seat, playerid, conmode, fromid
|
||||
);
|
||||
};
|
||||
|
||||
imp.deduct_roomcard = function(o_room) {
|
||||
// ⚠️ 必须在第一小局结算时调用
|
||||
return mod_jinxianmahjong.app.youle_room.export.deduct_roomcard(o_room);
|
||||
};
|
||||
|
||||
imp.save_grade = function(o_room, o_gameinfo1, o_gameinfo2, freeroomflag) {
|
||||
// ⚠️ 必须在大局结束时调用
|
||||
mod_jinxianmahjong.app.youle_room.export.save_grade(
|
||||
o_room, o_gameinfo1, o_gameinfo2, freeroomflag
|
||||
);
|
||||
};
|
||||
|
||||
// ... 其他接口
|
||||
|
||||
return imp;
|
||||
}
|
||||
};
|
||||
|
||||
// 挂载到模块
|
||||
mod_jinxianmahjong.import = cls_jinxianmahjong_import.new();
|
||||
```
|
||||
|
||||
### 3.3 文件加载顺序
|
||||
|
||||
**严格的加载顺序**(不可变更):
|
||||
|
||||
```
|
||||
1. mod.js # 首先加载,创建模块实例
|
||||
↓
|
||||
2. export.js # 加载输出接口
|
||||
↓
|
||||
3. import.js # 加载输入接口
|
||||
↓
|
||||
4. RpcHandler.js # 加载RPC处理器
|
||||
↓
|
||||
5. 其他业务文件 # 加载游戏逻辑
|
||||
```
|
||||
|
||||
**为什么顺序重要**:
|
||||
- `export.js`和`import.js`在模块初始化时就需要
|
||||
- 后续文件可能依赖这两个接口
|
||||
- 错误的顺序会导致`undefined`错误
|
||||
|
||||
---
|
||||
|
||||
## 4. 模块加载机制
|
||||
|
||||
### 4.1 双环境加载支持
|
||||
|
||||
友乐游戏框架支持两种运行环境:
|
||||
|
||||
#### Node.js环境(服务器/测试)
|
||||
```javascript
|
||||
if (typeof require !== 'undefined' && typeof module !== 'undefined') {
|
||||
// Node.js环境:使用require同步加载
|
||||
require('./export.js');
|
||||
require('./import.js');
|
||||
require('./rpc/RpcHandler.js');
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
#### 友乐平台环境(生产服务器)
|
||||
```javascript
|
||||
else if (typeof min_loadJsFile !== 'undefined') {
|
||||
// 友乐平台:使用min_loadJsFile异步加载
|
||||
min_loadJsFile("games2/jinxianmahjong/export.js", function() {
|
||||
min_loadJsFile("games2/jinxianmahjong/import.js", function() {
|
||||
min_loadJsFile("games2/jinxianmahjong/rpc/RpcHandler.js", function() {
|
||||
// 嵌套回调加载
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 模块初始化
|
||||
|
||||
```javascript
|
||||
function initializeModule() {
|
||||
console.log("[mod_jinxianmahjong] 模块初始化开始");
|
||||
|
||||
// 1. 验证必需接口
|
||||
if (!mod_jinxianmahjong.export) {
|
||||
throw new Error("export接口未加载");
|
||||
}
|
||||
if (!mod_jinxianmahjong.import) {
|
||||
throw new Error("import接口未加载");
|
||||
}
|
||||
|
||||
// 2. 设置就绪状态
|
||||
mod_jinxianmahjong.isReady = true;
|
||||
mod_jinxianmahjong.loadTime = new Date();
|
||||
|
||||
console.log("[mod_jinxianmahjong] ✅ 模块加载完成");
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 数据包协议基础
|
||||
|
||||
### 5.1 统一包结构
|
||||
|
||||
所有数据包必须遵循以下结构:
|
||||
|
||||
```javascript
|
||||
{
|
||||
"app": "youle", // 应用标识(固定为"youle")
|
||||
"route": "jinxianmahjong", // 路由模块名(游戏ID)
|
||||
"rpc": "player_draw", // RPC方法名
|
||||
"data": { // 业务数据
|
||||
"agentid": "agent001",
|
||||
"playerid": 12345,
|
||||
"gameid": "jinxianmahjong",
|
||||
"roomcode": 100001,
|
||||
"seat": 0,
|
||||
// ... 其他业务参数
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5.2 三层路由机制
|
||||
|
||||
```
|
||||
客户端发送数据包
|
||||
↓
|
||||
【1】packet.js(网络层)
|
||||
↓ 根据app字段路由
|
||||
【2】youle_app(应用层)
|
||||
↓ 根据route字段路由
|
||||
【3】mod_jinxianmahjong(模块层)
|
||||
↓ 根据rpc字段调用
|
||||
【4】mod_jinxianmahjong.player_draw(pack)
|
||||
↓
|
||||
执行游戏逻辑
|
||||
```
|
||||
|
||||
**路由过程**:
|
||||
1. **packet.js**:接收网络数据包,解析app字段
|
||||
2. **youle_app**:根据route字段找到对应游戏模块
|
||||
3. **mod_jinxianmahjong**:根据rpc字段调用对应方法
|
||||
4. **RPC方法**:执行具体游戏逻辑
|
||||
|
||||
### 5.3 RPC方法标准模板
|
||||
|
||||
```javascript
|
||||
mod_jinxianmahjong.player_draw = function(pack) {
|
||||
// 1. 提取参数
|
||||
var agentid = pack.data.agentid;
|
||||
var playerid = parseInt(pack.data.playerid);
|
||||
var gameid = pack.data.gameid;
|
||||
var roomcode = parseInt(pack.data.roomcode);
|
||||
var seat = parseInt(pack.data.seat);
|
||||
|
||||
// 2. 验证玩家(必须)
|
||||
var o_room = mod_jinxianmahjong.import.check_player(
|
||||
agentid, gameid, roomcode, seat, playerid,
|
||||
pack.conmode, pack.fromid
|
||||
);
|
||||
if (!o_room) {
|
||||
return; // 验证失败
|
||||
}
|
||||
|
||||
// 3. 获取游戏桌对象
|
||||
var o_desk = o_room.o_desk;
|
||||
|
||||
// 4. 执行业务逻辑
|
||||
var result = o_desk.gameService.playerDraw(seat);
|
||||
|
||||
// 5. 构造响应包
|
||||
var msg = {
|
||||
app: "youle",
|
||||
route: "jinxianmahjong",
|
||||
rpc: "player_draw_result",
|
||||
data: {
|
||||
seat: seat,
|
||||
card: result.card,
|
||||
// ...
|
||||
}
|
||||
};
|
||||
|
||||
// 6. 发送响应
|
||||
o_room.method.sendpack_toall(msg);
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 开发环境与兼容性
|
||||
|
||||
### 6.1 ES5语法规范
|
||||
|
||||
**必须遵循ES5语法**,不使用任何ES6+特性:
|
||||
|
||||
✅ **允许使用**:
|
||||
```javascript
|
||||
// 函数声明
|
||||
function myFunction() { }
|
||||
|
||||
// 变量声明
|
||||
var myVar = 123;
|
||||
|
||||
// 对象字面量
|
||||
var obj = {
|
||||
key: 'value',
|
||||
method: function() { }
|
||||
};
|
||||
|
||||
// 数组操作
|
||||
var arr = [1, 2, 3];
|
||||
arr.push(4);
|
||||
arr.forEach(function(item) { });
|
||||
|
||||
// 原型继承
|
||||
function MyClass() { }
|
||||
MyClass.prototype.method = function() { };
|
||||
```
|
||||
|
||||
❌ **禁止使用**:
|
||||
```javascript
|
||||
// 箭头函数(ES6)
|
||||
const func = () => { };
|
||||
|
||||
// let/const(ES6)
|
||||
let myVar = 123;
|
||||
const MY_CONST = 456;
|
||||
|
||||
// 类语法(ES6)
|
||||
class MyClass { }
|
||||
|
||||
// 模板字符串(ES6)
|
||||
var str = `Hello ${name}`;
|
||||
|
||||
// 解构赋值(ES6)
|
||||
var {x, y} = point;
|
||||
|
||||
// async/await(ES7)
|
||||
async function func() { }
|
||||
```
|
||||
|
||||
### 6.2 环境检测
|
||||
|
||||
```javascript
|
||||
// 检测Node.js环境
|
||||
if (typeof require !== 'undefined' && typeof module !== 'undefined') {
|
||||
// Node.js环境
|
||||
}
|
||||
|
||||
// 检测浏览器环境
|
||||
if (typeof window !== 'undefined') {
|
||||
// 浏览器环境
|
||||
}
|
||||
|
||||
// 检测友乐平台环境
|
||||
if (typeof min_loadJsFile !== 'undefined') {
|
||||
// 友乐平台环境
|
||||
}
|
||||
```
|
||||
|
||||
### 6.3 兼容性要求
|
||||
|
||||
| 特性 | Node.js环境 | 浏览器环境 | 要求 |
|
||||
|-----|------------|-----------|------|
|
||||
| **语法** | ES5 | ES5 | 不使用ES6+特性 |
|
||||
| **模块系统** | require() | `<script>`标签 | 双环境兼容 |
|
||||
| **全局对象** | global | window | 使用条件判断 |
|
||||
| **文件系统** | fs模块 | ❌ 不可用 | 共享代码不能依赖 |
|
||||
| **DOM API** | ❌ 不可用 | document等 | 共享代码不能依赖 |
|
||||
|
||||
---
|
||||
|
||||
## 7. 关键概念总结
|
||||
|
||||
### 7.1 核心术语
|
||||
|
||||
| 术语 | 含义 | 重要性 |
|
||||
|-----|------|-------|
|
||||
| **mod** | 游戏模块实例(如mod_jinxianmahjong) | ⭐⭐⭐⭐⭐ |
|
||||
| **export** | 框架调用子游戏的接口集合 | ⭐⭐⭐⭐⭐ |
|
||||
| **import** | 子游戏调用框架的接口集合 | ⭐⭐⭐⭐⭐ |
|
||||
| **RPC** | 远程过程调用,客户端调用服务端方法 | ⭐⭐⭐⭐⭐ |
|
||||
| **o_room** | 房间对象,管理房间状态和玩家 | ⭐⭐⭐⭐ |
|
||||
| **o_desk** | 游戏桌对象,管理具体游戏状态 | ⭐⭐⭐⭐ |
|
||||
| **roomtype** | 房间类型配置数组(局数、玩法等) | ⭐⭐⭐ |
|
||||
| **pack** | 数据包对象,包含app、route、rpc、data | ⭐⭐⭐⭐⭐ |
|
||||
|
||||
### 7.2 开发要点
|
||||
|
||||
1. **严格遵循三文件架构**:mod.js、export.js、import.js
|
||||
2. **加载顺序不可变更**:export → import → 其他文件
|
||||
3. **使用ES5语法**:确保浏览器和Node.js双环境兼容
|
||||
4. **理解前后端分离**:通过网络通信,不是函数调用
|
||||
5. **掌握RPC机制**:packet.js → app → mod → RPC方法
|
||||
6. **注意调用时机**:deduct_roomcard(第一局结算)、save_grade(大局结束)
|
||||
|
||||
---
|
||||
|
||||
## 8. 相关文档
|
||||
|
||||
- [01-Export接口说明](./01-Export接口说明.md) - 详细的8个必需接口说明
|
||||
- [02-Import接口说明](./02-Import接口说明.md) - 详细的4个框架服务接口
|
||||
- [03-RPC处理机制](./03-RPC处理机制.md) - RPC路由和处理流程
|
||||
- [08-游戏流程概述](../architecture/08-游戏流程概述.md) - 完整游戏流程说明
|
||||
|
||||
---
|
||||
|
||||
## 9. 快速上手
|
||||
|
||||
### 9.1 创建新游戏模块的步骤
|
||||
|
||||
1. **创建游戏目录**:`server/games2/yourgame/`
|
||||
2. **创建mod.js**:定义模块实例和RPC方法
|
||||
3. **创建export.js**:实现8个必需接口
|
||||
4. **创建import.js**:封装4个框架接口
|
||||
5. **创建RPC处理器**:处理客户端请求
|
||||
6. **创建游戏逻辑**:实现具体游戏功能
|
||||
7. **测试验证**:确保接口正确调用
|
||||
|
||||
### 9.2 常见问题
|
||||
|
||||
**Q: export和import的区别?**
|
||||
A: export是框架调用子游戏(框架→游戏),import是子游戏调用框架(游戏→框架)。
|
||||
|
||||
**Q: 为什么必须使用ES5语法?**
|
||||
A: 客户端部署在浏览器中,需要兼容老版本浏览器。ES5语法兼容性最好。
|
||||
|
||||
**Q: 什么时候调用deduct_roomcard?**
|
||||
A: 必须在第一小局结算时调用,不是开战时。
|
||||
|
||||
**Q: o_room和o_desk的关系?**
|
||||
A: o_room是房间对象(框架管理),o_desk是游戏桌对象(子游戏管理),通过`o_room.o_desk`关联。
|
||||
|
||||
---
|
||||
|
||||
**下一步**:阅读[01-Export接口说明](./01-Export接口说明.md)了解8个必需接口的详细实现。
|
||||
1097
codes/games/server/docs/guides/framework/01-Export接口说明.md
Normal file
1097
codes/games/server/docs/guides/framework/01-Export接口说明.md
Normal file
File diff suppressed because it is too large
Load Diff
1160
codes/games/server/docs/guides/framework/02-Import接口说明.md
Normal file
1160
codes/games/server/docs/guides/framework/02-Import接口说明.md
Normal file
File diff suppressed because it is too large
Load Diff
894
codes/games/server/docs/guides/framework/03-RPC处理机制.md
Normal file
894
codes/games/server/docs/guides/framework/03-RPC处理机制.md
Normal file
@@ -0,0 +1,894 @@
|
||||
# RPC处理机制详解
|
||||
|
||||
> **文档目标**:详细说明进贤麻将的RPC请求处理流程,包括RpcHandler、OperationEnumerator、AIRpcHandler的功能和使用方法。
|
||||
|
||||
## 📚 目录
|
||||
|
||||
1. [RPC机制概述](#1-rpc机制概述)
|
||||
2. [RpcHandler - RPC请求处理器](#2-rpchandler---rpc请求处理器)
|
||||
3. [OperationEnumerator - 操作列举器](#3-operationenumerator---操作列举器)
|
||||
4. [AIRpcHandler - AI玩家处理器](#4-airpchandler---ai玩家处理器)
|
||||
5. [RPC处理标准流程](#5-rpc处理标准流程)
|
||||
6. [数据包构建规范](#6-数据包构建规范)
|
||||
7. [实现示例](#7-实现示例)
|
||||
|
||||
---
|
||||
|
||||
## 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方法执行
|
||||
```
|
||||
|
||||
**示例数据包**:
|
||||
```javascript
|
||||
{
|
||||
"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处理模块**,负责:
|
||||
|
||||
1. **接收和解析RPC请求**:提取参数、验证格式
|
||||
2. **玩家身份验证**:调用`check_player`验证玩家
|
||||
3. **委托业务逻辑**:调用GameController或OperationManager
|
||||
4. **构建响应数据**:按"一包多信息"原则构建响应
|
||||
5. **发送响应**:调用发包接口推送给客户端
|
||||
6. **错误处理**:统一的错误处理和日志记录
|
||||
|
||||
### 2.2 主要RPC方法
|
||||
|
||||
进贤麻将实现的核心RPC方法:
|
||||
|
||||
| RPC方法 | 对应操作 | 说明 |
|
||||
|--------|---------|------|
|
||||
| `handlePlayCard` | 出牌 | 玩家打出一张手牌 |
|
||||
| `handleDeclarePeng` | 碰牌 | 玩家碰别人打出的牌 |
|
||||
| `handleDeclareGang` | 杠牌 | 玩家杠牌(明杠/暗杠/加杠) |
|
||||
| `handleDeclareHu` | 胡牌 | 玩家胡牌(自摸/点炮) |
|
||||
| `handlePass` | 过牌 | 玩家放弃吃碰杠胡机会 |
|
||||
| `handleReady` | 准备 | 玩家准备开始游戏 |
|
||||
| `getGameState` | 获取状态 | 获取当前游戏状态 |
|
||||
|
||||
### 2.3 RPC方法标准结构
|
||||
|
||||
每个RPC方法遵循统一的处理流程:
|
||||
|
||||
```javascript
|
||||
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遵循友乐平台的**"一包多信息"设计原则**:
|
||||
|
||||
**原则说明**:
|
||||
- 单个响应包包含**完整的状态更新信息**
|
||||
- 减少网络请求次数
|
||||
- 确保客户端状态同步
|
||||
|
||||
**示例**:出牌响应包包含
|
||||
```javascript
|
||||
{
|
||||
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实现**分层推送**,为不同玩家定制不同的信息:
|
||||
|
||||
```javascript
|
||||
// 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负责**生成玩家可执行的所有操作选项**:
|
||||
|
||||
1. **枚举所有可能操作**:出牌、吃、碰、杠、胡、过
|
||||
2. **计算操作参数**:每个操作需要的牌、组合等
|
||||
3. **生成choiceIndex**:为每个操作分配索引
|
||||
4. **验证操作合法性**:确保操作符合规则
|
||||
|
||||
### 3.2 操作类型
|
||||
|
||||
```javascript
|
||||
var operationTypes = {
|
||||
discard: [], // 出牌操作
|
||||
chi: [], // 吃牌操作
|
||||
peng: [], // 碰牌操作
|
||||
gang: [], // 杠牌操作(明杠、暗杠、加杠)
|
||||
hu: [], // 胡牌操作
|
||||
pass: [] // 过牌操作
|
||||
};
|
||||
```
|
||||
|
||||
### 3.3 核心方法
|
||||
|
||||
#### generateAvailableOperations
|
||||
|
||||
生成完整的可执行操作列表:
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* 为指定玩家生成完整的可执行操作列表
|
||||
* @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
|
||||
|
||||
生成出牌操作:
|
||||
|
||||
```javascript
|
||||
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
|
||||
|
||||
生成碰牌操作:
|
||||
|
||||
```javascript
|
||||
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**是操作选择的索引,用于客户端选择和服务端执行:
|
||||
|
||||
```javascript
|
||||
// 服务端生成操作列表
|
||||
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玩家的自动操作**:
|
||||
|
||||
1. **AI决策**:根据游戏状态做出决策
|
||||
2. **自动操作**:自动出牌、吃碰杠胡
|
||||
3. **延时模拟**:模拟人类玩家的思考时间
|
||||
4. **策略选择**:根据难度选择不同策略
|
||||
|
||||
### 4.2 AI决策流程
|
||||
|
||||
```javascript
|
||||
// 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策略
|
||||
|
||||
```javascript
|
||||
// 简单策略示例
|
||||
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方法实现模板
|
||||
|
||||
```javascript
|
||||
// 在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 响应包标准结构
|
||||
|
||||
```javascript
|
||||
{
|
||||
status: 200, // HTTP状态码风格
|
||||
message: "操作成功", // 消息说明
|
||||
data: { // 业务数据
|
||||
// 具体业务数据
|
||||
},
|
||||
timestamp: 1234567890 // 时间戳
|
||||
}
|
||||
```
|
||||
|
||||
### 6.2 错误响应结构
|
||||
|
||||
```javascript
|
||||
{
|
||||
status: 400, // 错误状态码
|
||||
error: "操作失败", // 错误消息
|
||||
code: "INVALID_OPERATION", // 错误码
|
||||
details: "详细错误信息", // 详细说明
|
||||
timestamp: 1234567890
|
||||
}
|
||||
```
|
||||
|
||||
### 6.3 状态码规范
|
||||
|
||||
| 状态码 | 含义 | 使用场景 |
|
||||
|-------|------|---------|
|
||||
| 200 | 成功 | 操作执行成功 |
|
||||
| 400 | 请求错误 | 参数错误、操作不合法 |
|
||||
| 401 | 未授权 | 玩家验证失败 |
|
||||
| 403 | 禁止操作 | 不是当前玩家的回合 |
|
||||
| 404 | 未找到 | 房间或玩家不存在 |
|
||||
| 500 | 服务器错误 | 内部错误 |
|
||||
|
||||
---
|
||||
|
||||
## 7. 实现示例
|
||||
|
||||
### 7.1 完整的出牌RPC实现
|
||||
|
||||
```javascript
|
||||
// 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 操作枚举示例
|
||||
|
||||
```javascript
|
||||
// 使用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 参数提取
|
||||
|
||||
```javascript
|
||||
// ✅ 统一的参数提取方法
|
||||
_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 错误处理
|
||||
|
||||
```javascript
|
||||
// ✅ 统一的错误响应
|
||||
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 日志记录
|
||||
|
||||
```javascript
|
||||
// ✅ 详细的日志记录
|
||||
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. 下一步
|
||||
|
||||
阅读以下文档继续学习:
|
||||
|
||||
- [04-游戏核心服务](../core/04-游戏核心服务.md) - GameController和OperationManager详解
|
||||
- [05-共享代码模块](../core/05-共享代码模块.md) - 核心算法实现
|
||||
- [08-游戏流程概述](../architecture/08-游戏流程概述.md) - 完整游戏流程
|
||||
|
||||
---
|
||||
|
||||
**相关代码文件**:
|
||||
- `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处理器
|
||||
Reference in New Issue
Block a user