@@ -0,0 +1,191 @@
// S-1 curmultiple(当前抓分倍数,design §7.2.0)与 S-2 mustcard(跟牌必出牌,design §5.2)
// 两者都是「随现有包下发、供客户端显示/自动选中」的辅助字段:
// curmultiple —— 三家同值(grade 本就公开),随 shangzhuang / chupai1-3 / PushCards 下发
// mustcard —— 【只发给 nextseat 一家】,是该玩家自己手牌的子集;整表下发会泄露他家手牌结构
// 除正面用例外,本文件重点覆盖:可见性(谁不该有)、边界(何时不该有)、与结算的一致性。
const R = require ( './_rpc.js' ) ; const mod = R . mod , setup = R . setup , make108 = R . make108 , id = R . id ;
const P = global . cls _youle _erqiwang _paiju , A = global . cls _youle _erqiwang _arith ;
const E = require ( '../class.export.js' ) . new ( ) ;
const t = require ( './_assert' ) ( ) ;
const pack = ( s , d ) => ( { conmode : 0 , fromid : s , data : Object . assign ( { agentid : 1 , playerid : s , gameid : 1 , roomcode : 1 , seat : s } , d || { } ) } ) ;
// ============================================================================
// S-1 curmultiple
// ============================================================================
// —— 算法层:与 design §7.2.0 判定表逐档对齐(常规算子 Q=40)——
const cm = ( call , grade , climb ) => {
const q = A . get _qvalue ( call , climb ? 1 : 0 ) ;
const u = A . get _upgrade ( call , grade , q ) ;
return u < 0 ? - u : u ;
} ;
t . eq ( 'curmultiple 65分 捡0 → 大光x3' , cm ( 65 , 0 ) , 3 ) ;
t . eq ( 'curmultiple 65分 捡35 → 小光x2' , cm ( 65 , 35 ) , 2 ) ;
t . eq ( 'curmultiple 65分 捡40 → 过庄x1' , cm ( 65 , 40 ) , 1 ) ;
t . eq ( 'curmultiple 65分 捡65 → 升1级x1' , cm ( 65 , 65 ) , 1 ) ;
t . eq ( 'curmultiple 65分 捡105 → 升2级x2' , cm ( 65 , 105 ) , 2 ) ;
t . eq ( 'curmultiple 65分 捡145 → 升3级x3' , cm ( 65 , 145 ) , 3 ) ;
// 爬坡:40分档 Q=20,分界线与常规不同(design §7.3.2)
t . eq ( 'curmultiple 爬坡40分 捡15 → 小光x2' , cm ( 40 , 15 , true ) , 2 ) ;
t . eq ( 'curmultiple 爬坡40分 捡20 → 过庄x1' , cm ( 40 , 20 , true ) , 1 ) ;
t . eq ( 'curmultiple 爬坡40分 捡60 → 升2级x2' , cm ( 40 , 60 , true ) , 2 ) ;
// —— 端到端:走完整流程,验证包里带且值正确 ——
function driveToPlay ( roomtype ) {
const pj = P . new ( { paiju _list : [ ] } , 0 ) ;
const c = setup ( roomtype , pj ) ;
mod . jiaofen ( pack ( 0 , { call : 65 } ) ) ; mod . jiaofen ( pack ( 1 , { call : 0 } ) ) ; mod . jiaofen ( pack ( 2 , { call : 0 } ) ) ;
mod . xuanzhu ( pack ( 0 , { flower : 1 } ) ) ;
mod . maipai ( pack ( 0 , { cards : P . get _seat _cards ( pj , 0 ) . slice ( 0 , 8 ) } ) ) ;
return Object . assign ( { pj } , c ) ;
}
const e = driveToPlay ( "00000" ) ;
// shangzhuang:叫分刚定、捡分为 0 → 大光 x3
const szPk = e . sent . filter ( m => m . rpc === 'shangzhuang' ) [ 0 ] ;
t . eq ( 'shangzhuang 带curmultiple' , szPk && szPk . data . curmultiple !== undefined , true ) ;
t . eq ( 'shangzhuang curmultiple=3(此刻大光)' , szPk && szPk . data . curmultiple , 3 ) ;
// 出牌包:三家同值
e . sent . length = 0 ;
mod . chupai ( pack ( 0 , { cards : [ P . get _seat _cards ( e . pj , 0 ) [ 0 ] ] } ) ) ;
const cp1 = e . sent . filter ( m => m . rpc === 'chupai1' ) ;
t . eq ( 'chupai1 下发给三家' , cp1 . length , 3 ) ;
t . eq ( 'chupai1 都带curmultiple' , cp1 . every ( m => m . data . curmultiple !== undefined ) , true ) ;
t . eq ( 'chupai1 三家curmultiple同值' , new Set ( cp1 . map ( m => m . data . curmultiple ) ) . size , 1 ) ;
// 重连 PushCards 也带,且与 chupai 包同值
const pcNow = E . get _deskinfo ( e . o _room , 1 ) . PushCards ;
t . eq ( '重连 PushCards 带curmultiple' , pcNow . curmultiple !== undefined , true ) ;
t . eq ( '重连 PushCards curmultiple 与出牌包一致' , pcNow . curmultiple , cp1 [ 0 ] . data . curmultiple ) ;
// —— 边界:叫分未定时为 0 ——
const pjNoCall = P . new ( { paiju _list : [ ] } , 0 ) ;
setup ( "00000" , pjNoCall ) ;
t . eq ( '叫分未定 curmultiple=0' , P . get _curmultiple ( pjNoCall ) , 0 ) ;
// —— 一致性:curmultiple 与结算 aset.upgrade 同源 ——
// 构造:庄家 0 叫 65、闲家一分未捡 → 局末应为大光,|upgrade| 须等于出牌过程中的 curmultiple
{
const pj = P . new ( { paiju _list : [ ] } , 0 ) ;
const c = setup ( "00000" , pj ) ;
mod . jiaofen ( pack ( 0 , { call : 65 } ) ) ; mod . jiaofen ( pack ( 1 , { call : 0 } ) ) ; mod . jiaofen ( pack ( 2 , { call : 0 } ) ) ;
mod . xuanzhu ( pack ( 0 , { flower : 1 } ) ) ;
mod . maipai ( pack ( 0 , { cards : P . get _seat _cards ( pj , 0 ) . slice ( 0 , 8 ) } ) ) ;
const before = pj . method . get _curmultiple ( ) ;
const q = A . get _qvalue ( pj . call , 0 ) ;
const u = A . get _upgrade ( pj . call , P . get _jian _grade ( pj ) . grade , q ) ;
t . eq ( 'curmultiple 与 get_upgrade 绝对值一致' , before , u < 0 ? - u : u ) ;
}
// ============================================================================
// S-2 mustcard
// ============================================================================
// 构造一个「跟牌方有必出牌」的确定场景:
// 主花色 ♦(1)。庄家 0 首出 ♥ 对子;闲家 1 手上恰有一对 ♥ → 必须打出该对(design §5.2)
function mkFollow ( ) {
const pj = {
idx : 1 , step : 5 , banker : 0 , call : 65 , flower : 1 , callproc : [ ] ,
seatlist : [ [ [ 0 , 0 ] , [ 0 , 0 ] , [ 0 , 0 ] , [ 0 , 0 ] , [ - 1 , - 1 ] ] , [ [ 0 , 0 ] , [ 0 , 0 ] , [ 0 , 0 ] , [ 0 , 0 ] , [ - 1 , - 1 ] ] , [ [ 0 , 0 ] , [ 0 , 0 ] , [ 0 , 0 ] , [ 0 , 0 ] , [ - 1 , - 1 ] ] ] ,
playproc : { } , cards : make108 ( )
} ;
for ( let i = 0 ; i < 108 ; i ++ ) pj . cards [ i ] . dealowner = - 1 ;
const give = ( cid , s ) => { pj . cards [ cid ] . dealowner = s + 1 ; pj . cards [ cid ] . playround = - 1 ; } ;
give ( id ( 1 , 3 , 9 ) , 0 ) ; give ( id ( 2 , 3 , 9 ) , 0 ) ; // 庄家:♥9 对(首出)
give ( id ( 1 , 3 , 10 ) , 1 ) ; give ( id ( 2 , 3 , 10 ) , 1 ) ; // 闲1:♥10 对 ← 必出
// 闲2:♣8 对 + ♣9 对共 4 张(缺♥)。必须给够 4 张——若只给 2 张,手牌数恰等于首家张数,
// get_followcard 会判「全是必出」(这是对的),就测不出「缺门方可自由垫」这个点了
give ( id ( 1 , 2 , 8 ) , 2 ) ; give ( id ( 2 , 2 , 8 ) , 2 ) ; give ( id ( 1 , 2 , 9 ) , 2 ) ; give ( id ( 2 , 2 , 9 ) , 2 ) ;
pj . method = {
check _cards _valid : c => P . check _cards _valid ( pj , c ) ,
check _cards _inhand : ( c , s ) => P . check _cards _inhand ( pj , c , s ) ,
do _playcard : c => P . do _playcard ( pj , c ) ,
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 ,
have _baofu : ( ) => false ,
get _curmultiple : ( ) => P . get _curmultiple ( pj ) ,
get _mustcard : s => P . get _mustcard ( pj , s )
} ;
P . new _playround ( pj , 1 , 0 ) ;
return pj ;
}
const f = mkFollow ( ) ; const fc = setup ( "00000" , f ) ;
// 首家出牌前:本轮无人出牌 → 谁都没有必出牌
t . eq ( '本轮未开始 首家无mustcard' , P . get _mustcard ( f , 0 ) , null ) ;
mod . chupai ( pack ( 0 , { cards : [ id ( 1 , 3 , 9 ) , id ( 2 , 3 , 9 ) ] } ) ) ; // 庄家首出 ♥9 对
const fp = fc . sent . filter ( m => m . rpc === 'chupai1' ) ;
t . eq ( 'chupai1 下发给三家' , fp . length , 3 ) ;
const toNext = fp . find ( m => m . fromid === 1 ) ; // nextseat = 1
const toOther = fp . filter ( m => m . fromid !== 1 ) ;
// 正面:nextseat 收到 mustcard,且正是那对 ♥10
t . eq ( 'mustcard 只有nextseat收到' , Array . isArray ( toNext . data . mustcard ) , true ) ;
t . eq ( 'mustcard = 闲1的♥10对' , toNext . data . mustcard . slice ( ) . sort ( ( a , b ) => a - b ) ,
[ id ( 1 , 3 , 10 ) , id ( 2 , 3 , 10 ) ] . sort ( ( a , b ) => a - b ) ) ;
// 可见性(防泄露):另外两家一律没有该字段
t . eq ( 'mustcard 其他两家都没有' , toOther . every ( m => m . data . mustcard === undefined ) , true ) ;
// 反面:mustcard 必须是收包者自己的牌,不得含他家手牌
t . eq ( 'mustcard 全部属于nextseat自己' , toNext . data . mustcard . every ( cid => f . cards [ cid ] . dealowner === 2 ) , true ) ;
// 首家自己那一包也不该有(他已经出完了)
t . eq ( 'mustcard 出牌者本人无' , fp . find ( m => m . fromid === 0 ) . data . mustcard , undefined ) ;
// 边界:缺门且手牌有富余(闲2 有 4 张 ♣、首家出 2 张)→ 可自由垫,无必出牌
t . eq ( '缺门且手牌有富余 无mustcard' , P . get _mustcard ( f , 2 ) , null ) ;
// 把桩推进到「庄家已首出 ♥9 对、轮到跟牌」的状态。
// 直接设 playproc 而不走 mod.chupai,是为了让下面每个边界用例【只变动一个因素】——
// 否则 startcount 仍是 -1,前置守卫就会让所有用例都返回 null 而「假通过」,测不到被测因素。
function asFollowing ( s , currseat ) {
s . playproc . start = 0 ;
s . playproc . currseat = currseat ;
s . playproc . startcount = 2 ;
s . playproc . startflower = 3 ; // ♥
s . playproc . starttype = 201 ; // 一对
return s ;
}
// 先自证:推进后确实能算出必出牌(否则下面的 null 断言仍可能是假通过)
{
const s = asFollowing ( mkFollow ( ) , 1 ) ;
setup ( "00000" , s ) ;
t . eq ( '自证:推进到跟牌态后 闲1有mustcard' , Array . isArray ( P . get _mustcard ( s , 1 ) ) , true ) ;
}
// 对照:手牌数恰等于首家张数时,整手都是必出(没有任何选择余地)
{
const s = asFollowing ( mkFollow ( ) , 2 ) ;
setup ( "00000" , s ) ;
s . cards [ id ( 1 , 2 , 9 ) ] . dealowner = - 1 ; s . cards [ id ( 2 , 2 , 9 ) ] . dealowner = - 1 ; // 把闲2 削回 2 张
t . eq ( '手牌数恰等于首家张数 整手必出' , P . get _mustcard ( s , 2 ) . slice ( ) . sort ( ( a , b ) => a - b ) ,
[ id ( 1 , 2 , 8 ) , id ( 2 , 2 , 8 ) ] . sort ( ( a , b ) => a - b ) ) ;
}
// 边界:甩牌不下发 mustcard(甩牌跟牌走 flush_follow_ok 的逐分量匹配,见 design §5.4.4)
// 与上面的「自证」用例只差 shuai_demand 一个因素
{
const s = asFollowing ( mkFollow ( ) , 1 ) ;
setup ( "00000" , s ) ;
s . playproc . shuai _demand = { tractors : [ ] , pairs : 1 , singles : 1 } ;
t . eq ( '甩牌局面 不下发mustcard' , P . get _mustcard ( s , 1 ) , null ) ;
}
// 边界:非出牌阶段不下发(与「自证」用例只差 step 一个因素)
{
const s = asFollowing ( mkFollow ( ) , 1 ) ;
setup ( "00000" , s ) ;
s . step = 3 ;
t . eq ( '非出牌阶段 不下发mustcard' , P . get _mustcard ( s , 1 ) , null ) ;
}
// 边界:首家自己没有必出牌(与「自证」用例只差 seat 一个因素)
{
const s = asFollowing ( mkFollow ( ) , 0 ) ;
setup ( "00000" , s ) ;
t . eq ( '本轮首家 无mustcard' , P . get _mustcard ( s , 0 ) , null ) ;
}
// 重连 PushCards:轮到本座位才有 mustcard,其他座位没有
{
const pcNext = E . get _deskinfo ( fc . o _room , 1 ) . PushCards ; // 当前轮到 1
const pcIdle = E . get _deskinfo ( fc . o _room , 2 ) . PushCards ;
t . eq ( '重连 轮到本家 PushCards有mustcard' , Array . isArray ( pcNext . mustcard ) , true ) ;
t . eq ( '重连 未轮到 PushCards无mustcard' , pcIdle . mustcard , undefined ) ;
}
t . done ( 'hint' ) ;