二七王:S-6 大局结算按 基础分/冲关分/傍王分 三项分解
问题:大局结算面板要分项显示,而 account 只有「累积得分 + 每局得分数组」。
傍王分尤其拆不出来——N = 冲关奖数 + 傍王王数,两个来源【求和之后】才代入
grade_aw = X×(2Ni−Nj−Nk),事后无法反推各自占比。
解法:在算钱时就拆。把 N 分成 N_冲关(只计庄家)与 N_傍王(勾选后庄闲都算),
分别代入同一公式各算一次。公式对 N 线性,故恒有
grade_cg + grade_bw == grade_aw
这条恒等式正好当回归断言用。
服务端:
- class.paiju.js 抽出 award_pay(n) 复用同一公式,新增 grade_cg / grade_bw;
desk.seatlist 追加三个累计位(基础/冲关/傍王)。
- class.desk.js seatlist 初始化为 5 元素;account 下发改为【对象数组】
{ score, grades, grade_jf_total, grade_cg_total, grade_bw_total }。
原二元数组下标语义不清,且前端未开工,此时改代价最小(同 S-4 的判断)。
协议:aset.seatlist 增补 grade_cg / grade_bw;account 结构与恒等式
grade_jf_total + grade_cg_total + grade_bw_total == score 写明;
并记录这次结构变更的时间与理由。
测试(endgame 26 → 39 checks):
- 核心不变量:两分量之和 == grade_aw;两分量各自零和。
- 未勾傍王局:grade_bw 恒 0、grade_cg == grade_aw。
- 【新增一整局开傍王的端到端】:不开傍王时 grade_bw 恒 0,只验到平凡情形,
必须有傍王局才算真验证了拆分。该局逐项复核两个分量的公式、naward 口径,
并钉住「王数不全相等时 grade_bw 必有非零」防止又退化成平凡。
- 大局累计恒等式:基础+冲关+傍王 == 累积总分。
反向验证:把 _awbw 置零 → 4 条断言失败;把冲关分量误算到闲家头上 →
2 条断言失败。均已还原、全量 17 文件全绿。
另修四处测试桩:_rpc.js / test_paiju / test_rpc / test_success 里手工构造的
o_desk.seatlist 仍是 2 元素,未跟上 class.desk.js 的结构变更(脚本缺陷,
非业务缺陷)。
顺带解决小局结算「冲关分」标签的不严谨:改取 grade_cg 而非 grade_aw,
开傍王时不再把傍王贡献算进来。
遗留 T-31:参考图「基础分」与「总得分」两行数值相同且与右侧大字对不上
(30/30 vs +135),mock 不自洽。当前按 基础分=grade_jf_total、总分=score
提供。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+27
-24
@@ -62,7 +62,7 @@
|
|||||||
|
|
||||||
说「**冲关**」指庄家那一份,说「**算奖**」指两者合计。
|
说「**冲关**」指庄家那一份,说「**算奖**」指两者合计。
|
||||||
|
|
||||||
> ⚠️ **小局结算的「冲关分」标签有一处不严谨**:它取的是 `grade_aw`,而 `grade_aw` 是按**合计后**的 `N`(冲关+傍王)算出来的。房间**没开傍王**时两者相等、标签准确;**开了傍王**时这个数其实含傍王的贡献,叫「冲关分」就偏窄了。根因与 S-6 同源——两个分量在算钱前就已合并、事后拆不开。**待 S-6 落地后此标签才能名副其实。**
|
> 小局结算的「冲关分」取 **`grade_cg`**(S-6 已把算奖得分拆成冲关 / 傍王两个分量),开傍王时也不会把傍王的贡献算进来。
|
||||||
|
|
||||||
#### 四类「公开信息」——名字像、含义差很远
|
#### 四类「公开信息」——名字像、含义差很远
|
||||||
|
|
||||||
@@ -746,7 +746,7 @@ EQW_RoomOptions = {
|
|||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| 大字 | 本局总分 | `grade` | `+20` 橙色(赢)/ `-10` 蓝色(输),底带光晕 |
|
| 大字 | 本局总分 | `grade` | `+20` 橙色(赢)/ `-10` 蓝色(输),底带光晕 |
|
||||||
| 小字左 | **`牌局分 N`** | `grade_jf`(捡分子数得分) | 白标签 + 数值 |
|
| 小字左 | **`牌局分 N`** | `grade_jf`(捡分子数得分) | 白标签 + 数值 |
|
||||||
| 小字右 | **`冲关分 N`** | `grade_aw`(算奖得分) | 白标签 + 数值 |
|
| 小字右 | **`冲关分 N`** | **`grade_cg`**(算奖得分里的冲关分量,S-6 新增;开傍王时不含傍王那部分) | 白标签 + 数值 |
|
||||||
|
|
||||||
> 两个标签的文案已由更新后的 `小局结算.png` 确定(原图两个都写「牌局分」是 mock 重复,现已修正为 `牌局分` / `冲关分`)。
|
> 两个标签的文案已由更新后的 `小局结算.png` 确定(原图两个都写「牌局分」是 mock 重复,现已修正为 `牌局分` / `冲关分`)。
|
||||||
|
|
||||||
@@ -820,9 +820,7 @@ EQW_RoomOptions = {
|
|||||||
- **名次徽章**:3 帧(1 金 / 2 橙 / 3 蓝,花瓣形),按总分排名取帧。
|
- **名次徽章**:3 帧(1 金 / 2 橙 / 3 蓝,花瓣形),按总分排名取帧。
|
||||||
- **总分大字**:正分橙色带 `+`、负分蓝色带 `-`,复用 §3.6 的 `NUM_RESULT_WIN` / `NUM_RESULT_LOSE`。
|
- **总分大字**:正分橙色带 `+`、负分蓝色带 `-`,复用 §3.6 的 `NUM_RESULT_WIN` / `NUM_RESULT_LOSE`。
|
||||||
|
|
||||||
> ⚠️ **四项分数当前协议给不出,需服务端补**,见 §7.5 **S-6**:
|
> **四项分数已由 S-6 补齐**:`account` 改为对象数组,含 `grade_jf_total`(基础分)/ `grade_cg_total`(冲关分)/ `grade_bw_total`(傍王分)/ `score`(总分),三项之和恒等于 `score`。
|
||||||
> `account` 只有「累积得分 + 每局得分数组」,没有「基础分 / 冲关分 / **傍王分**」这三项分解。
|
|
||||||
> 尤其 **傍王分无法从现有字段拆出**——`naward = 冲关 + 傍王王数` 已经把两者合并,`grade_aw` 是按合并后的 `N` 算出来的。
|
|
||||||
|
|
||||||
### 1.10 解散结算 `free_room` 【待设计】
|
### 1.10 解散结算 `free_room` 【待设计】
|
||||||
|
|
||||||
@@ -1728,9 +1726,8 @@ codes/config/
|
|||||||
|
|
||||||
## 7. 待补充清单
|
## 7. 待补充清单
|
||||||
|
|
||||||
> **状态汇总**(2026-08-25):待确认规格 29 项**已全部有结论**(T-9 亮牌文案暂定 `x对 x主`;T-20 音效明确后续补充)。
|
> **状态汇总**(2026-08-26):设计稿只剩 **D-8**(判定结果文字)。
|
||||||
> **§7.5 服务端待补项**:S-1…S-5 已完成;**新增 S-6(大局结算四项分数,阻塞 D-9)与 S-7(明牌按钮条件与服务端校验不一致,有泄露口子)待处理**。
|
> **§7.5 服务端待补项**:S-1…S-6 已完成,**只剩 S-7**(明牌按钮条件与服务端校验不一致,有泄露口子)。
|
||||||
> **设计稿只剩 D-8**(判定结果文字)。
|
|
||||||
|
|
||||||
### 7.1 待出设计稿的界面
|
### 7.1 待出设计稿的界面
|
||||||
|
|
||||||
@@ -1948,31 +1945,37 @@ data.bottom.cards // 结算包
|
|||||||
| `docs/compliance/*.md`(3 份,24 处) | ⬜ **有意不改**——这三份是历次核对的**历史记录**,反映当时的 design 表述;下轮核对按新 design 重做即可,不追溯修改 |
|
| `docs/compliance/*.md`(3 份,24 处) | ⬜ **有意不改**——这三份是历次核对的**历史记录**,反映当时的 design 表述;下轮核对按新 design 重做即可,不追溯修改 |
|
||||||
| `test/*.js` 其余注释(约 14 处) | ⬜ 仅注释、不影响行为,后续顺手清理 |
|
| `test/*.js` 其余注释(约 14 处) | ⬜ 仅注释、不影响行为,后续顺手清理 |
|
||||||
|
|
||||||
#### S-6 · 大局结算缺「基础分 / 冲关分 / 傍王分」三项分解(新增,⚠️ 阻塞 D-9)
|
#### S-6 · 大局结算的三项分解 —— ✅ 已完成(2026-08-26)
|
||||||
|
|
||||||
**需求**:大局结算面板每行要显示 `基础分{n}` / `总得分{n}` / `冲关分{n}` / **`傍王分{n}`** 四项(见 `大局结算.png`)。
|
**问题**:大局结算面板要显示 `基础分` / `冲关分` / `傍王分`,而原 `account` 只有「累积得分 + 每局得分数组」。其中**傍王分尤其拆不出来**——`N = 冲关奖数 + 傍王王数` 两个来源在**求和之后**才代入 `grade_aw = X×(2Ni−Nj−Nk)`,事后无法反推各自占比。
|
||||||
|
|
||||||
**现状**:协议 `account` 分组只有
|
**解法**:在算钱时就拆。把 `N` 分成 `N_冲关`(只计庄家)与 `N_傍王`(勾选后庄闲都算),**分别代入同一公式**各算一次。公式对 `N` 线性,故恒有 `grade_cg + grade_bw == grade_aw`——这条恒等式正好当回归断言。
|
||||||
|
|
||||||
|
**小局包 `aset.seatlist[i]` 新增**:
|
||||||
|
|
||||||
|
| 字段 | 说明 |
|
||||||
|
| --- | --- |
|
||||||
|
| `grade_cg` | 算奖得分里的**冲关**分量(不含傍王) |
|
||||||
|
| `grade_bw` | 算奖得分里的**傍王**分量(未勾傍王时恒 0) |
|
||||||
|
|
||||||
|
**大局包 `account` 结构改了**(原为二元数组,现为对象数组):
|
||||||
|
|
||||||
```json
|
```json
|
||||||
[[累积得分, [每局得分...]], ...]
|
[{ "score": 0, "grades": [], "grade_jf_total": 0, "grade_cg_total": 0, "grade_bw_total": 0 }, …]
|
||||||
```
|
```
|
||||||
|
|
||||||
拿不出上面四项中的任何一项分解。小局包 `aset.seatlist[i]` 虽有 `grade_jf`(捡分子数得分)与 `grade_aw`(算奖得分),但:
|
| 面板栏位 | 取值 |
|
||||||
|
| --- | --- |
|
||||||
|
| 基础分 | `grade_jf_total` |
|
||||||
|
| 冲关分 | `grade_cg_total` |
|
||||||
|
| 傍王分 | `grade_bw_total` |
|
||||||
|
| 右侧总分大字 | `score` |
|
||||||
|
|
||||||
1. **只有小局值,没有大局累计**——大局要的是全部局数的累加;
|
恒等式:`grade_jf_total + grade_cg_total + grade_bw_total == score`。
|
||||||
2. **傍王分拆不出来**。这是最麻烦的一条:`naward = (庄家?冲关:0) + (傍王?王数:0)`,两个来源在**求和之后**才拿去算钱(`grade_aw = X×(2Ni−Nj−Nk)`)。等于说冲关与傍王的贡献在算钱前就已经混在一个 `N` 里,**事后无法从 `grade_aw` 反推各自占多少**。
|
|
||||||
|
|
||||||
**建议实现**:在 `class.paiju.js` 算 `grade_aw` 的地方,把 `N` 拆成 `N_chongguan`(冲关)与 `N_bangwang`(傍王)**分别代入同一公式**各算一次,得到两个分量后再相加(相加结果必须等于现在的 `grade_aw`,可作为回归断言)。然后在 `account` 里逐项累计下发,例如:
|
**顺带解决了小局结算「冲关分」标签的不严谨**(见 §0.0):该标签现在应改取 `grade_cg` 而非 `grade_aw`,开傍王时也名副其实了。
|
||||||
|
|
||||||
| 字段 | 类型 | 说明 |
|
> **T-31(「基础分」vs「总得分」两行分别是什么)仍未定**:参考图两者数值相同、且与右侧大字对不上(30/30 vs +135),是 mock 不自洽。当前实现按「基础分 = `grade_jf_total`、总分 = `score`」提供;若「总得分」另有所指需再调。
|
||||||
| --- | --- | --- |
|
|
||||||
| `grade_base_total` | 整数 | 基础分(捡分子数得分 `grade_jf`)的大局累计 |
|
|
||||||
| `grade_cg_total` | 整数 | **冲关分**的大局累计(**不含**傍王) |
|
|
||||||
| `grade_bw_total` | 整数 | 傍王分的大局累计 |
|
|
||||||
| `score` | 整数 | 总得分(已有) |
|
|
||||||
|
|
||||||
**需先确认**:面板上的「基础分」与「总得分」两行分别对应什么——参考图里两者数值相同(`基础分30` / `总得分30`),是 mock 巧合还是确有区别?【T-31】
|
|
||||||
|
|
||||||
#### S-7 · 明牌按钮的显示条件与服务端校验不一致(新增,⚠️ 与 design 冲突)
|
#### S-7 · 明牌按钮的显示条件与服务端校验不一致(新增,⚠️ 与 design 冲突)
|
||||||
|
|
||||||
|
|||||||
@@ -19,10 +19,13 @@ var cls_youle_erqiwang_desk = cls_youle_erqiwang_desk || {
|
|||||||
var desk = {};
|
var desk = {};
|
||||||
|
|
||||||
desk.o_room = o_room; //桌所属的房间对象
|
desk.o_room = o_room; //桌所属的房间对象
|
||||||
desk.seatlist = []; //玩家得分
|
//玩家得分。每个元素:[累积总分, [每局总分...], 基础分累计, 冲关分累计, 傍王分累计]
|
||||||
desk.seatlist.push([0, []]); //第一位为累积得分,第二位为每局得分
|
//后三位是大局结算面板要分项展示的累计(design §7 捡分子数 / §8.1 冲关 / §8.3 傍王),
|
||||||
desk.seatlist.push([0, []]);
|
//三者之和恒等于累积总分——下发时组装成对象(见 get_desk_account),数组下标只在内部用
|
||||||
desk.seatlist.push([0, []]);
|
desk.seatlist = [];
|
||||||
|
desk.seatlist.push([0, [], 0, 0, 0]);
|
||||||
|
desk.seatlist.push([0, [], 0, 0, 0]);
|
||||||
|
desk.seatlist.push([0, [], 0, 0, 0]);
|
||||||
desk.prepare = [0,0,0]; //玩家准备状态
|
desk.prepare = [0,0,0]; //玩家准备状态
|
||||||
desk.paiju_list = []; //牌局列表
|
desk.paiju_list = []; //牌局列表
|
||||||
|
|
||||||
@@ -186,7 +189,17 @@ var cls_youle_erqiwang_desk = cls_youle_erqiwang_desk || {
|
|||||||
}
|
}
|
||||||
min_ontimeout(do_save_grade, 1000);
|
min_ontimeout(do_save_grade, 1000);
|
||||||
|
|
||||||
msg.data.account = o_desk.seatlist;
|
//大局结算下发:组装成对象数组,字段自解释(内部 o_desk.seatlist 是数组,下标语义不清,不直接外抛)
|
||||||
|
msg.data.account = [];
|
||||||
|
for (var s = 0; s < o_desk.seatlist.length; s++){
|
||||||
|
var _a = {};
|
||||||
|
_a.score = o_desk.seatlist[s][0]; //累积总分
|
||||||
|
_a.grades = o_desk.seatlist[s][1]; //每局总分列表
|
||||||
|
_a.grade_jf_total = o_desk.seatlist[s][2]; //基础分(捡分子数得分)累计
|
||||||
|
_a.grade_cg_total = o_desk.seatlist[s][3]; //冲关分累计(design §8.1,不含傍王)
|
||||||
|
_a.grade_bw_total = o_desk.seatlist[s][4]; //傍王分累计(design §8.3)
|
||||||
|
msg.data.account.push(_a);
|
||||||
|
}
|
||||||
return msg;
|
return msg;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -978,11 +978,26 @@ var cls_youle_erqiwang_paiju = cls_youle_erqiwang_paiju || {
|
|||||||
];
|
];
|
||||||
|
|
||||||
//算奖并入结算(design §8.4):玩家i从另两家各收 X×Ni、各付 X×Nj,净得 X×(2Ni−Nj−Nk)
|
//算奖并入结算(design §8.4):玩家i从另两家各收 X×Ni、各付 X×Nj,净得 X×(2Ni−Nj−Nk)
|
||||||
var _aw = [
|
var award_pay = function(n){
|
||||||
_X * (2 * _nlist[0] - _nlist[1] - _nlist[2]),
|
return [
|
||||||
_X * (2 * _nlist[1] - _nlist[0] - _nlist[2]),
|
_X * (2 * n[0] - n[1] - n[2]),
|
||||||
_X * (2 * _nlist[2] - _nlist[0] - _nlist[1])
|
_X * (2 * n[1] - n[0] - n[2]),
|
||||||
];
|
_X * (2 * n[2] - n[0] - n[1])
|
||||||
|
];
|
||||||
|
}
|
||||||
|
var _aw = award_pay(_nlist);
|
||||||
|
|
||||||
|
//冲关与傍王【分别】再各算一次,供大局结算分项展示(S-6)。
|
||||||
|
//必须在这里拆——N = 冲关 + 傍王 求和后才代入公式,事后无法从 grade_aw 反推各自占比。
|
||||||
|
//公式对 N 是线性的,故 _awcg[i] + _awbw[i] 恒等于 _aw[i](下方 _p 赋值处有断言性注释)。
|
||||||
|
var _ncg = [0, 0, 0]; //冲关分量:只计庄家(design §8.1)
|
||||||
|
var _nbw = [0, 0, 0]; //傍王分量:勾选时庄闲都算(design §8.3)
|
||||||
|
for (var q = 0; q < 3; q++){
|
||||||
|
if (q == o_paiju.banker){ _ncg[q] = _cglist[q].count; }
|
||||||
|
if (_bangwang){ _nbw[q] = _cglist[q].wang; }
|
||||||
|
}
|
||||||
|
var _awcg = award_pay(_ncg);
|
||||||
|
var _awbw = award_pay(_nbw);
|
||||||
|
|
||||||
//捡分子数:_win=1庄赢→庄+X×2/闲−X;_win=-1闲赢→庄−X×2/闲+X
|
//捡分子数:_win=1庄赢→庄+X×2/闲−X;_win=-1闲赢→庄−X×2/闲+X
|
||||||
var _jf = [0, 0, 0];
|
var _jf = [0, 0, 0];
|
||||||
@@ -1003,12 +1018,17 @@ var cls_youle_erqiwang_paiju = cls_youle_erqiwang_paiju || {
|
|||||||
_p.chongguan = _cglist[i].count; //常规算奖奖数(仅庄家计入 N)
|
_p.chongguan = _cglist[i].count; //常规算奖奖数(仅庄家计入 N)
|
||||||
_p.wang = _cglist[i].wang; //王数
|
_p.wang = _cglist[i].wang; //王数
|
||||||
_p.naward = _nlist[i]; //该家总奖数 N
|
_p.naward = _nlist[i]; //该家总奖数 N
|
||||||
_p.grade_aw = _aw[i]; //算奖得分 X×(2Ni−Nj−Nk)
|
_p.grade_aw = _aw[i]; //算奖得分 X×(2Ni−Nj−Nk),= grade_cg + grade_bw
|
||||||
_p.grade_jf = _jf[i]; //捡分子数得分
|
_p.grade_cg = _awcg[i]; //其中的冲关分量(design §8.1,不含傍王)
|
||||||
|
_p.grade_bw = _awbw[i]; //其中的傍王分量(design §8.3)
|
||||||
|
_p.grade_jf = _jf[i]; //捡分子数得分(基础分)
|
||||||
_p.grade = _p.grade_aw + _p.grade_jf; //本局总得分
|
_p.grade = _p.grade_aw + _p.grade_jf; //本局总得分
|
||||||
//大局累积得分
|
//大局累积:总分 + 三项分量(基础/冲关/傍王),供大局结算面板分项展示
|
||||||
o_paiju.o_desk.seatlist[i][0] = o_paiju.o_desk.seatlist[i][0] + _p.grade;
|
o_paiju.o_desk.seatlist[i][0] = o_paiju.o_desk.seatlist[i][0] + _p.grade;
|
||||||
_p.score = o_paiju.o_desk.seatlist[i][0];
|
_p.score = o_paiju.o_desk.seatlist[i][0];
|
||||||
|
o_paiju.o_desk.seatlist[i][2] = o_paiju.o_desk.seatlist[i][2] + _p.grade_jf;
|
||||||
|
o_paiju.o_desk.seatlist[i][3] = o_paiju.o_desk.seatlist[i][3] + _p.grade_cg;
|
||||||
|
o_paiju.o_desk.seatlist[i][4] = o_paiju.o_desk.seatlist[i][4] + _p.grade_bw;
|
||||||
//保存每局得分
|
//保存每局得分
|
||||||
o_paiju.o_desk.seatlist[i][1].push(_p.grade);
|
o_paiju.o_desk.seatlist[i][1].push(_p.grade);
|
||||||
msg.data.aset.seatlist.push(_p);
|
msg.data.aset.seatlist.push(_p);
|
||||||
|
|||||||
@@ -468,7 +468,9 @@
|
|||||||
"chongguan": 0, // 冲关奖数(design §8.1,旧称"常规算奖";只有庄家才计入自己的 N)
|
"chongguan": 0, // 冲关奖数(design §8.1,旧称"常规算奖";只有庄家才计入自己的 N)
|
||||||
"wang": 0, // 手牌中的王数(傍王按此计奖)
|
"wang": 0, // 手牌中的王数(傍王按此计奖)
|
||||||
"naward": 0, // 该家总奖数 N =(庄家?冲关奖数:0)+(傍王?王数:0)
|
"naward": 0, // 该家总奖数 N =(庄家?冲关奖数:0)+(傍王?王数:0)
|
||||||
"grade_aw": 0, // 算奖得分 = X×(2Ni−Nj−Nk),X 为每对子子数。注意这是【冲关 + 傍王合计】后的结果,两个分量事后拆不开(见 §0.0c)
|
"grade_aw": 0, // 算奖得分 = X×(2Ni−Nj−Nk),X 为每对子子数。= grade_cg + grade_bw
|
||||||
|
"grade_cg": 0, // 其中的【冲关】分量(design §8.1,不含傍王)
|
||||||
|
"grade_bw": 0, // 其中的【傍王】分量(design §8.3;未勾傍王时恒 0)
|
||||||
"grade_jf": 0, // 捡分子数得分
|
"grade_jf": 0, // 捡分子数得分
|
||||||
"grade": 0, // 本局总分 = grade_aw + grade_jf
|
"grade": 0, // 本局总分 = grade_aw + grade_jf
|
||||||
"score": 0 // 累计得分
|
"score": 0 // 累计得分
|
||||||
@@ -476,6 +478,11 @@
|
|||||||
```
|
```
|
||||||
|
|
||||||
> 结算数值模型(design §7~§8):每「庄–闲」对子的基础金额 `X = multiple × |upgrade|`(投降 X=1、解散 X=0)。捡分子数:庄赢时两闲家各付庄家 X、庄家收 2X;闲赢(升级)时庄家各付两闲家 X。算奖:持有 N 奖的玩家从另外两人各多收 `X×N`,三家两两独立叠加(含闲–闲),即 `grade_aw = X×(2Ni−Nj−Nk)`。
|
> 结算数值模型(design §7~§8):每「庄–闲」对子的基础金额 `X = multiple × |upgrade|`(投降 X=1、解散 X=0)。捡分子数:庄赢时两闲家各付庄家 X、庄家收 2X;闲赢(升级)时庄家各付两闲家 X。算奖:持有 N 奖的玩家从另外两人各多收 `X×N`,三家两两独立叠加(含闲–闲),即 `grade_aw = X×(2Ni−Nj−Nk)`。
|
||||||
|
>
|
||||||
|
> **算奖的两个分量**(供大局结算分项展示):`N = N_冲关 + N_傍王`(冲关只计庄家、傍王勾选后庄闲都算)。
|
||||||
|
> 服务端把两个分量**分别代入同一公式**各算一次,得到 `grade_cg` 与 `grade_bw`。
|
||||||
|
> 公式对 `N` 是线性的,因此恒有 **`grade_cg + grade_bw == grade_aw`**,且两个分量各自零和。
|
||||||
|
> 之所以必须在算钱时就拆,是因为两个 `N` 一旦相加就再也分不开——事后无法从 `grade_aw` 反推各自占比。
|
||||||
|
|
||||||
**account(大局结算包)**
|
**account(大局结算包)**
|
||||||
|
|
||||||
@@ -483,16 +490,28 @@
|
|||||||
|
|
||||||
| 参数名 | 类型 | 说明 |
|
| 参数名 | 类型 | 说明 |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| account | 数组 | 玩家列表,第一位为累积得分,第二位为每局得分,结构见下 |
|
| account | 数组 | 玩家列表(长度 3,下标 = 座位序号),元素结构见下 |
|
||||||
|
|
||||||
```json
|
```json
|
||||||
[
|
[
|
||||||
[0, []],
|
{ "score": 0, "grades": [], "grade_jf_total": 0, "grade_cg_total": 0, "grade_bw_total": 0 },
|
||||||
[0, []],
|
{ "score": 0, "grades": [], "grade_jf_total": 0, "grade_cg_total": 0, "grade_bw_total": 0 },
|
||||||
[0, []]
|
{ "score": 0, "grades": [], "grade_jf_total": 0, "grade_cg_total": 0, "grade_bw_total": 0 }
|
||||||
]
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| score | 整数 | 累积总分(全部小局 `aset.seatlist[i].grade` 之和) |
|
||||||
|
| grades | 数组 | 每局总分列表,按局序 |
|
||||||
|
| grade_jf_total | 整数 | **基础分**累计:各局 `grade_jf`(捡分子数得分)之和 |
|
||||||
|
| grade_cg_total | 整数 | **冲关分**累计:各局 `grade_cg` 之和(design §8.1,**不含**傍王) |
|
||||||
|
| grade_bw_total | 整数 | **傍王分**累计:各局 `grade_bw` 之和(design §8.3;未勾傍王时恒 0) |
|
||||||
|
|
||||||
|
> 恒等式:`grade_jf_total + grade_cg_total + grade_bw_total == score`,供大局结算面板分项展示(基础分 / 冲关分 / 傍王分 / 总分)。
|
||||||
|
>
|
||||||
|
> ⚠️ **结构变更(2026-08-26)**:原为 `[[累积得分, [每局得分...]], ...]` 的二元数组,现改为**对象数组**并增加三项分解。数组下标语义不清,且前端尚未开工,此时改代价最小。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 15. 准备(zhunbei,客户端→服务器)
|
## 15. 准备(zhunbei,客户端→服务器)
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ function setup(roomtype, o_paiju, seatlist) {
|
|||||||
const o_desk = {
|
const o_desk = {
|
||||||
paiju_list: [o_paiju],
|
paiju_list: [o_paiju],
|
||||||
o_room: null,
|
o_room: null,
|
||||||
seatlist: [[0, []], [0, []], [0, []]],
|
seatlist: [[0, [], 0, 0, 0], [0, [], 0, 0, 0], [0, [], 0, 0, 0]],
|
||||||
prepare: [0, 0, 0],
|
prepare: [0, 0, 0],
|
||||||
method: {
|
method: {
|
||||||
curr_paiju: () => o_paiju,
|
curr_paiju: () => o_paiju,
|
||||||
|
|||||||
@@ -72,9 +72,58 @@ t.eq('§7 捡分子数分配 庄±2X / 闲各∓X', aset.seatlist.map(p => p.gra
|
|||||||
const N = aset.seatlist.map(p => p.naward);
|
const N = aset.seatlist.map(p => p.naward);
|
||||||
t.eq('§8.4 算奖分配 X×(2Ni−Nj−Nk)', aset.seatlist.map(p => p.grade_aw),
|
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])]);
|
[X * (2 * N[0] - N[1] - N[2]), X * (2 * N[1] - N[0] - N[2]), X * (2 * N[2] - N[0] - N[1])]);
|
||||||
|
// S-6:算奖得分按【冲关 / 傍王】两个分量分别再算一次,供大局结算分项展示。
|
||||||
|
// 公式对 N 线性,故两分量之和必须恒等于 grade_aw——这是拆分正确性的核心不变量。
|
||||||
|
t.eq('S-6 grade_cg + grade_bw == grade_aw',
|
||||||
|
aset.seatlist.map(p => p.grade_cg + p.grade_bw), aset.seatlist.map(p => p.grade_aw));
|
||||||
|
// 分量口径:冲关只计庄家;傍王未勾选(roomtype "00000" 位2=0)时应恒为 0
|
||||||
|
t.eq('S-6 未勾傍王 → grade_bw 全 0', aset.seatlist.map(p => p.grade_bw), [0, 0, 0]);
|
||||||
|
t.eq('S-6 未勾傍王 → grade_cg == grade_aw', aset.seatlist.map(p => p.grade_cg), aset.seatlist.map(p => p.grade_aw));
|
||||||
t.eq('本局结算零和', aset.seatlist.reduce((s, p) => s + p.grade, 0), 0);
|
t.eq('本局结算零和', aset.seatlist.reduce((s, p) => s + p.grade, 0), 0);
|
||||||
|
t.eq('S-6 两个分量各自零和', [
|
||||||
|
aset.seatlist.reduce((s, p) => s + p.grade_cg, 0),
|
||||||
|
aset.seatlist.reduce((s, p) => s + p.grade_bw, 0)], [0, 0]);
|
||||||
t.eq('§4.6 result 与判定一致(0庄赢/1闲赢)', pj.result, aset.upgrade > 0 ? 0 : 1);
|
t.eq('§4.6 result 与判定一致(0庄赢/1闲赢)', pj.result, aset.upgrade > 0 ? 0 : 1);
|
||||||
|
|
||||||
|
// ============ S-6:开傍王局的分量拆分 + 大局累计 ============
|
||||||
|
// 上面那局没开傍王,grade_bw 恒 0,只验到平凡情形。这里开傍王(roomtype 位2=1)跑一整局,
|
||||||
|
// 让两个分量都非零,才算真的验证了「N 拆成冲关/傍王分别代入公式」这件事。
|
||||||
|
{
|
||||||
|
const bw = seatBanker("00100", 65); // 位2=1 → 傍王开
|
||||||
|
mod.xuanzhu(pk(0, { flower: 1 }));
|
||||||
|
mod.maipai(pk(0, { cards: P.get_seat_cards(bw.pj, 0).slice(-8) }));
|
||||||
|
t.eq('S-6 傍王局 打满28轮', playAll(bw.pj) && bw.pj.playproc.round, 28);
|
||||||
|
const a = bw.ctx.sent[bw.ctx.sent.length - 1].data.aset;
|
||||||
|
t.eq('S-6 傍王局 bangwang 标志', a.bangwang, 1);
|
||||||
|
// 核心不变量:两分量之和 == grade_aw
|
||||||
|
t.eq('S-6 傍王局 grade_cg + grade_bw == grade_aw',
|
||||||
|
a.seatlist.map(p => p.grade_cg + p.grade_bw), a.seatlist.map(p => p.grade_aw));
|
||||||
|
// 分量各自零和(每条支付线都是三家之间的转移,不产生也不消灭分数)
|
||||||
|
t.eq('S-6 傍王局 两分量各自零和', [
|
||||||
|
a.seatlist.reduce((s, p) => s + p.grade_cg, 0),
|
||||||
|
a.seatlist.reduce((s, p) => s + p.grade_bw, 0)], [0, 0]);
|
||||||
|
// 口径:naward = 冲关(仅庄家) + 王数(傍王开)
|
||||||
|
const X = a.multiple * Math.abs(a.upgrade);
|
||||||
|
t.eq('S-6 傍王局 naward 口径', a.seatlist.map((p, i) => p.naward),
|
||||||
|
a.seatlist.map((p, i) => (i === a.banker ? p.chongguan : 0) + p.wang));
|
||||||
|
// 傍王分量必须真的非零,否则这一整块又是平凡情形(三家王数全相等时净额才会是 0)
|
||||||
|
const wangs = a.seatlist.map(p => p.wang);
|
||||||
|
const wangAllSame = wangs[0] === wangs[1] && wangs[1] === wangs[2];
|
||||||
|
t.eq('S-6 傍王局 王数不全相等时 grade_bw 必有非零', wangAllSame || a.seatlist.some(p => p.grade_bw !== 0), true);
|
||||||
|
// 逐项复核傍王分量的公式
|
||||||
|
const NB = a.seatlist.map(p => p.wang);
|
||||||
|
t.eq('S-6 傍王分量 == X×(2Ni−Nj−Nk)', a.seatlist.map(p => p.grade_bw),
|
||||||
|
[X * (2 * NB[0] - NB[1] - NB[2]), X * (2 * NB[1] - NB[0] - NB[2]), X * (2 * NB[2] - NB[0] - NB[1])]);
|
||||||
|
// 冲关分量:只有庄家有 N_cg,闲家为 0
|
||||||
|
const NC = a.seatlist.map((p, i) => (i === a.banker ? p.chongguan : 0));
|
||||||
|
t.eq('S-6 冲关分量 == X×(2Ni−Nj−Nk)(仅庄家有 N)', a.seatlist.map(p => p.grade_cg),
|
||||||
|
[X * (2 * NC[0] - NC[1] - NC[2]), X * (2 * NC[1] - NC[0] - NC[2]), X * (2 * NC[2] - NC[0] - NC[1])]);
|
||||||
|
// 大局累计:desk.seatlist 后三位分别累计 基础/冲关/傍王,三者之和 == 累积总分
|
||||||
|
const dsk = bw.pj.o_desk.seatlist;
|
||||||
|
t.eq('S-6 大局累计 基础+冲关+傍王 == 累积总分',
|
||||||
|
dsk.map(s => s[2] + s[3] + s[4]), dsk.map(s => s[0]));
|
||||||
|
}
|
||||||
|
|
||||||
// ============ §4.4 / §7.1 投降局 ============
|
// ============ §4.4 / §7.1 投降局 ============
|
||||||
const tx = seatBanker("00000", 70);
|
const tx = seatBanker("00000", 70);
|
||||||
t.eq('§4.3 70分坐庄:暗牌亮3秒标志 + 闲家也收到暗牌',
|
t.eq('§4.3 70分坐庄:暗牌亮3秒标志 + 闲家也收到暗牌',
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ t.eq('亮牌 已埋的牌不计入快照', sortIds(P.get_liangpai(buried).cards)
|
|||||||
|
|
||||||
// ============ §7/§8 结算 get_paiju_account 集成 ============
|
// ============ §7/§8 结算 get_paiju_account 集成 ============
|
||||||
// 受控 o_paiju/o_desk/o_room:控制庄家算奖手牌(dealowner=1)与闲1捡分(playowner=1,score)
|
// 受控 o_paiju/o_desk/o_room:控制庄家算奖手牌(dealowner=1)与闲1捡分(playowner=1,score)
|
||||||
const mkDesk = roomtype => ({ seatlist: [[0, []], [0, []], [0, []]], o_room: { roomtype, asetcount: 6 }, method: { get_desk_account: m => m } });
|
const mkDesk = roomtype => ({ seatlist: [[0, [], 0, 0, 0], [0, [], 0, 0, 0], [0, [], 0, 0, 0]], o_room: { roomtype, asetcount: 6 }, method: { get_desk_account: m => m } });
|
||||||
const paiju = (call, flower, cards, roomtype) => ({ call, banker: 0, flower, cards, idx: 2, o_desk: mkDesk(roomtype), endtime: null, result: null });
|
const paiju = (call, flower, cards, roomtype) => ({ call, banker: 0, flower, cards, idx: 2, o_desk: mkDesk(roomtype), endtime: null, result: null });
|
||||||
// bankerIds→庄家算奖手牌(dealowner=1);xian1grade→闲1捡分(每张≤10分)
|
// bankerIds→庄家算奖手牌(dealowner=1);xian1grade→闲1捡分(每张≤10分)
|
||||||
function mkCards(bankerIds, xian1grade) {
|
function mkCards(bankerIds, xian1grade) {
|
||||||
|
|||||||
@@ -285,7 +285,7 @@ t.eq('重连 常规房 ChooseMain.multiple=6', E.get_deskinfo(n45.c.o_room, 0).C
|
|||||||
t.eq('重连 爬坡房 ChooseMain.multiple=7', E.get_deskinfo(p45.c.o_room, 0).ChooseMain.multiple, 7);
|
t.eq('重连 爬坡房 ChooseMain.multiple=7', E.get_deskinfo(p45.c.o_room, 0).ChooseMain.multiple, 7);
|
||||||
// 与结算包 aset.multiple 一致(同一 call 同一房间规则)
|
// 与结算包 aset.multiple 一致(同一 call 同一房间规则)
|
||||||
const aset45 = P.get_paiju_account(
|
const aset45 = P.get_paiju_account(
|
||||||
{ call: 45, banker: 0, flower: 1, cards: [], idx: 1, endtime: null, result: null, o_desk: { seatlist: [[0, []], [0, []], [0, []]], o_room: { roomtype: "00010", asetcount: 6 }, method: { get_desk_account: m => m } } },
|
{ call: 45, banker: 0, flower: 1, cards: [], idx: 1, endtime: null, result: null, o_desk: { seatlist: [[0, [], 0, 0, 0], [0, [], 0, 0, 0], [0, [], 0, 0, 0]], o_room: { roomtype: "00010", asetcount: 6 }, method: { get_desk_account: m => m } } },
|
||||||
0, {}).data.aset.multiple;
|
0, {}).data.aset.multiple;
|
||||||
t.eq('爬坡房 下发面 multiple 与结算 aset.multiple 一致', szm(p45.c), aset45);
|
t.eq('爬坡房 下发面 multiple 与结算 aset.multiple 一致', szm(p45.c), aset45);
|
||||||
|
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ t.eq('chupai1 下发3包', cp1.length, 3);
|
|||||||
t.eq('chupai1 带 success=true', cp1.every(m => m.data.success === true), true);
|
t.eq('chupai1 带 success=true', cp1.every(m => m.data.success === true), true);
|
||||||
|
|
||||||
// -- 结算包 jiesuan:正常/投降/解散 三种 type 都带 success --
|
// -- 结算包 jiesuan:正常/投降/解散 三种 type 都带 success --
|
||||||
const mkDesk = () => ({ seatlist: [[0, []], [0, []], [0, []]], o_room: { roomtype: '00000', asetcount: 6 }, method: { get_desk_account: m => m } });
|
const mkDesk = () => ({ seatlist: [[0, [], 0, 0, 0], [0, [], 0, 0, 0], [0, [], 0, 0, 0]], o_room: { roomtype: '00000', asetcount: 6 }, method: { get_desk_account: m => m } });
|
||||||
const mkPaiju = () => ({ call: 65, banker: 0, flower: 1, cards: [], idx: 2, o_desk: mkDesk(), endtime: null, result: null });
|
const mkPaiju = () => ({ call: 65, banker: 0, flower: 1, cards: [], idx: 2, o_desk: mkDesk(), endtime: null, result: null });
|
||||||
t.eq('jiesuan 正常结算 success=true', P.get_paiju_account(mkPaiju(), 0, {}).data.success, true);
|
t.eq('jiesuan 正常结算 success=true', P.get_paiju_account(mkPaiju(), 0, {}).data.success, true);
|
||||||
t.eq('jiesuan 投降结算 success=true', P.get_paiju_account(mkPaiju(), 1, {}).data.success, true);
|
t.eq('jiesuan 投降结算 success=true', P.get_paiju_account(mkPaiju(), 1, {}).data.success, true);
|
||||||
|
|||||||
Reference in New Issue
Block a user