diff --git a/docs/client/development-guide/01-前端架构与运行环境.md b/docs/client/development-guide/01-前端架构与运行环境.md index b018458..2ee431b 100644 --- a/docs/client/development-guide/01-前端架构与运行环境.md +++ b/docs/client/development-guide/01-前端架构与运行环境.md @@ -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 组件怎么写。 - diff --git a/docs/client/development-guide/03-事件·动画·音频·Spine.md b/docs/client/development-guide/03-事件·动画·音频·Spine.md index d6945b0..1adb5d8 100644 --- a/docs/client/development-guide/03-事件·动画·音频·Spine.md +++ b/docs/client/development-guide/03-事件·动画·音频·Spine.md @@ -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) 讲:怎么收发包、新旧架构如何对接、一局怎么启动。 - diff --git a/docs/client/development-guide/04-网络对接与启动编排.md b/docs/client/development-guide/04-网络对接与启动编排.md index 51007c0..dd42571 100644 --- a/docs/client/development-guide/04-网络对接与启动编排.md +++ b/docs/client/development-guide/04-网络对接与启动编排.md @@ -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) 汇总前端所有工程纪律。 - diff --git a/docs/client/development-guide/05-开发规范与红线.md b/docs/client/development-guide/05-开发规范与红线.md index f1a823c..b5cacbe 100644 --- a/docs/client/development-guide/05-开发规范与红线.md +++ b/docs/client/development-guide/05-开发规范与红线.md @@ -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) 查看导航与分层模型。 - +至此 01–05 构成一套完整的前端子游戏开发指导。回到 [README](./README.md) 查看导航与分层模型。 diff --git a/docs/client/development-guide/06-子游戏接入模式与Hooks外置.md b/docs/client/development-guide/06-子游戏接入模式与Hooks外置.md index 3b9a43e..7f6e71c 100644 --- a/docs/client/development-guide/06-子游戏接入模式与Hooks外置.md +++ b/docs/client/development-guide/06-子游戏接入模式与Hooks外置.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. 接口覆盖要求 diff --git a/docs/games/engineering/01-架构总则与分层.md b/docs/games/engineering/01-架构总则与分层.md index 1465557..2291d12 100644 --- a/docs/games/engineering/01-架构总则与分层.md +++ b/docs/games/engineering/01-架构总则与分层.md @@ -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 后端分层(自上而下依赖) diff --git a/docs/games/engineering/02-可扩展性与配置化.md b/docs/games/engineering/02-可扩展性与配置化.md index 17ca8d0..961be1f 100644 --- a/docs/games/engineering/02-可扩展性与配置化.md +++ b/docs/games/engineering/02-可扩展性与配置化.md @@ -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 次容忍重复。 -- **就近演进**:先把直接实现写对、写清;当扩展点**自然浮现**(真来了第二个变体), - 再**收敛**成注册表/策略——此时抽象是"被现实拉出来的",最贴合,也最不会过度。 +- **就近演进**:先把直接实现写对、写清;当扩展点**自然浮现**(真来了第二个变体),再**收敛**成注册表/策略——此时抽象是"被现实拉出来的",最贴合,也最不会过度。 > 一句话:**扩展性是留给"已知会变"的地方的;对"稳定不变"的地方,简单直接才是最好的设计。** diff --git a/docs/games/engineering/03-数据权威·错误处理·演进.md b/docs/games/engineering/03-数据权威·错误处理·演进.md index 5980fa3..a3ecc6b 100644 --- a/docs/games/engineering/03-数据权威·错误处理·演进.md +++ b/docs/games/engineering/03-数据权威·错误处理·演进.md @@ -1,7 +1,6 @@ # 03 · 数据权威 · 错误处理 · 演进 -本篇是**正确性**的地基:数据从哪来、错了怎么暴露、代码怎么随规则长大而不腐化。 -数据权威部分在项目既有的「数据权威原则」基础上,扩展到**前后端全景**。 +本篇是**正确性**的地基:数据从哪来、错了怎么暴露、代码怎么随规则长大而不腐化;数据权威部分在既有「数据权威原则」上扩展到**前后端全景**。 --- @@ -75,7 +74,7 @@ ## 3. 演进与重构纪律 -代码会随规则长大。让它**长而不腐**的关键,是持续把"并行/重复/临时"收敛掉。 +代码随规则长大;让它**长而不腐**的关键,是持续把"并行/重复/临时"收敛掉。 ### 3.1 收敛并行实现 diff --git a/docs/server/development-guide/01-服务端环境与框架基础.md b/docs/server/development-guide/01-服务端环境与框架基础.md index bad9e92..07d49e8 100644 --- a/docs/server/development-guide/01-服务端环境与框架基础.md +++ b/docs/server/development-guide/01-服务端环境与框架基础.md @@ -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 个接口各做什么、一次操作的完整数据流。 - diff --git a/docs/server/development-guide/02-子游戏接入与开发流程.md b/docs/server/development-guide/02-子游戏接入与开发流程.md index 5c58a22..cdae98e 100644 --- a/docs/server/development-guide/02-子游戏接入与开发流程.md +++ b/docs/server/development-guide/02-子游戏接入与开发流程.md @@ -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` 成败协议。 - diff --git a/docs/server/development-guide/03-数据收发与通信协议.md b/docs/server/development-guide/03-数据收发与通信协议.md index 0c93f1e..5e5dbc4 100644 --- a/docs/server/development-guide/03-数据收发与通信协议.md +++ b/docs/server/development-guide/03-数据收发与通信协议.md @@ -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); +// ❌ 推送只带 status、少了 success → 前端读 data.success 恒 undefined,误判失败 +data: { status: 200, hosting: true } +// ✅ 成败语义放 success,status 仅作细分 +data: { success: true, status: 200, hosting: true } -// ✅ 正确:成败语义放 success,status 仅作细分 -o_room.method.sendpack_toseat({ - app:"youle", route:"<游戏>", rpc:"setHostingState", - 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) 汇总所有必须遵守的工程纪律。 - diff --git a/docs/server/development-guide/04-开发规范与红线.md b/docs/server/development-guide/04-开发规范与红线.md index 62fde92..c819a6f 100644 --- a/docs/server/development-guide/04-开发规范与红线.md +++ b/docs/server/development-guide/04-开发规范与红线.md @@ -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) 查看导航与一页纸模型。 -