二七王:新增 Node 单元测试,CLAUDE.md 记录测试命令
- test/_shim.js:平台全局垫片(min_ary_include/deduct、min_random/now/ontimeout、 youle_erqiwang.import mock),忠实复刻 minhttp.js;仅测试用,不进正式代码 - test/_assert.js + run.js:极简断言与总运行器 - test/test_arith.js(扣底/算子常规+爬坡/算奖/甩牌/跟牌分量) test_config.js(位串/局数扣卡) test/test_paiju.js(亮牌/结算 get_paiju_account 集成),共 89 项断言全过 - test/README.md:运行方式与双运行时原理 - CLAUDE.md:常用命令补测试入口,说明服务端双运行时规范 (CLAUDE.md 内另含此前一处措辞微调,随本次文档更新一并纳入) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -10,18 +10,21 @@
|
||||
|
||||
## 常用命令
|
||||
|
||||
没有配置任何构建/lint/测试工具。仓库里唯一的脚本:
|
||||
没有配置任何构建/lint 工具。仓库里的脚本:
|
||||
|
||||
```bash
|
||||
# 根据 client/assets/spine/*.json 重新生成 client/generated/spine_assets.js 与 spine_data.js
|
||||
client/scripts/build_spine_data.cmd # 内部调用 build_spine_data.ps1
|
||||
|
||||
# 二七王服务端单元测试(纯 node 直跑,无框架)
|
||||
node server/games/erqiwang/test/run.js # 跑全部;退出码 0 全过、非 0 有失败
|
||||
```
|
||||
|
||||
在 `client/assets/spine/` 增删 Spine 导出文件后运行;不要手改这两个生成文件。
|
||||
在 `client/assets/spine/` 增删 Spine 导出文件后运行第一条;不要手改这两个生成文件。
|
||||
|
||||
查看客户端效果需用 HTTP 方式(而非 `file://`)打开 `client/index.html`,例如 `npx http-server client -p 8080`。
|
||||
|
||||
`shared/`(共享算法,待子游戏接入后才有)的改动应按两份文档中「§10 测试纪律」的要求跑 Node 单测验证——目前尚未接入测试框架,意味着需要直接用 `node` 运行相应脚本。
|
||||
服务端子游戏代码遵循「一套代码,两个运行时」(见 `docs/server/development-guide/01` §1):友乐/浏览器无 `require`、由 `min_loadJsFile` 加载为全局;Node(本地/单元测试)用文件顶部 `if (typeof require!=='undefined')` 守卫 require、底部 `module.exports` 导出。二七王据此已可用 `node` 直接单测(见 `server/games/erqiwang/test/README.md`);`shared/`(共享算法)改动同样按两份文档「§10 测试纪律」跑 Node 单测验证。
|
||||
|
||||
## 架构
|
||||
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
# 二七王服务端单元测试
|
||||
|
||||
## 运行
|
||||
|
||||
```bash
|
||||
node server/games/erqiwang/test/run.js # 跑全部单测
|
||||
node server/games/erqiwang/test/test_arith.js # 单独跑某一组
|
||||
```
|
||||
|
||||
退出码 0 表示全过,非 0 表示有失败;`run.js` 逐个以独立进程 spawn 各 `test_*.js` 并汇总。
|
||||
|
||||
## 原理:一套代码,两个运行时(dev-guide 01 §1)
|
||||
|
||||
正式代码(`class.*.js`)遵循平台的双运行时规范:
|
||||
|
||||
- **友乐 / 浏览器**:无 `require`,由 `mod.js` 用 `min_loadJsFile` 加载为**全局对象**;文件顶部的 `if (typeof require !== 'undefined')` 守卫被跳过,跨模块按**同名全局**引用。
|
||||
- **Node(本地 / 单元测试)**:有 `require`,守卫块 `var X = require('./class.X.js')` 取到依赖;文件底部 `if (typeof module !== 'undefined') module.exports = ...` 导出。
|
||||
|
||||
因此这两处守卫是**纯增量**:友乐运行时行为完全不变,只在 Node 下额外启用 require/exports。
|
||||
|
||||
## `_shim.js`:平台全局垫片(仅测试用,不进正式代码)
|
||||
|
||||
正式代码按全局名引用少量平台工具(`min_ary_include/min_ary_deduct/min_random/min_now/min_ontimeout`)与模块入口 `youle_erqiwang.import`。友乐由平台提供,Node 单测由 `test/_shim.js` 提供等价实现(数组/随机工具忠实复刻 `server/minhttp.js`,`youle_erqiwang.import` 用 mock)。测试文件顶部先 `require('./_shim')` 再 `require('../class.*.js')`。
|
||||
|
||||
## 覆盖
|
||||
|
||||
- `test_arith.js`:扣底倍数(§6.3)、算子常规+爬坡逐档(§7)、算奖 get_chongguan(§8.1)、甩牌分解/最大性/合法性(§5.4)、跟甩牌强制分量拆解(§5.4.4)。
|
||||
- `test_config.js`:roomtype 位串解析、局数/扣卡映射(§10.1)。
|
||||
- `test_paiju.js`:亮牌 get_liangpai(§8.2)、结算 get_paiju_account 集成(§7/§8,含投降)。
|
||||
|
||||
## 约定(dev-guide §10 测试纪律)
|
||||
|
||||
- 测试代码可用现代 JS(不受严格 ES5 限制);**正式代码不得为测试放宽/加后门**。
|
||||
- 正反面 + 边界都要覆盖;失败先裁根因(业务缺陷改业务、脚本缺陷改脚本),不软化断言。
|
||||
@@ -0,0 +1,16 @@
|
||||
// 极简断言助手(测试代码不受严格 ES5 限制,dev-guide 05/04 §10)
|
||||
module.exports = function () {
|
||||
let pass = true;
|
||||
let count = 0;
|
||||
function eq(name, got, exp) {
|
||||
const p = JSON.stringify(got) === JSON.stringify(exp);
|
||||
count++;
|
||||
if (!p) pass = false;
|
||||
console.log((p ? 'PASS' : 'FAIL') + ' ' + name + ' got=' + JSON.stringify(got) + ' exp=' + JSON.stringify(exp));
|
||||
}
|
||||
function done(label) {
|
||||
console.log((pass ? 'ALL PASS' : 'HAS FAILURE') + ' [' + label + '] ' + count + ' checks');
|
||||
return pass;
|
||||
}
|
||||
return { eq, done };
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
// Node 单测环境:提供正式代码按全局名引用的平台工具(不进正式代码,dev-guide 01 §1)
|
||||
global.min_ary_include = function(aryparent, arychild){
|
||||
for (var i = 0; i < arychild.length; i++){
|
||||
var found = false;
|
||||
for (var j = 0; j < aryparent.length; j++){ if (aryparent[j] == arychild[i]){ found = true; break; } }
|
||||
if (!found){ return false; }
|
||||
}
|
||||
return true;
|
||||
};
|
||||
global.min_ary_deduct = function(aryparent, arychild){
|
||||
var re = [];
|
||||
for (var i = 0; i < aryparent.length; i++){
|
||||
var found = false;
|
||||
for (var j = 0; j < arychild.length; j++){ if (aryparent[i] == arychild[j]){ found = true; break; } }
|
||||
if (!found){ re.push(aryparent[i]); }
|
||||
}
|
||||
return re;
|
||||
};
|
||||
global.min_random = function(min, max){ return min + Math.round(Math.random() * (max - min)); };
|
||||
global.min_now = function(ts){ return ts ? ts : Date.now(); };
|
||||
global.min_ontimeout = function(func, time){ /* 单测不真正异步执行 */ };
|
||||
global.youle_erqiwang = global.youle_erqiwang || {
|
||||
import: { check_player: function(){ return null; }, deduct_roomcard: function(){}, save_grade: function(){} },
|
||||
app: { SendPack: function(){} }
|
||||
};
|
||||
module.exports = {};
|
||||
@@ -0,0 +1,16 @@
|
||||
// 二七王服务端单测总运行器:逐个 spawn test_*.js(各自独立进程/全局),汇总结果
|
||||
// 用法:node server/games/erqiwang/test/run.js
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const cp = require('child_process');
|
||||
|
||||
const dir = __dirname;
|
||||
const files = fs.readdirSync(dir).filter(f => /^test_.*\.js$/.test(f)).sort();
|
||||
let allOk = true;
|
||||
for (const f of files) {
|
||||
console.log('\n===== ' + f + ' =====');
|
||||
const r = cp.spawnSync(process.execPath, [path.join(dir, f)], { stdio: 'inherit' });
|
||||
if (r.status !== 0) allOk = false;
|
||||
}
|
||||
console.log('\n' + (allOk ? '===== 全部单测通过 =====' : '===== 存在失败单测 ====='));
|
||||
process.exit(allOk ? 0 : 1);
|
||||
@@ -0,0 +1,124 @@
|
||||
// arith 纯算法单测:扣底(§6.3) / 算子(§7) / 算奖(§8.1) / 甩牌(§5.4) / 跟甩牌(§5.4.4)
|
||||
require('./_shim');
|
||||
const A = require('../class.arith.js');
|
||||
const t = require('./_assert')();
|
||||
|
||||
// 牌id构造:deck d(1-2), flower f(1-4), number n(1-13)
|
||||
const id = (d, f, n) => (d - 1) * 54 + (f - 1) * 13 + (n - 1);
|
||||
const big = d => (d - 1) * 54 + 53; // 大王
|
||||
const small = d => (d - 1) * 54 + 52; // 小王
|
||||
const MF = 1; // 主花色=方块(1)
|
||||
|
||||
// ============ §6.3 扣底倍数 get_bottom_multiple ============
|
||||
const mult = cards => A.get_bottom_multiple(MF, cards);
|
||||
t.eq('扣底 单张主A', mult([id(1, 1, 1)]), 1);
|
||||
t.eq('扣底 单张正7', mult([id(1, 1, 7)]), 1);
|
||||
t.eq('扣底 主对K', mult([id(1, 1, 13), id(2, 1, 13)]), 2);
|
||||
t.eq('扣底 两连对KQ', mult([id(1, 1, 13), id(2, 1, 13), id(1, 1, 12), id(2, 1, 12)]), 4);
|
||||
t.eq('扣底 三连对KQJ', mult([id(1, 1, 13), id(2, 1, 13), id(1, 1, 12), id(2, 1, 12), id(1, 1, 11), id(2, 1, 11)]), 6);
|
||||
t.eq('扣底 单张副A(非主)', mult([id(1, 2, 1)]), 0);
|
||||
t.eq('扣底 小王对', mult([small(1), small(2)]), 2);
|
||||
t.eq('扣底 8-6连对', mult([id(1, 1, 8), id(2, 1, 8), id(1, 1, 6), id(2, 1, 6)]), 4);
|
||||
t.eq('扣底 两连对+单(取最高规格)', mult([id(1, 1, 13), id(2, 1, 13), id(1, 1, 12), id(2, 1, 12), id(1, 1, 9)]), 4);
|
||||
|
||||
// ============ §7 算子:判定标签 + 每对子子数 X ============
|
||||
function judge(call, grade, climb) {
|
||||
const base = A.get_base_bycall(call, climb);
|
||||
const q = A.get_qvalue(call, climb);
|
||||
const upg = A.get_upgrade(call, grade, q);
|
||||
const X = Math.abs(upg) * base;
|
||||
let label = upg === 3 ? '大光' : upg === 2 ? '小光' : upg === 1 ? '过庄' : upg < 0 ? '升' + (-upg) + '级' : '?';
|
||||
return label + '/' + X;
|
||||
}
|
||||
// 常规算子 (design §7.2.1)
|
||||
t.eq('常规65 大光', judge(65, 0, false), '大光/6');
|
||||
t.eq('常规65 小光', judge(65, 20, false), '小光/4');
|
||||
t.eq('常规65 过庄', judge(65, 50, false), '过庄/2');
|
||||
t.eq('常规65 升1', judge(65, 65, false), '升1级/2');
|
||||
t.eq('常规65 升2', judge(65, 105, false), '升2级/4');
|
||||
t.eq('常规60 大光', judge(60, 0, false), '大光/9');
|
||||
t.eq('常规55 大光', judge(55, 0, false), '大光/12');
|
||||
t.eq('常规50 大光', judge(50, 0, false), '大光/18');
|
||||
t.eq('常规45 过庄', judge(45, 44, false), '过庄/6');
|
||||
t.eq('常规40 grade20 小光(无过庄)', judge(40, 20, false), '小光/12');
|
||||
t.eq('常规40 升1', judge(40, 40, false), '升1级/6');
|
||||
t.eq('常规10 grade5 小光', judge(10, 5, false), '小光/12');
|
||||
t.eq('常规10 升1', judge(10, 10, false), '升1级/6');
|
||||
t.eq('常规5 大光', judge(5, 0, false), '大光/18');
|
||||
t.eq('常规5 升1(无小光)', judge(5, 5, false), '升1级/6');
|
||||
t.eq('常规70打牌 大光', judge(70, 0, false), '大光/6');
|
||||
t.eq('常规70打牌 过庄', judge(70, 69, false), '过庄/2');
|
||||
// 爬坡 (design §7.3.3)
|
||||
t.eq('爬坡40 大光', judge(40, 0, true), '大光/24');
|
||||
t.eq('爬坡40 小光', judge(40, 19, true), '小光/16');
|
||||
t.eq('爬坡40 过庄', judge(40, 20, true), '过庄/8');
|
||||
t.eq('爬坡40 升1', judge(40, 40, true), '升1级/8');
|
||||
t.eq('爬坡40 升2', judge(40, 60, true), '升2级/16');
|
||||
t.eq('爬坡30 过庄', judge(30, 15, true), '过庄/10');
|
||||
t.eq('爬坡20 升1', judge(20, 20, true), '升1级/12');
|
||||
t.eq('爬坡10 过庄(无小光)', judge(10, 5, true), '过庄/14');
|
||||
t.eq('爬坡10 升1', judge(10, 10, true), '升1级/14');
|
||||
t.eq('爬坡5 大光', judge(5, 0, true), '大光/45');
|
||||
t.eq('爬坡5 升1(无小光过庄)', judge(5, 5, true), '升1级/15');
|
||||
t.eq('爬坡45 过庄', judge(45, 44, true), '过庄/7');
|
||||
|
||||
// ============ §8.1 算奖 get_chongguan ============
|
||||
function fill28(req) {
|
||||
const used = new Set(req), out = req.slice(), NN = [1, 5, 6, 8, 9, 10, 11, 12, 13];
|
||||
for (const f of [1, 2, 3, 4]) for (const n of NN) {
|
||||
if (out.length >= 28) break;
|
||||
const c = id(1, f, n);
|
||||
if (!used.has(c)) { used.add(c); out.push(c); }
|
||||
}
|
||||
return out;
|
||||
}
|
||||
const cg = (req, key) => A.get_chongguan(MF, fill28(req))[key];
|
||||
t.eq('算奖 3王 count', cg([big(1), small(1), small(2)], 'count'), 1);
|
||||
t.eq('算奖 3王 wang', cg([big(1), small(1), small(2)], 'wang'), 3);
|
||||
t.eq('算奖 4王 count', cg([big(1), big(2), small(1), small(2)], 'count'), 3);
|
||||
t.eq('算奖 6个7(无王)', cg([id(1, 1, 7), id(2, 1, 7), id(1, 2, 7), id(2, 2, 7), id(1, 3, 7), id(2, 3, 7)], 'count'), 1);
|
||||
t.eq('算奖 8个2(无王)', cg([id(1, 1, 2), id(2, 1, 2), id(1, 2, 2), id(2, 2, 2), id(1, 3, 2), id(2, 3, 2), id(1, 4, 2), id(2, 4, 2)], 'count'), 3);
|
||||
t.eq('算奖 3王+正7对+副7对(连对链)', cg([big(1), small(1), small(2), id(1, 1, 7), id(2, 1, 7), id(1, 2, 7), id(2, 2, 7)], 'count'), 3);
|
||||
// ≥10固定主但互不成对 => 只3王(旧代码会误+1)
|
||||
t.eq('算奖 10固定主无对无链', cg([big(1), small(1), small(2), id(1, 1, 7), id(1, 2, 7), id(1, 3, 7), id(1, 4, 7), id(1, 1, 2), id(1, 2, 2), id(1, 3, 2)], 'count'), 1);
|
||||
// 连对链延伸到主A / 副花色A不算链
|
||||
t.eq('算奖 连对链到主A对', cg([big(1), small(1), small(2), 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), id(1, 1, 1), id(2, 1, 1)], 'count'), 6);
|
||||
t.eq('算奖 副花色A对不续链', cg([big(1), small(1), small(2), 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), id(1, 2, 1), id(2, 2, 1)], 'count'), 5);
|
||||
|
||||
// ============ §5.4 甩牌 decompose / 最大性 / can_playcard ============
|
||||
const types = cards => A.decompose_trump(MF, cards).map(c => c.type + (c.len > 1 ? c.len : '')).sort().join(',');
|
||||
t.eq('分解 大王对+主K单', types([big(1), big(2), id(1, 1, 13)]), 'pair,single');
|
||||
t.eq('分解 主K主Q两连对', types([id(1, 1, 13), id(2, 1, 13), id(1, 1, 12), id(2, 1, 12)]), 'tractor2');
|
||||
t.eq('分解 主K对+主9对(不连)', types([id(1, 1, 13), id(2, 1, 13), id(1, 1, 9), id(2, 1, 9)]), 'pair,pair');
|
||||
const play = (cards, opps) => A.can_playcard(MF, cards, 0, null, opps);
|
||||
const K1 = id(1, 1, 13), K2 = id(2, 1, 13), A1 = id(1, 1, 1), P5 = id(1, 1, 5), P6 = id(1, 1, 6), fK1 = id(1, 2, 13), f9 = id(1, 2, 9), fK2 = id(2, 2, 13), Q1 = id(1, 1, 12), Q2 = id(2, 1, 12);
|
||||
t.eq('甩牌 合法(大王对+主K单)', play([big(1), big(2), K1], [[P5, P6], []]).result, true);
|
||||
let r = play([big(1), big(2), K1], [[A1, P5], []]);
|
||||
t.eq('甩牌 甩错(对手有主A>主K) shuaicuo', r.shuaicuo, true);
|
||||
t.eq('甩牌 甩错 smallest=主K', r.smallest, K1);
|
||||
r = play([fK1, f9], [[], []]);
|
||||
t.eq('甩牌 副牌禁甩 result', r.result, false);
|
||||
t.eq('甩牌 副牌禁甩 无shuaicuo', r.shuaicuo, undefined);
|
||||
t.eq('甩牌 主单张(非甩)可出', play([big(1)], [[A1], [K1]]).result, true);
|
||||
t.eq('甩牌 副对子(非甩)可出', play([fK1, fK2], [[], []]).result, true);
|
||||
t.eq('甩牌 主拖拉机(单分量不受最大性约束)', play([K1, K2, Q1, Q2], [[big(1), big(2)], []]).result, 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);
|
||||
const fA = id(1, 2, 1), fB = id(1, 2, 5), fC = id(1, 2, 6), fD = id(1, 2, 8);
|
||||
const ffo = (hand, follow, demand) => A.flush_follow_ok(MF, hand, follow, demand);
|
||||
t.eq('跟牌 含主对KK', ffo([K1, K2, J1, N9a], [K1, K2, J1], { tractors: [], pairs: 1, singles: 1 }), true);
|
||||
t.eq('跟牌 拆散主对(3散)→拒', ffo([K1, K2, J1, N9a], [J1, N9a, K1], { tractors: [], pairs: 1, singles: 1 }), false);
|
||||
t.eq('跟牌 无主对散张可跟', ffo([J1, N9a, P6], [J1, N9a, P6], { tractors: [], pairs: 1, singles: 1 }), true);
|
||||
const hand5 = [K1, K2, Q1, Q2, P6, J1, N9a];
|
||||
t.eq('跟牌 打2连拖+单', ffo(hand5, [K1, K2, Q1, Q2, P6], { tractors: [2], pairs: 0, singles: 1 }), true);
|
||||
t.eq('跟牌 有2连拖却打5散→拒', ffo(hand5, [K1, Q1, P6, J1, N9a], { tractors: [2], pairs: 0, singles: 1 }), false);
|
||||
t.eq('跟牌 有2连拖却只打一对→拒', ffo(hand5, [K1, K2, P6, J1, N9a], { tractors: [2], pairs: 0, singles: 1 }), false);
|
||||
const hand6 = [K1, K2, N9a, N9b, P6, J1, P8];
|
||||
t.eq('跟牌 无同长拖两对退化', ffo(hand6, [K1, K2, N9a, N9b, P6], { tractors: [2], pairs: 0, singles: 1 }), true);
|
||||
t.eq('跟牌 有两对却打5散→拒', ffo(hand6, [K1, N9a, P6, J1, P8], { tractors: [2], pairs: 0, singles: 1 }), false);
|
||||
t.eq('跟牌 主牌最大化(有主用尽)', ffo([K1, Q1, fA, fB], [K1, Q1, fA], { tractors: [], pairs: 1, singles: 1 }), true);
|
||||
t.eq('跟牌 留主牌垫副→拒', ffo([K1, Q1, fA, fB], [K1, fA, fB], { tractors: [], pairs: 1, singles: 1 }), false);
|
||||
t.eq('跟牌 无主全垫副', ffo([fA, fB, fC, fD], [fA, fB, fC], { tractors: [], pairs: 1, singles: 1 }), true);
|
||||
|
||||
process.exit(t.done('arith') ? 0 : 1);
|
||||
@@ -0,0 +1,28 @@
|
||||
// roomtype 位串解析 + 局数/扣卡映射(design §10.1)
|
||||
require('./_shim');
|
||||
const C = require('../class.config.js');
|
||||
const t = require('./_assert')();
|
||||
|
||||
// 复刻 export 的两个映射(现为2行)做端到端验证
|
||||
const needroomcard = rt => { const c = C.parse(rt); return c.deductAA ? (c.aset12 ? 2 : 1) : (c.aset12 ? 4 : 2); };
|
||||
const asetcount = rt => { const c = C.parse(rt); return c.aset12 ? 12 : 6; };
|
||||
|
||||
// --- parse 位串 ---
|
||||
t.eq('"00000"', C.parse("00000"), { aset12: false, deductAA: false, bangwang: false, climb: false, nocheck: false });
|
||||
t.eq('"10100"', C.parse("10100"), { aset12: true, deductAA: false, bangwang: true, climb: false, nocheck: false });
|
||||
t.eq('"01011"', C.parse("01011"), { aset12: false, deductAA: true, bangwang: false, climb: true, nocheck: true });
|
||||
t.eq('"" 兜底', C.parse(""), { aset12: false, deductAA: false, bangwang: false, climb: false, nocheck: false });
|
||||
t.eq('undefined 兜底', C.parse(undefined), { aset12: false, deductAA: false, bangwang: false, climb: false, nocheck: false });
|
||||
t.eq('"1" 短串', C.parse("1"), { aset12: true, deductAA: false, bangwang: false, climb: false, nocheck: false });
|
||||
|
||||
// --- 局数 ---
|
||||
t.eq('6局 "0"', asetcount("00000"), 6);
|
||||
t.eq('12局 "1"', asetcount("10000"), 12);
|
||||
|
||||
// --- 开房房卡 (design §10.1) ---
|
||||
t.eq('房主6局', needroomcard("00000"), 2);
|
||||
t.eq('房主12局', needroomcard("10000"), 4);
|
||||
t.eq('AA6局', needroomcard("01000"), 1);
|
||||
t.eq('AA12局', needroomcard("11000"), 2);
|
||||
|
||||
process.exit(t.done('config') ? 0 : 1);
|
||||
@@ -0,0 +1,53 @@
|
||||
// paiju 单测:亮牌(§8.2) + 结算 get_paiju_account(§7/§8) 集成
|
||||
require('./_shim');
|
||||
const P = require('../class.paiju.js');
|
||||
const t = require('./_assert')();
|
||||
|
||||
const id = (d, f, n) => (d - 1) * 54 + (f - 1) * 13 + (n - 1);
|
||||
const big = d => (d - 1) * 54 + 53;
|
||||
const small = d => (d - 1) * 54 + 52;
|
||||
|
||||
// ============ §8.2 亮牌 get_liangpai ============
|
||||
// 构造庄家(seat0)手牌 o_paiju;副花色中性单张填充到28
|
||||
function fill(req, n) {
|
||||
const u = new Set(req), o = req.slice();
|
||||
for (const f of [2, 3, 4]) for (const x of [1, 5, 6, 8, 9, 10, 11, 12, 13]) {
|
||||
if (o.length >= n) break;
|
||||
const c = id(1, f, x);
|
||||
if (!u.has(c)) { u.add(c); o.push(c); }
|
||||
}
|
||||
return o;
|
||||
}
|
||||
const mkHand = ids => ({ flower: 1, banker: 0, cards: ids.map(x => ({ id: x, playround: -1, dealowner: 1 })) });
|
||||
const lp = ids => P.get_liangpai(mkHand(fill(ids, 28)));
|
||||
t.eq('亮牌 3王', lp([big(1), small(1), small(2)]), { wang: 3 });
|
||||
t.eq('亮牌 4王+6个7(固定主10)', lp([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)]), { zhu: 10, zhupair: 5, zhutuo: 1, wang: 4, qi: 6 });
|
||||
t.eq('亮牌 6个2', lp([id(1, 1, 2), id(2, 1, 2), id(1, 2, 2), id(2, 2, 2), id(1, 3, 2), id(2, 3, 2)]), { er: 6 });
|
||||
t.eq('亮牌 2王2个7(不达标)', lp([big(1), small(1), id(1, 1, 7), id(2, 1, 7)]), null);
|
||||
|
||||
// ============ §7/§8 结算 get_paiju_account 集成 ============
|
||||
// 最小 o_paiju/o_desk/o_room:控制捡分与手牌,验证最终 seatlist 各家 grade
|
||||
function mkPaiju(call, flower, cards, roomtype) {
|
||||
const o_desk = {
|
||||
seatlist: [[0, []], [0, []], [0, []]],
|
||||
o_room: { roomtype: roomtype, asetcount: 6 },
|
||||
method: { get_desk_account: msg => msg }
|
||||
};
|
||||
return { call, banker: 0, flower, cards, idx: 2, o_desk, endtime: null, result: null };
|
||||
}
|
||||
// 闲家1(seat1) 赢两张K(各10分)=捡分20;庄家(seat0)仅1张手牌(不足28→无算奖)
|
||||
const cards = [
|
||||
{ id: 0, dealowner: 2, playround: 1, playowner: 1, score: 10, number: 13, flower: 1 },
|
||||
{ id: 1, dealowner: 3, playround: 1, playowner: 1, score: 10, number: 13, flower: 1 },
|
||||
{ id: 2, dealowner: 1, playround: -1, playowner: -1, score: 0, number: 5, flower: 1 }
|
||||
];
|
||||
// 常规 65 分、捡分20 → 小光(×2)、基础2 → X=4:庄+8、闲各 -4
|
||||
let msg = P.get_paiju_account(mkPaiju(65, 1, cards, "00000"), 0, {});
|
||||
t.eq('结算 65小光 grade', msg.data.aset.seatlist.map(p => p.grade), [8, -4, -4]);
|
||||
t.eq('结算 65小光 upgrade/multiple', [msg.data.aset.upgrade, msg.data.aset.multiple], [2, 2]);
|
||||
// 投降(type1):基础1、X=1、庄输1子/闲 → 庄-2、闲各 +1
|
||||
let msg2 = P.get_paiju_account(mkPaiju(70, -1, cards, "00000"), 1, {});
|
||||
t.eq('结算 投降 grade', msg2.data.aset.seatlist.map(p => p.grade), [-2, 1, 1]);
|
||||
t.eq('结算 投降 upgrade(标识)', msg2.data.aset.upgrade, -99);
|
||||
|
||||
process.exit(t.done('paiju') ? 0 : 1);
|
||||
Reference in New Issue
Block a user