降耗②·铺开:按试点尺度精简其余 12 篇编号正文
沿用 client 02 的保守尺度(规范条款/表格/关键代码/标题/链接全保留, 只压缩冗长代码示例、重复解说、演进历史、✅/❌ 成对代码块)逐篇精简 client 01/03/04/05/06、server 01/02/03/04、engineering 01/02/03。 - 12 篇合计 78,171 → 74,726 字符(省 3,445,~4.4%);红线密集篇(client 05、server 04)极保守、几乎不动,符合"不丢规范优先于省字数"。 - 已核验:51 个跨文档链接目标全部存在、3 个锚点全部命中真实标题; server 03 §6、server 04 §8/§10/§11 等被引用小节标题逐字未改;README 未动。 - 每篇均随附规范保留清单逐条自查(并行子代理完成、逐份复核)。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# 01 · 前端架构与运行环境
|
||||
|
||||
本篇建立**全局认知**:前端跑在什么环境、由哪几层构成、新旧两套架构如何并存、文件按什么顺序加载。读懂这一篇,后面的渲染、系统、网络才有坐标。
|
||||
本篇建立**全局认知**:前端跑在什么环境、由哪几层构成、新旧两套架构如何并存、文件按什么顺序加载。
|
||||
|
||||
> 举例以麻将为主,但 `gameabc-framework` 是游戏中立的通用框架,本篇机制对任意子游戏一致。
|
||||
|
||||
@@ -40,7 +40,7 @@ js/01_SubGame/ 子游戏前端
|
||||
|
||||
> `codes/` 内部如何分目录、如何命名文件,均由子游戏自行决定,本套文档不作规定;唯一例外是 `shared/`——它是服务端共享算法的同步副本,前端只读(见 05)。
|
||||
|
||||
依赖方向是**单向**的:`01_SubGame/codes` → `gameabc-framework` → `gameabc.min.js`。框架**不反向依赖**任何子游戏(这正是上一步把 `EventBus` 里的麻将事件剥离出去的原因,见 03/05 的「框架中立」)。
|
||||
依赖方向是**单向**的:`01_SubGame/codes` → `gameabc-framework` → `gameabc.min.js`。框架**不反向依赖**任何子游戏(见 03/05 的「框架中立」)。
|
||||
|
||||
---
|
||||
|
||||
@@ -88,8 +88,6 @@ js/01_SubGame/ 子游戏前端
|
||||
1. **被依赖者先加载**。例如框架 `EventBus.js` 必须在子游戏事件常量之前、事件常量又必须在任何注册这些事件的视图之前;整合所有精灵常量的入口文件必须**最后**加载。
|
||||
2. **新增文件要插对位置**。新增一个常量/组件文件,必须在 `index.html` 里插到其依赖之后、使用者之前,否则运行时拿到 `undefined`。
|
||||
|
||||
> 示例:把玩法专属事件从框架剥到子游戏事件常量文件时,正是把它插在 `EventBus.js`(框架)之后、其使用者(视图组件)之前,引用零改动。
|
||||
|
||||
---
|
||||
|
||||
## 5. codes/ 内部组织
|
||||
@@ -111,4 +109,3 @@ js/01_SubGame/ 子游戏前端
|
||||
- 加载顺序即依赖,新增文件务必插对位置。
|
||||
|
||||
下一篇 [02-渲染与UI组件体系](./02-渲染与UI组件体系.md) 讲:精灵怎么画、资源常量怎么组织、UI 组件怎么写。
|
||||
</content>
|
||||
|
||||
@@ -21,18 +21,13 @@
|
||||
throw new Error('[GameEvents] EventBus 未加载;请检查 index.html 加载顺序');
|
||||
}
|
||||
var E = EventBus.Events;
|
||||
// 通用语义事件
|
||||
E.GAME_STARTED = 'game:started';
|
||||
E.PLAYER_DISCARDED = 'player:discarded';
|
||||
E.SCENE_CHANGED = 'ui:sceneChanged';
|
||||
// 玩法专属事件
|
||||
E.TILES_DEALT = 'mahjong:tilesDealt';
|
||||
E.MELD_FORMED = 'mahjong:meldFormed';
|
||||
E.GAME_STARTED = 'game:started'; // 通用语义事件
|
||||
E.TILES_DEALT = 'mahjong:tilesDealt'; // 玩法专属事件
|
||||
// ...本子游戏用到的全部事件
|
||||
})();
|
||||
```
|
||||
|
||||
> 这正是本仓库的落点:框架 `EventBus` 不预置任何事件常量、保持纯粹中立,所有事件(含通用语义)都由子游戏在其事件常量文件定义——既保证框架可被任意玩法复用,也避免「引用了未定义常量」导致的 `emit(undefined)`(见 05「框架中立」)。
|
||||
> 框架 `EventBus` 不预置任何事件常量、保持纯粹中立,避免「引用了未定义常量」导致的 `emit(undefined)`(见 05「框架中立」)。
|
||||
|
||||
### 用法
|
||||
|
||||
@@ -60,7 +55,7 @@ EventBus.once(EventBus.Events.GAME_FINISHED, fn); // 一次性
|
||||
3) 动画完成回调里只刷新静态界面(refresh)
|
||||
```
|
||||
|
||||
好处:动画是“锦上添花”,**即使不播动画,静态界面也始终正确**(据组件 `this.data` 调 `refresh` 即可还原,见 02 组件范式 / 04 重连)。**动画的开始/结束/出错等生命周期回调里只刷新界面、绝不设置核心数据**;动画期间**绝不修改游戏数据**。这样动画卡住/播错、或开发前期没有动画时,数据、逻辑与界面依然正确、互不影响。
|
||||
即使不播动画,静态界面也始终正确(据组件 `this.data` 调 `refresh` 即可还原,见 02 组件范式 / 04 重连)。**动画的开始/结束/出错等生命周期回调里只刷新界面、绝不设置核心数据**;动画期间**绝不修改游戏数据**。
|
||||
|
||||
### AnimationManager API(框架,通用)
|
||||
|
||||
@@ -117,12 +112,7 @@ AnimationManager.frame(spriteId, opt.startFrame, opt.endFrame, opt.duration, opt
|
||||
|
||||
- **`AudioManager`(框架)**:`playSound(file)`、`playVoice(baseId, sex)`、`playVoiceBySeat(seat, baseId)`、`playMusic/stopMusic`。约定女音 ID = 男音 ID + 偏移;**不含任何牌值/动作映射**。
|
||||
- **音效资源常量(子游戏)**:集中定义音效/语音文件 ID(音效、男/女语音等分类)。新增音效**只改这里**。
|
||||
- **游戏音频管理(子游戏)**:把“出牌/碰/某张牌”等概念映射到音效资源键,再调 `AudioManager`。**不写文件名/数字 ID**。
|
||||
|
||||
```js
|
||||
// 子游戏音频管理:按概念播放,内部映射到资源键并按座位性别选男/女语音
|
||||
// 通用音效直接走框架:AudioManager.playSound(音效资源常量.某音效)
|
||||
```
|
||||
- **游戏音频管理(子游戏)**:把“出牌/碰/某张牌”等概念映射到音效资源键,再调 `AudioManager`。**不写文件名/数字 ID**。通用音效直接走框架 `AudioManager.playSound(音效资源常量.某音效)`。
|
||||
|
||||
**声音文件手动准备**:音频文件由开发者**手动**按平台命名规范放入资源目录(文件名/存放目录/ID→文件名转换规则以平台的资源管理接口规范为准),代码只按 ID 引用、不生成文件。因此**音效资源常量每个 ID 必须注明【类型(背景音乐 / 音效 / 语音)+ 用途 + 时长 + 播放方式(循环 / 单次)】**,让人据注释就能准确准备对应音频。
|
||||
|
||||
@@ -181,4 +171,3 @@ Spine 骨骼资源(`json` / `atlas` / 贴图)由开发者**手动**制作并
|
||||
| Spine | 概念走 Spine 动作配置,回调走 Spine 回调分发器,改资源跑脚本 | 硬编码 spineId/animName;散接回调 |
|
||||
|
||||
下一篇 [04-网络对接与启动编排](./04-网络对接与启动编排.md) 讲:怎么收发包、新旧架构如何对接、一局怎么启动。
|
||||
</content>
|
||||
|
||||
@@ -19,10 +19,9 @@
|
||||
|
||||
### RpcHelper(自动注入平台字段)
|
||||
|
||||
`RpcHelper` 把每个请求自动补齐平台必需字段,业务只传业务数据:
|
||||
`RpcHelper` 把每个请求自动补齐平台必需字段(`agentid / gameid / playerid / roomcode / seat ...`),业务只传业务数据:
|
||||
|
||||
```js
|
||||
// 自动注入:agentid / gameid / playerid / roomcode / seat ...
|
||||
RpcHelper.sendGameRpc(rpc, gameData, options); // route = "room"/子游戏路由
|
||||
```
|
||||
|
||||
@@ -38,13 +37,11 @@ RpcHelper.sendGameRpc(rpc, gameData, options); // route = "room"/子游戏路
|
||||
|
||||
## 2. 收包:统一分发
|
||||
|
||||
服务端的主动推送统一格式 `{ rpc, data }`,前端有**唯一收包入口**,再按 `rpc` 分发到对应处理器。收包分发器是**纯路由表 + 分发器**:
|
||||
服务端的主动推送统一格式 `{ rpc, data }`,前端有**唯一收包入口**,再按 `rpc` 分发到对应处理器。收包分发器是**纯路由表 + 分发器**(一 rpc 一处理器):
|
||||
|
||||
```js
|
||||
// 纯路由表 + 分发器:一 rpc 一处理器
|
||||
var _handlers = {
|
||||
'someRpc': function (d) { return someHandler.handleSomeRpc(d); },
|
||||
'anotherRpc': function (d) { return anotherHandler.handleAnother(d); },
|
||||
'error': function (d) { console.error('[dispatch]', d.message); }
|
||||
// ...每个 rpc 一条
|
||||
};
|
||||
@@ -103,7 +100,6 @@ Game_Modify.StartWar = function (_msg) {
|
||||
前端 RPC 没有“同步返回”,操作结果由服务端**后续主动推送**告知。判成败的唯一权威是推送 `data` 里的 **`success`**:
|
||||
|
||||
```js
|
||||
// 处理器统一写法
|
||||
function handleXxx(data) {
|
||||
if (!data || !data.success) { /* 失败处理 */ return; }
|
||||
// 成功逻辑
|
||||
@@ -168,4 +164,3 @@ function handleXxx(data) {
|
||||
| 业务在 controllers/managers,各司其职 | 处理器交叉重造别人的能力 |
|
||||
|
||||
下一篇 [05-开发规范与红线](./05-开发规范与红线.md) 汇总前端所有工程纪律。
|
||||
</content>
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
- 资源/布局 → 子游戏精灵结构/图片资源/布局常量。
|
||||
- **违例信号**:框架文件里出现 `mahjong:`、牌型、吃碰杠胡等具体玩法字样(注释举例除外,且举例应尽量中立)。
|
||||
|
||||
> 范例:本仓库把框架 `EventBus.js` 内**所有**预定义事件常量清空(只留空容器 `EventBus.Events={}`),全部事件(通用语义 + 玩法专属)改由子游戏的事件常量文件定义;并把通用的精灵事件控制器从子游戏抽到框架 `system/SpriteEventController.js`、剥离其中的玩法标记耦合为「全局 draw 钩子」;把 `UIManager` 的全局 UI(Loading/Message/Confirm)从「主动读子游戏精灵常量」改为「由子游戏 `init(config)` 注入」。新玩法照此扩展,框架零改动。
|
||||
> 范例:框架 `EventBus.js` 预定义事件常量全部清空(只留 `EventBus.Events={}`),事件改由子游戏事件常量文件定义;通用精灵事件控制器抽到框架 `system/SpriteEventController.js` 并把玩法标记耦合改为「全局 draw 钩子」;`UIManager` 的全局 UI(Loading/Message/Confirm)由子游戏 `init(config)` 注入而非主动读子游戏常量。新玩法照此扩展,框架零改动。
|
||||
|
||||
---
|
||||
|
||||
@@ -129,5 +129,4 @@
|
||||
|
||||
---
|
||||
|
||||
至此,从架构与环境(01)、渲染与组件(02)、表现系统(03)、网络与启动(04)到工程红线(05),构成一套完整的前端子游戏开发指导。回到 [README](./README.md) 查看导航与分层模型。
|
||||
</content>
|
||||
至此 01–05 构成一套完整的前端子游戏开发指导。回到 [README](./README.md) 查看导航与分层模型。
|
||||
|
||||
@@ -44,21 +44,8 @@
|
||||
|
||||
`00` 是平台配置项的**结构定义**文件,配置项的**集合与结构由平台/框架固定**。子游戏接入只做一件事:**把已有配置项的值改成本游戏需要的**。
|
||||
|
||||
✅ **允许(改值)**:
|
||||
```js
|
||||
Game_Config.Max.PlayerCnt = 4; // 改人数
|
||||
Game_Config.Share.title = "进贤麻将"; // 改分享标题
|
||||
Game_Config.Info.TextContent = ["你好","谢谢","快点","不要","加油","收到","稍等"]; // 改常用语的【值】
|
||||
Game_Config.Chat.ChatLoc = [[35,517],[1100,315]/* ... */]; // 改聊天气泡坐标【值】
|
||||
```
|
||||
|
||||
❌ **禁止(改定义 / 结构)**:
|
||||
```js
|
||||
Game_Config.Info.MyNewField = 1; // ✗ 新增配置项(增定义)
|
||||
delete Game_Config.Voice; // ✗ 删除配置项(删定义)
|
||||
Game_Config.Max = [4]; // ✗ 改变项的类型/结构
|
||||
// ✗ 重命名平台已定义的字段(如把 PlayerCnt 改成 playerCount)
|
||||
```
|
||||
- ✅ **允许(改值)**:给已有配置项赋值,如 `Game_Config.Max.PlayerCnt = 4;`、`Game_Config.Share.title = "进贤麻将";`、改 `Game_Config.Info.TextContent`(常用语)/`Game_Config.Chat.ChatLoc`(气泡坐标)等数组元素的【值】。
|
||||
- ❌ **禁止(改定义 / 结构)**:新增配置项(`Game_Config.Info.MyNewField = 1;`);删除配置项(`delete Game_Config.Voice;`);改变项的类型/结构(`Game_Config.Max = [4];`);重命名平台已定义字段(如 `PlayerCnt` → `playerCount`)。
|
||||
|
||||
> **为什么**:平台代码(`00_Surface/*`)按**固定字段名**读取 `Game_Config.*`。增 / 删 / 改名定义会让平台读到 `undefined` 或破坏约定,引发线上故障。**配置项的集合与结构是平台契约,只有「值」属于子游戏**。
|
||||
>
|
||||
@@ -75,12 +62,10 @@ Hooks 外置模式下,职责分为三层,单向向下:
|
||||
00_SubGame_Config.js 平台配置项结构 + 子游戏填【配置值】
|
||||
01_SubGame_modify.js 平台接口骨架,每个接口是固定【转发壳】
|
||||
02_SubGame_Input.js 平台接口骨架,每个接口是固定【转发壳】
|
||||
|
|
||||
| 转发壳查 SubGameHooks.X,存在则委托,不存在则平台默认(B类)/空操作(A类)
|
||||
v
|
||||
子游戏入口层(codes/,Hooks 外置新方案核心)
|
||||
SubGameHooks{} 全局对象,接口名与平台接口一一对应;子游戏只填需要的
|
||||
|
|
||||
| SubGameHooks.StartWar = function(_msg){ /* 委托子游戏的开局处理 */ };
|
||||
v
|
||||
子游戏实现层(codes/,已有)
|
||||
@@ -136,12 +121,6 @@ Game_Modify.StartWar = function (_msg) {
|
||||
return SubGameHooks.StartWar(_msg);
|
||||
}
|
||||
};
|
||||
|
||||
Game_Modify.Reconnect = function (_msg) {
|
||||
if (window.SubGameHooks && SubGameHooks.Reconnect) {
|
||||
return SubGameHooks.Reconnect(_msg);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### B 类 — 有平台默认返回值(无 hook 即返回默认)
|
||||
@@ -156,37 +135,15 @@ Game_Modify.getMaxPlayerCount = function (roomtype) {
|
||||
}
|
||||
return (Game_Config.Max && Game_Config.Max.PlayerCnt) || 4;
|
||||
};
|
||||
|
||||
Game_Modify.getLeaveLimit = function (roomtype) {
|
||||
if (window.SubGameHooks && SubGameHooks.getLeaveLimit) {
|
||||
return SubGameHooks.getLeaveLimit(roomtype);
|
||||
}
|
||||
return 10;
|
||||
};
|
||||
|
||||
gameHallImport.isInstalled = function () {
|
||||
if (window.SubGameHooks && SubGameHooks.isInstalled) {
|
||||
return SubGameHooks.isInstalled();
|
||||
}
|
||||
return 1;
|
||||
};
|
||||
```
|
||||
|
||||
`gameHallImport.*` 侧同理(如 `isInstalled` 无 hook 返回 `1`、`getLeaveLimit` 返回 `10`)。
|
||||
|
||||
### C 类 — 配置数据(不是 hook,留配置文件)
|
||||
|
||||
`Game_Config.*`(00 全部)与房型数据(`Game_Modify.Type_1/Type_2/CreateRoomData/game_config`、`Game_Modify.combat/roomDes`)属于**配置数据**而非行为接口,不进 `SubGameHooks`。
|
||||
|
||||
模板在 01 文件顶部设置「配置区」,由子游戏直接填值:
|
||||
|
||||
```js
|
||||
// 01_SubGame_modify.js 顶部:配置区(子游戏填值,与转发壳接口段物理分开)
|
||||
Game_Modify.combat = null; // 战绩配置,子游戏按需赋值
|
||||
Game_Modify.roomDes = ''; // 房间描述
|
||||
Game_Modify.Type_1 = []; // 房型一级选项
|
||||
Game_Modify.Type_2 = []; // 房型二级选项
|
||||
Game_Modify.CreateRoomData = []; // 创建房间数据
|
||||
Game_Modify.game_config = {}; // 游戏配置
|
||||
```
|
||||
模板在 01 文件顶部设置「配置区」,由子游戏直接填值(与转发壳接口段物理分开):`Game_Modify.combat/roomDes/Type_1/Type_2/CreateRoomData/game_config`。
|
||||
|
||||
### D 类 — 模板默认 UI 渲染(可变人数)
|
||||
|
||||
@@ -211,17 +168,6 @@ Game_Modify.game_config = {}; // 游戏配置
|
||||
|
||||
目前仅 `appStart` 存在此冲突。`gameHallImport` 侧所有同名接口均加 `hall` 前缀以区分。
|
||||
|
||||
```js
|
||||
// SubGameHooks.js 中的写法
|
||||
SubGameHooks.appStart = function () {
|
||||
// 对应 Game_Modify.appStart:委托子游戏的启动编排
|
||||
};
|
||||
|
||||
SubGameHooks.hallAppStart = function () {
|
||||
// 对应 gameHallImport.appStart(大厅启动):委托子游戏的大厅初始化
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 接口覆盖要求
|
||||
|
||||
@@ -9,8 +9,7 @@
|
||||
|
||||
### 1.1 单一权威数据源(Single Source of Truth)
|
||||
|
||||
同一业务数据**只能有一个计算/写入的地方**,其余模块只读取、不重算。多处并行计算同一结果,必随
|
||||
规则演化而分叉、互相矛盾。
|
||||
同一业务数据**只能有一个计算/写入的地方**,其余模块只读取、不重算;多处并行计算必随规则演化而分叉、矛盾。
|
||||
|
||||
- 上游**算 + 写 + 校验**,下游**只读 + 消费**。
|
||||
- 需要某数据时,**读权威源**,而不是"顺手再算一遍"。
|
||||
@@ -43,18 +42,14 @@
|
||||
| 编排 vs 算法 | 流程编排(controller) | 纯计算(领域算法) |
|
||||
| 输入 vs 逻辑 | 收发包/参数校验 | 业务处理 |
|
||||
|
||||
> 例(决策 vs 机制):自动操作模块只决定"打哪张/是否碰杠胡过",**不实现**胡牌检测/听牌分析;
|
||||
> 后者是领域算法,决策层只**读取其结果**。这既是关注点分离,也是职责边界。
|
||||
> 例(决策 vs 机制):自动操作模块只决定"打哪张/是否碰杠胡过",**不实现**胡牌检测/听牌分析;后者是领域算法,决策层只**读取其结果**。既是关注点分离,也是职责边界。
|
||||
|
||||
### 1.5 对扩展开放、对修改封闭(OCP)
|
||||
|
||||
新增一种玩法/规则/策略/牌型,应当是**新增一段代码并注册**,而**不是**去改动已经稳定、已被测试
|
||||
覆盖的核心流程。
|
||||
新增一种玩法/规则/策略/牌型,应当是**新增一段代码并注册**,而**不是**去改动已经稳定、已被测试覆盖的核心流程。
|
||||
|
||||
- 手段:**注册表 + 策略**、**管线/中间件**、**工厂**(见 [02 篇](./02-可扩展性与配置化.md))。
|
||||
- 收益:核心不动 → 回归风险小;扩展点清晰 → 新人能照葫芦画瓢。
|
||||
- 例:分级决策框架——核心是"取上下文 → 跑管线 → 选结果",新增高级策略只是**写策略对象 +
|
||||
注册 + 单测**三步,核心零改动。
|
||||
- 例:分级决策框架核心是"取上下文 → 跑管线 → 选结果",新增高级策略只是**写策略对象 + 注册 + 单测**三步,核心零改动。
|
||||
|
||||
### 1.6 配置优先于硬编码
|
||||
|
||||
@@ -69,7 +64,7 @@
|
||||
关键路径上数据缺失,**优先报错或返回 `null`**,把问题暴露在**离根因最近**的地方;不要用
|
||||
`|| 0`、`|| []`、`|| ''`、双源回退把缺失悄悄填平。
|
||||
|
||||
- 兜底会把 bug 藏进"看似正常"的流程里,等到很远的下游才爆发,极难定位。
|
||||
- 兜底会把 bug 藏进"看似正常"的流程里,到很远的下游才爆发,极难定位。
|
||||
- 只有**展示层、纯 UI 兼容、非关键日志**字段,才可在明确边界内用安全默认值。
|
||||
- 详见 [03 篇](./03-数据权威·错误处理·演进.md)。
|
||||
|
||||
@@ -77,7 +72,7 @@
|
||||
|
||||
## 2. 前后端参考分层
|
||||
|
||||
下面是一套**成熟、可直接照搬思路**的分层。层次是稳定的,层内文件如何组织由子游戏自定。
|
||||
下面是一套可直接照搬思路的分层。层次是稳定的,层内文件如何组织由子游戏自定。
|
||||
|
||||
### 2.1 后端分层(自上而下依赖)
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
# 02 · 可扩展性与配置化
|
||||
|
||||
本篇把总则里的 **OCP(对扩展开放)** 和 **配置优先** 落成可直接套用的模式与判据,
|
||||
并给出**避免过度设计**的红线——扩展性是为了"改得动",不是为了炫技。
|
||||
本篇把总则的 **OCP(对扩展开放)** 和 **配置优先** 落成可套用的模式与判据,并给出**避免过度设计**的红线——扩展性是为了"改得动",不是炫技。
|
||||
|
||||
---
|
||||
|
||||
@@ -11,30 +10,25 @@
|
||||
|
||||
### 1.1 注册表 + 策略(Registry + Strategy)— 最常用
|
||||
|
||||
把"一族可替换的行为"抽象成**策略对象**,用**注册表**收集,运行时按上下文选一个执行。
|
||||
新增行为 = 写一个策略 + 注册,**核心零改动**。
|
||||
把"一族可替换的行为"抽象成**策略对象**,用**注册表**收集,运行时按上下文选一个执行。新增行为 = 写一个策略 + 注册,**核心零改动**。
|
||||
|
||||
```
|
||||
Registry(注册表) ── register(strategy) ──▶ [strategyA, strategyB, ...]
|
||||
│
|
||||
Context(只读上下文)──▶ 选择器 select(ctx) ─────────┘──▶ 命中的策略.execute(ctx)
|
||||
Registry ──register(strategy)──▶ [strategyA, strategyB, ...] ──select(ctx)──▶ 命中策略.execute(ctx)
|
||||
```
|
||||
|
||||
- **适用**:AI 决策分级、规则变体、牌型识别族、结算规则族——"同一类事有多种做法"。
|
||||
- **要点**:
|
||||
- 策略只依赖**只读上下文**,不反向修改全局;上下文封装它需要的权威数据。
|
||||
- 有**默认策略兜底**(保证任何输入都有结果),高级策略**按需叠加**。
|
||||
- 策略只依赖**只读上下文**(封装其所需权威数据),不反向修改全局。
|
||||
- 有**默认策略兜底**(任何输入都有结果),高级策略**按需叠加**。
|
||||
- 策略之间**互不知道**对方,新增不影响既有。
|
||||
- **例**:分级决策框架——`Context/Registry/Pipeline + 基础策略 + 占位高级策略`,默认走最低级,
|
||||
高级留扩展点;新增高级策略三步(写策略对象 + register + 单测)零侵入。
|
||||
- **例**:分级决策框架默认走最低级、高级留扩展点;新增高级策略三步(写策略对象 + register + 单测)零侵入。
|
||||
|
||||
### 1.2 管线 / 中间件(Pipeline)
|
||||
|
||||
把一个复杂处理拆成**有序的小步骤**,每步只做一件事、可独立增删。
|
||||
|
||||
```
|
||||
输入 ──▶ [校验] ──▶ [规则A] ──▶ [规则B] ──▶ [收敛] ──▶ 输出
|
||||
每一节点单一职责,可插拔、可测试
|
||||
输入 ──▶ [校验] ──▶ [规则A] ──▶ [规则B] ──▶ [收敛] ──▶ 输出(每节点单一职责、可插拔、可测试)
|
||||
```
|
||||
|
||||
- **适用**:决策流水线、校验链、结算的多阶段计分(比精 → 冲关 → 霸王 → 零和)。
|
||||
@@ -42,7 +36,7 @@ Context(只读上下文)──▶ 选择器 select(ctx) ──────
|
||||
|
||||
### 1.3 工厂(Factory)
|
||||
|
||||
把"根据类型创建/选择实现"的分支收敛到一处,调用方只要"我要一个 X",不关心怎么造。
|
||||
把"根据类型创建/选择实现"的分支收敛到一处,调用方只说"我要一个 X",不关心怎么造。
|
||||
|
||||
- **适用**:胡牌检测按牌型分派、可用操作枚举、不同房型的配置构建。
|
||||
- **要点**:工厂是**唯一**的创建入口,避免 `if(type==...)` 散落各处(那是并行逻辑的温床)。
|
||||
@@ -54,14 +48,13 @@ Context(只读上下文)──▶ 选择器 select(ctx) ──────
|
||||
|
||||
- **适用**:一次数据变化要驱动多个互不相关的表现(动画 + 音效 + 计分板)。
|
||||
- **克制**:
|
||||
- **不要**用事件做"隐式流程控制"——关键流程的因果应显式可追踪,别埋进一堆事件里。
|
||||
- **不要**用事件做"隐式流程控制"——关键流程的因果应显式可追踪。
|
||||
- 事件是**通知**,不是**命令**;订阅方不该反向决定发布方的流程。
|
||||
- 能直接函数调用讲清的因果,就别为"解耦"硬拆成事件。
|
||||
|
||||
### 1.5 统一访问层(Facade over data)
|
||||
|
||||
对"读权威数据"提供一个**统一入口**(如 DataAccessHelper 之类),下游都走它读,不各自摸索
|
||||
数据结构。好处:数据结构演化时只改访问层一处;配合"缺失显式失败",把校验集中。
|
||||
对"读权威数据"提供一个**统一入口**(如 DataAccessHelper),下游都走它读,不各自摸索数据结构。好处:数据结构演化时只改访问层一处;配合"缺失显式失败",把校验集中。
|
||||
|
||||
---
|
||||
|
||||
@@ -89,23 +82,19 @@ Context(只读上下文)──▶ 选择器 select(ctx) ──────
|
||||
|
||||
### 2.3 规则驱动:把玩法开关变成数据
|
||||
|
||||
复杂玩法的差异(人数/局数/扣卡方式/各种开关)应编码成**一份配置**,在入口处**解析一次**,
|
||||
之后全流程**只读消费**:
|
||||
复杂玩法的差异(人数/局数/扣卡方式/各种开关)应编码成**一份配置**,在入口处**解析一次**,之后全流程**只读消费**:
|
||||
|
||||
```
|
||||
房型/配置编码 ──(解析器,解析一次)──▶ 规则对象(结构化、只读)
|
||||
│
|
||||
各模块按需读取规则对象的字段,不再各自解析原始编码、不再散落 if
|
||||
房型/配置编码 ──(解析器,解析一次)──▶ 规则对象(结构化、只读)──▶ 各模块按需读字段,不再各自解析、不再散落 if
|
||||
```
|
||||
|
||||
- **解析只此一处**:避免"多处各自解析同一编码"产生的不一致(这是 SSOT 在配置上的体现)。
|
||||
- **解析只此一处**:避免"多处各自解析同一编码"产生的不一致(SSOT 在配置上的体现)。
|
||||
- **规则对象只读**:下游不回写、不推断缺省;缺字段是**配置或解析的 bug**,应显式暴露。
|
||||
- **新增一个玩法开关** = 编码加一位 + 解析器认它 + 消费点读它,**不改无关逻辑**。
|
||||
|
||||
### 2.4 配置注入优于全局魔法值
|
||||
|
||||
模块需要的配置**从入口传入 / 从权威配置对象读取**,而不是在模块内部埋一堆魔法值或直接摸全局。
|
||||
这让模块**可测试**(注入不同配置跑用例)、**可复用**(换配置即换行为)。
|
||||
模块需要的配置**从入口传入 / 从权威配置对象读取**,而不是在模块内部埋魔法值或直接摸全局。这让模块**可测试**(注入不同配置跑用例)、**可复用**(换配置即换行为)。
|
||||
|
||||
---
|
||||
|
||||
@@ -115,8 +104,8 @@ Context(只读上下文)──▶ 选择器 select(ctx) ──────
|
||||
|
||||
### 3.1 什么时候**不要**加抽象
|
||||
|
||||
- **只有一种实现、且看不到第二种的现实需求** → 不要为"将来也许"预留策略/工厂。先写直接实现。
|
||||
- **YAGNI**:没有当前需求的扩展点就是负债;等第二个变体真的出现,再**重构**成模式(那时你更懂共性)。
|
||||
- **只有一种实现、且看不到第二种的现实需求** → 不要为"将来也许"预留策略/工厂,先写直接实现。
|
||||
- **YAGNI**:没有当前需求的扩展点就是负债;等第二个变体真的出现,再**重构**成模式(那时更懂共性)。
|
||||
- **一次性逻辑** → 不要包装成"通用框架"。通用性从**重复中提炼**,不是凭空设计。
|
||||
|
||||
### 3.2 判断"值不值得抽象"
|
||||
@@ -131,8 +120,7 @@ Context(只读上下文)──▶ 选择器 select(ctx) ──────
|
||||
### 3.3 成熟的判据:三次法则 + 就近演进
|
||||
|
||||
- **三次法则**:同样的东西第 3 次出现时再抽象;第 1、2 次容忍重复。
|
||||
- **就近演进**:先把直接实现写对、写清;当扩展点**自然浮现**(真来了第二个变体),
|
||||
再**收敛**成注册表/策略——此时抽象是"被现实拉出来的",最贴合,也最不会过度。
|
||||
- **就近演进**:先把直接实现写对、写清;当扩展点**自然浮现**(真来了第二个变体),再**收敛**成注册表/策略——此时抽象是"被现实拉出来的",最贴合,也最不会过度。
|
||||
|
||||
> 一句话:**扩展性是留给"已知会变"的地方的;对"稳定不变"的地方,简单直接才是最好的设计。**
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
# 03 · 数据权威 · 错误处理 · 演进
|
||||
|
||||
本篇是**正确性**的地基:数据从哪来、错了怎么暴露、代码怎么随规则长大而不腐化。
|
||||
数据权威部分在项目既有的「数据权威原则」基础上,扩展到**前后端全景**。
|
||||
本篇是**正确性**的地基:数据从哪来、错了怎么暴露、代码怎么随规则长大而不腐化;数据权威部分在既有「数据权威原则」上扩展到**前后端全景**。
|
||||
|
||||
---
|
||||
|
||||
@@ -75,7 +74,7 @@
|
||||
|
||||
## 3. 演进与重构纪律
|
||||
|
||||
代码会随规则长大。让它**长而不腐**的关键,是持续把"并行/重复/临时"收敛掉。
|
||||
代码随规则长大;让它**长而不腐**的关键,是持续把"并行/重复/临时"收敛掉。
|
||||
|
||||
### 3.1 收敛并行实现
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 01 · 服务端环境与框架基础
|
||||
|
||||
本篇建立**全局认知**:子游戏运行在什么环境里、平台框架由哪些对象构成、一个数据包如何被路由到你的代码、一个房间从生到死经历哪些阶段。读懂这一篇,后面的接入与协议才有坐标。
|
||||
本篇建立**全局认知**:子游戏运行在什么环境里、平台框架由哪些对象构成、一个数据包如何被路由到你的代码、一个房间从生到死经历哪些阶段。
|
||||
|
||||
> 举例以麻将类房卡游戏为主,但本篇所有机制对任意子游戏一致。
|
||||
|
||||
@@ -23,14 +23,13 @@
|
||||
2. **`require` 只能在文件开头的守卫块内**:
|
||||
|
||||
```js
|
||||
// ✅ 正确:集中在顶部守卫块;运行时按全局名引用
|
||||
if (typeof require !== 'undefined') {
|
||||
var GameStateManager = require('./dataStructures/GameStateManager.js');
|
||||
}
|
||||
// ... 之后直接用全局名 GameStateManager.xxx() ——浏览器由 mod.js 加载为同名全局
|
||||
// 之后按全局名引用 GameStateManager.xxx()
|
||||
```
|
||||
|
||||
浏览器运行时没有 `require`,任何**无守卫的、函数体内的中途 `require`** 都会抛 `ReferenceError: require is not defined`,导致功能崩溃。跨模块统一**按全局名引用**:Node 由守卫块 `var X = require(...)` 得到,浏览器由 `mod.js` 加载的同名全局解析,二者靠共享全局作用域下的同名 `var` 统一。
|
||||
浏览器运行时没有 `require`,任何**无守卫的、函数体内的中途 `require`** 都会抛 `ReferenceError: require is not defined`。跨模块统一**按全局名引用**:Node 由守卫块 `var X = require(...)` 得到,浏览器由 `mod.js` 加载的同名全局解析,二者靠共享全局作用域下的同名 `var` 统一。
|
||||
|
||||
### 前后端是物理分离的两端
|
||||
|
||||
@@ -202,4 +201,3 @@ var mod_<你的游戏> = mod_<你的游戏> || cls_mod.new("mod_<你的游戏>",
|
||||
- 房间生命周期由 export/import 接缝串起:`makewar` 开局、`get_deskinfo` 重连、`deduct_roomcard` 首局扣卡、`save_grade` 终局保存并自动回收。
|
||||
|
||||
下一篇 [02-子游戏接入与开发流程](./02-子游戏接入与开发流程.md) 讲:这三个文件具体怎么写、8+4 个接口各做什么、一次操作的完整数据流。
|
||||
</content>
|
||||
|
||||
@@ -32,7 +32,7 @@ server/<游戏容器目录>/<你的游戏>/ (容器目录名由接入方
|
||||
└── tests/ 单元/集成测试
|
||||
```
|
||||
|
||||
> 举例(麻将):`game/RoomAdapter.js`(房间↔对局适配)、`game/GameController.js`(对局编排)、`rpc/BroadcastManager.js`(差异化广播)、`rpc/handlers/*`(各操作处理)。换个玩法,分层思路不变,文件内容不同。
|
||||
> 举例(麻将):`game/RoomAdapter.js`(房间↔对局适配)、`game/GameController.js`(对局编排)、`rpc/BroadcastManager.js`(差异化广播)、`rpc/handlers/*`(各操作处理)。换个玩法,分层思路不变。
|
||||
|
||||
---
|
||||
|
||||
@@ -51,17 +51,12 @@ var mod_<你的游戏> = mod_<你的游戏> || cls_mod.new("mod_<你的游戏>",
|
||||
|
||||
### 2.2 按依赖顺序加载文件
|
||||
|
||||
被依赖的先加载;`export.js`/`import.js` 通常在业务类之后加载(它们会用到业务模块):
|
||||
被依赖的先加载;`export.js`/`import.js` 通常在业务类之后加载(它们会用到业务模块)。浏览器/友乐用 `min_loadJsFile` 异步链式加载:
|
||||
|
||||
```js
|
||||
// 浏览器/友乐:min_loadJsFile 异步链式加载
|
||||
min_loadJsFile("<容器目录>/<你的游戏>/常量与工具.js", function(){
|
||||
min_loadJsFile("<容器目录>/<你的游戏>/数据结构.js", function(){
|
||||
min_loadJsFile("<容器目录>/<你的游戏>/export.js", function(){
|
||||
min_loadJsFile("<容器目录>/<你的游戏>/import.js", function(){
|
||||
min_loadJsFile("<容器目录>/<你的游戏>/业务与rpc.js", function(){
|
||||
console.log("模块 [" + mod_<你的游戏>.modname + "] 加载完成");
|
||||
});});});});});
|
||||
min_loadJsFile("<容器目录>/<你的游戏>/export.js", function(){ /* ...嵌套加载 import.js、业务与rpc.js... */ });
|
||||
});
|
||||
```
|
||||
|
||||
> 推荐分层加载顺序:**常量 → 工具 → 数据结构 → export/import → 收发包层 → 业务层 → 规则引擎**。Node 测试侧用 `require`(写在守卫块内),浏览器侧用 `min_loadJsFile`,两套并存但按全局名统一引用(见 01)。
|
||||
@@ -83,7 +78,6 @@ RPC 方法是**前后端交互的服务端入口**。以下是必须遵守的规
|
||||
RPC 方法本身**不写业务逻辑**,只把 `pack` 转交给收发包层的 handler;真正的参数校验、业务编排、广播都在 handler 里:
|
||||
|
||||
```js
|
||||
// mod_<你的游戏> 上挂的 RPC 方法(示例名,可自定)
|
||||
mod_<你的游戏>.playCard = function(pack) {
|
||||
// 就绪守卫:handler 由 min_loadJsFile 异步加载,未就绪时防御性返回(见规则四)
|
||||
if (typeof RpcHandler === 'undefined' || !RpcHandler) {
|
||||
@@ -91,9 +85,6 @@ mod_<你的游戏>.playCard = function(pack) {
|
||||
}
|
||||
return RpcHandler.handlePlayCard(pack); // 委托到收发包层,真正逻辑在这里
|
||||
};
|
||||
mod_<你的游戏>.declareHu = function(pack) {
|
||||
return RpcHandler.handleDeclareHu(pack);
|
||||
};
|
||||
```
|
||||
|
||||
> `RpcHandler`、`handlePlayCard` 这些是**本项目的命名示例**;换成任何风格都行,关键是"薄入口 + 委托"的分层。
|
||||
@@ -184,14 +175,13 @@ mod_<你的游戏>.export = cls_<游戏>_export.new(); // 文件末尾自动
|
||||
|
||||
```js
|
||||
exp.makewar = function(o_room, o_game_config) {
|
||||
// 1) 创建子游戏牌桌对象,并与房间建立【双向引用】
|
||||
// 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; // 此后所有业务都从这里读对局态
|
||||
o_room.o_desk.data.gameState = createGameState(o_room, o_game_config);
|
||||
|
||||
// 3) 返回开战数据包(通常按座位差异化下发)
|
||||
return {
|
||||
@@ -217,26 +207,15 @@ exp.makewar = function(o_room, o_game_config) {
|
||||
|
||||
## 4. `import.js` —— 子游戏调用平台的 4 个接口
|
||||
|
||||
这 4 个接口是对平台 `youle_room.export` 的薄封装,**逻辑由平台实现,你只管在正确时机调用**:
|
||||
这 4 个接口是对平台 `youle_room.export` 的薄封装,**逻辑由平台实现,你只管在正确时机调用**。每个都形如 `imp.<接口> = function(...args){ return mod_<你的游戏>.app.youle_room.export.<接口>(...args); }`,本项目 4 个:
|
||||
|
||||
```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);
|
||||
};
|
||||
imp.check_player = function(agentid, gameid, roomcode, seat, playerid, conmode, fromid) { /* → youle_room.export.check_player(...) */ };
|
||||
imp.deduct_roomcard = function(o_room) { /* → youle_room.export.deduct_roomcard(o_room) */ };
|
||||
imp.save_grade = function(o_room, o_gameinfo1, o_gameinfo2, freeroomflag) { /* → youle_room.export.save_grade(...) */ };
|
||||
imp.finish_gametask = function(agentid, o_player, taskid, finishamount) { /* → youle_room.export.finish_gametask(...) */ };
|
||||
return imp;
|
||||
})();
|
||||
```
|
||||
@@ -306,4 +285,3 @@ mod_<你的游戏>.import = (function() {
|
||||
- [ ] 改了下发结构,前端 `StartWar`/`Reconnect`/对应操作解析同步检查。
|
||||
|
||||
下一篇 [03-数据收发与通信协议](./03-数据收发与通信协议.md) 详解包结构、发包方式、主动推送与 `success` 成败协议。
|
||||
</content>
|
||||
|
||||
@@ -27,20 +27,19 @@
|
||||
|
||||
`route` 决定包被投到**哪个模块**,是前后端必须对齐的字符串。它的权威定义与匹配链如下(均可在代码验证):
|
||||
|
||||
- **服务端:`routename` 的唯一定义点**是 `mod.js` 里创建模块的 `cls_mod.new(模块名, 路由名, 所属应用)` 的**第二个参数**:
|
||||
- **服务端:`routename` 的唯一定义点**是 `mod.js` 里 `cls_mod.new(模块名, 路由名, 所属应用)` 的**第二个参数**:
|
||||
|
||||
```js
|
||||
// server/games2/<你的游戏>/mod.js
|
||||
// server/games2/<你的游戏>/mod.js —— 第二参 "<你的游戏>" 即 routename(route 要用的值)
|
||||
var mod_<你的游戏> = global.mod_<你的游戏>
|
||||
|| cls_mod.new("mod_<你的游戏>", "<你的游戏>", youle_app);
|
||||
// ▲ 第一参 modname ▲ 第二参 routename(就是 route 要用的值)
|
||||
```
|
||||
|
||||
`cls_mod.new` 把第二参存进 `mod.routename`,并把模块 `push` 进 `app.modlist`(`server/class/class.mod.js`)。
|
||||
- **它是你自定义的字符串**:由你起名,只要**在应用内唯一**即可;与目录名、与模块名 `modname`(第一参,用于全局暴露 `global[modname]`、`app[modname]`)都**无强制绑定**——本项目三者恰好都叫 `jinxianmahjong` 只是约定(见 02 §2.1、01 §5)。
|
||||
- **平台按它匹配模块**:收包时 `cls_app.ReceivePack` 用 `pack.route == modlist[i].routename` 找到模块,再 `DoPack` 进第三层按 `rpc` 调方法(`server/class/class.app.js`,见 01 §4)。
|
||||
- **前端发包的 `route` 必须与它逐字一致**:前端把该值固化为常量(本项目 `codes/game/network/RpcSender.js` 里 `var ROUTE_NAME = 'jinxianmahjong'`,经 `Utl.sendData(app, route, rpc, data)` 发出)。**两端字符串不一致 → 平台匹配不到模块,包被静默丢弃**(前端也收不到任何响应)。
|
||||
- **与平台房间模块区分**:平台自带的房间模块 `routename` 是 `"room"`,处理创建/加入/开战(`createRoom`、`self_join_room`、`self_makewar` 等);**你的子游戏自定义 RPC**(`playCard` 等)走**你自己的 `routename`**。两者不要混用:平台流程发 `route:"room"`,玩法操作发 `route:"<你的游戏>"`。
|
||||
- **与平台房间模块区分**:平台自带房间模块 `routename` 是 `"room"`,处理创建/加入/开战(`createRoom`、`self_join_room`、`self_makewar` 等);**你的子游戏自定义 RPC**(`playCard` 等)走**你自己的 `routename`**。两者不要混用:平台流程发 `route:"room"`,玩法操作发 `route:"<你的游戏>"`。
|
||||
|
||||
> 一句话:**`routename` 在服务端 `cls_mod.new` 第二参定义(自定、应用内唯一),前端发包 `route` 必须与它逐字相同,平台据此把包投到你的模块**;改名要两端同步改。
|
||||
|
||||
@@ -65,9 +64,7 @@
|
||||
```js
|
||||
XxxHandler.handlePlayCard = function(pack) { // 由 mod_<游戏>.playCard 委托进来
|
||||
try {
|
||||
// 1) 提取并校验参数:列出必填字段 + 数值型字段的类型要求
|
||||
// (本项目用统一工具 ValidationHelper.extractAndValidateParams 做这件事)
|
||||
// 等价于:agentid/playerid/gameid/roomcode/seat 必填,playerid/roomcode/seat 需为数值
|
||||
// 1) 提取并校验参数:必填字段 + 数值型字段类型(本项目用 ValidationHelper.extractAndValidateParams)
|
||||
var params = extractAndValidateParams(
|
||||
pack,
|
||||
['agentid', 'playerid', 'gameid', 'roomcode', 'seat', 'cardUniqueId'],
|
||||
@@ -85,15 +82,10 @@ XxxHandler.handlePlayCard = function(pack) { // 由 mod_<游戏>.playCard 委
|
||||
var o_desk = o_room.o_desk;
|
||||
if (!o_desk) return { success: false, error: '游戏桌不存在' };
|
||||
|
||||
// 4) 调试记录(若框架提供)——便于复盘
|
||||
if (o_desk.debug && o_desk.debug.save_receivepack) {
|
||||
o_desk.debug.save_receivepack(pack, p.seat, p.playerid);
|
||||
}
|
||||
|
||||
// 4) 调试记录(若框架提供 o_desk.debug.save_receivepack)——便于复盘
|
||||
// 5) 业务校验 + 执行业务:委托权威模块(handler 不内联规则,见 04)
|
||||
// 如:OperationExecutor.executePlayCard(o_room, {...})
|
||||
// 6) 构建响应 + 【主动推送】:ResponseBuilder 组包 → BroadcastManager 逐座位 sendpack_toseat
|
||||
// 推送 data 自带 success(见 §5);return 不是下发通道(见 §4)
|
||||
// 6) 构建响应 + 【主动推送】:组包 → 逐座位 sendpack_toseat;推送 data 自带 success(见 §5);
|
||||
// return 不是下发通道(见 §4)
|
||||
} catch (e) { /* 记录日志;必要时给该座推送失败包 */ }
|
||||
};
|
||||
```
|
||||
@@ -126,11 +118,8 @@ for (var seat = 0; seat < o_room.seatlist.length; seat++) {
|
||||
app: "youle", route: "<游戏>", rpc: "playCard",
|
||||
data: deepCopy(baseData) // 公共信息
|
||||
};
|
||||
if (seat === actionSeat) {
|
||||
msg.data.handCards = hands[seat]; // 仅本人可见手牌
|
||||
} else {
|
||||
msg.data.handCards = []; // 他人看不到
|
||||
}
|
||||
// 敏感信息只发本人:本人给真实手牌,他人给 []
|
||||
msg.data.handCards = (seat === actionSeat) ? hands[seat] : [];
|
||||
o_room.method.sendpack_toseat(msg, seat);
|
||||
}
|
||||
```
|
||||
@@ -163,23 +152,13 @@ for (var seat = 0; seat < o_room.seatlist.length; seat++) {
|
||||
### 正反例
|
||||
|
||||
```js
|
||||
// ❌ 错误:推送只带 status,前端读 data.success 恒 undefined → 误判失败
|
||||
o_room.method.sendpack_toseat({
|
||||
app:"youle", route:"<游戏>", rpc:"setHostingState",
|
||||
data: { status: 200, hosting: true } // 少了 success
|
||||
}, seat);
|
||||
|
||||
// ✅ 正确:成败语义放 success,status 仅作细分
|
||||
o_room.method.sendpack_toseat({
|
||||
app:"youle", route:"<游戏>", rpc:"setHostingState",
|
||||
// ❌ 推送只带 status、少了 success → 前端读 data.success 恒 undefined,误判失败
|
||||
data: { status: 200, hosting: true }
|
||||
// ✅ 成败语义放 success,status 仅作细分
|
||||
data: { success: true, status: 200, hosting: true }
|
||||
}, seat);
|
||||
```
|
||||
|
||||
```js
|
||||
// 前端:只认 success
|
||||
// 前端:只认 success,status/code 仅用于展示或日志
|
||||
if (!data.success) { /* 失败处理 */ return; }
|
||||
// data.status / data.code 仅用于展示或日志细分
|
||||
```
|
||||
|
||||
> 典型事故:托管状态推送 `{ status: 200, ... }` 不带 `success`,前端读 `data.success` 恒为 `undefined`,于是"已进入托管却报失败"。根因就是违反了本协议第 3 条。
|
||||
@@ -274,4 +253,3 @@ if (!data.success) { /* 失败处理 */ return; }
|
||||
- 改下发结构必同步核对前端 `StartWar`/`Reconnect`/对应 `rpc` 解析。
|
||||
|
||||
下一篇 [04-开发规范与红线](./04-开发规范与红线.md) 汇总所有必须遵守的工程纪律。
|
||||
</content>
|
||||
|
||||
@@ -38,14 +38,12 @@
|
||||
- 跨模块运行时**按全局名引用**:Node 由守卫块 `var X = require(...)` 得到,浏览器由 `mod.js` 加载的同名全局解析。
|
||||
|
||||
```js
|
||||
// ✅ 正确
|
||||
// ✅ 标准形态:require 仅在顶部守卫块,函数体内直接用全局名
|
||||
if (typeof require !== 'undefined') {
|
||||
var GameStateManager = require('./dataStructures/GameStateManager.js');
|
||||
}
|
||||
function foo() { GameStateManager.doSomething(); } // 直接用全局名
|
||||
|
||||
// ❌ 错误:函数体内中途 require —— 浏览器崩溃
|
||||
function bar() { var GSM = require('./dataStructures/GameStateManager.js'); }
|
||||
function foo() { GameStateManager.doSomething(); }
|
||||
// ❌ 函数体内中途 require → 浏览器崩溃:function bar(){ var GSM = require('...'); }
|
||||
```
|
||||
|
||||
### 双运行时全局暴露陷阱
|
||||
@@ -179,4 +177,3 @@ function bar() { var GSM = require('./dataStructures/GameStateManager.js'); }
|
||||
---
|
||||
|
||||
至此,从框架运作(01)、子游戏接入(02)、收发协议(03)到工程红线(04),构成一套完整的服务端子游戏开发指导。回到 [README](./README.md) 查看导航与一页纸模型。
|
||||
</content>
|
||||
|
||||
Reference in New Issue
Block a user