Compare commits
10
Commits
8948a2950f
...
685b629854
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
685b629854 | ||
|
|
b255730c1b | ||
|
|
681be60e20 | ||
|
|
916066ba96 | ||
|
|
b6aeba412f | ||
|
|
35affa7c20 | ||
|
|
26a52e1a11 | ||
|
|
ca7352b76c | ||
|
|
c4256c51db | ||
|
|
a049498f2c |
@@ -1,12 +1,12 @@
|
||||
# CLAUDE.md
|
||||
|
||||
本文件为 Claude Code(claude.ai/code)在本仓库中工作时提供指导。**它是入口与强制指引**:三套文档的 README(含各自「红线速查 / 一页纸总则」)已通过 `@import` 常驻上下文(见下「文档」一节),**红线以它们为权威、始终在上下文里,本文件不再另抄一份**;规范的完整细节在 `docs/` 编号文档里,改动前先读对应文档。
|
||||
本文件为 Claude Code(claude.ai/code)在本仓库中工作时提供指导。**它是入口与强制指引**:三套文档的「红线速查 / 一页纸总则」单页已通过 `@import` 常驻上下文(见下「文档」一节),**红线以它们为权威、始终在上下文里,本文件不再另抄一份**;规范的完整细节在 `docs/` 编号文档里,改动前先读对应文档。
|
||||
|
||||
## 这是一个什么仓库
|
||||
|
||||
一个友乐/gameabc 房卡类小游戏平台:浏览器端由私有的 `gameabc.min.js` 2D Canvas 引擎驱动,服务端是一个 Node.js 游戏服务器平台(`youle` 应用),双方通过 WebSocket/HTTP 收发 JSON 包通信。项目根目录**没有 `package.json`、没有 npm 构建/lint/测试流水线**——这是纯静态 JS:客户端靠 `<script>` 标签加载,服务端靠平台自带的 `min_loadJsFile`/`require` 加载。
|
||||
|
||||
`client/js/01_SubGame/codes/` 和 `server/games` 目前都是**空的**——尚未接入任何具体子游戏。本仓库是平台层 + 可复用框架的脚手架,未来第一个子游戏会在此基础上搭建。
|
||||
本仓库是平台层 + 可复用框架的脚手架,尚未接入任何具体子游戏,未来第一个子游戏会在此基础上搭建。
|
||||
|
||||
本仓库是**模板项目**——后续会被克隆出去开新项目。因此所有需要长期保留的约定与知识**一律进仓库**(`docs/` 权威文档、本 CLAUDE.md、`.claude/` 钩子与 `settings.json`、`.githooks/`),**不依赖 Claude memory**(memory 按机器/会话存放、不随 `git clone` 迁移,对模板克隆无效)。
|
||||
|
||||
@@ -56,20 +56,13 @@ git config core.hooksPath .githooks
|
||||
|
||||
三者互补不冲突:engineering 讲「该怎么设计、怎么长久演进」,dev-guide 讲「平台怎么接、红线是什么」;硬红线冲突时以 dev-guide 为准。
|
||||
|
||||
以下三份 README(含各自「红线速查 / 一页纸总则」)通过 `@import` **自动加载进每次会话的上下文**——红线以它们为权威,无需在本文件另抄一份;改动具体区域时再深入读对应编号文档:
|
||||
以下三份「红线速查 / 一页纸总则」通过 `@import` **自动加载进每次会话的上下文**——红线以它们为权威,无需在本文件另抄一份;改动具体区域时再深入读对应编号文档:
|
||||
|
||||
@docs/client/development-guide/README.md
|
||||
@docs/server/development-guide/README.md
|
||||
@docs/games/engineering/README.md
|
||||
|
||||
各篇「编号 ↔ 主题」见上面 `@import` 的三份 README 的「阅读顺序 / 阅读导航」表,此处不再复述。
|
||||
|
||||
具体子游戏「二七王」(server/games/erqiwang/)另有两份权威文档,改动该子游戏前必读:
|
||||
|
||||
- server/games/erqiwang/docs/design/design.md —— *二七王玩法规则的唯一权威说明,必须严格遵守*。牌局构成、主牌顺序、叫分坐庄、出牌/跟牌/甩牌、捡分扣底、算子升级、算奖、房间选项等一切玩法规则以此为准;代码实现与该文档冲突时,属于代码缺陷,应改代码去符合规则(除非该规则项标注为「待确认」),*不得反过来改规则去迁就代码*。
|
||||
|
||||
- server/games/erqiwang/docs/protocol/packet_protocol.md —— 二七王前后端收发包协议与包数据定义,*必须完全符合子游戏服务器代码实现(server/games/erqiwang/\*.js**)***。方向与 design.md 相反:协议文档是对既有代码行为的如实记录,两者不一致时以*代码为准*——应修改本协议文档去适配代码;代码新增/删除包或字段时,也要同步补全/删除本文档对应条目。
|
||||
@docs/client/development-guide/红线速查.md
|
||||
@docs/server/development-guide/红线速查.md
|
||||
@docs/games/engineering/一页纸总则.md
|
||||
|
||||
各套文档的「编号 ↔ 主题」导航表、面向人的阅读顺序与定位说明留在各自 README(`docs/client/development-guide/README.md`、`docs/server/development-guide/README.md`、`docs/games/engineering/README.md`)——那些是查阅时才需要的内容,不常驻上下文,需要时直接读。
|
||||
|
||||
## 测试与 Git
|
||||
|
||||
|
||||
@@ -151,7 +151,8 @@ GameData.hallConfigName = ["enablePay",
|
||||
'channelKey',//支付key
|
||||
'payCSS',//支付css
|
||||
'payJS',//支付js
|
||||
'openVideo'
|
||||
'openVideo',
|
||||
'showVisitor'//是否显示游客登陆
|
||||
];//是否开启视频];
|
||||
GameData.hallConfig = {
|
||||
enablePay:0,
|
||||
@@ -185,6 +186,7 @@ GameData.hallConfig = {
|
||||
payCSS:"",//支付css
|
||||
payJS:"",//支付js
|
||||
openVideo:0,//是否开启视频
|
||||
showVisitor:0//是否显示游客登陆
|
||||
};
|
||||
GameData.starName = "星星";
|
||||
GameData.roomCardName = "房卡";
|
||||
|
||||
+2948
-2968
File diff suppressed because it is too large
Load Diff
@@ -3315,7 +3315,7 @@ GameUI.JumpWxAuth = function() { //跳转微信授权
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (GameData.versionState == 1) { //审核版本
|
||||
set_self(405, 37, 0, 0, 0);
|
||||
//set_self(3,18,113,0,0);
|
||||
@@ -3332,7 +3332,10 @@ GameUI.JumpWxAuth = function() { //跳转微信授权
|
||||
//set_self(3,19,382,0,0);
|
||||
//set_self(3,20,400,0,0);
|
||||
//set_self(3,21,116,0,0);
|
||||
|
||||
if(GameData.hallConfig.showVisitor){
|
||||
set_self(3,37,0,0,0);
|
||||
set_self(177, 37, 1, 0, 0);
|
||||
}
|
||||
}
|
||||
if (GameData.hallConfig.qqLogin) {
|
||||
set_self(106, 37, 1, 0, 0);
|
||||
|
||||
@@ -33,6 +33,21 @@ RpcHelper.sendGameRpc(rpc, gameData, options); // route = "room"/子游戏路
|
||||
|
||||
> 平台字段缺失(如 `playerid` 为 0/空)应 fail-fast 暴露,不静默发出残缺包。
|
||||
|
||||
### 请求包只带「意图」,不带结论
|
||||
|
||||
**请求包表达的是「我想做什么」,不是「结果是什么」。** 这是前端数据驱动架构([05 §6](./05-开发规范与红线.md))在发包侧的必然推论:前端不是数据源,凡由前端算出并回传的"结论",都等于把裁定权交给了不可信的客户端。
|
||||
|
||||
| 可以带(意图) | 禁止带(结论) |
|
||||
|---|---|
|
||||
| 操作类型(出牌/碰/杠/过/胡/叫分…) | 算好的得分、番数、结算金额 |
|
||||
| 目标标识(牌的 `uniqueId`、`targetCard`、`choiceIndex`) | "我胡了 / 我听了 / 这步合法" 之类的判定结果 |
|
||||
| 座位号(仅供服务端做一致性校验,**不作身份依据**) | 下一阶段是什么、下一个该谁、剩余时间 |
|
||||
| 纯客户端偏好(音量、语言等非对局字段) | 手牌全量、他人信息等本应由服务端持有的状态 |
|
||||
|
||||
- **服务端按自己的权威数据重算**,对请求里出现的结论字段一律**忽略**(服务端侧见 [04 §8](../../server/development-guide/04-开发规范与红线.md#8-操作请求合法性验证不可信客户端));协议设计阶段就**不应该定义**这类入参——定义了它,就是留了一个可被伪造的洞。
|
||||
- **前端 `shared/` 算出的结果不回传**:它只用于本地提示与预校验(见 05 §6.1、§8),发包时只发意图。
|
||||
- **违例信号**:发包方法里出现 `score`、`isWin`、`nextSeat`、`phase`、`result`、`handCards` 之类"由前端填的结论字段"。
|
||||
|
||||
---
|
||||
|
||||
## 2. 收包:统一分发
|
||||
@@ -156,6 +171,7 @@ AI 托管:服务端 AI 决策 →┘
|
||||
|
||||
- **不得假设「本座刚点过」**:所需数据一律从**包内权威字段**读取,**禁止**依赖「点击时暂存的本地变量」。
|
||||
- **先判 `data.success`**(§4),再据包字段渲染;无对应点击也能正确渲染。
|
||||
- **不得据本地推断补齐服务端未下发的状态**:包里没有的阶段/控制权/可用操作,不由前端算出来顶上——那是服务端漏发,修在服务端发包处(见 [05 §6.1](./05-开发规范与红线.md)、服务端 [03 §1.2](../../server/development-guide/03-数据收发与通信协议.md))。
|
||||
|
||||
### 5.4 适用范围
|
||||
|
||||
@@ -235,6 +251,8 @@ AI 托管:服务端 AI 决策 →┘
|
||||
| DO ✅ | DON'T ❌ |
|
||||
|------|---------|
|
||||
| 发包走语义化发包封装/`RpcHelper` | 业务里直接 `Utl.sendData` 手拼包 |
|
||||
| 请求包只带意图(操作类型+目标标识) | 包里回传前端算出的分数/判定/阶段等结论字段 |
|
||||
| 阶段/控制权/可用操作/倒计时只读包内权威字段 | 前端自建对局状态机、本地推导或用定时器自行推进阶段 |
|
||||
| 收包统一进收包分发器,一 rpc 一处理器 | 在 `Game_Modify.*` 或散点里直接处理推送 |
|
||||
| 受限入口只解包转交,业务在新架构 handler | 在受限文件堆业务逻辑 |
|
||||
| 成败只认 `data.success` | 用 `status`/`code` 判成败、写 status 兜底 |
|
||||
|
||||
@@ -70,9 +70,43 @@
|
||||
|
||||
---
|
||||
|
||||
## 6. 数据权威、组件数据与表现延后
|
||||
## 6. 数据驱动架构:服务端状态的投影
|
||||
|
||||
**前端是服务端对局状态的一个投影(view),不是状态的第二个来源。** 阶段、轮次、控制权、可用操作、倒计时、分数、按钮可用性……一切对局态都由服务端唯一维护并随包下发,前端只做「**读包字段 → 写 `this.data` → 据 `this.data` 画界面**」这一条链路。这既是正确性要求(两端各推一套必然分叉),也是**反作弊**要求:**前端能自己推出来的东西,就是玩家能改的东西**。
|
||||
|
||||
- **服务端权威**:所有核心运算与胜负裁定以服务端为准、数据以服务端为权威;前端只做界面展示与玩家交互,本地数据仅为「渲染副本」,不做权威计算。
|
||||
|
||||
### 6.1 前端无对局状态机
|
||||
|
||||
- **状态机唯一在服务端**:阶段(`phase`)、当前控制权(轮到谁)、该座位可用操作(`availableActions`)、倒计时剩余、比分/结算——一律**读服务端下发的权威字段**渲染。
|
||||
- **禁止本地推导**:不得由「上一个包 + 本地规则」推出「现在该轮到谁 / 现在进入哪个阶段 / 现在该显示哪几个按钮」。缺字段是**服务端漏发**,修在服务端发包处(见服务端 [03 §1.2](../../server/development-guide/03-数据收发与通信协议.md)),**前端不推、不补、不猜**。
|
||||
- **禁止前端推进流程**:不得用本地定时器自动推进阶段、自行结算、超时后自判胜负或自动补发包。**超时/托管一律由服务端驱动**,前端只显示服务端下发的结果。
|
||||
- **倒计时的正确形态**:服务端下发锚点/剩余时长,前端可本地 tick 做插值显示;但**归零不代表状态改变**——超时的裁定与后续推进仍等服务端推送。
|
||||
- **`shared/` 预判不是状态**:前端跑 `shared/` 算出的胡牌/听牌/合法性只用于**提示与预校验**(如置灰不可点的牌),**不得**据其改写对局态、也**不得**用来替代服务端下发的 `availableActions`(见 §8)。
|
||||
|
||||
### 6.2 视图 = f(服务端快照)
|
||||
|
||||
- `this.data` 是**服务端状态的镜像**,不是第二份真相;**不得存在「只活在前端、服务端不知道」的对局态**。
|
||||
- **判据(可直接用于自检与代码审查)**:任意时刻丢弃全部 `this.data`,仅用**最近一次服务端快照**(重连 `get_deskinfo` 或最近一次推送)重画,界面与交互状态必须**完全一致**。做不到 → 要么前端私存了对局态、要么服务端漏发了字段,二者必居其一,**都要修**。
|
||||
- 这与「重连即重画」是同一条路径(见 [04 §3.3](./04-网络对接与启动编排.md)):**重连之所以能只靠服务端快照还原,正因为前端从来没有过独占状态。**
|
||||
|
||||
### 6.3 允许的本地 UI 态(白名单)
|
||||
|
||||
只有**不影响对局裁定、服务端根本不关心**的纯表现态,才可以只存在于前端:
|
||||
|
||||
| 允许只在前端 | 不允许(属对局态,必须来自服务端) |
|
||||
|---|---|
|
||||
| 选中/待出牌的高亮、拖拽位置 | 这张牌能不能出、出了之后轮到谁 |
|
||||
| 按钮按下高亮/缩放、点击音效 | 按钮**该不该出现**、**能不能点** |
|
||||
| 列表滚动位置、面板展开、设置开关 | 阶段、控制权、倒计时基准、分数、结算 |
|
||||
| 动画进度、特效播放中标记 | 手牌/牌河/副露内容、亮牌信息、托管状态 |
|
||||
|
||||
判别:**这个值若被玩家改成任意值,会不会影响对局结果、或让他看到/做到本不该的事?** 会 → 它是对局态,必须服务端权威。
|
||||
|
||||
> **与 [04 §5.6](./04-网络对接与启动编排.md#56-受控例外响应交互按钮乐观清除--服务端合法性验证) 受控例外的关系**:本玩家点击响应/掷骰交互按钮后的「乐观隐藏」仍然允许——它是抢先一步做了服务端稍后会确认的事,**不是前端私有状态**:按钮**该不该出现**依旧由服务端下发的 `availableActions` 决定,收包侧必须能独立得出同一结果(AI 托管无点击时也正确)。判据仍成立:丢弃 `this.data` 后按最近快照重画,该按钮的显隐与服务端一致。
|
||||
|
||||
### 6.4 组件数据与表现延后
|
||||
|
||||
- **组件数据自持 + set-refresh**:组件(及其下每个「零件 UI」)的自有数据集中在 `this.data`,**不散落**各处;每个 UI 都有 `setXxx`(只写数据)/`refreshXxx`(只据数据画界面)成对方法,组件另有总 `refresh()` 据 `this.data` 重建整块界面(见 02)。
|
||||
- **更新时机——发包只请求、收包才表现(悲观 UI / 输入-渲染解耦)**:用户点击**只发请求包**,**绝不**在点击时改动任何对局状态界面(提示显隐、按钮增删、当前控制权、倒计时启停、落牌、阶段/托管图标);这些表现一律在**收到服务端结果/推送包后**更新。点击回调只做「发包 +(可选)纯本地物理反馈(按下高亮/音效、防连点 `disable`)」。收包处理器**触发源无关**——数据从包字段读、不依赖「点击时暂存的本地变量」,故真人操作与 **AI 托管/他人广播共用同一更新路径、对前端透明**(无点击时也正确更新);把更新挂在「点击」而非「收包」会导致 AI 托管时界面卡死。详见 [04 §5](./04-网络对接与启动编排.md#5-输入渲染解耦发包只请求收包才表现)。**受控例外**:本玩家点击的**响应/掷骰交互按钮**(碰/杠/过/胡按钮、手动掷骰按钮)的**隐藏**允许乐观清除(兼作防连点+即时反馈),前提是**服务端合法性验证 + 收包侧兜底(AI 托管一致)**,见 [04 §5.6](./04-网络对接与启动编排.md#56-受控例外响应交互按钮乐观清除--服务端合法性验证) 与服务端 [04 §8](../../server/development-guide/04-开发规范与红线.md#8-操作请求合法性验证不可信客户端);其余提示/控制权/倒计时/落牌仍严格收包驱动。
|
||||
- **收包节奏**:**先 `setXxx` 写数据 →(必要时 `refresh` 刷静态界面)→ 再播动画 → 动画回调里只刷新界面**;动画的开始/结束/出错等生命周期回调里**绝不设置核心数据**,动画期间**不改数据**。
|
||||
@@ -125,6 +159,8 @@
|
||||
| 渲染 | 只走 `SpriteManager`;ID 守范围、不编造;查返回值 |
|
||||
| 常量 | 精灵/群组/图层/图片/声音/Spine/坐标/动画/事件全进常量;UI 代码禁硬编码 id 与裸值 |
|
||||
| 组件 | 继承 `BaseComponent`;事件 `addEventListener`;`destroy` 清动态精灵/定时器 |
|
||||
| 数据驱动 | 前端**无对局状态机**:阶段/控制权/可用操作/倒计时/分数只读包内权威字段,禁本地推导与本地推进流程;`this.data` 只是服务端快照的镜像,丢弃后仅凭最近快照重画须完全一致;只有白名单纯表现态可只存在于前端(§6.1–6.3) |
|
||||
| 发包内容 | 请求包只带「意图」(操作类型+目标标识),**不带结论**(分数/判定结果/阶段指令);服务端按自己的权威数据重算(04 §1「请求包只带意图」) |
|
||||
| 数据 | 服务端权威(核心运算/裁定在服务端);组件自有数据集中 `this.data`;每个 UI/零件有 set/refresh 对、组件有总 `refresh` |
|
||||
| 更新时机 | 点击只发请求包,对局状态表现等**收包后**更新(悲观 UI);收包处理器触发源无关、数据从包字段读;AI 托管/他人广播共用同一更新路径。**受控例外**:响应/掷骰交互按钮隐藏可乐观清除(防连点+即时反馈),前提是服务端合法性验证+收包兜底(04 §5.6、服务端 04 §8) |
|
||||
| 节奏 | 先写数据后表现;动画回调只刷界面、**不写核心数据**;重画可随时据数据还原界面 |
|
||||
|
||||
@@ -18,60 +18,24 @@
|
||||
|
||||
| 篇 | 文档 | 解决什么问题 |
|
||||
|----|------|--------------|
|
||||
| 00 | 本文 README | 这套文档是什么、怎么读、红线速查 |
|
||||
| 00 | 本文 README | 这套文档是什么、怎么读 |
|
||||
| — | [红线速查.md](./红线速查.md) | **一页纸分层模型 + 红线清单**(由 `CLAUDE.md` 常驻加载,改代码前先对照) |
|
||||
| 01 | [01-前端架构与运行环境.md](./01-前端架构与运行环境.md) | 双运行时/ES5、平台/框架/子游戏三层、新旧架构并存、目录与加载顺序 |
|
||||
| 02 | [02-渲染与UI组件体系.md](./02-渲染与UI组件体系.md) | 精灵 ID 体系、SpriteManager 分层、资源常量组织、BaseComponent 组件化、组件数据/set-refresh 范式、UIManager 场景、动态列表 |
|
||||
| 03 | [03-事件·动画·音频·Spine.md](./03-事件·动画·音频·Spine.md) | EventBus、AnimationManager+配置、AudioManager+音效资源、SpineMgr 全链路 |
|
||||
| 04 | [04-网络对接与启动编排.md](./04-网络对接与启动编排.md) | 发包链路(RpcHelper 注入平台字段)、收包统一分发、新旧架构对接边界、启动编排、处理器/管理器职责 |
|
||||
| 05 | [05-开发规范与红线.md](./05-开发规范与红线.md) | 可编辑范围、ES5、框架中立、常量集中、组件生命周期、服务端权威/组件数据/表现延后、data.success、模块职责、测试 |
|
||||
| 05 | [05-开发规范与红线.md](./05-开发规范与红线.md) | 可编辑范围、ES5、框架中立、常量集中、组件生命周期、**数据驱动架构(前端无状态机/视图=服务端快照投影/本地 UI 态白名单)**、组件数据与表现延后、data.success、模块职责、测试 |
|
||||
| 06 | [06-子游戏接入模式与Hooks外置.md](./06-子游戏接入模式与Hooks外置.md) | 内联模式 vs Hooks 外置模式、三契约文件可改性、退化为纯转发壳 + SubGameHooks 委托、subgame-entry 模板 |
|
||||
|
||||
建议第一次**从 01 顺序读到 05**;之后把 02–05 当手册随用随查。06 在接入新游戏或迁移到 Hooks 外置模式时选读。
|
||||
|
||||
---
|
||||
|
||||
## 一页纸:前端分层模型
|
||||
## 一页纸与红线
|
||||
|
||||
```
|
||||
gameabc.min.js(引擎,js/vendor,第三方)
|
||||
▲
|
||||
GameABCUtils(core,唯一直接调引擎原生 API 的模块)
|
||||
▲
|
||||
SpriteManager(core,业务级精灵 API:ID 校验 + 单位换算) EventBus / AnimationManager / AudioManager / SpineMgr(system)
|
||||
▲ ▲
|
||||
BaseComponent / UIManager(ui,组件化与场景) ← gameabc-framework(游戏中立,可复用)
|
||||
══════════════════════════════════════════════════════════════════════════
|
||||
01_SubGame/codes(子游戏实例,单向依赖框架;内部结构由子游戏自行组织)
|
||||
shared 前后端共享算法(与服务端同源,脚本同步,只读)
|
||||
══════════════════════════════════════════════════════════════════════════
|
||||
旧受限对接层(平台入口,尽量不改)
|
||||
00/01/02_SubGame_*.js → Game_Modify.StartWar / Reconnect / _ReceiveData / appStart
|
||||
```
|
||||
**分层模型与红线清单已独立成篇:[红线速查.md](./红线速查.md)。**
|
||||
|
||||
- **框架(gameabc-framework)**:游戏中立,提供精灵/组件/事件/动画/音频/Spine 通用能力,可被任何子游戏复用。
|
||||
- **子游戏(01_SubGame/codes)**:单向依赖框架,在其内部自由组织实现(目录/文件命名由子游戏自定,仅 `shared/` 为只读同步副本)。
|
||||
- **旧受限对接层**:平台框架的固定入口(`Game_Modify.*`),把平台事件转交给新架构,**尽量不改**(详见 01/04)。
|
||||
|
||||
---
|
||||
|
||||
## 红线速查(详见 05)
|
||||
|
||||
- **可编辑范围**:前端平台代码 `js/00_Surface/` 禁改;受限接口文件 `01_SubGame/00_/01_/02_SubGame_*.js` 不新增接口、尽量不改;其余在 `gameabc-framework/`、`01_SubGame/codes/` 内开发。
|
||||
- **严格 ES5**:用 `var`/`function`/`Object.create`,禁 `let`/`const`/箭头/模板串/`class`。
|
||||
- **框架游戏中立**:`gameabc-framework/` 内**不得**出现任何具体玩法逻辑或专属常量;玩法专属的事件/资源/配置一律定义在子游戏侧(如玩法事件在子游戏的事件常量文件里追加到 `EventBus.Events`)。
|
||||
- **精灵只走 SpriteManager**:UI 代码**禁止**直接调 `GameABCUtils` 或引擎原生 API;ID 必须落在规定范围(精灵 1001–3000、群组 ≥201、图层 101–200/弹窗 301–400,另有 501–600、701+ 备用段,框架保留 1–1000 及 3001+ 等交替段)。
|
||||
- **常量集中、禁硬编码**:UI 代码**禁止任何硬编码 id / 裸值**——精灵 ID / 群组 ID / 图层 ID / 图片资源 ID / 声音 ID / Spine 资源 / 坐标尺寸 / 动画时长 / 事件名 一律定义在对应常量文件、只引用常量。
|
||||
- **服务端权威、前端只展示**:核心运算与胜负裁定以服务端为准、数据以服务端为权威;前端只做界面展示与玩家交互,本地数据仅为渲染副本,不做权威计算。
|
||||
- **组件走 BaseComponent**:UI 组件继承 `BaseComponent`,事件用 `this.addEventListener` 注册(`destroy()` 自动清理,防泄漏);动态精灵/定时器在 `onDestroy` 清理。
|
||||
- **组件数据自持 + set-refresh**:组件自有数据集中 `this.data`;每个 UI/零件有 `setXxx`(只写数据)/`refreshXxx`(只据数据画)对,组件有总 `refresh()` 可据数据重建界面。
|
||||
- **UI 组件专职自己的界面**:每个界面的数据与渲染只由其对应 UI 组件实现;别的模块要改/刷该界面一律**调该组件的公开接口**(`setXxx`/`refreshXxx`/语义方法),**禁止**在别处重复实现重叠或类似的界面数据/渲染逻辑。
|
||||
- **显隐走 `showXxx`/`hideXxx`**:UI 组件/零件的精灵、群组显隐必须由组件暴露的 `showXxx`/`hideXxx` 接口控制;**禁止**别处直接用其精灵 ID/群组 ID 去 `SpriteManager.show/hide` 控显隐。
|
||||
- **发包只请求、收包才表现(悲观 UI / 输入-渲染解耦)**:用户点击**只发请求包**,对局状态界面(提示/按钮/控制权/倒计时/落牌/阶段)一律**收到服务端结果或推送包后**才更新,点击时不做乐观预测;收包处理器**触发源无关**(数据从包字段读,不依赖点击时的本地变量),故真人操作与 **AI 托管/他人广播共用同一更新路径、对前端透明**——挂在「点击」而非「收包」会导致 AI 托管时界面卡死(详见 04 §5)。**受控例外**:本玩家点击的**响应/掷骰交互按钮**(碰/杠/过/胡、手动掷骰按钮)的隐藏允许乐观清除(兼作防连点+即时反馈),前提是**服务端合法性验证 + 收包侧兜底(AI 托管一致)**,见 04 §5.6 与服务端 04 §8;其余提示/控制权/倒计时/落牌仍严格收包驱动。
|
||||
- **数据优先、表现延后**:收到推送先 `setXxx` 写数据、再播动画;动画的开始/结束/出错回调里**只刷界面、不写核心数据**。即使动画缺失/卡住/出错,数据、逻辑、界面仍正确、互不影响。
|
||||
- **重连即重画(断线重连 + 硬刷新都要处理)**:重连/页面重载的本质是**恢复数据 → 调各 UI 组件 set-refresh 恢复数据与界面状态**,复用同一条重画路径,不为重连单写一套渲染(详见 04 §3.3、05 §6)。
|
||||
- **成败只认 `data.success`**:前端一律 `if (!data.success)` 判成败,**不**用 `status`/`code`,**不**写 `status` 兼容兜底。
|
||||
- **收发包走统一通道**:发包经统一发送封装(底层 `RpcHelper` 自动注入平台字段),收包统一分发(一 rpc 一处理器);业务逻辑放处理器,**不**写进 `Game_Modify.*`。
|
||||
- **shared 只读**:`01_SubGame/codes/shared/` 是服务端 `shared/` 的同步副本,**不在前端改**,改服务端权威源后跑同步脚本。
|
||||
该文件由 `CLAUDE.md` 通过 `@import` 常驻每次会话上下文,是红线的权威源;本 README 只负责导航,不重复抄写红线(避免两处不同步)。红线的完整细节见 [05-开发规范与红线.md](./05-开发规范与红线.md)。
|
||||
|
||||
---
|
||||
|
||||
@@ -80,4 +44,3 @@ BaseComponent / UIManager(ui,组件化与场景) ← gameabc-framework(
|
||||
- 服务端的对应文档见 [服务端开发指导文档](../../server/development-guide/);前端「成败标志 `data.success`」「收发包链路」与之同源,互为对照。
|
||||
- 子游戏前端各层可能另有局部说明文档;本套是总纲,与之不冲突时以本套的通用原则为准。
|
||||
- **平台无关的通用工程与架构规范**(分层、可扩展模式、配置化、数据权威、反模式与审查清单)见 [工程与架构通则](../../games/engineering/):本套讲前端接入与红线,工程通则讲前后端通用的设计方法论,互补阅读。
|
||||
</content>
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
# 前端 · 一页纸分层模型与红线速查
|
||||
|
||||
> 本文是前端开发**必须常驻在手边**的那一页:分层模型 + 红线清单。
|
||||
> 它由 `CLAUDE.md` 通过 `@import` 常驻每次会话上下文;**红线以本文为权威**。
|
||||
> 文档导航、阅读顺序、各篇主题见 [README](./README.md);红线的完整细节见 [05-开发规范与红线.md](./05-开发规范与红线.md)。
|
||||
|
||||
---
|
||||
|
||||
## 一页纸:前端分层模型
|
||||
|
||||
```
|
||||
gameabc.min.js(引擎,js/vendor,第三方)
|
||||
▲
|
||||
GameABCUtils(core,唯一直接调引擎原生 API 的模块)
|
||||
▲
|
||||
SpriteManager(core,业务级精灵 API:ID 校验 + 单位换算) EventBus / AnimationManager / AudioManager / SpineMgr(system)
|
||||
▲ ▲
|
||||
BaseComponent / UIManager(ui,组件化与场景) ← gameabc-framework(游戏中立,可复用)
|
||||
══════════════════════════════════════════════════════════════════════════
|
||||
01_SubGame/codes(子游戏实例,单向依赖框架;内部结构由子游戏自行组织)
|
||||
shared 前后端共享算法(与服务端同源,脚本同步,只读)
|
||||
══════════════════════════════════════════════════════════════════════════
|
||||
旧受限对接层(平台入口,尽量不改)
|
||||
00/01/02_SubGame_*.js → Game_Modify.StartWar / Reconnect / _ReceiveData / appStart
|
||||
```
|
||||
|
||||
- **框架(gameabc-framework)**:游戏中立,提供精灵/组件/事件/动画/音频/Spine 通用能力,可被任何子游戏复用。
|
||||
- **子游戏(01_SubGame/codes)**:单向依赖框架,在其内部自由组织实现(目录/文件命名由子游戏自定,仅 `shared/` 为只读同步副本)。
|
||||
- **旧受限对接层**:平台框架的固定入口(`Game_Modify.*`),把平台事件转交给新架构,**尽量不改**(详见 01/04)。
|
||||
|
||||
---
|
||||
|
||||
## 红线速查(详见 05)
|
||||
|
||||
- **可编辑范围**:前端平台代码 `js/00_Surface/` 禁改;受限接口文件 `01_SubGame/00_/01_/02_SubGame_*.js` 不新增接口、尽量不改;其余在 `gameabc-framework/`、`01_SubGame/codes/` 内开发。
|
||||
- **严格 ES5**:用 `var`/`function`/`Object.create`,禁 `let`/`const`/箭头/模板串/`class`。
|
||||
- **框架游戏中立**:`gameabc-framework/` 内**不得**出现任何具体玩法逻辑或专属常量;玩法专属的事件/资源/配置一律定义在子游戏侧(如玩法事件在子游戏的事件常量文件里追加到 `EventBus.Events`)。
|
||||
- **精灵只走 SpriteManager**:UI 代码**禁止**直接调 `GameABCUtils` 或引擎原生 API;ID 必须落在规定范围(精灵 1001–3000、群组 ≥201、图层 101–200/弹窗 301–400,另有 501–600、701+ 备用段,框架保留 1–1000 及 3001+ 等交替段)。
|
||||
- **常量集中、禁硬编码**:UI 代码**禁止任何硬编码 id / 裸值**——精灵 ID / 群组 ID / 图层 ID / 图片资源 ID / 声音 ID / Spine 资源 / 坐标尺寸 / 动画时长 / 事件名 一律定义在对应常量文件、只引用常量。
|
||||
- **服务端权威、前端只展示**:核心运算与胜负裁定以服务端为准、数据以服务端为权威;前端只做界面展示与玩家交互,本地数据仅为渲染副本,不做权威计算。
|
||||
- **数据驱动、前端无对局状态机**:阶段、轮次/控制权、可用操作、倒计时、分数、按钮该不该出现,一律**读服务端下发的权威字段**渲染;**禁止**前端自建状态机、由本地规则推导「现在轮到谁/进入哪个阶段/显示哪些按钮」,**禁止**用本地定时器自行推进阶段或自判超时(超时与托管由服务端驱动,倒计时前端只做插值显示)。包里没有的状态是**服务端漏发**,修在服务端发包处,前端不推、不补、不猜(详见 05 §6.1)。
|
||||
- **视图 = f(服务端快照)**:`this.data` 只是服务端状态的镜像,**不得存在只活在前端、服务端不知道的对局态**。判据:任意时刻丢弃 `this.data`、仅凭最近一次服务端快照重画,界面必须完全一致——做不到即"前端私存了状态"或"服务端漏发了字段",都要修。只有不影响裁定的纯表现态(选中高亮、按下反馈、滚动位置、动画进度)可只存在于前端(详见 05 §6.2–6.3)。
|
||||
- **请求包只带「意图」**:发包只带「做什么 + 目标标识」(操作类型、牌 `uniqueId`、`choiceIndex`),**禁止**回传前端算出的结论(分数/番数、"我胡了"之类判定、结算结果、阶段推进指令);前端 `shared/` 的计算只用于本地提示与预校验,结果不回传(详见 04 §1)。**前端能自己推出来的东西,就是玩家能改的东西**——这是防作弊的根本。
|
||||
- **组件走 BaseComponent**:UI 组件继承 `BaseComponent`,事件用 `this.addEventListener` 注册(`destroy()` 自动清理,防泄漏);动态精灵/定时器在 `onDestroy` 清理。
|
||||
- **组件数据自持 + set-refresh**:组件自有数据集中 `this.data`;每个 UI/零件有 `setXxx`(只写数据)/`refreshXxx`(只据数据画)对,组件有总 `refresh()` 可据数据重建界面。
|
||||
- **UI 组件专职自己的界面**:每个界面的数据与渲染只由其对应 UI 组件实现;别的模块要改/刷该界面一律**调该组件的公开接口**(`setXxx`/`refreshXxx`/语义方法),**禁止**在别处重复实现重叠或类似的界面数据/渲染逻辑。
|
||||
- **显隐走 `showXxx`/`hideXxx`**:UI 组件/零件的精灵、群组显隐必须由组件暴露的 `showXxx`/`hideXxx` 接口控制;**禁止**别处直接用其精灵 ID/群组 ID 去 `SpriteManager.show/hide` 控显隐。
|
||||
- **发包只请求、收包才表现(悲观 UI / 输入-渲染解耦)**:用户点击**只发请求包**,对局状态界面(提示/按钮/控制权/倒计时/落牌/阶段)一律**收到服务端结果或推送包后**才更新,点击时不做乐观预测;收包处理器**触发源无关**(数据从包字段读,不依赖点击时的本地变量),故真人操作与 **AI 托管/他人广播共用同一更新路径、对前端透明**——挂在「点击」而非「收包」会导致 AI 托管时界面卡死(详见 04 §5)。**受控例外**:本玩家点击的**响应/掷骰交互按钮**(碰/杠/过/胡、手动掷骰按钮)的隐藏允许乐观清除(兼作防连点+即时反馈),前提是**服务端合法性验证 + 收包侧兜底(AI 托管一致)**,见 04 §5.6 与服务端 04 §8;其余提示/控制权/倒计时/落牌仍严格收包驱动。
|
||||
- **数据优先、表现延后**:收到推送先 `setXxx` 写数据、再播动画;动画的开始/结束/出错回调里**只刷界面、不写核心数据**。即使动画缺失/卡住/出错,数据、逻辑、界面仍正确、互不影响。
|
||||
- **重连即重画(断线重连 + 硬刷新都要处理)**:重连/页面重载的本质是**恢复数据 → 调各 UI 组件 set-refresh 恢复数据与界面状态**,复用同一条重画路径,不为重连单写一套渲染(详见 04 §3.3、05 §6)。
|
||||
- **成败只认 `data.success`**:前端一律 `if (!data.success)` 判成败,**不**用 `status`/`code`,**不**写 `status` 兼容兜底。
|
||||
- **收发包走统一通道**:发包经统一发送封装(底层 `RpcHelper` 自动注入平台字段),收包统一分发(一 rpc 一处理器);业务逻辑放处理器,**不**写进 `Game_Modify.*`。
|
||||
- **shared 只读**:`01_SubGame/codes/shared/` 是服务端 `shared/` 的同步副本,**不在前端改**,改服务端权威源后跑同步脚本。
|
||||
@@ -25,35 +25,22 @@
|
||||
|
||||
| 篇 | 文档 | 解决什么 |
|
||||
|----|------|----------|
|
||||
| 00 | 本文 README | 定位、适用范围、一页纸总则、与既有文档的关系 |
|
||||
| 00 | 本文 README | 定位、怎么读、与既有文档的关系 |
|
||||
| — | [一页纸总则.md](./一页纸总则.md) | **七大总则 + 适用边界**(由 `CLAUDE.md` 常驻加载,设计取舍时对照) |
|
||||
| 01 | [01-架构总则与分层.md](./01-架构总则与分层.md) | 七大架构总则;前后端参考分层;依赖方向与稳定依赖 |
|
||||
| 02 | [02-可扩展性与配置化.md](./02-可扩展性与配置化.md) | 扩展模式(注册表/策略/管线/工厂/事件)何时用;配置化与去硬编码;避免过度设计 |
|
||||
| 03 | [03-数据权威·错误处理·演进.md](./03-数据权威·错误处理·演进.md) | 数据权威(前后端);错误处理与可观测;演进与重构;反模式与审查清单 |
|
||||
|
||||
---
|
||||
|
||||
## 一页纸:七大总则
|
||||
## 一页纸总则
|
||||
|
||||
1. **单一权威数据源(SSOT)**:同一业务数据只有一个计算/写入处,其他只读;缺失即显式失败,不兜底掩盖。
|
||||
2. **单向依赖**:分层自上而下依赖,稳定的被依赖、易变的作依赖方;**禁止环形依赖**。
|
||||
3. **职责单一、边界清晰**:一个职能只在一个模块实现,别处**调用而非重造**。
|
||||
4. **关注点分离**:决策与机制分离、数据与表现分离、编排与算法分离。
|
||||
5. **对扩展开放、对修改封闭(OCP)**:用注册/策略/管线**加**能力,不改动已稳定的核心。
|
||||
6. **配置优先于硬编码**:会变、复用、无语义的值一律外提为常量/配置,用数据驱动行为。
|
||||
7. **显式失败优于隐式兜底**:关键路径缺数据就报错/返回 `null`,把问题暴露在最近处。
|
||||
**七大总则与适用边界已独立成篇:[一页纸总则.md](./一页纸总则.md)。**
|
||||
|
||||
> 这七条互相支撑:**SSOT + 显式失败**保正确,**单向依赖 + 职责单一 + 关注点分离**保清晰,
|
||||
> **OCP + 配置化**保可演进。任何设计取舍,回到这七条对照。
|
||||
该文件由 `CLAUDE.md` 通过 `@import` 常驻每次会话上下文,是总则的权威源;本 README 只负责导航,不重复抄写总则(避免两处不同步)。总则的展开见 [01-架构总则与分层.md](./01-架构总则与分层.md)。
|
||||
|
||||
---
|
||||
|
||||
## 适用范围与边界
|
||||
|
||||
- **适用**:子游戏自身的前后端业务代码(玩法逻辑、对局编排、收发包处理、表现层、共享算法)。
|
||||
- **不覆盖**:平台框架代码(不可改)、平台接入契约(见各端 `development-guide/`)。
|
||||
- **与硬约束的关系**:ES5、`require` 守卫、可编辑范围、成败标志 `data.success` 等**硬红线**仍以
|
||||
`development-guide/` 为准;本套是**方法论层**,与之互补不冲突。
|
||||
|
||||
## 与既有文档的关系
|
||||
|
||||
| 文档 | 定位 |
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
# 工程与架构通则 · 一页纸
|
||||
|
||||
> 本文是与平台无关的**工程方法论那一页**:七大总则 + 适用边界。
|
||||
> 它由 `CLAUDE.md` 通过 `@import` 常驻每次会话上下文;**总则以本文为权威**。
|
||||
> 文档导航、各篇主题、与既有文档的关系见 [README](./README.md)。
|
||||
|
||||
---
|
||||
|
||||
## 一页纸:七大总则
|
||||
|
||||
1. **单一权威数据源(SSOT)**:同一业务数据只有一个计算/写入处,其他只读;缺失即显式失败,不兜底掩盖。
|
||||
2. **单向依赖**:分层自上而下依赖,稳定的被依赖、易变的作依赖方;**禁止环形依赖**。
|
||||
3. **职责单一、边界清晰**:一个职能只在一个模块实现,别处**调用而非重造**。
|
||||
4. **关注点分离**:决策与机制分离、数据与表现分离、编排与算法分离。
|
||||
5. **对扩展开放、对修改封闭(OCP)**:用注册/策略/管线**加**能力,不改动已稳定的核心。
|
||||
6. **配置优先于硬编码**:会变、复用、无语义的值一律外提为常量/配置,用数据驱动行为。
|
||||
7. **显式失败优于隐式兜底**:关键路径缺数据就报错/返回 `null`,把问题暴露在最近处。
|
||||
|
||||
> 这七条互相支撑:**SSOT + 显式失败**保正确,**单向依赖 + 职责单一 + 关注点分离**保清晰,
|
||||
> **OCP + 配置化**保可演进。任何设计取舍,回到这七条对照。
|
||||
|
||||
---
|
||||
|
||||
## 适用范围与边界
|
||||
|
||||
- **适用**:子游戏自身的前后端业务代码(玩法逻辑、对局编排、收发包处理、表现层、共享算法)。
|
||||
- **不覆盖**:平台框架代码(不可改)、平台接入契约(见各端 `development-guide/`)。
|
||||
- **与硬约束的关系**:ES5、`require` 守卫、可编辑范围、成败标志 `data.success` 等**硬红线**仍以
|
||||
`development-guide/` 为准;本套是**方法论层**,与之互补不冲突。
|
||||
@@ -55,6 +55,28 @@
|
||||
|
||||
> 一句话:**服务端权威不仅是"服务端算",也是"服务端把界面要用的核心数据发全"——前端只据包渲染,不推算、不兜底。**
|
||||
|
||||
### 1.3 发全 ≠ 发多:按可见性下发(防作弊)
|
||||
|
||||
§1.2 要求"该看到的发全",本节是它的另一半:**不该看到的一律不发**。前端是不可信环境——**下发即泄露**:包到了客户端就能被抓包/改内存看到,"前端拿到但不渲染"等于零防护。
|
||||
|
||||
- **按座位裁剪可见面**:他人手牌、牌堆剩余序列与顺序、未公开的判定结果(他人是否听牌/能否胡、暗牌内容、未到揭示时机的底牌与亮牌明细)——**对不该看到的座位不放进 `data`**,用差异化下发(`sendtype:1` + `seatlist[]` 逐座位组包,见 §3)实现。
|
||||
- **只发"公开面"的统计量**:他人的信息若界面确实要显示,只发**已公开的派生量**(如剩余张数、已亮出的花色/数量),不发原始牌面。
|
||||
- **不发未来**:尚未发生或尚未公开的权威结果(下一张要摸的牌、预先算好的胜负、待揭晓的底牌)不提前下发,哪怕前端"只是缓存"。
|
||||
- **判别**:问一句 **"这个字段落到一个改过的客户端手里,玩家会不会因此获得优势?"** 会 → 不发(或只发到该看到的座位)。
|
||||
- **违例信号**:`deepCopy(baseData)` 后**没有**逐座位裁剪敏感字段就全员广播;把全量 `handCards`/`cardPool` 塞进公共包;重连快照 `get_deskinfo` 直接回整张桌的内部状态。
|
||||
|
||||
> §1.2 与 §1.3 合起来才是完整的下发面:**该看到的一个不少(否则前端缺数据),不该看到的一个不多(否则等于送作弊入口)。**
|
||||
|
||||
### 1.4 阶段与状态机唯一在服务端,并随包下发
|
||||
|
||||
前端**不持有对局状态机**(前端侧规范见 [客户端 05 §6](../../client/development-guide/05-开发规范与红线.md)):阶段、轮到谁、该座位可用操作、倒计时基准,前端一概不推导。因此这些字段**必须由服务端唯一维护、并在每个相关下发包里明确给出**:
|
||||
|
||||
- **阶段/状态标志**(当前 `phase`、是否在响应窗口、是否已结束等)——不让前端按"收到了什么包"去反推阶段。
|
||||
- **控制权**(下一个该谁操作,如 `nextControlSeat`/`currentPlayer`)——显式字段,不让前端按座位顺序自己算。
|
||||
- **该座位可用操作**(`availableActions[seat]`)——按座位下发;它同时是服务端准入白名单(见 [04 §8](./04-开发规范与红线.md#8-操作请求合法性验证不可信客户端)),**同一份权威数据既驱动前端按钮显示、又校验请求合法性**,天然不会两边判得不一致。
|
||||
- **倒计时锚点与时长**——前端只做本地插值显示,**超时的裁定与后续推进仍在服务端**,不接受前端上报"我超时了"。
|
||||
- **状态变更必须有包**:任何阶段推进都要有一个下发包承载;**没有包的状态变化 = 前端不可能正确显示**,只能靠前端猜——那正是本节要杜绝的。
|
||||
|
||||
---
|
||||
|
||||
## 2. 收包处理的固定步骤(在 handler 里)
|
||||
@@ -124,7 +146,7 @@ for (var seat = 0; seat < o_room.seatlist.length; seat++) {
|
||||
}
|
||||
```
|
||||
|
||||
> 敏感信息(手牌、暗牌、未公开的判定)**只发给该看到的人**,这是服务器权威的一部分。
|
||||
> 敏感信息(手牌、暗牌、未公开的判定)**只发给该看到的人**,这是服务器权威的一部分。裁剪清单与判别标准见 [§1.3 按可见性下发](#13-发全--发多按可见性下发防作弊)——**下发即泄露,前端"拿到但不渲染"不算防护**。
|
||||
|
||||
---
|
||||
|
||||
@@ -246,6 +268,8 @@ if (!data.success) { /* 失败处理 */ return; }
|
||||
|
||||
- 包结构恒为 `{app, route, rpc, data}`;收包先 `check_player`,失败静默 `return`。
|
||||
- **下发包必须发全前端界面所需的核心数据**(§1.2):前端以服务端为权威、不自算,漏发修在服务端发包处、前端不补洞。
|
||||
- **不该看到的一律不发**(§1.3):下发即泄露,按座位裁剪可见面;"该看到的一个不少、不该看到的一个不多"。
|
||||
- **阶段/控制权/可用操作/倒计时锚点由服务端唯一维护并显式下发**(§1.4):前端不持有状态机、不反推阶段;状态变更必须有包承载。
|
||||
- 三种发包方式按"谁该看到什么"选;差异化广播逐座位定制,敏感信息只发本人。
|
||||
- **主动推送是唯一可靠下发通道**,`return` 不算。
|
||||
- **成败只认 `data.success`**,推送必自带 `success`,禁止 `status`/`code` 判成败与兼容兜底。
|
||||
|
||||
@@ -59,6 +59,7 @@ function foo() { GameStateManager.doSomething(); }
|
||||
- **下游不修补**:下游不得为"让流程跑起来"而补写/重算上游本该提供的数据;发现下游在修补,应把责任前移到权威来源。
|
||||
- **边界**:只有展示层、纯 UI 兼容、非关键日志字段,才可在明确边界内用安全默认值。
|
||||
- **下发面要发全**:因前端以服务端为权威、不自算权威结果,服务端**每个下发包必须携带前端界面所需的全部核心数据**(界面要显示、或前端 set-refresh 要用的字段都发全);漏发 = 前端缺数据,**修在服务端发包处、前端不补洞**(详见 [03 §1.2](./03-数据收发与通信协议.md))。
|
||||
- **前端不是数据源**:客户端只提供"意图"(想做什么、对哪个目标),**永远不是任何业务数据的权威来源**。凡进入服务端状态的值——分数、牌面、阶段、控制权、结算——只能由服务端自己算出或从 `o_desk.data.*` 读出,**绝不采信包体里前端算好的同名字段**(见 §8)。对应地,阶段/控制权/`availableActions`/倒计时锚点由服务端唯一维护并显式下发,前端不推导(见 [03 §1.4](./03-数据收发与通信协议.md)、前端 [05 §6](../../client/development-guide/05-开发规范与红线.md))。
|
||||
|
||||
> 审查信号:看到 `|| []`、`|| 0`、`|| ''`、三元默认、双源字段,先判断它是不是**权威字段**(影响发牌/庄家/手牌/规则/结算/重连/状态流转)。是 → 改成权威读取;只是展示/日志 → 才可保留默认。
|
||||
|
||||
@@ -117,6 +118,15 @@ function foo() { GameStateManager.doSomething(); }
|
||||
4. **幂等 / 去重 / 时序(连点的根治)**:不是限流,而是**状态机的单调推进**——一个操作被处理后立即推进状态并更新 `availableActions`,使**重复包因落在新 `availableActions` 之外而天然非法**;响应窗口对同座位重复响应去重(如 `addPlayerResponse` 记录后再收同座位响应即忽略);对已结束窗口/轮次的迟到包丢弃。
|
||||
5. **参数 / 数据合法性**:牌 `uniqueId` 是否真在该玩家手里、`targetCard`/`fromSeat`/`choiceIndex` 是否与权威牌局一致 —— 全按**服务端权威数据**校验,不信客户端传的牌面。
|
||||
|
||||
### 只接受「意图」入参:客户端回传的结论一律忽略
|
||||
|
||||
第 5 层的前提是**入参里根本不该出现结论**。请求包只表达"我想做什么"(操作类型 + 目标标识),**不表达"结果是什么"**(前端侧规范见 [客户端 04 §1「请求包只带意图」](../../client/development-guide/04-网络对接与启动编排.md)):
|
||||
|
||||
- **协议层不定义结论入参**:`score`/`isWin`/`multiplier`/`nextSeat`/`phase`/`result`/`handCards` 之类字段**不出现在请求包定义里**——定义了它,就是留了一个可被伪造的洞。
|
||||
- **收到也忽略**:即便非正规客户端硬塞这些字段,handler **一律不读、不落地**,全部按服务端权威数据重算。审查信号:handler 里出现 `pack.data.score`、`pack.data.isWin`、`pack.data.phase` 这类读取。
|
||||
- **包内 `seat` 只作一致性校验**:身份由连接反查(第 1 层),包内座位不等即拒,**不作身份依据**。
|
||||
- **前端 `shared/` 的计算结果不是输入**:前端跑 `shared/` 只为提示与预校验,其结论不回传、服务端也不采信;服务端自己跑同一份 `shared/` 得出权威结果(见 §9)。
|
||||
|
||||
### 设计形态:统一裁决网关,默认拒绝(fail-closed)
|
||||
|
||||
把五层收敛成**一道所有操作共用的准入网关**(而非每个 handler 各写一遍散点 if)。网关默认 **deny**,仅当请求显式**命中该座位 `availableActions`** 且通过座位/阶段/参数校验才 **allow**。好处:
|
||||
@@ -204,6 +214,9 @@ function foo() { GameStateManager.doSomething(); }
|
||||
| 语言 | 纯 ES5;`require` 仅在顶部守卫块 |
|
||||
| 成败 | 只认 `data.success`,推送必自带,禁 `status` 兜底 |
|
||||
| 数据 | 权威唯一、缺失报错/返回 null、禁兜底、下游不修补;下发包发全前端界面所需核心数据 |
|
||||
| 前端不是数据源 | 请求包只接受「意图」(操作类型+目标标识),协议不定义 `score`/`isWin`/`phase` 等结论入参,收到也忽略并按权威数据重算;包内 `seat` 只作一致性校验 |
|
||||
| 下发可见性 | 不该看到的不发(他人手牌/牌堆序列/未公开判定/未来结果),按座位差异化裁剪;"下发即泄露",前端不渲染不算防护(03 §1.3) |
|
||||
| 状态机归属 | 阶段/控制权/`availableActions`/倒计时锚点由服务端唯一维护并显式下发,前端不推导;状态变更必须有包承载(03 §1.4) |
|
||||
| 职责 | 一职能一模块,调用不重造 |
|
||||
| 隔离 | 状态挂 `o_desk.data.*`,禁全局;`房间+seat` 作 key;定时器随房清理 |
|
||||
| 自动操作 | 复用真人链路,对前端透明 |
|
||||
|
||||
@@ -18,74 +18,22 @@
|
||||
|
||||
| 篇 | 文档 | 解决什么问题 |
|
||||
|----|------|--------------|
|
||||
| 00 | 本文 README | 这套文档是什么、怎么读、红线速查 |
|
||||
| 00 | 本文 README | 这套文档是什么、怎么读 |
|
||||
| — | [红线速查.md](./红线速查.md) | **一页纸运作模型 + 命名约定 + 红线清单**(由 `CLAUDE.md` 常驻加载,改代码前先对照) |
|
||||
| 01 | [01-服务端环境与框架基础.md](./01-服务端环境与框架基础.md) | 平台与子游戏的关系、核心对象模型、三层路由、房间生命周期 |
|
||||
| 02 | [02-子游戏接入与开发流程.md](./02-子游戏接入与开发流程.md) | 三文件架构、export/import 接口、makewar/重连、一次操作的完整数据流 |
|
||||
| 03 | [03-数据收发与通信协议.md](./03-数据收发与通信协议.md) | 包结构、收发包方式、主动推送、成败标志协议、前后端对接点 |
|
||||
| 04 | [04-开发规范与红线.md](./04-开发规范与红线.md) | 可编辑范围、ES5/require、数据权威、模块职责、房间隔离、测试纪律 |
|
||||
| 03 | [03-数据收发与通信协议.md](./03-数据收发与通信协议.md) | 包结构、收发包方式、**下发面发全 / 按可见性裁剪 / 状态机唯一在服务端**、主动推送、成败标志协议、前后端对接点 |
|
||||
| 04 | [04-开发规范与红线.md](./04-开发规范与红线.md) | 可编辑范围、ES5/require、数据权威(含**前端不是数据源**)、模块职责、房间隔离、请求合法性验证、测试纪律 |
|
||||
|
||||
建议第一次**从 01 顺序读到 04**;之后把 03/04 当手册随用随查。
|
||||
|
||||
---
|
||||
|
||||
## 一页纸:核心运作模型
|
||||
## 一页纸与红线
|
||||
|
||||
```
|
||||
客户端数据包 { app, route, rpc, data }
|
||||
│
|
||||
▼
|
||||
packet_face.ReceivePack 按 pack.app 找到「应用」
|
||||
│
|
||||
▼
|
||||
app.ReceivePack 按 pack.route 找到「模块」(子游戏)
|
||||
│
|
||||
▼
|
||||
mod.DoPack 按 pack.rpc 调到「方法」 mod[rpc](pack)
|
||||
│
|
||||
▼
|
||||
子游戏 RPC handler 校验玩家 → 取对局状态 → 执行业务 → 主动推送
|
||||
│
|
||||
▼
|
||||
o_room.method.sendpack_toseat 按座位取连接信息(conmode/fromid) → 下发客户端
|
||||
```
|
||||
**运作模型、命名约定与红线清单已独立成篇:[红线速查.md](./红线速查.md)。**
|
||||
|
||||
- **平台框架**负责:网络收发、应用/模块/房间/玩家对象、路由、房卡、战绩、房间生命周期。
|
||||
- **子游戏**负责:玩法规则、对局状态、每个操作的处理与广播。两者通过 **export / import** 两组接口对接。
|
||||
|
||||
> 上图是**入站半程**(前端 → 服务端 handler)。完整的往返链路(含服务端 `sendpack_toseat` 推回 → 前端 `Game_Modify._ReceiveData` 按 `rpc` 分发)见 [03 §6「端到端收发全链路」](./03-数据收发与通信协议.md#6-端到端收发全链路前后端对照),前端侧细节见 [前端 04 网络对接与启动编排](../../client/development-guide/04-网络对接与启动编排.md)。
|
||||
|
||||
---
|
||||
|
||||
## 命名约定:哪些是框架契约,哪些只是示例
|
||||
|
||||
本套文档的代码示例里出现的**具体名字,绝大多数是举例,并非强制标准**。请区分两类:
|
||||
|
||||
- **框架契约(必须照用)**——由平台代码决定,改了就对接不上:
|
||||
- 数据包四字段 `{ app, route, rpc, data }`,其中 `app` 固定为 `"youle"`;
|
||||
- 平台真实回调的 export 钩子名、你调用的 import 接口名(如 `makewar`、`get_deskinfo`、`check_player`、`deduct_roomcard`、`save_grade`);
|
||||
- 成败字段 `data.success`;
|
||||
- 平台 API 名(`cls_mod.new`、`min_loadJsFile`、`o_room.method.sendpack_toseat` / `sendpack_toother` 等)。
|
||||
- **示例命名(可自定)**——本项目的举例,你的子游戏可按自己的风格命名:
|
||||
- 业务 `rpc` 名(如 `playCard`、`declareHu`,只需**前后端约定一致**即可);
|
||||
- handler / 类 / 文件 / 变量 / 分层目录名(如 `RpcHandler.handlePlayCard`、`OperationManager`、`ResponseBuilder`、`BroadcastManager`、`RoomAdapter`、`GameController`、`game/`、`rpc/`、`rules/` 等)。
|
||||
|
||||
> 一句话:**平台接缝上的名字是契约、要照用;接缝之内你自己代码里的名字都是示例、可自定。** 后续 01–04 的代码块同样遵循这条约定,不再逐处重复声明。
|
||||
|
||||
---
|
||||
|
||||
## 红线速查(详见 04)
|
||||
|
||||
> 以下每一条都有过真实事故或返工,**改代码前先对照**。
|
||||
|
||||
- **可编辑范围**:服务端只能改子游戏目录 `server/<游戏容器目录>/<你的游戏>/`(容器目录名由接入方自定,**非框架强制**,换任何不与框架冲突的目录皆可),`server/` 其余皆平台代码,禁改。接入新游戏还需在 `server/youle/app.js` 里加一行 `min_loadJsFile` 加载其 `mod.js`(唯一必须触碰的平台文件,属游戏注册接入点)。
|
||||
- **双运行时**:代码同时跑在 Node 与浏览器/友乐平台。必须严格 **ES5**;`require` 只能写在文件开头 `if (typeof require !== 'undefined') {}` 守卫块内,**禁止函数体内/中途 require**。
|
||||
- **成败标志唯一是 `data.success`**:前端只认 `if (!data.success)`;**主动推送的 `data` 必须自带 `success`**(mod 的 `return` 值不可靠下发前端);禁止用 `status`/`code` 判成败、禁止 `status` 兼容兜底。
|
||||
- **数据权威**:同一业务数据只有一个权威来源,下游只读不重算;权威字段缺失要**显式报错/返回 null**,禁止 `|| 0`、`|| []`、`|| ''` 兜底掩盖。
|
||||
- **模块职责边界**:一个职能只在一个模块实现,其他模块**调用而非重造**。
|
||||
- **房间隔离**:一切随对局变化的状态都挂 `o_room.o_desk.data.*`;**禁止模块级单例/全局变量存对局态**;缓存/定时器/决策表不得只用 `seat` 作 key,必须 `房间 + seat`;结束/解散/开新局时按房间清理全部定时器与残留状态。
|
||||
- **服务端自动操作复用真人链路**:服务端代替玩家做的操作(如 AI 托管)必须复用「真人操作」的同一套处理入口与广播链路,对前端透明,不为它单开一套下发分支。
|
||||
- **服务器权威 + 发全下发面**:房卡、胜负、积分、状态等关键数据与操作一律以服务器为准、前端只作显示不自算;因此**服务端每个下发包必须携带前端界面所需的全部核心数据**,漏发修在服务端发包处、前端不补(详见 03 §1.2 / 04 §4)。
|
||||
- **测试纪律**:测试失败先用证据裁定「业务缺陷 vs 测试脚本缺陷」,禁止 skip/软化断言/吞异常掩盖;**正式代码不得为兼容测试而加逻辑**。
|
||||
该文件由 `CLAUDE.md` 通过 `@import` 常驻每次会话上下文,是红线的权威源;本 README 只负责导航,不重复抄写红线(避免两处不同步)。红线的完整细节见 [04-开发规范与红线.md](./04-开发规范与红线.md)。
|
||||
|
||||
---
|
||||
|
||||
@@ -94,5 +42,3 @@ o_room.method.sendpack_toseat 按座位取连接信息(conmode/fromid) → 下
|
||||
- 本套文档是平台级收发包/子游戏开发规范的**落地版**:结合本仓库真实代码,给出可直接照做的接入步骤与红线,并对其中已被本项目实践修正的部分(如成败标志由 `status` 收敛为 `success`)以本套为准。
|
||||
- 各子游戏内部的架构细节(模块划分、算法)仍以各自 `<游戏容器目录>/<游戏>/docs/` 为准。
|
||||
- **平台无关的通用工程与架构规范**(分层、可扩展模式、配置化、数据权威、反模式与审查清单)见 [工程与架构通则](../../games/engineering/):本套讲"平台怎么接、红线是什么",工程通则讲"该怎么设计、怎么长久演进",互补阅读。
|
||||
</content>
|
||||
</invoke>
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
# 服务端 · 一页纸运作模型与红线速查
|
||||
|
||||
> 本文是服务端开发**必须常驻在手边**的那一页:运作模型 + 命名约定 + 红线清单。
|
||||
> 它由 `CLAUDE.md` 通过 `@import` 常驻每次会话上下文;**红线以本文为权威**。
|
||||
> 文档导航、阅读顺序、各篇主题见 [README](./README.md);红线的完整细节见 [04-开发规范与红线.md](./04-开发规范与红线.md)。
|
||||
|
||||
---
|
||||
|
||||
## 一页纸:核心运作模型
|
||||
|
||||
```
|
||||
客户端数据包 { app, route, rpc, data }
|
||||
│
|
||||
▼
|
||||
packet_face.ReceivePack 按 pack.app 找到「应用」
|
||||
│
|
||||
▼
|
||||
app.ReceivePack 按 pack.route 找到「模块」(子游戏)
|
||||
│
|
||||
▼
|
||||
mod.DoPack 按 pack.rpc 调到「方法」 mod[rpc](pack)
|
||||
│
|
||||
▼
|
||||
子游戏 RPC handler 校验玩家 → 取对局状态 → 执行业务 → 主动推送
|
||||
│
|
||||
▼
|
||||
o_room.method.sendpack_toseat 按座位取连接信息(conmode/fromid) → 下发客户端
|
||||
```
|
||||
|
||||
- **平台框架**负责:网络收发、应用/模块/房间/玩家对象、路由、房卡、战绩、房间生命周期。
|
||||
- **子游戏**负责:玩法规则、对局状态、每个操作的处理与广播。两者通过 **export / import** 两组接口对接。
|
||||
|
||||
> 上图是**入站半程**(前端 → 服务端 handler)。完整的往返链路(含服务端 `sendpack_toseat` 推回 → 前端 `Game_Modify._ReceiveData` 按 `rpc` 分发)见 [03 §6「端到端收发全链路」](./03-数据收发与通信协议.md#6-端到端收发全链路前后端对照),前端侧细节见 [前端 04 网络对接与启动编排](../../client/development-guide/04-网络对接与启动编排.md)。
|
||||
|
||||
---
|
||||
|
||||
## 命名约定:哪些是框架契约,哪些只是示例
|
||||
|
||||
本套文档的代码示例里出现的**具体名字,绝大多数是举例,并非强制标准**。请区分两类:
|
||||
|
||||
- **框架契约(必须照用)**——由平台代码决定,改了就对接不上:
|
||||
- 数据包四字段 `{ app, route, rpc, data }`,其中 `app` 固定为 `"youle"`;
|
||||
- 平台真实回调的 export 钩子名、你调用的 import 接口名(如 `makewar`、`get_deskinfo`、`check_player`、`deduct_roomcard`、`save_grade`);
|
||||
- 成败字段 `data.success`;
|
||||
- 平台 API 名(`cls_mod.new`、`min_loadJsFile`、`o_room.method.sendpack_toseat` / `sendpack_toother` 等)。
|
||||
- **示例命名(可自定)**——本项目的举例,你的子游戏可按自己的风格命名:
|
||||
- 业务 `rpc` 名(如 `playCard`、`declareHu`,只需**前后端约定一致**即可);
|
||||
- handler / 类 / 文件 / 变量 / 分层目录名(如 `RpcHandler.handlePlayCard`、`OperationManager`、`ResponseBuilder`、`BroadcastManager`、`RoomAdapter`、`GameController`、`game/`、`rpc/`、`rules/` 等)。
|
||||
|
||||
> 一句话:**平台接缝上的名字是契约、要照用;接缝之内你自己代码里的名字都是示例、可自定。** 01–04 的代码块同样遵循这条约定,不再逐处重复声明。
|
||||
|
||||
---
|
||||
|
||||
## 红线速查(详见 04)
|
||||
|
||||
> 以下每一条都有过真实事故或返工,**改代码前先对照**。
|
||||
|
||||
- **可编辑范围**:服务端只能改子游戏目录 `server/<游戏容器目录>/<你的游戏>/`(容器目录名由接入方自定,**非框架强制**,换任何不与框架冲突的目录皆可),`server/` 其余皆平台代码,禁改。接入新游戏还需在 `server/youle/app.js` 里加一行 `min_loadJsFile` 加载其 `mod.js`(唯一必须触碰的平台文件,属游戏注册接入点)。
|
||||
- **双运行时**:代码同时跑在 Node 与浏览器/友乐平台。必须严格 **ES5**;`require` 只能写在文件开头 `if (typeof require !== 'undefined') {}` 守卫块内,**禁止函数体内/中途 require**。
|
||||
- **成败标志唯一是 `data.success`**:前端只认 `if (!data.success)`;**主动推送的 `data` 必须自带 `success`**(mod 的 `return` 值不可靠下发前端);禁止用 `status`/`code` 判成败、禁止 `status` 兼容兜底。
|
||||
- **数据权威**:同一业务数据只有一个权威来源,下游只读不重算;权威字段缺失要**显式报错/返回 null**,禁止 `|| 0`、`|| []`、`|| ''` 兜底掩盖。
|
||||
- **模块职责边界**:一个职能只在一个模块实现,其他模块**调用而非重造**。
|
||||
- **房间隔离**:一切随对局变化的状态都挂 `o_room.o_desk.data.*`;**禁止模块级单例/全局变量存对局态**;缓存/定时器/决策表不得只用 `seat` 作 key,必须 `房间 + seat`;结束/解散/开新局时按房间清理全部定时器与残留状态。
|
||||
- **服务端自动操作复用真人链路**:服务端代替玩家做的操作(如 AI 托管)必须复用「真人操作」的同一套处理入口与广播链路,对前端透明,不为它单开一套下发分支。
|
||||
- **服务器权威 + 发全下发面**:房卡、胜负、积分、状态等关键数据与操作一律以服务器为准、前端只作显示不自算;因此**服务端每个下发包必须携带前端界面所需的全部核心数据**,漏发修在服务端发包处、前端不补(详见 03 §1.2 / 04 §4)。
|
||||
- **状态机唯一在服务端**:前端是**数据驱动**的、不持有对局状态机(见前端 05 §6),所以阶段、控制权(轮到谁)、该座位 `availableActions`、倒计时锚点**必须由服务端唯一维护并在相关下发包里显式给出**;**任何状态变更都要有包承载**——没有包的状态变化等于逼前端去猜。超时裁定在服务端,不接受前端上报"我超时了"(详见 03 §1.4)。
|
||||
- **前端不是数据源,只收「意图」**:请求包只接受「操作类型 + 目标标识」;协议**不定义** `score`/`isWin`/`phase`/`nextSeat`/`handCards` 之类**结论入参**,即便客户端硬塞也一律**不读、不落地**,全部按服务端权威数据重算;包内 `seat` 只作一致性校验、身份由连接反查(详见 04 §8)。
|
||||
- **发全 ≠ 发多,按可见性下发**:他人手牌、牌堆剩余序列、未公开判定、尚未揭晓的结果**不发给不该看到的座位**(差异化下发逐座位裁剪)。**下发即泄露**——包到了客户端就能被抓包看到,"前端拿到但不渲染"不算防护(详见 03 §1.3)。
|
||||
- **测试纪律**:测试失败先用证据裁定「业务缺陷 vs 测试脚本缺陷」,禁止 skip/软化断言/吞异常掩盖;**正式代码不得为兼容测试而加逻辑**。
|
||||
@@ -499,8 +499,11 @@ var cls_youle_erqiwang_arith = cls_youle_erqiwang_arith || {
|
||||
delete can.cardvalue;
|
||||
}
|
||||
|
||||
//规则外的约定,一次最多只能出14张牌
|
||||
if (cards.length > 14){
|
||||
//张数上界:design §5.4.3 明确甩牌「无组合数量限制」,故不设规则性上限;
|
||||
//这里只挡结构性越界——出牌阶段一名玩家最多持 28 张(埋牌后),
|
||||
//超过即不可能是合法出牌(调用方 mod.chupai 已先做 check_cards_inhand 持牌校验)。
|
||||
//注:牌型编码在 28 张内不会溢出各自区间(单张≤128、多对≤214、拖拉机≤314)
|
||||
if (cards.length > 28){
|
||||
do_returnfalse();
|
||||
return can;
|
||||
}
|
||||
|
||||
@@ -166,38 +166,42 @@ var cls_youle_erqiwang_export = cls_youle_erqiwang_export || {
|
||||
}
|
||||
}
|
||||
}
|
||||
//出牌历史
|
||||
deskinfo.PushCards.pushlist = [];
|
||||
for (var i = 0; i < paiju.cards.length; i++) {
|
||||
var pai = paiju.cards[i];
|
||||
if (pai.playround > 0){
|
||||
if (deskinfo.PushCards.pushlist.length < pai.playround){
|
||||
deskinfo.PushCards.pushlist.length = pai.playround;
|
||||
}
|
||||
if (!deskinfo.PushCards.pushlist[pai.playround - 1]){
|
||||
deskinfo.PushCards.pushlist[pai.playround - 1] = [[], [], []];
|
||||
}
|
||||
//出牌历史:往轮打出、已被收走的牌属于「查牌」范畴,仅可查牌模式下发(design §9)。
|
||||
//不查牌模式下不带此属性——当前这一轮桌面上的牌由上面的 playproc 恢复,两种模式都有,
|
||||
//否则后出的人无从跟牌(design §9 末尾的边界说明)
|
||||
if (!cfg.nocheck){
|
||||
deskinfo.PushCards.pushlist = [];
|
||||
for (var i = 0; i < paiju.cards.length; i++) {
|
||||
var pai = paiju.cards[i];
|
||||
if (pai.playround > 0){
|
||||
if (deskinfo.PushCards.pushlist.length < pai.playround){
|
||||
deskinfo.PushCards.pushlist.length = pai.playround;
|
||||
}
|
||||
if (!deskinfo.PushCards.pushlist[pai.playround - 1]){
|
||||
deskinfo.PushCards.pushlist[pai.playround - 1] = [[], [], []];
|
||||
}
|
||||
|
||||
var owner = pai.dealowner;
|
||||
if (owner == 0){
|
||||
owner = paiju.banker + 1;
|
||||
var owner = pai.dealowner;
|
||||
if (owner == 0){
|
||||
owner = paiju.banker + 1;
|
||||
}
|
||||
deskinfo.PushCards.pushlist[pai.playround - 1][owner - 1].push(i);
|
||||
}
|
||||
}
|
||||
//按大小排序:外层下标=轮次、内层恒为 3 个座位;排序须按本局主牌花色
|
||||
//(内层曾误用轮次数作上界,会把内层数组撑成轮次长度并填入 undefined;
|
||||
// 且曾误用上一个循环泄漏出来的 pai.flower——那是最后一张牌大王的花色 5,
|
||||
// 等于按「无主牌」排序)
|
||||
for (var i = 0; i < deskinfo.PushCards.pushlist.length; i++) {
|
||||
if (!deskinfo.PushCards.pushlist[i]){
|
||||
continue;
|
||||
}
|
||||
for (var j = 0; j < 3; j++) {
|
||||
deskinfo.PushCards.pushlist[i][j] = cls_youle_erqiwang_arith.order_cards(paiju.flower, deskinfo.PushCards.pushlist[i][j]);
|
||||
}
|
||||
deskinfo.PushCards.pushlist[pai.playround - 1][owner - 1].push(i);
|
||||
}
|
||||
}
|
||||
//按大小排序:外层下标=轮次、内层恒为 3 个座位;排序须按本局主牌花色
|
||||
//(内层曾误用轮次数作上界,会把内层数组撑成轮次长度并填入 undefined;
|
||||
// 且曾误用上一个循环泄漏出来的 pai.flower——那是最后一张牌大王的花色 5,
|
||||
// 等于按「无主牌」排序)
|
||||
for (var i = 0; i < deskinfo.PushCards.pushlist.length; i++) {
|
||||
if (!deskinfo.PushCards.pushlist[i]){
|
||||
continue;
|
||||
}
|
||||
for (var j = 0; j < 3; j++) {
|
||||
deskinfo.PushCards.pushlist[i][j] = cls_youle_erqiwang_arith.order_cards(paiju.flower, deskinfo.PushCards.pushlist[i][j]);
|
||||
}
|
||||
}
|
||||
break;
|
||||
break;
|
||||
case 6: //结算
|
||||
deskinfo.Balance = {};
|
||||
//玩家的准备状态
|
||||
|
||||
@@ -749,11 +749,14 @@ var cls_youle_erqiwang_paiju = cls_youle_erqiwang_paiju || {
|
||||
|
||||
//获取庄家亮牌信息(design §8.2):基于庄家埋牌后 28 张手牌,达标才亮,只亮数量/结构
|
||||
//返回 null 表示无需亮牌;否则返回 {zhu,zhupair,zhutuo?, wang?, qi?, er?}
|
||||
//手牌来源必须是 get_seat_cards_award 的【静态快照】(埋牌后 28 张,含之后已打出的),
|
||||
//不能用 get_seat_cards(只取未出的牌):出牌开始亮出的这份统计全局固定、不随出牌缩水,
|
||||
//否则闲家断线重连后 get_deskinfo 会按庄家当前剩余手牌重算,亮牌信息缩水甚至变成 null
|
||||
get_liangpai: function(o_paiju){
|
||||
if (o_paiju.banker < 0){
|
||||
return null;
|
||||
}
|
||||
var cards = cls_youle_erqiwang_paiju.get_seat_cards(o_paiju, o_paiju.banker);
|
||||
var cards = cls_youle_erqiwang_paiju.get_seat_cards_award(o_paiju, o_paiju.banker);
|
||||
var wang = 0, qi = 0, er = 0;
|
||||
var zhucards = [];
|
||||
for (var i = 0; i < cards.length; i++){
|
||||
|
||||
@@ -49,3 +49,5 @@
|
||||
---
|
||||
|
||||
以上为初步清单。经逐节精查后,另发现若干**运行期代码缺陷**(如 `can_followcard` 引用未定义变量 `tuolaji_list` 会抛异常、`can_playcard` 的 14 张上限 `do_returnfalse` 漏写括号导致失效等)、以及**甩牌合法性 / 强制跟牌 / 局数扣卡 / 查牌模式**等更多不一致,详见 [`01-design合规逐节核对.md`](./01-design合规逐节核对.md)。
|
||||
|
||||
> **本文与 01 的前两轮都只沿 design.md 逐节核对玩法规则**,因此平台接入、客户端入参校验、下发面一致性、协议红线这几类问题整类不在核对范围内。第三轮补上这些维度后又查出 1 项阻断级(子游戏从未在 `server/youle/app.js` 注册,运行时完全不可达)、3 项严重(重复牌id 可伪造牌型、入参无类型校验、下发面 `multiple` 忽略爬坡开关)与协议红线缺失(全仓无 `data.success`),见 [`01`「第三轮核对」](./01-design合规逐节核对.md#第三轮核对平台接入--入参--下发面--协议红线)。
|
||||
|
||||
@@ -17,8 +17,10 @@
|
||||
|
||||
> 本轮已按 design.md 修正的项(均已用 Node 脚本对 design 判定表逐格验证,见"验证"列)。下方"结论摘要"与各节详评仍保留**原始不一致**描述以留痕;本节记录当前状态。
|
||||
|
||||
> **二次全量复核结论**:对整改后代码做了一次独立对抗性复核(§7/§8 算子算奖、§5 出牌甩牌、§4 开局投降暗牌阶段机、§2/§3/§9/§10 编码查牌房间,逐条比对 + Node 实测)。结果:**服务端全部符合 design.md**,且未发现整改引入的符号/下标/边界错、无 step4 死代码残留、投降 36 张快照与 X×N 算奖均正确。
|
||||
> **二次全量复核结论(限定范围)**:对整改后代码做了一次独立对抗性复核(§7/§8 算子算奖、§5 出牌甩牌、§4 开局投降暗牌阶段机、§2/§3/§9/§10 编码查牌房间,逐条比对 + Node 实测)。结果:**就 design.md 的玩法规则维度而言,服务端符合**,且未发现整改引入的符号/下标/边界错、无 step4 死代码残留、投降 36 张快照与 X×N 算奖均正确。
|
||||
> - 复核时发现的唯一未落地项 **§5.4.4 强制跟牌分量拆解已于本轮修复**:合法甩牌在 `can_playcard` 记录分量需求 `shuai_demand`(拖拉机长度/对子/单张),存入 `playproc`;跟牌时 `do_playcard` 调 `arith.flush_follow_ok` 在"同花色凑张数、主牌最大化"之上追加"拖拉机→对子→单张"的强制分量匹配(能凑同长主拖必凑、能凑主对必凑,不得拆散去垫)。已用 Node 正反面/边界用例验证。
|
||||
>
|
||||
> ⚠️ **该结论只覆盖"玩法规则是否符合 design.md"这一个维度**。第三轮核对(见下方「第三轮核对」)在**平台接入、客户端入参校验、下发面一致性、协议红线**四个维度上又发现 1 项阻断级、3 项严重、若干中等缺陷——它们不属于 design.md 的玩法条款,所以逐节核对没有覆盖到,但同样会让服务端跑不起来或被利用。**"符合 design.md" ≠ "实现完整正确"**,后续复核不要再把前者当成后者。
|
||||
|
||||
| 项 | 位置 | 整改内容 | 验证 |
|
||||
| --- | --- | --- | --- |
|
||||
@@ -64,6 +66,73 @@
|
||||
|
||||
---
|
||||
|
||||
## 第三轮核对(平台接入 / 入参 / 下发面 / 协议红线)
|
||||
|
||||
> 前两轮只沿 design.md 逐节核对玩法规则,因此**整类问题在核对范围之外**。本轮补上这些维度,结论如下(均已修复并有单测回归)。
|
||||
|
||||
| # | 严重度 | 位置 | 问题 | 处置 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| A1 | 🟥 阻断 | `server/youle/app.js` | 只加载 `games2/jinxianmahjong/mod.js`,**从未加载 `games/erqiwang/mod.js`**,全仓平台侧对 erqiwang 零引用 → `youle_erqiwang` 模块从不创建、`pack.route="erqiwang"` 无法命中三层路由,**整个子游戏运行时不可达** | 已在 app.js 补注册(server dev-guide README 允许触碰的唯一平台文件) |
|
||||
| B1 | 🟥 严重 | `class.paiju.js` `check_cards_inhand` | 不查重复牌id:`do_playcard([方块K,方块K])` 实测 `result=true, cardtype=201`——同一张牌被判成一对,可伪造拖拉机/甩牌分量,且该分牌 score 被重复累加;`maipai` 传 8 个相同 id 实测**只埋下 1 张**,庄家带 35 张进入出牌阶段 | 新增 `check_cards_valid`(非空数组 / 0~107 整数 / 无重复)作为唯一入参校验入口 |
|
||||
| B2 | 🟥 严重 | `mod.js` `maipai`/`chupai` | 越界 id 或非数组入参在解引用处抛异常,中断 `DoPack`;且 `cards.length` 读在校验之前 | 校验前置于 length 判断之前 |
|
||||
| B3 | 🟥 严重 | `arith.get_multiple_bycall` + 6 个调用点 | 写死 `climb=false`:**爬坡房对局全程界面显示常规算子子数,与结算 `aset.multiple` 不符**(叫 45 分:界面 6、结算 7) | 删除该兼容包装,全部改调 `get_base_bycall(call, cfg.climb)`,与结算同源 |
|
||||
| C1 | 🟥 红线 | 全部下发包 + 所有失败分支 | **全仓无 `data.success`**;失败一律裸 `return`、不回任何包,前端只能干等倒计时。违反 server dev-guide「成败唯一是 data.success、主动推送必须自带」与 client dev-guide「发包只请求、收包才表现」 | 成功包统一补 `success:true`;新增 `ERR` 码表 + `do_sendfail`,每条失败分支回 `success:false + errcode` 给请求者 |
|
||||
| D1 | 🟧 中等 | `arith.is_continuous` | `is_8`/`is_6` 只判 `code % 100`、不校验花色:`is_continuous(♥8=308, ♣6=206)` 实测为 `true`。`get_chongguan` 是唯一扫描整手牌(含各花色副牌)的调用点,连对链走到主 8 后会被任意花色的一对 6 错误接续、多算奖(design §8.1 要求 A~5 段必须是主花色) | 追加「编码相差 2」约束(同花色 8/6 恒差 2) |
|
||||
| D2 | 🟧 中等 | `export.get_deskinfo` pushlist | ① 内层循环上界误用轮次数当座位数,实测 5 轮时内层被撑成长度 5 且含 `undefined`;② 排序传的是上个循环泄漏的 `pai.flower`(=最后一张牌大王的花色 5),等于按「无主牌」排序 | 内层固定 3、改用 `paiju.flower`,并对缺失轮次加守卫 |
|
||||
| D3 | ✅ 规则如此 | `class.desk.js` | 下发了 `countdown`,但服务端无任何定时器做超时托管,玩家不操作则牌局停住等待 | **规则设计者确认:维持现状、不做超时动作**。已写入 design §11「倒计时只作展示,不触发任何自动操作」并注明不是缺口;牌局停住由玩家走房间解散流程收场(按当前累计分结算,design §12.2)。server dev-guide「服务端自动操作复用真人链路」约束的是「若要做代打就必须复用真人链路」,本局不做代打,故不适用 |
|
||||
| D4 | 🟨 轻微 | `paiju.do_playcard` | 报无主后只更新出牌者自己那一格,B/C 要等各自轮到出牌才补上,界面最多滞后两次出牌(design §9 要求「为全体三人显示另外两家」) | 改为一旦 `have_baofu()` 成立即按实际手牌刷新三个座位 |
|
||||
| D5 | 🟨 轻微 | `can_playcard` / `chupai1` | `cardtype` 只能表达单一牌型,甩牌被压平成 1xx/2xx(「主K对+主5」→103),客户端连"这是一次甩牌"都看不出来 | `chupai1` 增发 `shuai` 分量构成(与 `shuai_demand` 同源) |
|
||||
| D6 | 🟧 中等 | `export.get_deskinfo` | 不查牌模式下 `pushlist`(已打出牌历史)仍照发。规则设计者已确认:**不可查牌模式必须屏蔽出牌历史,只有可查牌模式才能查看** | 已按确认修复:`pushlist` 按 `cfg.nocheck` 门控;design §9 同步改写为无歧义表述(四项功能整体开关 + 「当前轮桌面牌不属于查牌、两种模式都可见」的边界) |
|
||||
| D7 | 🟨 轻微 | `check_cards_inhand` | 牌已出/已埋时 `return;`(undefined)而非 `false` | 已改为 `return false` |
|
||||
|
||||
### 尚未处置
|
||||
|
||||
**无。** 本轮发现的 11 项已全部收口:
|
||||
|
||||
- A1/B1/B2/B3/C1/D1/D2/D4/D5/D7 共 10 项为缺陷,已改代码 + 补单测;
|
||||
- D6(不查牌是否屏蔽出牌历史)原为规则歧义,已由规则设计者拍板「屏蔽」,改 design §9 + 代码 + 单测;
|
||||
- D3(超时托管)原列为待外部输入,已由规则设计者拍板 **维持现状、不做超时动作**,属**规则如此、并非缺陷**,已写入 design §11 固化,避免后续核对反复把它当缺口重提。
|
||||
|
||||
---
|
||||
|
||||
## 第四轮核对(全量重核 design.md:先流程、后规则细节)
|
||||
|
||||
> 从 design.md §1~§12 重新逐条对照当前代码(含流程阶段机与全部规则细则),**流程层无缺环**(发牌→叫分→摸暗牌/70分亮3秒→选主/投降互斥→先选主后埋牌→出牌→末轮扣底→算子+算奖→轮庄→大局/解散结算,`step` 迁移 1→2→3→5→6 闭合)。规则层新发现 3 项不一致,已全部修复并补回归单测。
|
||||
|
||||
| # | 严重度 | 位置 | 问题 | 处置 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| E1 | 🟧 中等 | `class.paiju.js` `get_liangpai` | 亮牌统计取 `get_seat_cards`(只含未出的牌),而 §8.2 的依据是**庄家埋牌后的手牌**。埋牌那一刻算对,但 `get_deskinfo` 在 step5 重连时会按庄家**当前剩余手牌**重算:实测「4王+6个7」的庄家打掉 2 大王 + 1 对♦7 后,重连再取从 `{zhu:10,zhupair:5,zhutuo:1,wang:4,qi:6}` 变成 `null`,闲家重连即丢失亮牌 | 改用已有的静态快照 `get_seat_cards_award(o_paiju, banker)`(埋牌后 28 张、含之后已打出的),并在函数注释里固化"不得用 get_seat_cards"的原因;补 3 条回归用例(快照 / 打出后重连不缩水 / 已埋牌不计入) |
|
||||
| E2 | 🟨 轻微 | `mod.js` `chupai` | 第三轮 D4 只修了服务端状态(`do_playcard` 里已按 `have_baofu()` 同时刷新三个座位),**下发面没跟上**:出牌包仍只带 `info = seatlist[seat]` 一家,另两家的主牌数/对子数要等它们各自出牌才送达,仍最多滞后两次出牌,与 §9「一旦有人报无主,立即为全体三人显示另外两家」不符 | 出牌包改发整表 `seatlist`(三家),字段名/结构与重连包 `PushCards.seatlist` 统一;协议文档三处 `info` 条目同步改写;补「整表带三家 + 含非出牌者统计」用例 |
|
||||
| E3 | 🟨 轻微 | `class.arith.js` `can_playcard` | 开头 `if (cards.length > 14)` 是"规则外的约定"硬上限,与 §5.4.3「甩牌无组合数量限制」冲突:手持 15 张以上顶级主牌时的合法甩牌会被整个拒掉(如 4王+8个7+正2对+副2对 共 16 张) | 上限改为结构性上界 28(出牌阶段单人最多持 28 张,且 28 张内牌型编码不溢出 1xx/2xx/3xx 区间),不再设规则性限制;补 16 张合法甩牌用例 |
|
||||
|
||||
**非缺陷的观察(本轮确认,勿再重提)**:
|
||||
|
||||
- 70 分「暗牌亮 3 秒」:服务端在上庄包里一次性下发 8 张暗牌 + `ancard3s=1`,"摸入前亮 3 秒"的先后由前端表现;重连不重放该一次性事件。design 未要求服务端表达先后,**按现状即合规**。
|
||||
- §4/§11「选主按钮显示各花色对子数」:design 标注为**前端表现**,服务端已把庄家全部手牌下发,前端可自算,**不是下发面缺口**。
|
||||
|
||||
### 尚未处置
|
||||
|
||||
**无。** E1/E2/E3 已全部改代码 + 补单测,协议文档同步。
|
||||
|
||||
### 第四轮·复验(E1~E3 修复后的独立取证复核)
|
||||
|
||||
对修复后的实现又做了一次**取证式**复核:不再靠通读,而是把 design 的判定语句直接做成可执行探针打在代码上。**未发现新的不一致**,全部符合。取证覆盖:
|
||||
|
||||
| 维度 | 取证方式 | 结果 |
|
||||
| --- | --- | --- |
|
||||
| §5.1/§5.2 跟牌 | 层级 2/3/4(放着更长拖拉机不出→拒、拆对出散张→拒、留着对子只出散张→拒)、同花色不足全出+缺口任意、混合出牌牌面判 0、毙牌须完全缺门且牌型对应、首家出主必须跟主 | 15/15 符合 |
|
||||
| §8.1 算奖 | 三/四王、链从正 7 起、无正 7 链断、正 7-副 7-正 2-副 2-主 A-主 K 链 5、副 K 对(非主花色)不入链、六/七/八个 7 与 2、固定主 ≥10 不给奖 | 13/13 符合 |
|
||||
| §7 算子 | 常规与爬坡逐档抽查 18 格(含大光/小光/过庄/升 N 级、爬坡 45 独有档、10 分与 5 分无小光档) | 18/18 符合 |
|
||||
| §5.4 甩牌 | 单一牌型不算甩牌、分量被更大同长拖拉机压→甩错、对手仅持更大散张压不了对子、甩错只打最小一张、"持有即算被压" | 6/6 符合 |
|
||||
| §6.3 扣底 | 混合甩牌取最高规格倍数、末轮用副牌赢不扣底 | 符合 |
|
||||
| §8.4 结算 | **闲家也持奖时的三条独立支付线**(此前未覆盖):`N=[4,1,0]`→`aw=[28,-8,-20]`,与捡分子数叠加后零和 | 符合 |
|
||||
| §12 全流程 | 端到端跑通整局 + 18 局随机模糊(含甩牌轮),校验牌张守恒/捡分一致/结算零和/分配公式/扣底触发条件 | 无异常 |
|
||||
| §4/§9/§10/§11 | 叫分 6 种序列(含跳过已"不叫"者)、70 分暗牌下发与投降资格、重连各阶段门控(暗牌仅庄家、pushlist/seatlist/亮牌按 `nocheck`、当前轮桌面牌两种模式都有)、爬坡房 `multiple` 三处同源、提示只发对家、明牌返回另两家全部主牌、扣卡 4 组合 | 全部符合 |
|
||||
|
||||
固化产物:新增 `test/test_endgame.js`(L4 端到端,21 项,断言均与具体牌面无关、连跑 10 次无抖动),全套单测 **389 项**全绿。
|
||||
|
||||
---
|
||||
|
||||
## 结论摘要
|
||||
|
||||
> **注**:下表是**首轮核对**的初始发现快照(保留以记录起点)。其中 §4.4、§5.4、§6.3、§7、§8.1-8.4、§9、§10、§11 等标 🟥/🟧/🟨 的项**后续均已修复并验证**——当前逐节状态以本文档后面的分节详情及全套单测(270 项全绿)为准。
|
||||
|
||||
@@ -147,7 +147,7 @@
|
||||
|
||||
## 5. 当前状态小结 —— ✅ DoD 达成,P0/P1/P2 全部完成
|
||||
|
||||
**共 270 项断言全绿**(`test_arith` 101 / `test_callgrade` 6 / `test_config` 19 / `test_deal` 12 / `test_desk` 6 / `test_follow` 50 / `test_paiju` 31 / `test_rpc` 45)。
|
||||
**共 361 项断言全绿**(`test_arith` 106 / `test_callgrade` 6 / `test_config` 19 / `test_deal` 12 / `test_desk` 6 / `test_follow` 50 / `test_input` 34 / `test_paiju` 31 / `test_rpc` 68 / `test_success` 29)。
|
||||
|
||||
design 全部服务端可验证章节均有正/反/边界单测:
|
||||
- **§2 构成**:`test_deal` 92张/去3-4/三家28+底8/分值。
|
||||
@@ -162,3 +162,26 @@ design 全部服务端可验证章节均有正/反/边界单测:
|
||||
- **§12**:端到端(叫分→上庄→选主→埋牌→出牌ready,真实 RPC 驱动)。
|
||||
|
||||
> 说明:§12 覆盖到"出牌准备就绪"(各阶段串接正确);完整 28 墩逐墩对打需一个合法出牌选择器,属后续增强,非规则合规缺口(逐墩规则已由 §5/§6 单测覆盖)。
|
||||
|
||||
---
|
||||
|
||||
## 6. 第三轮补充:design 之外的可验证维度
|
||||
|
||||
> 前两轮的覆盖矩阵完全按 design 章节组织,因此**平台接入、客户端入参、下发面一致性、协议红线**这几类根本不在矩阵里——270 项全绿却漏掉了 1 项阻断级与 3 项严重缺陷(见 `01-design合规逐节核对.md`「第三轮核对」)。本节把这些维度补进测试计划,后续复核必须一并跑。
|
||||
|
||||
| 维度 | 被测 | 用例(正/反/边界) | 文件 | 状态 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 客户端入参合法性 | `check_cards_valid` / `check_cards_inhand` | 正:单张/多张/边界 id 0 与 107;反:非数组(undefined/null/字符串/伪数组)、空数组、重复 id、越界、负数、小数、数字字符串;反:不在手上/已出/已埋 | `test_input` | ✅ 21 |
|
||||
| 重复 id 不得伪造牌型 | `mod.maipai` / `mod.chupai` | 反:8 个重复 id 埋牌被拒且一张未埋;反:重复 id 出牌被拒;正:8 张不同牌埋牌成功且实埋 8 张;反:越界/非数组入参不抛异常且不产生出牌 | `test_input` | ✅ 7 |
|
||||
| §9 报无主即时刷新 | `do_playcard` | 正:打空瞬间三家统计与主花色标志同时刷新;边界:报无主前三家均为 `[-1,-1]` | `test_input` | ✅ 6 |
|
||||
| 红线 `data.success`(成功包) | `class.desk`/`mod.js`/`get_paiju_account` | 正:fapai/zhunbei/jiaofen/shangzhuang/xuanzhu/maipai/chupai1/mingpai/tishi 均带 `success:true`;正:jiesuan 的正常/投降/解散三种 type 都带 | `test_success` | ✅ 12 |
|
||||
| 红线 `data.success`(失败回包) | 各 handler 失败分支 | 反:每个 handler 取代表性失败路径,断言"恰好回 1 包 + `success:false` + errcode 正确 + 回到请求者 fromid",覆盖 STEP/SEAT/PARAM/RULE/PLAYER 五类 | `test_success` | ✅ 17 |
|
||||
| §7.3 下发面 multiple 与结算同源 | `mod.jiaofen` / `get_deskinfo` / `get_paiju_account` | 正:叫 45 分在常规房/爬坡房分别得 6/7;正:上庄包与重连 ChooseMain 一致;边界:与结算包 `aset.multiple` 逐一对齐 | `test_rpc` | ✅ 7 |
|
||||
| §5.4 甩牌下发面 | `chupai1` | 正:合法甩牌带 `shuai` 分量;反:非甩牌不带;反:甩错只打最小一张且不带 `shuai` | `test_rpc` | ✅ 8 |
|
||||
| 重连 pushlist 结构 | `get_deskinfo` | 正:轮数正确、内层恒 3 个座位、无 undefined;正:按本局主牌花色排序(主5 排在副A 前) | `test_rpc` | ✅ 4 |
|
||||
| §9 出牌历史按查牌模式门控 | `get_deskinfo` | 反:不查牌房无 `pushlist`;正:可查牌房有 `pushlist`;边界:两种模式下当前轮桌面牌 `playproc.cards` 都必须恢复 | `test_rpc` | ✅ 4 |
|
||||
| §3 相邻链花色 | `is_continuous` | 正:同花色 8-6 连;反:三组跨花色 8-6 不连;反:差 2 但非 8/6 不连 | `test_arith` | ✅ 5 |
|
||||
|
||||
**关于超时**:`countdown` 只作展示、服务端不做任何超时动作,这是 design §11 确认的规则(并非缺口),因此**没有也不需要「超时自动操作」的用例**;反过来,若将来有人给服务端加了超时代打,那才是违反 design §11,应由核对拦下。
|
||||
|
||||
> 原列在此处的两项「待外部输入」均已由规则设计者拍板:「不查牌模式是否屏蔽出牌历史」确认屏蔽,已改 design §9 + 代码 + 用例(见上表);「超时托管默认动作」确认维持现状、不做超时动作,已写入 design §11。至此测试计划无待外部输入项。
|
||||
|
||||
@@ -619,12 +619,20 @@
|
||||
|
||||
## 9. 牌局查看模式
|
||||
|
||||
- **可查牌模式**:
|
||||
- 当前牌局中,所有玩家已经打出的牌,任何时候都可以查看。
|
||||
- 一旦有玩家的主牌全部打空(即 5.4.2 节所说的"报无主"状态;"主牌"指大王、小王、所有花色的 2、所有花色的 7,以及主花色的其余普通牌,见第 3 节),系统就为**全体三人**(含刚刚报无主的这位玩家自己)显示另外两家各自的主牌数量、以及这些主牌里有多少对子。
|
||||
- 同时界面上会出现一个"明牌"按钮:点击后可以查看另外两家手中全部主牌的**具体牌面**(不只是数量 / 对子结构),再点一次取消查看、恢复隐藏。
|
||||
- 庄家若触发了 8.2 节的"亮牌要求",两个闲家可以看到对应的统计信息(只亮数量 / 结构,不亮具体牌面,见 8.2 节)。
|
||||
- **不可查牌模式**:不提供以上任何查看 / 明牌功能——即使有玩家报无主,也不会显示任何人的主牌数量、对子结构,也没有"明牌"按钮;庄家即使触发了 8.2 节的亮牌条件,闲家也看不到相应的统计信息(见 8.2 节)。
|
||||
"查牌"指的是**回看那些不该随时可见的信息**:已经打过去的牌、他家手里还剩多少主牌、他家主牌的具体牌面。房间的"查牌模式"开关决定下列四项功能**整体**是否提供。
|
||||
|
||||
- **可查牌模式**(四项功能全部提供):
|
||||
1. **出牌历史**:当前牌局中,所有玩家已经打出的牌(含之前每一轮打出、已被收走的牌),任何时候都可以回看。
|
||||
2. 一旦有玩家的主牌全部打空(即 5.4.2 节所说的"报无主"状态;"主牌"指大王、小王、所有花色的 2、所有花色的 7,以及主花色的其余普通牌,见第 3 节),系统就为**全体三人**(含刚刚报无主的这位玩家自己)显示另外两家各自的主牌数量、以及这些主牌里有多少对子。
|
||||
3. 同时界面上会出现一个"明牌"按钮:点击后可以查看另外两家手中全部主牌的**具体牌面**(不只是数量 / 对子结构),再点一次取消查看、恢复隐藏。
|
||||
4. 庄家若触发了 8.2 节的"亮牌要求",两个闲家可以看到对应的统计信息(只亮数量 / 结构,不亮具体牌面,见 8.2 节)。
|
||||
|
||||
- **不可查牌模式**:以上四项**一项都不提供**——
|
||||
- **不提供出牌历史**:之前各轮打出过什么牌一律不能回看,只能靠玩家自己记牌;
|
||||
- 即使有玩家报无主,也不会显示任何人的主牌数量、对子结构,也没有"明牌"按钮;
|
||||
- 庄家即使触发了 8.2 节的亮牌条件,闲家也看不到相应的统计信息(见 8.2 节)。
|
||||
|
||||
> **"查牌"与"当前这一轮桌面上的牌"是两回事**:本轮已经出过牌的玩家、打在桌面上的这几张牌,**两种模式下都必须对全体可见**——否则后出的人无从跟牌、也无从判断本轮谁最大。查牌模式管的是**已经收走的往轮牌**与**他家手牌信息**能不能回看,不影响当前这一轮的正常出牌展示(断线重连回到牌桌时同理:当前轮桌面上的牌照常恢复,往轮历史则按本节的模式开关决定给不给)。
|
||||
|
||||
---
|
||||
|
||||
@@ -656,6 +664,9 @@
|
||||
1. **踩**:提示对家"我能大过庄家"。
|
||||
2. **没分**:提示对家"我手上没分了"。
|
||||
3. **有分**:提示对家"我手上有分"。
|
||||
- **倒计时只作展示,不触发任何自动操作**:叫分 / 选主 / 埋牌 / 出牌四个阶段都会给出一个倒计时秒数,用于界面提醒当前该谁操作。**倒计时归零后不做任何代打**——不自动叫分、不自动选主、不自动埋牌、不自动出牌,也不判负、不跳过该玩家;轮到谁而谁不操作,牌局就停在这一步一直等。
|
||||
|
||||
> **"无超时托管"是确认过的规则,不是待实现的缺口**:本局不设服务端代打/AI 托管,也不因超时改变任何对局状态。若玩家长时间不操作导致牌局停住,由玩家走房间的**解散**流程收场,按当前累计分结算(见 12.2)。**后续核对不要把"没有超时动作"当成缺陷去补"超时托管/自动出牌"。**
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -4,6 +4,61 @@
|
||||
|
||||
---
|
||||
|
||||
## 0. 通用约定(所有包适用,下文各表不再逐一重复)
|
||||
|
||||
### 0.1 成败标志 `data.success`
|
||||
|
||||
**每一个「服务器 → 客户端」的包,`data` 都必带 `success`**(布尔):
|
||||
|
||||
- `success: true` —— 操作成功 / 正常推送。下文各包的字段表描述的都是这种情况,表里不再重复列 `success` 这一行。
|
||||
- `success: false` —— 操作失败,见 0.2。
|
||||
|
||||
前端一律 `if (!data.success)` 判成败,**不看 `status` / `code`,也不写 `status` 兼容兜底**。
|
||||
|
||||
### 0.2 失败回包
|
||||
|
||||
服务端受理请求时,任一校验不通过都会**回一个失败包**(此前是静默丢弃、前端只能干等倒计时):
|
||||
|
||||
- **rpc 与请求包同名**(如 `chupai` 失败回 `chupai`,而不是 `chupai1/2/3`);
|
||||
- **只回发给请求者**(`conmode`/`fromid` 取自请求包),其他两家收不到;
|
||||
- `data` 只含 `success: false` 与 `errcode`,无业务字段。
|
||||
|
||||
| 参数名 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| success | 布尔 | 恒为 `false` |
|
||||
| errcode | 整数 | 失败原因,见下表 |
|
||||
|
||||
`errcode` 取值(服务端 `youle_erqiwang.ERR`,`mod.js`):
|
||||
|
||||
| 值 | 名称 | 含义 |
|
||||
| --- | --- | --- |
|
||||
| 1 | PLAYER | 玩家/房间/座位校验不通过(平台 `check_player` 返回 null) |
|
||||
| 2 | NODESK | 牌桌或牌局不存在 |
|
||||
| 3 | STEP | 当前阶段不允许该操作(如非出牌阶段发 `chupai`) |
|
||||
| 4 | SEAT | 位置不符,或还没轮到该玩家操作 |
|
||||
| 5 | PARAM | 参数非法:类型/范围/张数不对、牌不在手上、牌id 重复 |
|
||||
| 6 | RULE | 规则不允许:叫分未更低、出牌不合法、投降条件不满足、房间模式禁止等 |
|
||||
|
||||
> **牌id 列表的入参约束**(`cards` 字段,`maipai`/`chupai`):必须是**非空数组**,元素必须是 `0 ~ 107` 的**整数**牌id,且**互不重复**。字符串形式的数字(如 `"5"`)、小数、越界值、重复值一律按 `PARAM` 拒绝,服务端不做类型兜底转换。
|
||||
|
||||
> **例外:`tishi` 成功时不回执**。该包是闲家给对家的主观提示、不含任何对局状态,服务端只转发给对家(design §11),发送者收不到成功包;只有失败才回给发送者。
|
||||
|
||||
### 0.3 `countdown` 只是展示用的秒数
|
||||
|
||||
多个包带 `countdown`(叫分 / 选主 / 埋牌 / 出牌倒计时,取自 `class.desk.js` 的四个常量)。它**只供客户端显示提醒,服务端不据此做任何事**:
|
||||
|
||||
- 服务端**没有**对应的定时器,倒计时归零后**不会**自动叫分 / 选主 / 埋牌 / 出牌,也不判负、不跳过该玩家;
|
||||
- 轮到谁而谁不操作,牌局就停在该阶段一直等,`step` 与 `playproc` 都不变;
|
||||
- 这是 design §11 确认过的规则(无超时托管),不是未实现的功能。牌局因此停住时,由玩家走平台的**房间解散**流程收场(平台回调子游戏 `get_disbandRoom` → 解散结算,见 §14)。
|
||||
|
||||
因此**客户端不要在倒计时归零时做任何乐观的界面推进**(不要自行跳阶段、不要清控制权),一切仍以收到服务端推送为准。
|
||||
|
||||
### 0.4 断线重连包不在此约定内
|
||||
|
||||
`deskinfo` 由平台组装在 `pack.data.deskinfo` 下(见文末「断线重连」),`data.success` 由**平台**填写,子游戏不注入。
|
||||
|
||||
---
|
||||
|
||||
## 1. 发牌(fapai)
|
||||
|
||||
发包者:服务器 收包者:游戏 app:youle route:erqiwang rpc:fapai
|
||||
@@ -42,7 +97,7 @@
|
||||
| seat | 整数 | 叫分者的位置序号 |
|
||||
| call | 整数 | 分数,0表示不叫 |
|
||||
| currcall | 整数 | 当前叫到的分数 |
|
||||
| multiple | 整数 | 叫分对应的基础子数(`get_multiple_bycall`,按常规算子取值:65→2 60→3 55→4 50及以下→6 70→2 无叫分→0)|
|
||||
| multiple | 整数 | 叫分对应的基础子数 `get_base_bycall(call, climb)`,**随房间「爬坡」开关取值**(roomtype 位3):常规算子 65→2 60→3 55→4 50及以下→6;爬坡 65→2 60→3 55→4 50→6 45→7 40→8 35→9 30→10 25→11 20→12 15→13 10→14 5→15;两种模式下 70→2;无叫分→0。与结算包 `aset.multiple` 同源同值(design §7.1/§7.3.1)|
|
||||
| nextseat | 整数 | 下一个叫分者的位置序号 |
|
||||
| countdown | 整数 | 叫分倒计时 |
|
||||
|
||||
@@ -58,7 +113,7 @@
|
||||
| call | 整数 | 分数,0表示不叫 |
|
||||
| banker | 整数 | 庄家的位置序号 |
|
||||
| grade | 整数 | 庄家的叫分 |
|
||||
| multiple | 整数 | 叫分对应的基础子数(`get_multiple_bycall`,按常规算子取值:65→2 60→3 55→4 50及以下→6 70→2 无叫分→0)|
|
||||
| multiple | 整数 | 叫分对应的基础子数 `get_base_bycall(call, climb)`,**随房间「爬坡」开关取值**(roomtype 位3):常规算子 65→2 60→3 55→4 50及以下→6;爬坡 65→2 60→3 55→4 50→6 45→7 40→8 35→9 30→10 25→11 20→12 15→13 10→14 5→15;两种模式下 70→2;无叫分→0。与结算包 `aset.multiple` 同源同值(design §7.1/§7.3.1)|
|
||||
| bottomcards | 数组 | 8 张暗牌。**庄家恒有**;**闲家仅当 70 分坐庄时才有**(供 3 秒亮牌用),非 70 分时闲家没有该属性(暗牌只有庄家可见,design §4)|
|
||||
| ancard3s | 整数 | 仅 70 分坐庄时出现且为 `1`:表示庄家把暗牌摸入手前,需将 `bottomcards` 这 8 张暗牌向所有玩家亮出 3 秒(design §4/§7.1);非 70 分无此属性 |
|
||||
| cards | 数组 | 拿了暗牌后手上的牌(36 张,含暗牌),庄家才有此属性,闲家没有该属性 |
|
||||
@@ -134,7 +189,7 @@
|
||||
| bottomcards | 数组 | 埋的牌,庄家才有此属性,闲家没有该属性 |
|
||||
| seat | 整数 | 出牌者的位置序号(即将首出的庄家)|
|
||||
| countdown | 整数 | 出牌倒计时 |
|
||||
| liangpai | json | 庄家亮牌信息(design §8.2),**只有闲家、且可查牌模式、且庄家埋牌后手牌达标时才有**。结构(各字段按是否达标出现):`zhu` 主牌总数、`zhupair` 主对子数、`zhutuo` 主拖拉机组数(固定主牌王+2+7 总数 ≥10 时有这三项);`wang` 王数(王≥3 时);`qi` 7 数(7≥6 时);`er` 2 数(2≥6 时)。都不达标则无此属性 |
|
||||
| liangpai | json | 庄家亮牌信息(design §8.2),**只有闲家、且可查牌模式、且庄家埋牌后手牌达标时才有**。结构(各字段按是否达标出现):`zhu` 主牌总数、`zhupair` 主对子数、`zhutuo` 主拖拉机组数(固定主牌王+2+7 总数 ≥10 时有这三项);`wang` 王数(王≥3 时);`qi` 7 数(7≥6 时);`er` 2 数(2≥6 时)。都不达标则无此属性。统计口径是庄家**埋牌后 28 张的静态快照**(排除已埋的 8 张、但包含之后已打出的牌),全局固定不随出牌缩水,故重连包里的同名字段取值一致 |
|
||||
|
||||
---
|
||||
|
||||
@@ -162,10 +217,11 @@
|
||||
| seat | 整数 | 出牌者的位置序号 |
|
||||
| cards | 数组 | 实际打出的牌id列表;**甩错时**(见 `shuaicuo`)为被强制打出的那一张最小主牌单张 |
|
||||
| shuaicuo | 整数 | 甩错标志,仅甩错时出现且为 `1`(design §5.4.5:甩牌未通过最大性判定,整套甩牌收回,本轮只强制打出最小一张、失去本轮甩牌资格);正常出牌无此属性 |
|
||||
| info | 数组 | 出牌者的座位牌况 `o_paiju.seatlist[seat]`,**仅可查牌模式下发**(不查牌无此属性,design §9)。5 元素数组:前 4 个对应花色1~4 的 `[无该花色标志, 该花色无对标志]`(0/1),第 5 个为报副情况 `[剩余主牌数, 剩余主对数]`(初始 `[-1,-1]`,报无主后为实际数量) |
|
||||
| seatlist | 数组 | 三家座位牌况 `o_paiju.seatlist`(长度 3,下标 = 座位序号),**仅可查牌模式下发**(不查牌无此属性,design §9)。每个座位为 5 元素数组:前 4 个对应花色1~4 的 `[无该花色标志, 该花色无对标志]`(0/1),第 5 个为报副情况 `[剩余主牌数, 剩余主对数]`(初始 `[-1,-1]`)。**一旦全场有人报无主,服务端会按各家实际手牌同时刷新三个座位并整表下发**(design §9「为全体三人显示另外两家」),无需等另两家各自出牌才补。字段名与结构同重连包 `PushCards.seatlist` |
|
||||
| count | 整数 | 出牌数量(甩错时为 1) |
|
||||
| flower | 整数 | 出牌花色 |
|
||||
| cardtype | 整数 | 出牌牌型 |
|
||||
| cardtype | 整数 | 出牌牌型:`>100` 单张(101 一张、102 两张…)、`>200` 对子(201 一对、202 两对…)、`>300` 拖拉机(302 两连对、303 三连对…),见 `class.pai.js` 顶部注释。**注意:cardtype 只能表达单一牌型**,而甩牌是单张/对子/拖拉机自由混搭(design §5.4.3),会被牌型推导压平成 1xx 或 2xx(例如「主K对 + 主5」得 103、「两连对 + 一散对」得 203)。**甩牌的真实结构请读 `shuai`,不要据 cardtype 反推** |
|
||||
| shuai | json | 甩牌分量构成,**仅本次出牌是合法甩牌时才有**(非甩牌、以及甩错退化为单张时都没有该属性)。结构 `{ tractors: [连对数...], pairs: 独立对子数, singles: 单张数 }`,例如「主K对 + 主5」为 `{tractors:[],pairs:1,singles:1}`。与服务端跟牌时逐分量强制匹配用的 `shuai_demand` 同源(design §5.4.3/§5.4.4)|
|
||||
| nextseat | 整数 | 下一个出牌者的位置序号 |
|
||||
| countdown | 整数 | 下一个出牌者的出牌倒计时 |
|
||||
| baozhu | 整数 | 是否已有玩家报无主(主牌出空)0否 1是;对应服务端 `have_baofu()`(任一玩家 `seatlist[seat][4][0]==0`)。**仅可查牌模式**下才可能为 1,供客户端展示他家主牌数量/对子结构并出现"明牌"按钮;**不查牌模式恒为 0**(design §9)|
|
||||
@@ -181,7 +237,7 @@
|
||||
| --- | --- | --- |
|
||||
| seat | 整数 | 出牌者的位置序号 |
|
||||
| cards | 数组 | 出的牌id列表 |
|
||||
| info | 数组 | 出牌者的座位牌况 `o_paiju.seatlist[seat]`,**仅可查牌模式下发**(不查牌无此属性,design §9)。5 元素数组:前 4 个对应花色1~4 的 `[无该花色标志, 该花色无对标志]`(0/1),第 5 个为报副情况 `[剩余主牌数, 剩余主对数]`(初始 `[-1,-1]`,报无主后为实际数量) |
|
||||
| seatlist | 数组 | 三家座位牌况 `o_paiju.seatlist`(长度 3,下标 = 座位序号),**仅可查牌模式下发**(不查牌无此属性,design §9)。每个座位为 5 元素数组:前 4 个对应花色1~4 的 `[无该花色标志, 该花色无对标志]`(0/1),第 5 个为报副情况 `[剩余主牌数, 剩余主对数]`(初始 `[-1,-1]`)。**一旦全场有人报无主,服务端会按各家实际手牌同时刷新三个座位并整表下发**(design §9「为全体三人显示另外两家」),无需等另两家各自出牌才补。字段名与结构同重连包 `PushCards.seatlist` |
|
||||
| nextseat | 整数 | 下一个出牌者的位置序号 |
|
||||
| countdown | 整数 | 出牌倒计时 |
|
||||
| baozhu | 整数 | 是否已有玩家报无主(主牌出空)0否 1是;对应服务端 `have_baofu()`(任一玩家 `seatlist[seat][4][0]==0`)。**仅可查牌模式**下才可能为 1,供客户端展示他家主牌数量/对子结构并出现"明牌"按钮;**不查牌模式恒为 0**(design §9)|
|
||||
@@ -197,7 +253,7 @@
|
||||
| --- | --- | --- |
|
||||
| seat | 整数 | 出牌者的位置序号 |
|
||||
| cards | 数组 | 出的牌id列表 |
|
||||
| info | 数组 | 出牌者的座位牌况 `o_paiju.seatlist[seat]`,**仅可查牌模式下发**(不查牌无此属性,design §9)。5 元素数组:前 4 个对应花色1~4 的 `[无该花色标志, 该花色无对标志]`(0/1),第 5 个为报副情况 `[剩余主牌数, 剩余主对数]`(初始 `[-1,-1]`,报无主后为实际数量) |
|
||||
| seatlist | 数组 | 三家座位牌况 `o_paiju.seatlist`(长度 3,下标 = 座位序号),**仅可查牌模式下发**(不查牌无此属性,design §9)。每个座位为 5 元素数组:前 4 个对应花色1~4 的 `[无该花色标志, 该花色无对标志]`(0/1),第 5 个为报副情况 `[剩余主牌数, 剩余主对数]`(初始 `[-1,-1]`)。**一旦全场有人报无主,服务端会按各家实际手牌同时刷新三个座位并整表下发**(design §9「为全体三人显示另外两家」),无需等另两家各自出牌才补。字段名与结构同重连包 `PushCards.seatlist` |
|
||||
| nextseat | 整数 | 下一个出牌者的位置序号 |
|
||||
| countdown | 整数 | 出牌倒计时 |
|
||||
| baozhu | 整数 | 是否已有玩家报无主(主牌出空)0否 1是;对应服务端 `have_baofu()`(任一玩家 `seatlist[seat][4][0]==0`)。**仅可查牌模式**下才可能为 1,供客户端展示他家主牌数量/对子结构并出现"明牌"按钮;**不查牌模式恒为 0**(design §9)|
|
||||
@@ -211,7 +267,7 @@
|
||||
|
||||
发包者:游戏 收包者:服务器 app:youle route:erqiwang rpc:mingpai
|
||||
|
||||
查看另外两家手中全部主牌的具体牌面(design §9)。服务端仅在**可查牌模式、出牌阶段(step5)、且已有玩家报无主**(`have_baofu()`)时受理;否则忽略。
|
||||
查看另外两家手中全部主牌的具体牌面(design §9)。服务端仅在**可查牌模式、出牌阶段(step5)、且已有玩家报无主**(`have_baofu()`)时受理;不满足时按 0.2 回 `mingpai` 失败包(不查牌 / 未报无主 → `RULE`,非出牌阶段 → `STEP`)。
|
||||
|
||||
| 参数名 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
@@ -236,7 +292,7 @@
|
||||
|
||||
发包者:游戏 收包者:服务器 app:youle route:erqiwang rpc:tishi
|
||||
|
||||
闲家在出牌阶段向对家(另一闲家)发"踩/没分/有分"提示(design §11)。服务端仅在**出牌阶段(step5)、且发起者为闲家**(`seat != banker`)、**tip 合法(1/2/3)** 时受理;**不校验提示真实性**(玩家可主观发送);庄家无对家,不受理。
|
||||
闲家在出牌阶段向对家(另一闲家)发"踩/没分/有分"提示(design §11)。服务端仅在**出牌阶段(step5)、且发起者为闲家**(`seat != banker`)、**tip 合法(1/2/3)** 时受理;**不校验提示真实性**(玩家可主观发送);庄家无对家,不受理。不满足时按 0.2 回 `tishi` 失败包给发送者(庄家发 → `RULE`,非出牌阶段 → `STEP`,tip 非法或缺失 → `PARAM`)。**成功时不给发送者回执**,只转发给对家。
|
||||
|
||||
| 参数名 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
@@ -362,7 +418,7 @@
|
||||
|
||||
## 断线重连(deskinfo)
|
||||
|
||||
> 由平台在玩家进入房间/断线重连时回调 `youle_erqiwang.export.get_deskinfo(o_room, seat)` 生成并下发(服务器→客户端);下发的 app/route/rpc 由平台的进房/重连流程决定,不在本子游戏代码内固定。下列字段即该函数返回的 `deskinfo` 对象结构,按当前 `paiju.step` 只带对应阶段的分组。
|
||||
> 由平台在玩家进入房间/断线重连时回调 `youle_erqiwang.export.get_deskinfo(o_room, seat)` 生成并下发(服务器→客户端);下发的 app/route/rpc 由平台的进房/重连流程决定,不在本子游戏代码内固定。平台把它挂在 `pack.data.deskinfo` 下,**`data.success` 由平台填写、子游戏不注入**(见 0.4)。下列字段即该函数返回的 `deskinfo` 对象结构,按当前 `paiju.step` 只带对应阶段的分组。
|
||||
|
||||
| 参数名 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
@@ -379,7 +435,7 @@
|
||||
| seat | 整数 | 当前叫分位置 |
|
||||
| countdown | 整数 | 叫分倒计时 |
|
||||
| nowcall | 整数 | 当前叫分 |
|
||||
| multiple | 整数 | 叫分对应的基础子数(`get_multiple_bycall`,按常规算子取值:65→2 60→3 55→4 50及以下→6 70→2 无叫分→0)|
|
||||
| multiple | 整数 | 叫分对应的基础子数 `get_base_bycall(call, climb)`,**随房间「爬坡」开关取值**(roomtype 位3):常规算子 65→2 60→3 55→4 50及以下→6;爬坡 65→2 60→3 55→4 50→6 45→7 40→8 35→9 30→10 25→11 20→12 15→13 10→14 5→15;两种模式下 70→2;无叫分→0。与结算包 `aset.multiple` 同源同值(design §7.1/§7.3.1)|
|
||||
| call | 数组 | 三家叫分 `[null,0,65]`,null还未叫分,0不叫,>0叫了多少分 |
|
||||
|
||||
**ChooseMain(step 2 选主/投降阶段,不在此阶段无此属性)**
|
||||
@@ -388,7 +444,7 @@
|
||||
| --- | --- | --- |
|
||||
| banker | 整数 | 庄家 |
|
||||
| call | 整数 | 叫分 |
|
||||
| multiple | 整数 | 叫分对应的基础子数(`get_multiple_bycall`,按常规算子取值:65→2 60→3 55→4 50及以下→6 70→2 无叫分→0)|
|
||||
| multiple | 整数 | 叫分对应的基础子数 `get_base_bycall(call, climb)`,**随房间「爬坡」开关取值**(roomtype 位3):常规算子 65→2 60→3 55→4 50及以下→6;爬坡 65→2 60→3 55→4 50→6 45→7 40→8 35→9 30→10 25→11 20→12 15→13 10→14 5→15;两种模式下 70→2;无叫分→0。与结算包 `aset.multiple` 同源同值(design §7.1/§7.3.1)|
|
||||
| countdown | 整数 | 选主倒计时 |
|
||||
| bottomcards | 数组 | 8 张暗牌,**只有庄家有此属性**(暗牌仅庄家可见;70 分的 3 秒亮牌是上庄时的一次性事件,重连不重放)|
|
||||
| touxiang | 整数 | 是否允许投降 0:不允许 1:允许(仅 70 分坐庄为 1)。投降与选主互斥、同一决策点,见 touxiang 包与 design §4 |
|
||||
@@ -401,7 +457,7 @@
|
||||
| --- | --- | --- |
|
||||
| banker | 整数 | 庄家 |
|
||||
| call | 整数 | 叫分 |
|
||||
| multiple | 整数 | 叫分对应的基础子数(`get_multiple_bycall`,按常规算子取值:65→2 60→3 55→4 50及以下→6 70→2 无叫分→0)|
|
||||
| multiple | 整数 | 叫分对应的基础子数 `get_base_bycall(call, climb)`,**随房间「爬坡」开关取值**(roomtype 位3):常规算子 65→2 60→3 55→4 50及以下→6;爬坡 65→2 60→3 55→4 50→6 45→7 40→8 35→9 30→10 25→11 20→12 15→13 10→14 5→15;两种模式下 70→2;无叫分→0。与结算包 `aset.multiple` 同源同值(design §7.1/§7.3.1)|
|
||||
| flower | 整数 | 主牌花色 |
|
||||
| countdown | 整数 | 埋牌倒计时 |
|
||||
| bottomcards | 数组 | 8 张暗牌,**只有庄家有此属性**(暗牌仅庄家可见;70 分的 3 秒亮牌是上庄时的一次性事件,重连不重放)|
|
||||
@@ -414,16 +470,16 @@
|
||||
| --- | --- | --- |
|
||||
| banker | 整数 | 庄家 |
|
||||
| call | 整数 | 叫分 |
|
||||
| multiple | 整数 | 叫分对应的基础子数(`get_multiple_bycall`,按常规算子取值:65→2 60→3 55→4 50及以下→6 70→2 无叫分→0)|
|
||||
| multiple | 整数 | 叫分对应的基础子数 `get_base_bycall(call, climb)`,**随房间「爬坡」开关取值**(roomtype 位3):常规算子 65→2 60→3 55→4 50及以下→6;爬坡 65→2 60→3 55→4 50→6 45→7 40→8 35→9 30→10 25→11 20→12 15→13 10→14 5→15;两种模式下 70→2;无叫分→0。与结算包 `aset.multiple` 同源同值(design §7.1/§7.3.1)|
|
||||
| flower | 整数 | 主牌花色 |
|
||||
| countdown | 整数 | 出牌倒计时 |
|
||||
| bottomcards | 数组 | 埋的牌,只有庄家有此属性 |
|
||||
| grade | 整数 | 当前的捡分分数 |
|
||||
| gradecards | 数组 | 当前的捡分分牌,只有闲家有此属性 |
|
||||
| playproc | json | 当前出牌情况 `o_paiju.playproc`:`round` 第几轮、`start` 本轮首出位置、`currseat` 当前出牌位置、`startcount` 首出张数、`startflower` 首出花色、`starttype` 首出牌型、`maxseat` 本轮最大者位置、`maxcard` 最大牌编码、`cards` 本轮三家各自出的牌(数组下标=位置序号)、`shuai_demand` 首家甩牌的分量需求 `{tractors:[连对数...],pairs,singles}`(非甩牌为 null,供跟牌逐分量强制匹配,design §5.4.4)|
|
||||
| seatlist | 数组 | 三家座位牌况,元素结构同上文 chupai 包的 `info`(每个玩家一个 5 元素数组:4 个花色的 `[无该花色,无对]` + 报副 `[剩余主牌数,剩余主对数]`)。**仅可查牌模式下有此属性**(design §9)|
|
||||
| playproc | json | 当前出牌情况 `o_paiju.playproc`:`round` 第几轮、`start` 本轮首出位置、`currseat` 当前出牌位置、`startcount` 首出张数、`startflower` 首出花色、`starttype` 首出牌型、`maxseat` 本轮最大者位置、`maxcard` 最大牌编码、`cards` 本轮三家各自出的牌(数组下标=位置序号)、`shuai_demand` 首家甩牌的分量需求 `{tractors:[连对数...],pairs,singles}`(非甩牌为 null,供跟牌逐分量强制匹配,design §5.4.4)。**两种查牌模式下恒有此属性**——`cards` 是当前这一轮桌面上的牌,不属于「查牌」,屏蔽了后出的人就无从跟牌(design §9 末尾)|
|
||||
| seatlist | 数组 | 三家座位牌况,与上文 chupai 包的 `seatlist` 同名同结构(每个玩家一个 5 元素数组:4 个花色的 `[无该花色,无对]` + 报副 `[剩余主牌数,剩余主对数]`)。**仅可查牌模式下有此属性**(design §9)|
|
||||
| liangpai | json | 庄家亮牌信息,结构同 maipai 包的 `liangpai`;**仅可查牌模式、且请求者为闲家、且庄家达标时有**(供闲家重连后仍能看到亮牌,design §8.2)|
|
||||
| pushlist | 数组 | 出牌历史 `[[[], [], []], [[], [], []], ...]`,外层下标=轮次,内层 3 个数组按位置序号存该轮各家出的牌id(已按大小排序) |
|
||||
| pushlist | 数组 | 出牌历史 `[[[], [], []], [[], [], []], ...]`,外层下标=轮次(从第 1 轮起,含进行中的当前轮),内层**恒为 3 个数组**、按位置序号存该轮各家出的牌id,各自已按**本局主牌花色**从大到小排序。**仅可查牌模式下有此属性**——往轮打出、已被收走的牌属于「查牌」范畴,不查牌模式一律不下发(design §9)。注意当前这一轮桌面上的牌由 `playproc.cards` 恢复,**两种模式下都有**,否则后出的人无从跟牌 |
|
||||
|
||||
**Balance(不在结算阶段无此属性)**
|
||||
|
||||
|
||||
@@ -456,7 +456,12 @@ youle_erqiwang.chupai = function(pack){
|
||||
msg.data.seat = seat;
|
||||
msg.data.cards = re.cards ? re.cards : cards; //实际打出的牌(甩错时为单张)
|
||||
if (!_cfg.nocheck){
|
||||
msg.data.info = o_paiju.seatlist[seat]; //座位牌况(报副统计),仅可查牌下发
|
||||
//三家座位牌况(缺门/无对 + 报副统计),仅可查牌下发。
|
||||
//必须整表下发、不能只带出牌者一家:design §9 要求「一旦有玩家报无主,就为全体三人
|
||||
//显示另外两家各自的主牌数量与对子数」,服务端已在 do_playcard 里同时刷新三个座位
|
||||
//(class.paiju.js),只带一家会让另两家的统计滞后到它们各自出牌时才补上。
|
||||
//字段名与重连包 PushCards.seatlist 一致,前端两条路径复用同一份解析
|
||||
msg.data.seatlist = o_paiju.seatlist;
|
||||
}
|
||||
msg.data.nextseat = o_paiju.playproc.currseat;
|
||||
msg.data.countdown = o_desk.method.get_countdown_chupai();
|
||||
@@ -522,7 +527,7 @@ youle_erqiwang.chupai = function(pack){
|
||||
}
|
||||
delete msg.data.seat;
|
||||
delete msg.data.cards;
|
||||
delete msg.data.info;
|
||||
delete msg.data.seatlist;
|
||||
delete msg.data.nextseat;
|
||||
delete msg.data.countdown;
|
||||
delete msg.data.baozhu;
|
||||
|
||||
@@ -32,10 +32,11 @@ node server/games/erqiwang/test/test_arith.js # 单独跑某一组
|
||||
- `test_follow.js`:正常跟牌/毙牌/垫牌,单张/对子/拖拉机的必出与缺门毙牌数量对应(§5.1/5.2)。
|
||||
- `test_callgrade.js`:叫分坐庄 do_callgrade(叫5立即/两家不叫/后叫更低)(§4.2)。
|
||||
- `test_config.js`:roomtype 位串解析、局数/扣卡映射(§10.1)。
|
||||
- `test_paiju.js`:亮牌 get_liangpai(§8.2)、结算 get_paiju_account 集成(§7/§8,大光/过庄/升级/爬坡/傍王/投降)、扣底触发 get_bottom_account(§6.3)。
|
||||
- `test_paiju.js`:亮牌 get_liangpai(§8.2,含「埋牌后静态快照、出牌后重连不缩水」回归)、结算 get_paiju_account 集成(§7/§8,大光/过庄/升级/爬坡/傍王/投降)、扣底触发 get_bottom_account(§6.3)。
|
||||
- `test_deal.js`:发牌构成——92张/去3-4/三家28+底8/分值(§2/§6.1)。
|
||||
- `test_desk.js`:坐庄轮换 do_prepare(庄赢连庄/闲赢·投降下家)(§4.6)。
|
||||
- `test_rpc.js`(L3,用 `_rpc.js` 装配 mod.js + 捕获下发包):明牌 mingpai、出牌 chupai 的 info/baozhu 查牌门控(§9)、投降 touxiang 条件、jiaofen 入参校验、70分上庄暗牌亮牌下发(§4)。
|
||||
- `test_endgame.js`(L4 端到端,用真实发牌跑完整一局):叫分→上庄→选主→埋牌→28 轮出牌→小局结算的流程闭合(§12.1),以及与牌面无关的不变量(牌张守恒 84/8/16、捡分=闲家赢得分牌+扣底分、`X` 分配庄±2X/闲∓X、算奖 `X×(2Ni−Nj−Nk)`、结算零和、`result` 与判定一致);另覆盖 §4.4/§7.1 投降局与 §12.2 中途解散两条支线。
|
||||
- `test_rpc.js`(L3,用 `_rpc.js` 装配 mod.js + 捕获下发包):明牌 mingpai、出牌 chupai 的 seatlist/baozhu 查牌门控(§9,含「seatlist 整表带三家」)、投降 touxiang 条件、jiaofen 入参校验、70分上庄暗牌亮牌下发(§4)。
|
||||
|
||||
## 约定(dev-guide §10 测试纪律)
|
||||
|
||||
|
||||
@@ -114,6 +114,13 @@ t.eq('甩牌 甩错·双对之一被压', r.shuaicuo, true);
|
||||
t.eq('甩牌 合法·单张+拖拉机', play([big(1), K1, K2, Q1, Q2], [[P5, P6], []]).result, true);
|
||||
t.eq('甩牌 合法·单+对+拖三混', play([big(1), id(1, 1, 10), id(2, 1, 10), K1, K2, Q1, Q2], [[P5, P6], []]).result, true);
|
||||
t.eq('甩牌 副禁甩·对手非缺门仍禁', play([fK1, f9], [[fK2, id(1, 2, 10)], []]).result, false);
|
||||
// design §5.4.3「无组合数量限制」:曾有「一次最多14张」的规则外硬上限,会把 15 张以上的
|
||||
// 合法甩牌整个拒掉。16 张全固定主牌(4王 + 8个7 + 正2对 + 副2对)、两对手无主牌 → 必须可甩
|
||||
const SHUAI16 = [big(1), big(2), small(1), small(2),
|
||||
id(1, 1, 7), id(2, 1, 7), id(1, 2, 7), id(2, 2, 7), id(1, 3, 7), id(2, 3, 7), id(1, 4, 7), id(2, 4, 7),
|
||||
id(1, 1, 2), id(2, 1, 2), id(1, 4, 2), id(2, 4, 2)];
|
||||
t.eq('甩牌 16张(超14张上限)仍可甩', play(SHUAI16, [[], []]).result, true);
|
||||
t.eq('甩牌 16张 有分量需求下发', !!play(SHUAI16, [[], []]).shuai_demand, true);
|
||||
|
||||
// ============ §5.4.4 跟甩牌强制分量拆解 flush_follow_ok ============
|
||||
const J1 = id(1, 1, 11), N9a = id(1, 1, 9), N9b = id(2, 1, 9), P8 = id(1, 1, 8);
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
// L4 端到端:用真实发牌跑完整一局(叫分→上庄→选主→埋牌→28轮出牌→小局结算),
|
||||
// 校验 design §12.1 的流程闭合与一批与牌面无关的不变量(牌张守恒/捡分一致/结算零和/分配公式)。
|
||||
// 另覆盖 §4.4 投降局与 §12.2 中途解散两条支线。
|
||||
// 说明:牌是随机发的,但断言的都是**与具体牌面无关**的不变量,不会因发牌不同而抖动。
|
||||
const { mod, setup } = require('./_rpc.js');
|
||||
const t = require('./_assert')();
|
||||
const P = global.cls_youle_erqiwang_paiju;
|
||||
const AR = global.cls_youle_erqiwang_arith;
|
||||
|
||||
const pk = (seat, d) => ({ conmode: 0, fromid: seat, data: Object.assign({ agentid: 1, playerid: seat, gameid: 1, roomcode: 1, seat: seat }, d || {}) });
|
||||
|
||||
// 开局并让 0 号位以 call 分坐庄(另两家不叫)
|
||||
function seatBanker(roomtype, call) {
|
||||
const pj = P.new({ paiju_list: [] }, 0);
|
||||
const ctx = setup(roomtype, pj);
|
||||
mod.jiaofen(pk(pj.method.get_callgrade_seat(), { call: call }));
|
||||
mod.jiaofen(pk(pj.method.get_callgrade_seat(), { call: 0 }));
|
||||
mod.jiaofen(pk(pj.method.get_callgrade_seat(), { call: 0 }));
|
||||
return { pj, ctx };
|
||||
}
|
||||
// 出牌驱动:首家出手上最小的单张,跟家取第一张能通过 can_followcard 的牌
|
||||
function playAll(pj) {
|
||||
let guard = 0;
|
||||
while (pj.step === 5 && ++guard < 200) {
|
||||
const seat = pj.playproc.currseat, hand = P.get_seat_cards(pj, seat);
|
||||
let pick = null;
|
||||
if (seat === pj.playproc.start) {
|
||||
pick = [hand[hand.length - 1]];
|
||||
} else {
|
||||
for (const c of hand) {
|
||||
if (AR.can_followcard(pj.flower, hand, [c], pj.playproc.startcount, pj.playproc.startflower, pj.playproc.starttype).result) { pick = [c]; break; }
|
||||
}
|
||||
}
|
||||
if (!pick) { return false; }
|
||||
mod.chupai(pk(seat, { cards: pick }));
|
||||
}
|
||||
return pj.step === 6;
|
||||
}
|
||||
|
||||
// ============ §12.1 正常局全流程 ============
|
||||
const { pj, ctx } = seatBanker("00000", 65);
|
||||
t.eq('§4.2 坐庄(banker/call/step)', [pj.banker, pj.call, pj.step], [0, 65, 2]);
|
||||
t.eq('§4.3 庄家摸暗牌后36张 / 闲家28张', [P.get_seat_cards(pj, 0).length, P.get_seat_cards(pj, 1).length], [36, 28]);
|
||||
mod.xuanzhu(pk(0, { flower: 1 }));
|
||||
t.eq('§4.4 选主后进埋牌阶段', [pj.flower, pj.step], [1, 3]);
|
||||
mod.maipai(pk(0, { cards: P.get_seat_cards(pj, 0).slice(-8) }));
|
||||
t.eq('§4.5 埋牌后庄家28张、进出牌阶段', [P.get_seat_cards(pj, 0).length, pj.step], [28, 5]);
|
||||
t.eq('§5.1 第一轮固定由庄家先出', [pj.playproc.round, pj.playproc.start], [1, 0]);
|
||||
t.eq('§5/§6 28轮打完进入结算', playAll(pj) && pj.playproc.round, 28);
|
||||
t.eq('§2 牌张守恒(出84 / 埋8 / 去掉3-4共16)',
|
||||
[pj.cards.filter(c => c.playround > 0).length, pj.cards.filter(c => c.playround === 0).length, pj.cards.filter(c => c.number === 3 || c.number === 4).length],
|
||||
[84, 8, 16]);
|
||||
|
||||
const last = ctx.sent[ctx.sent.length - 1];
|
||||
const aset = last.data.aset;
|
||||
t.eq('结算包 rpc/success', [last.rpc, last.data.success], ['jiesuan', true]);
|
||||
t.eq('§11 结束亮出底牌8张', last.data.bottom.cards.length, 8);
|
||||
// §6.2/§6.3:闲家捡分 = 闲家赢下的分牌 + 扣底(可能翻倍)分
|
||||
let jian = 0;
|
||||
for (const c of pj.cards) { if (c.playowner > -1 && c.playowner !== pj.banker) { jian += c.score; } }
|
||||
const bonus = last.data.bottom.grade2 || 0;
|
||||
t.eq('§6.2/§6.3 捡分 = 闲家赢得分牌 + 扣底分', aset.grade, jian + bonus);
|
||||
// §6.3:扣底只在「闲家用主牌赢下末轮」时发生
|
||||
const lastWinIsXian = pj.playproc.maxseat !== pj.banker;
|
||||
const lastMult = lastWinIsXian ? AR.get_bottom_multiple(pj.flower, pj.playproc.cards[pj.playproc.maxseat]) : 0;
|
||||
t.eq('§6.3 扣底触发条件(闲家 + 主牌)', lastMult > 0, !!last.data.bottom.multiple);
|
||||
// §7 第3层:X = 基础子数 × |判定倍率|,庄家 ±2X、闲家各 ∓X
|
||||
const X = aset.multiple * Math.abs(aset.upgrade);
|
||||
const win = aset.upgrade > 0 ? 1 : -1;
|
||||
t.eq('§7 捡分子数分配 庄±2X / 闲各∓X', aset.seatlist.map(p => p.grade_jf), [win * X * 2, -win * X, -win * X]);
|
||||
// §8.4 第4层:算奖线 X×(2Ni−Nj−Nk)
|
||||
const N = aset.seatlist.map(p => p.naward);
|
||||
t.eq('§8.4 算奖分配 X×(2Ni−Nj−Nk)', aset.seatlist.map(p => p.grade_aw),
|
||||
[X * (2 * N[0] - N[1] - N[2]), X * (2 * N[1] - N[0] - N[2]), X * (2 * N[2] - N[0] - N[1])]);
|
||||
t.eq('本局结算零和', aset.seatlist.reduce((s, p) => s + p.grade, 0), 0);
|
||||
t.eq('§4.6 result 与判定一致(0庄赢/1闲赢)', pj.result, aset.upgrade > 0 ? 0 : 1);
|
||||
|
||||
// ============ §4.4 / §7.1 投降局 ============
|
||||
const tx = seatBanker("00000", 70);
|
||||
t.eq('§4.3 70分坐庄:暗牌亮3秒标志 + 闲家也收到暗牌',
|
||||
[tx.ctx.sent[tx.ctx.sent.length - 1].data.ancard3s, tx.ctx.sent[tx.ctx.sent.length - 2].data.bottomcards.length], [1, 8]);
|
||||
mod.touxiang(pk(0));
|
||||
const txa = tx.ctx.sent[tx.ctx.sent.length - 1].data.aset;
|
||||
t.eq('§7.1 投降:base=1 / 庄-2、闲各+1', [txa.multiple, txa.seatlist.map(p => p.grade_jf)], [1, [-2, 1, 1]]);
|
||||
t.eq('§4.4 投降不选主、不埋牌', [tx.pj.flower, tx.pj.cards.filter(c => c.playround === 0).length], [-1, 0]);
|
||||
t.eq('§8.4 投降算奖按庄家36张快照', P.get_seat_cards_award(tx.pj, 0).length, 36);
|
||||
t.eq('§4.6 投降 result=2(下家坐庄)', tx.pj.result, 2);
|
||||
|
||||
// ============ §12.2 中途解散 ============
|
||||
const ds = seatBanker("00000", 65);
|
||||
mod.xuanzhu(pk(0, { flower: 1 }));
|
||||
const dsmsg = require('../class.export.js').new().get_disbandRoom(ds.ctx.o_room);
|
||||
t.eq('§12.2 解散:不结算子数、result=3', [dsmsg.data.aset.multiple, dsmsg.data.aset.seatlist.map(p => p.grade), ds.pj.result], [0, [0, 0, 0], 3]);
|
||||
|
||||
process.exit(t.done('endgame') ? 0 : 1);
|
||||
@@ -27,6 +27,20 @@ t.eq('亮牌 2王2个7(不达标)', lp([big(1), small(1), id(1, 1, 7), id(2, 1,
|
||||
t.eq('亮牌 6个7(仅qi)', lp([id(1, 1, 7), id(2, 1, 7), id(1, 2, 7), id(2, 2, 7), id(1, 3, 7), id(2, 3, 7)]), { qi: 6 });
|
||||
t.eq('亮牌 固定主9不达标→null', lp([big(1), small(1), id(1, 1, 7), id(2, 1, 7), id(1, 2, 7), id(2, 2, 7), id(1, 1, 2), id(2, 1, 2), id(1, 2, 2)]), null);
|
||||
t.eq('亮牌 固定主10边界(仅zhu)', lp([big(1), small(1), id(1, 1, 7), id(2, 1, 7), id(1, 2, 7), id(2, 2, 7), id(1, 1, 2), id(2, 1, 2), id(1, 2, 2), id(2, 2, 2)]), { zhu: 10, zhupair: 4, zhutuo: 1 });
|
||||
// design §8.2:亮牌依据是【庄家埋牌后】的静态手牌快照,全程固定、不随出牌缩水
|
||||
// (闲家断线重连时 get_deskinfo 会再取一次;若按当前剩余手牌重算,亮牌会缩水甚至变 null)
|
||||
const LIANG28 = fill([big(1), big(2), small(1), small(2), id(1, 1, 7), id(2, 1, 7), id(1, 2, 7), id(2, 2, 7), id(1, 3, 7), id(2, 3, 7)], 28);
|
||||
const EXPECT_LIANG = { zhu: 10, zhupair: 5, zhutuo: 1, wang: 4, qi: 6 };
|
||||
t.eq('亮牌 埋牌后快照(出牌开始)', P.get_liangpai(mkHand(LIANG28)), EXPECT_LIANG);
|
||||
const played = mkHand(LIANG28); // 同一手牌,已打出2大王+1对♦7
|
||||
for (const c of played.cards){
|
||||
if (c.id === big(1) || c.id === big(2) || c.id === id(1, 1, 7) || c.id === id(2, 1, 7)){ c.playround = 1; }
|
||||
}
|
||||
t.eq('亮牌 打出4张后重连仍按快照', P.get_liangpai(played), EXPECT_LIANG);
|
||||
// 埋下的牌(playround==0)不算在快照里
|
||||
const buried = mkHand(LIANG28);
|
||||
for (const c of buried.cards){ if (c.id === big(1) || c.id === big(2)){ c.playround = 0; } }
|
||||
t.eq('亮牌 已埋的牌不计入快照', P.get_liangpai(buried), { qi: 6 });
|
||||
|
||||
// ============ §7/§8 结算 get_paiju_account 集成 ============
|
||||
// 受控 o_paiju/o_desk/o_room:控制庄家算奖手牌(dealowner=1)与闲1捡分(playowner=1,score)
|
||||
|
||||
@@ -36,7 +36,7 @@ t.eq('投降 埋牌后step3 拒(回失败包STEP)', rej(ctx, ERR.STEP), true);
|
||||
p = mkTX(2, 1, 70); ctx = setup("00000", p); mod.touxiang(pack(0));
|
||||
t.eq('投降 非庄家 拒(回失败包SEAT)', rej(ctx, ERR.SEAT), true);
|
||||
|
||||
// ===================== §9 出牌 chupai 的查牌门控(info/baozhu)=====================
|
||||
// ===================== §9 出牌 chupai 的查牌门控(seatlist/baozhu)=====================
|
||||
// 造真实牌局 → 强制到首出(step5),庄家(0)出一张副牌单张;seat1 已报无主
|
||||
function mkChupai(roomtype) {
|
||||
const o_desk_stub = { paiju_list: [] };
|
||||
@@ -55,10 +55,14 @@ function mkChupai(roomtype) {
|
||||
return c;
|
||||
}
|
||||
let cv = mkChupai("00000"); // 可查牌
|
||||
t.eq('出牌 可查牌 有info', cv.sent[0] && cv.sent[0].data.info !== undefined, true);
|
||||
t.eq('出牌 可查牌 有seatlist', cv.sent[0] && cv.sent[0].data.seatlist !== undefined, true);
|
||||
// design §9:报无主后要为全体三人显示【另外两家】的主牌数/对子数 → 下发面必须整表带三家,
|
||||
// 不能只带出牌者一家(否则另两家的统计要滞后到它们各自出牌才补上)
|
||||
t.eq('出牌 可查牌 seatlist 带三家', cv.sent[0] && cv.sent[0].data.seatlist.length, 3);
|
||||
t.eq('出牌 可查牌 seatlist 含非出牌者的报副统计', cv.sent[0] && cv.sent[0].data.seatlist[1][4][0], 0);
|
||||
t.eq('出牌 可查牌 baozhu=1(有人报无主)', cv.sent[0] && cv.sent[0].data.baozhu, 1);
|
||||
let cn = mkChupai("00001"); // 不查牌(位4=1)
|
||||
t.eq('出牌 不查牌 无info', cn.sent[0] && cn.sent[0].data.info, undefined);
|
||||
t.eq('出牌 不查牌 无seatlist', cn.sent[0] && cn.sent[0].data.seatlist, undefined);
|
||||
t.eq('出牌 不查牌 baozhu=0', cn.sent[0] && cn.sent[0].data.baozhu, 0);
|
||||
|
||||
// ===================== §4.2 mod.jiaofen 入参校验 =====================
|
||||
@@ -198,13 +202,17 @@ function mkPushlist() {
|
||||
// seat0 第1轮补出:主5(flower1) 与 副♥A(flower3),主牌必须排在副牌前
|
||||
const zhu5 = id(1, 1, 5), fuA = id(1, 3, 1);
|
||||
for (const c of [zhu5, fuA]) { pj.cards[c].dealowner = 1; pj.cards[c].playround = 1; }
|
||||
// 当前轮(第5轮)桌面上的牌:两种查牌模式下都必须能恢复(design §9 边界说明)
|
||||
const cur = id(2, 1, 10);
|
||||
pj.cards[cur].dealowner = 1; pj.cards[cur].playround = 5;
|
||||
pj.playproc.cards[0] = [cur];
|
||||
pj.method = {
|
||||
get_seat_cards: s => P.get_seat_cards(pj, s),
|
||||
get_jian_grade: () => P.get_jian_grade(pj),
|
||||
get_burycard: () => P.get_burycard(pj),
|
||||
get_liangpai: () => null
|
||||
};
|
||||
return { pj, zhu5, fuA };
|
||||
return { pj, zhu5, fuA, cur };
|
||||
}
|
||||
const pl = mkPushlist();
|
||||
const plRoom = setup("00000", pl.pj).o_room;
|
||||
@@ -215,6 +223,14 @@ t.eq('重连 pushlist 不含 undefined', di.some(r => r.some(x => x === undefine
|
||||
// 第1轮 seat0 的牌:主5 必须排在副♥A 之前(按 paiju.flower=1 排序,而非泄漏的大王花色5)
|
||||
t.eq('重连 pushlist 按本局主牌花色排序(主5在副A前)',
|
||||
di[0][0].indexOf(pl.zhu5) < di[0][0].indexOf(pl.fuA), true);
|
||||
// §9 不查牌模式:出牌历史属于「查牌」,一律不下发;但当前轮桌面上的牌必须照常恢复
|
||||
const pl2 = mkPushlist();
|
||||
const pc2 = E.get_deskinfo(setup("00001", pl2.pj).o_room, 1).PushCards;
|
||||
t.eq('重连 不查牌 无出牌历史 pushlist', pc2.pushlist, undefined);
|
||||
t.eq('重连 不查牌 当前轮桌面牌仍恢复(playproc.cards)', pc2.playproc.cards[0], [pl2.cur]);
|
||||
const pc1 = E.get_deskinfo(plRoom, 1).PushCards;
|
||||
t.eq('重连 可查牌 有出牌历史 pushlist', Array.isArray(pc1.pushlist), true);
|
||||
t.eq('重连 可查牌 当前轮桌面牌也在', pc1.playproc.cards[0], [pl.cur]);
|
||||
|
||||
// ===================== §7.3 下发面 multiple 必须随「爬坡」开关取值(与结算 aset.multiple 同源)=====================
|
||||
// 叫 45 分:常规算子基础子数 6(design §7.1「50 及以下统一 6」),爬坡 7(design §7.3.1)
|
||||
|
||||
+2
-2
@@ -11,7 +11,7 @@ min_loadJsFile("youle/server_room/mod.js", function(){
|
||||
//代理后台接口表
|
||||
// min_loadJsFile("youle/server_synch/mod.js", function(){
|
||||
//牛牛
|
||||
min_loadJsFile("games2/jinxianmahjong/mod.js", function(){
|
||||
// min_loadJsFile("games2/jinxianmahjong/mod.js", function(){
|
||||
//二七王
|
||||
min_loadJsFile("games/erqiwang/mod.js", function(){
|
||||
|
||||
@@ -23,7 +23,7 @@ min_loadJsFile("games/erqiwang/mod.js", function(){
|
||||
|
||||
|
||||
// });
|
||||
});
|
||||
// });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user