Files
erqiwang_youle/server/docs/development-guide/02-子游戏接入与开发流程.md
T

248 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 02 · 子游戏接入与开发流程
本篇讲**怎么动手**:一个子游戏由哪三个必需文件构成、`export.js` 的 8 个接口与 `import.js` 的 4 个接口各做什么、`makewar`/重连怎么写,以及一次玩家操作从收包到推送的完整数据流。
> 仍以麻将举例,但"三文件架构 + 8/4 接口 + 主动推送"对任意子游戏一致。
---
## 1. 文件结构
### 必需三件套(固定文件名)
```
server/games2/<你的游戏>/
├── mod.js 模块入口:创建模块、按序加载文件、定义 RPC 方法
├── export.js 对平台暴露的 8 个必需接口(+ 可选接口)
└── import.js 对平台服务的 4 个封装接口
```
### 业务分层(复杂游戏推荐)
按职责拆分,**一个文件一个明确职责**,被依赖者先加载(见 04 的"模块职责边界"):
```
├── game/ 对局编排:状态管理、控制器、对平台的适配器
├── rpc/ 收发包:各 RPC handler、广播、响应构建、序列化
├── rules/ 规则解析与规则引擎
├── shared/ 前后端共享代码(改这里,再用脚本同步到前端)
├── utils/ 日志、错误处理等工具
└── tests/ 单元/集成测试
```
> 举例(麻将):`game/RoomAdapter.js`(房间↔对局适配)、`game/GameController.js`(对局编排)、`rpc/BroadcastManager.js`(差异化广播)、`rpc/handlers/*`(各操作处理)。换个玩法,分层思路不变,文件内容不同。
---
## 2. `mod.js` —— 模块入口
`mod.js` 做三件事:**创建模块 → 按依赖顺序加载文件 → 定义 RPC 方法**。
### 2.1 创建模块
```js
// cls_mod.new(模块名, 路由名, 所属应用)
var mod_<你的游戏> = mod_<你的游戏> || cls_mod.new("mod_<你的游戏>", "<你的游戏>", youle_app);
```
路由名即前端包里的 `route`,要与目录/约定一致。
### 2.2 按依赖顺序加载文件
被依赖的先加载;`export.js`/`import.js` 通常在业务类之后加载(它们会用到业务模块):
```js
// 浏览器/友乐:min_loadJsFile 异步链式加载
min_loadJsFile("games2/<你的游戏>/常量与工具.js", function(){
min_loadJsFile("games2/<你的游戏>/数据结构.js", function(){
min_loadJsFile("games2/<你的游戏>/export.js", function(){
min_loadJsFile("games2/<你的游戏>/import.js", function(){
min_loadJsFile("games2/<你的游戏>/业务与rpc.js", function(){
console.log("模块 [" + mod_<你的游戏>.modname + "] 加载完成");
});});});});});
```
> 推荐分层加载顺序:**常量 → 工具 → 数据结构 → export/import → 收发包层 → 业务层 → 规则引擎**。Node 测试侧用 `require`(写在守卫块内),浏览器侧用 `min_loadJsFile`,两套并存但按全局名统一引用(见 01)。
### 2.3 定义 RPC 方法(一操作一函数,委托给 handler)
每个客户端操作对应一个 RPC 方法。**方法本身只做"接住请求并委托"**,真正逻辑放在 `rpc/handlers/*`,保持 `mod.js` 薄:
```js
mod_<你的游戏>.playCard = function(pack) {
return RpcHandler.handlePlayCard(pack); // 委托到收发包层
};
mod_<你的游戏>.declareHu = function(pack) {
return RpcHandler.handleDeclareHu(pack);
};
```
**新增一个前后端接口** = 在 `mod.js` 加一个 `mod_<游戏>.<rpc> = function(pack){...}` + 前端包里 `rpc: "<rpc>"`。**不要**在受限前端接口文件里新增接口(见 04),也**不要**用 `switch(action)` 二次路由。
---
## 3. `export.js` —— 平台调用子游戏的 8 个必需接口
平台在房间生命周期的各节点回调这 8 个接口。用工厂模式创建,文件末尾挂到 `mod.export`:
```js
var cls_<游戏>_export = cls_<游戏>_export || {
new: function() {
var exp = {};
exp.get_needroomcard = function(roomtype, o_game_config) { /* ... */ };
exp.get_asetcount = function(roomtype, o_game_config) { /* ... */ };
exp.get_needroomcard_joinroom = function(roomtype, o_game_config) { return 0; };
exp.makewar = function(o_room, o_game_config) { /* ... */ };
exp.get_deskinfo = function(o_room, seat) { /* ... */ };
exp.get_disbandRoom = function(o_room) { /* ... */ };
exp.player_enter = function(o_room, seat) { /* ... */ };
exp.player_leave = function(o_room, seat) { /* ... */ };
return exp;
}
};
mod_<你的游戏>.export = cls_<游戏>_export.new(); // 文件末尾自动挂载
```
| 接口 | 何时被调 | 返回 | 职责 |
|------|----------|------|------|
| `get_needroomcard` | 创建房间 | Number | 该房型创建需要的房卡数(由你解析 `roomtype`) |
| `get_asetcount` | 创建房间 | Number | 该房型总局数(小局数量) |
| `get_needroomcard_joinroom` | 他人加入 | Number | 加入需要的房卡数(多数玩法返回 0) |
| `makewar` | 开战 | Object | **创建 `o_desk`、初始化对局、返回开战数据包** |
| `get_deskinfo` | 重连/中途加入 | Object | **返回该座位当前完整快照** |
| `get_disbandRoom` | 解散达成 | Object | 返回解散数据包 |
| `player_enter` | 中途进入 | Object? | 处理新玩家加入 |
| `player_leave` | 中途退出 | Object? | 处理玩家离开(如清理其托管/占位) |
> `roomtype` 是一个由子游戏**自定义、自解析**的房型编码(数组或数字串),各位代表局数/人数/扣卡方式/玩法开关等。它的含义只有你的子游戏知道,平台不解释它。
### 3.1 `makewar` 是接入的核心
`makewar` 必须完成三件事,缺一不可:
```js
exp.makewar = function(o_room, o_game_config) {
// 1) 创建子游戏牌桌对象,并与房间建立【双向引用】
if (!o_room.o_desk) { o_room.o_desk = {}; }
if (!o_room.o_desk.data) { o_room.o_desk.data = {}; }
o_room.o_desk.o_room = o_room; // 反向引用
// 2) 创建对局状态,挂到 o_room.o_desk.data.*(房间隔离的落点)
var gameState = createGameState(o_room, o_game_config);
o_room.o_desk.data.gameState = gameState; // 此后所有业务都从这里读对局态
// 3) 返回开战数据包(通常按座位差异化下发)
return {
success: true,
sendtype: 1, // 差异化标记:平台逐座位下发
seatlist: [ { seat: 0, data: {/* 0号位能看到的 */} }, /* ... */ ]
};
};
```
要点:
- **此时不需要知道"谁"坐在每个位置**,只需知道有几个位置有人——身份由平台管理。
- **对局状态挂 `o_room.o_desk.data.*`**,不要放模块级单例/全局(见 04)。
- 开战包对应前端的 `Game_Modify.StartWar`:**改了 `makewar` 下发结构,必须同步改前端 StartWar 解析**(见 03)。
### 3.2 `get_deskinfo` 处理重连
重连/中途加入时,平台带着 `seat` 来要"当前快照"。你要把该座位此刻该看到的一切(手牌仅本人可见、弃牌区、轮到谁、可用操作、倒计时、比分等)从 `o_room.o_desk.data.*` 组装返回。它对应前端的 `Game_Modify.Reconnect`。
> 关键:重连快照应与正常对局推送**复用同一套状态组装逻辑**,避免两份并行实现随规则演化而分叉(数据权威原则,见 04)。
---
## 4. `import.js` —— 子游戏调用平台的 4 个接口
这 4 个接口是对平台 `youle_room.export` 的薄封装,**逻辑由平台实现,你只管在正确时机调用**:
```js
mod_<你的游戏>.import = (function() {
var imp = {};
imp.check_player = function(agentid, gameid, roomcode, seat, playerid, conmode, fromid) {
return mod_<你的游戏>.app.youle_room.export.check_player(
agentid, gameid, roomcode, seat, playerid, conmode, fromid);
};
imp.deduct_roomcard = function(o_room) {
return mod_<你的游戏>.app.youle_room.export.deduct_roomcard(o_room);
};
imp.save_grade = function(o_room, o_gameinfo1, o_gameinfo2, freeroomflag) {
return mod_<你的游戏>.app.youle_room.export.save_grade(
o_room, o_gameinfo1, o_gameinfo2, freeroomflag);
};
imp.finish_gametask = function(agentid, o_player, taskid, finishamount) {
return mod_<你的游戏>.app.youle_room.export.finish_gametask(
agentid, o_player, taskid, finishamount);
};
return imp;
})();
```
| 接口 | 调用时机(关键) | 作用 |
|------|------------------|------|
| `check_player` | **每个 RPC handler 开头** | 校验玩家位置/连接,返回 `o_room` 或 `null` |
| `deduct_roomcard` | **第一小局结算时(仅一次)** | 扣房卡。注意**不是开战时扣** |
| `save_grade` | **大局完全结束、战绩算好后** | 保存战绩;调用后**平台自动释放房间** |
| `finish_gametask` | 完成游戏内任务时(可选) | 上报任务进度 |
> 时机错误是高频 bug:`deduct_roomcard` 放到开战时会导致重复/错误扣卡;`save_grade` 在结算数据没算完就调用会保存到残缺战绩。
---
## 5. 一次玩家操作的完整数据流
把 01 的路由和本篇的接口串起来,一次"出牌"从收包到下发如下(其它操作同构):
```
① 客户端发包 { app:"youle", route:"<游戏>", rpc:"playCard", data:{ roomcode, seat, ... } }
│ 三层路由(见 01)
▼
② mod.<游戏>.playCard(pack) → 委托 RpcHandler.handlePlayCard(pack)
│
▼
③ handler:参数提取 + 校验
var o_room = mod.import.check_player(...); // 校验玩家;失败直接 return
if (!o_room) return;
var o_desk = o_room.o_desk; if (!o_desk) return;
// 状态/轮次/合法性校验(轮到该座?这张牌在手里?)
│
▼
④ 执行业务(委托给权威业务模块,handler 不内联规则)
var result = OperationManager.handleOperation(o_room, params);
│
▼
⑤ 构建响应(按需补充摸牌、可用操作、倒计时、比分、游戏状态等)
var responseData = ResponseBuilder.buildPlayCardResponse(...);
│
▼
⑥ 差异化广播:为每个座位定制其「可见数据」并主动推送
BroadcastManager.broadcastPlayCard(o_room, responseData);
→ 内部对每个座位组 msg,调用 o_room.method.sendpack_toseat(msg, seat)
→ 前端按既有 playCard 逻辑解析(无需区分触发源)
```
每个环节的纪律:
- **③ 校验必做且失败静默 `return`**:不要给客户端回作弊提示,`check_player` 失败、状态不对、轮次不对都直接 `return`。
- **④ 不在 handler 内重造规则**:胡牌检测、听牌分析、合法操作枚举等核心算法调用权威模块,handler 只编排(见 04)。
- **⑥ 主动推送、自带成败**:下发**唯一靠 `sendpack_toseat/toother` 主动推送**;凡有成败语义的推送,`data` 必须带 `success`(见 03)。
- **服务端代替玩家操作(如 AI 托管)走的是同一条 ④⑤⑥ 链路**,只是触发源从"前端请求"变成"服务端决策",对前端透明(见 04)。
---
## 6. 接入自检清单
新接入或改动子游戏时,逐条核对:
- [ ] `mod.js` 用 `cls_mod.new` 创建,路由名与前端 `route` 一致;文件按依赖顺序加载。
- [ ] `export.js` 实现 8 个必需接口并在末尾挂到 `mod.export`。
- [ ] `makewar` 创建了 `o_desk`、建立 `o_room.o_desk` ↔ `o_desk.o_room` 双向引用、对局态挂 `o_desk.data.*`、返回开战包。
- [ ] `get_deskinfo` 能给出与正常对局一致的当前快照(复用状态组装,不另写一份)。
- [ ] `import.js` 4 接口就位;`deduct_roomcard` 在首局结算调、`save_grade` 在终局调。
- [ ] 每个 RPC handler:先 `check_player` → 取 `o_desk` → 校验 → 委托业务 → 主动推送。
- [ ] 改了下发结构,前端 `StartWar`/`Reconnect`/对应操作解析同步检查。
下一篇 [03-数据收发与通信协议](./03-数据收发与通信协议.md) 详解包结构、发包方式、主动推送与 `success` 成败协议。
</content>