180 lines
13 KiB
Markdown
180 lines
13 KiB
Markdown
# 04 · 开发规范与红线
|
||
|
||
本篇汇总所有**必须遵守**的工程纪律。每一条都对应过真实事故或返工,是代码审查的清单来源。改任何代码前,按相关条目自检。
|
||
|
||
> **命名说明**:本篇提到的模块/类/文件名(如 `GameStateManager`、`RoomAdapter` 等)多为**示例、可自定**,红线约束的是**做法**而非具体名字;只有平台接缝上的名字(包字段、`export`/`import` 钩子、平台 API、`data.success`)是契约。详见 [README「命名约定」](./README.md)。
|
||
|
||
---
|
||
|
||
## 1. 可编辑范围
|
||
|
||
### 服务端
|
||
|
||
- **唯一可编辑目录**:子游戏自己的目录 `server/<游戏容器目录>/<你的游戏>/`。容器目录名由接入方自定,**非框架强制**,换任何不与框架冲突的目录皆可(框架按 `modename` 而非目录名定位子游戏,见 01)。
|
||
- **禁改**:`server/` 其余一切均为平台代码——`server/class/`、`server/config/`、`server/server/`、`server/youle/`、`server/update/`、`server/packet.js`、`server/applist.js`、`server/minhttp.js` 等。
|
||
- **唯一例外**:接入新游戏时需在平台文件 `server/youle/app.js` 里加一行 `min_loadJsFile("<容器目录>/<你的游戏>/mod.js", ...)` 完成注册加载——这是必须触碰平台代码的**唯一游戏接入点**,除此之外不得改动 `server/youle/`。
|
||
|
||
### 前端
|
||
|
||
- **平台代码禁改**:`client/js/00_Surface/` 下全部文件。
|
||
- **受限接口文件**(不可新增对外接口,尽量不改,确需则只在现有接口内部加逻辑):
|
||
`client/js/01_SubGame/00_SubGame_Config.js`、`01_SubGame_modify.js`、`02_SubGame_Input.js`。
|
||
- **新增前后端交互**一律走 `mod.js` 的 `mod_<游戏>.<rpc>` 机制,**不在受限文件里新增接口**。
|
||
|
||
---
|
||
|
||
## 2. 语言标准:严格 ES5
|
||
|
||
- 全部 JS **必须严格符合 ES5**:用 `var`/`function`、字符串用 `+` 拼接。
|
||
- **禁止** ES6+:`let`/`const`、箭头函数、模板字符串、解构、默认参数、展开运算符、`class`、`for...of`、`Promise`/`async`/`await`、对象简写等。
|
||
- 原因:线上浏览器/友乐运行时不保证 ES6+ 支持。
|
||
|
||
---
|
||
|
||
## 3. 模块加载:require 双运行时守卫
|
||
|
||
- **所有 `require` 必须写在文件开头的 `if (typeof require !== 'undefined') { ... }` 守卫块内。**
|
||
- **禁止函数体内 / 中途 `require`**(含"延迟 require 规避循环依赖"的写法)。浏览器/友乐运行时无 `require`,无守卫的 `require` 会抛 `ReferenceError: require is not defined`。
|
||
- 跨模块运行时**按全局名引用**: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('...'); }
|
||
```
|
||
|
||
### 双运行时全局暴露陷阱
|
||
|
||
"构造函数 + 单例实例"式模块,`module.exports` 之后必须**无条件**把全局名暴露出去(**不要放进 `else` 分支**),否则线上浏览器拿不到该全局,报 `xxx is not a function`,而 Node 测试测不出来。
|
||
|
||
---
|
||
|
||
## 4. 数据权威原则
|
||
|
||
- **数据源唯一**:同一业务数据只有一个权威来源;上游计算/写入/校验,下游只读取/消费,**不重复推断、不重复拼装**。
|
||
- **禁止猜测兜底**:不用默认值掩盖缺失数据。**关键权威字段缺失要显式报错或返回 `null`**,由上层决定是否终止,**禁止** `|| 0`、`|| []`、`|| ''`、双源回退等掩盖。
|
||
- **下游不修补**:下游不得为"让流程跑起来"而补写/重算上游本该提供的数据;发现下游在修补,应把责任前移到权威来源。
|
||
- **边界**:只有展示层、纯 UI 兼容、非关键日志字段,才可在明确边界内用安全默认值。
|
||
- **下发面要发全**:因前端以服务端为权威、不自算权威结果,服务端**每个下发包必须携带前端界面所需的全部核心数据**(界面要显示、或前端 set-refresh 要用的字段都发全);漏发 = 前端缺数据,**修在服务端发包处、前端不补洞**(详见 [03 §1.2](./03-数据收发与通信协议.md))。
|
||
|
||
> 审查信号:看到 `|| []`、`|| 0`、`|| ''`、三元默认、双源字段,先判断它是不是**权威字段**(影响发牌/庄家/手牌/规则/结算/重连/状态流转)。是 → 改成权威读取;只是展示/日志 → 才可保留默认。
|
||
|
||
---
|
||
|
||
## 5. 模块职责边界
|
||
|
||
- **一个职能只在一个模块实现**,其他模块**只调用、不重造**。
|
||
- 需要某能力时,调用对应**权威模块**;权威能力不满足,应在权威模块内扩展,而非在调用方旁路重写。
|
||
- **禁止**:在 A 模块内联 B 模块核心算法的"简化版";同一职能在两处各有一份实现并行演化。
|
||
- 后果:交叉实现产生双份并行逻辑,必随规则演化分叉、互相矛盾——这是数据权威原则在"代码职责"维度的同一条红线。
|
||
|
||
### "自动操作"模块只做决策,不重造规则
|
||
|
||
服务端自动替玩家操作的模块(如 AI 托管)**只负责决策**(出哪张、是否碰/杠/吃/胡/过),**核心算法一律复用权威实现**:胡牌检测、听牌/做牌分析、合法操作枚举、牌型判定等都已在权威模块实现,决策模块只能**读取/调用其结果**,禁止自行重算。
|
||
|
||
- 判别:回答"能否胡/听什么/有哪些合法操作"——属核心算法,必须复用;回答"在合法选项中选哪个更好"——才属决策。
|
||
|
||
---
|
||
|
||
## 6. 房间隔离
|
||
|
||
服务端同进程并发多张牌桌(多个 `o_room`)。**任何随对局变化的状态都必须以房间为单位隔离**:
|
||
|
||
- **状态挂房间**:对局数据存 `o_room.o_desk.data.*`,由 `o_room` 携带。**禁止存在模块级单例 / 全局变量 / 静态字段**。
|
||
- **不得只用 `seat` 作 key**:座位号仅 0–3,多房并发必碰撞。缓存、定时器表、决策状态、计数器等**必须用 `房间 + seat` 复合维度**(或每房一份实例)。
|
||
- **定时器随房生命周期**:所有 `setTimeout`/`setInterval` 必须能按房间定位与清理;小局结束、解散房间、开新局时,**清理该房名下全部定时器与残留状态**,禁止泄漏到下一局或别房。
|
||
- 覆盖范围:自动托管/决策表、算法中间态缓存(听牌/胡牌检测)、超时与回合定时器、待响应队列等,任一项跨房共享都会导致 A 房误改 B 房。
|
||
|
||
> 一句话:一切随对局变化的东西都属于某个房间,必须能用 `o_room` 唯一定位、隔离与回收。
|
||
|
||
---
|
||
|
||
## 7. 服务端自动操作复用真人链路
|
||
|
||
服务端代替玩家执行的操作(AI 托管等):
|
||
|
||
- **必须复用真人手动操作的同一套数据包链路**:走与真人相同的服务端处理入口与广播下发路径,产生的包结构与真人**完全一致**。
|
||
- **对前端透明**:前端只按既有"玩家操作"逻辑解析表现,**禁止为"自动操作"单开一套接收/解析/表现分支**,无需区分触发源。
|
||
- **唯一区别在触发源**:由"前端请求"变为"服务端决策",其后数据组织、下发协议、广播路径不变。
|
||
|
||
---
|
||
|
||
## 8. Shared 文件同步流程
|
||
|
||
### `shared/` 是什么:子游戏自己的游戏逻辑,与平台无关
|
||
|
||
- **`shared/` 属于子游戏,不是平台代码**:它存放**本玩法自身**的规则、算法、常量、数据结构(本项目如胡牌检测 `WinDetectionFactory`、精牌 `JingAlgorithm`、计分 `ScoreCalculation`、牌型/比精,以及 `constants/` 下各类常量)。平台**既不提供也不感知** `shared/`——它完全落在子游戏可编辑范围内,是你玩法逻辑的一部分。
|
||
- **为什么要"共享"**:**同一份玩法逻辑需要在两处运行**——服务端(Node)做**权威裁定**,前端(浏览器)做**即时表现/预校验**。若两端各写一份,必随规则演化而分叉、算出不一致结果。因此把这类"前后端必须完全一致"的纯逻辑收进 `shared/`,两端跑**逐字相同**的代码。
|
||
- **它与"数据权威"的关系**:`shared/` 是**逻辑同源**(两端同一套算法),不改变**数据权威**(结果仍以服务端为准,见 §4);前端算出的只是表现/预判,最终以服务端 `shared/` 算的为准。
|
||
|
||
### 同步流程:服务端权威源 → 前端只读副本
|
||
|
||
前后端各持一份 `shared/`,**必须**经同步流程单向更新,禁止直接编辑前端副本:
|
||
|
||
| 角色 | 路径 |
|
||
|------|------|
|
||
| 权威源(服务端,唯一可改) | `server/<游戏容器目录>/<你的游戏>/shared/` |
|
||
| 前端副本(只读,脚本生成) | `client/js/01_SubGame/codes/shared/` |
|
||
|
||
1. **只改服务端** `shared/` 下文件。
|
||
2. 改完运行根目录的同步脚本(如 `sync-shared.ps1`)同步到前端。
|
||
3. **禁止直接编辑前端 `codes/shared/`**(同步脚本会覆盖)。
|
||
|
||
> 判别一段逻辑该不该进 `shared/`:**它是不是"前后端必须算出完全相同结果"的玩法逻辑**?是(胡牌/听牌/比精/牌型/计分/规则常量)→ 放 `shared/`;只是服务端流程编排或只是前端表现 → 各自放自己那侧,不进 `shared/`。
|
||
|
||
---
|
||
|
||
## 9. 硬编码常量准则
|
||
|
||
**需提取为常量**(满足任一):会随规则变化、跨模块复用、裸值无法自解释。典型:分数数值、状态/类型字符串标识、配置阈值、跨模块共享的 key 名。常量按语义归入 `shared/constants/` 下对应文件(分数类、规则配置类、类型枚举类)。
|
||
|
||
**不必提取**(避免过度设计):单函数内一次性临时值(循环初值 `0`、空数组 `[]`)、框架约定固定串(`require` 路径)、自解释布尔开关、纯展示标点文字。
|
||
|
||
---
|
||
|
||
## 10. 测试纪律
|
||
|
||
- **测试唯一目的是验证业务正确性**。失败是有价值的信号,第一反应是**定位根因**,不是"让测试变绿"。
|
||
- **禁止任何掩盖手段**:skip/条件 return、软化断言、放宽阈值、try-catch 吞异常、把硬断言改成"存在才校验"。
|
||
- **每个失败必须裁定归属**(基于证据):【业务代码缺陷】还是【测试脚本缺陷】,二选一。手段:临时诊断探针、打印中间态、最小复现;拿证据再下结论,**禁止凭猜测定性**(诊断日志定位后清理)。
|
||
- **按归属修复**:业务缺陷 → 修业务、单独提交、写明根因;脚本缺陷 → 修置场/时序,断言保持硬断言。优先级:**确定性构造场景 > 有界重试采样 > 条件跳过**。
|
||
- **flaky 同样是缺陷**:要么业务竞态、要么测试非确定性置场,须根治;验收标准是**连跑 ≥5 次全绿**。
|
||
|
||
### 测试不绑架正式代码
|
||
|
||
- **正式核心代码禁止存在专为测试服务的逻辑**,更禁止为"让测试通过/兼容测试"而新增或修改正式代码。
|
||
- 方向永远是**测试适配正式代码的生产契约**,而非正式代码迁就测试的简化输入/不规范 stub。测试要构造符合生产契约的输入与 stub。
|
||
- 违例信号:正式代码注释出现"仅兼容测试 stub""如单元测试传 X"之类,即是违例。
|
||
|
||
---
|
||
|
||
## 11. Git 提交规范
|
||
|
||
- **及时自动提交**:每完成一个可独立成立的逻辑改动(一个修复/功能/重构/一批测试)就**立即提交**,不堆积工作区。
|
||
- **无需逐次询问**:完成阶段性改动后主动提交(`push` 按需)。
|
||
- **提交信息用中文**,简明说明"做了什么/为什么",一次提交聚焦一件事,结尾保留 `Co-Authored-By` 署名行。
|
||
|
||
---
|
||
|
||
## 12. 审查速查表
|
||
|
||
| 维度 | 红线 |
|
||
|------|------|
|
||
| 范围 | 只改子游戏目录 `<容器目录>/<游戏>/` 与允许的前端范围 |
|
||
| 语言 | 纯 ES5;`require` 仅在顶部守卫块 |
|
||
| 成败 | 只认 `data.success`,推送必自带,禁 `status` 兜底 |
|
||
| 数据 | 权威唯一、缺失报错/返回 null、禁兜底、下游不修补;下发包发全前端界面所需核心数据 |
|
||
| 职责 | 一职能一模块,调用不重造 |
|
||
| 隔离 | 状态挂 `o_desk.data.*`,禁全局;`房间+seat` 作 key;定时器随房清理 |
|
||
| 自动操作 | 复用真人链路,对前端透明 |
|
||
| Shared | 只改服务端 `shared/`,跑同步脚本 |
|
||
| 测试 | 失败裁定归属、禁掩盖;正式代码不迁就测试 |
|
||
| Git | 一事一提交、中文信息、及时提交 |
|
||
|
||
---
|
||
|
||
至此,从框架运作(01)、子游戏接入(02)、收发协议(03)到工程红线(04),构成一套完整的服务端子游戏开发指导。回到 [README](./README.md) 查看导航与一页纸模型。
|