Files
youle_framework/docs/server/development-guide/04-开发规范与红线.md
T

12 KiB
Raw Blame History

04 · 开发规范与红线

本篇汇总所有必须遵守的工程纪律。每一条都对应过真实事故或返工,是代码审查的清单来源。改任何代码前,按相关条目自检。

命名说明:本篇提到的模块/类/文件名(如 GameStateManager、RoomAdapter 等)多为示例、可自定,红线约束的是做法而非具体名字;只有平台接缝上的名字(包字段、export/import 钩子、平台 API、data.success)是契约。详见 README「命名约定」。


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 加载的同名全局解析。
// ✅ 正确
if (typeof require !== 'undefined') {
    var GameStateManager = require('./dataStructures/GameStateManager.js');
}
function foo() { GameStateManager.doSomething(); }   // 直接用全局名

// ❌ 错误:函数体内中途 require —— 浏览器崩溃
function bar() { var GSM = require('./dataStructures/GameStateManager.js'); }

双运行时全局暴露陷阱

"构造函数 + 单例实例"式模块,module.exports 之后必须无条件把全局名暴露出去(不要放进 else 分支),否则线上浏览器拿不到该全局,报 xxx is not a function,而 Node 测试测不出来。


4. 数据权威原则

  • 数据源唯一:同一业务数据只有一个权威来源;上游计算/写入/校验,下游只读取/消费,不重复推断、不重复拼装。
  • 禁止猜测兜底:不用默认值掩盖缺失数据。关键权威字段缺失要显式报错或返回 null,由上层决定是否终止,禁止 || 0、|| []、|| ''、双源回退等掩盖。
  • 下游不修补:下游不得为"让流程跑起来"而补写/重算上游本该提供的数据;发现下游在修补,应把责任前移到权威来源。
  • 边界:只有展示层、纯 UI 兼容、非关键日志字段,才可在明确边界内用安全默认值。

审查信号:看到 || []、|| 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 查看导航与一页纸模型。