@@ -331,7 +331,7 @@ function mkShuai(extraToSeat1) {
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 ( )
playproc : { } , playhistory : [ ] , 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 ; } ;
@@ -372,57 +372,129 @@ t.eq('甩错 shuaicuo=1', shPk && shPk.data.shuaicuo, 1);
t . eq ( '甩错 只打出最小一张(主♦5)' , shPk && shPk . data . cards , [ id ( 1 , 1 , 5 ) ] ) ;
t . eq ( '甩错 无 shuai 分量' , shPk && shPk . data . shuai , undefined ) ;
// ===================== 重连 PushCards.pushlist: 结构恒 3 座位 + 按本局主牌花色排序 =====================
// 造 5 轮已出牌的牌局(每轮每家 1 张),外加 seat0 在第 1 轮同时出「主5 + 副♥A」用于验证排序
function mkPushlist ( ) {
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 : { round : 5 , start : 0 , currseat : 0 , cards : [ [ ] , [ ] , [ ] ] } ,
cards : make108 ( )
} ;
for ( let i = 0 ; i < 108 ; i ++ ) pj . cards [ i ] . dealowner = ( i % 3 ) + 1 ;
// 每轮每家一张
let next = 0 ;
for ( let r = 1 ; r <= 5 ; r ++ ) for ( let s = 0 ; s < 3 ; s ++ ) {
for ( let i = next ; i < 108 ; i ++ ) {
if ( pj . cards [ i ] . dealowner === s + 1 && pj . cards [ i ] . playround === - 1 ) { pj . cards [ i ] . playround = r ; break ; }
// ===================== 重连 PushCards.pushlist: 与 chupai 下发的 data.cards 同一份数据 =====================
// 【契约】「这一手打出的牌」只有一个顺序口径:do_playcard 用 order_playcards 归一一次,
// 之后 chupai1/2/3 的 data.cards、playproc.cards、playhistory→PushCards.pushlist 全都读它。
// 因此重连包里的每一手,必须与当时那个 chupai 包的 cards【逐元素完全相等】。
//
// 旧实现在这里分岔:pushlist 是从 cards[i].playround/dealowner 反查后自己再排一次序,
// 与 chupai 回显的「客户端提交原序」是两个写入处——一手多张(甩牌/对子)时必然不同,
// 同编码的一对牌更是排序都区分不了。故本用例必须【一手多张 + 乱序提交】才抓得住。
//
// 【乱序提交】asc() 把选好的一手反转成升序再发,模拟真实客户端的点击顺序;
// 服务端不得回显它,一律给权威降序。
function ascSubmit ( pj , cards ) { return AR . order _cards ( pj . flower , cards . concat ( ) ) . reverse ( ) ; }
function drivePushlist ( roomtype , hands ) {
const c = driveToPlay ( roomtype ) ;
const pj = c . pj ;
const played = [ ] ; // [{round, seat, cards(下发的)}...]
for ( let h = 0 ; h < hands ; h ++ ) {
if ( pj . step !== 5 ) break ;
const seat = pj . playproc . currseat ;
const round = pj . playproc . round ;
const hand = P . get _seat _cards ( pj , seat ) ;
let pick = null ;
if ( seat === pj . playproc . start ) {
// 首家优先领一对(制造"一手多张";对子两张同编码,排序无法区分,正是分岔点)
const pairs = AR . get _pairlist ( pj . flower , hand . concat ( ) ) ;
for ( const pr of pairs ) {
if ( AR . can _playcard ( pj . flower , pr . concat ( ) , seat , pj . seatlist ,
[ 0 , 1 , 2 ] . filter ( s => s !== seat ) . map ( s => P . get _seat _zhucards ( pj , s ) ) ) . result ) {
pick = pr . concat ( ) ; break ;
}
}
if ( ! pick ) { pick = [ hand [ hand . length - 1 ] ] ; }
} else {
const n = pj . playproc . startcount ;
const get = AR . get _followcard ( pj . flower , hand . concat ( ) , n , pj . playproc . startflower , pj . playproc . starttype ) ;
const base = ( get && get . mustcard ) ? get . mustcard . concat ( ) : [ ] ;
if ( base . length === n ) { pick = base ; }
else {
const pool = ( get && get . cancard && get . cancard . length ? get . cancard : hand ) . filter ( x => base . indexOf ( x ) < 0 ) ;
const need = n - base . length ;
const combo = [ ] ;
( function walk ( st ) {
if ( pick ) return ;
if ( combo . length === need ) {
const cand = base . concat ( combo ) ;
if ( AR . can _followcard ( pj . flower , hand , cand , n , pj . playproc . startflower , pj . playproc . starttype ) . result
&& ( ! pj . playproc . shuai _demand || AR . flush _follow _ok ( pj . flower , hand , cand , pj . playproc . shuai _demand ) ) ) {
pick = cand ;
}
return ;
}
for ( let i = st ; i < pool . length && ! pick ; i ++ ) { combo . push ( pool [ i ] ) ; walk ( i + 1 ) ; combo . pop ( ) ; }
} ) ( 0 ) ;
}
}
if ( ! pick ) { break ; }
const before = c . sent . length ;
mod . chupai ( pack ( seat , { cards : ascSubmit ( pj , pick ) } ) ) ;
const cp = c . sent . slice ( before ) . filter ( m => / ^ chupai [ 123 ] $ / . test ( m . rpc ) ) [ 0 ] ;
if ( cp ) { played . push ( { round , seat , cards : cp . data . cards , submitted : ascSubmit ( pj , pick ) } ) ; }
}
// 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 ,
get _curmultiple : ( ) => P . get _curmultiple ( pj ) ,
get _mustcard : s => P . get _mustcard ( pj , s )
} ;
return { pj , zhu5 , fuA , cur } ;
return { c , pj , played } ;
}
const pl = mkPushlist ( ) ;
const plRoom = setup ( "00000" , pl . pj ) . o _room ;
const di = E . get _deskinfo ( plRoom , 1 ) . PushCards . pushlist ;
t . eq ( '重连 pushlist 轮数=5' , di . length , 5 ) ;
t . eq ( '重连 pushlist 每轮恒3个座位' , di . map ( r => r . length ) , [ 3 , 3 , 3 , 3 , 3 ] ) ;
const plv = drivePushlist ( "00000" , 12 ) ;
t . eq ( 'pushlist 用例真的出过一手多张' , plv . played . some ( x => x . cards . length > 1 ) , true ) ;
const di = E . get _deskinfo ( plv . c . o _room , 1 ) . PushCards . pushlist ;
t . eq ( '重连 pushlist 每轮恒3个座位' , di . every ( r => r . length === 3 ) , true ) ;
t . eq ( '重连 pushlist 不含 undefined' , di . some ( r => r . some ( x => x === undefined ) ) , false ) ;
// 第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 ) ;
// 核心:逐手比对「重连归档」与「当时的 chupai 包」——两条路径同一份数据,必须逐元素相等
let plMismatch = [ ] ;
for ( const h of plv . played ) {
const arc = di [ h . round - 1 ] && di [ h . round - 1 ] [ h . seat ] ;
if ( JS ( arc ) !== JS ( h . cards ) ) { plMismatch . push ( { round : h . round , seat : h . seat , arc , pkt : h . cards } ) ; }
}
t . eq ( '重连 pushlist 每一手都与当时的 chupai.cards 逐元素相等' , plMismatch , [ ] ) ;
// 反面:服务端不得回显客户端的提交顺序(提交是升序,下发必须是权威降序)
const plMulti = plv . played . filter ( x => x . cards . length > 1 ) ;
t . eq ( 'chupai.cards 不回显客户端提交原序(一手多张时)' ,
plMulti . some ( x => JS ( x . cards ) === JS ( x . submitted ) && JS ( x . cards ) !== JS ( AR . order _cards ( plv . pj . flower , x . cards . concat ( ) ) ) ) , false ) ;
t . eq ( 'chupai.cards 是本局主牌花色的权威降序' ,
plMulti . every ( x => JS ( x . cards ) === JS ( AR . order _cards ( plv . pj . flower , x . cards . concat ( ) ) ) ) , true ) ;
// 客户端提交的数组不被就地改写(can_playcard 是原地排序,do_playcard 必须先拷贝)
const inplace = driveToPlay ( "00000" ) ;
const inHand = P . get _seat _cards ( inplace . pj , inplace . pj . playproc . currseat ) ;
const inArg = [ inHand [ inHand . length - 1 ] , inHand [ inHand . length - 2 ] ] ;
const inArgCopy = inArg . concat ( ) ;
mod . chupai ( pack ( inplace . pj . playproc . currseat , { cards : inArg } ) ) ;
t . eq ( 'chupai 不就地改写 pack.data.cards' , inArg , inArgCopy ) ;
// §9 不查牌模式:出牌历史属于「查牌」,一律不下发;但当前轮桌面上的牌必须照常恢复
const pl2 = mkPushlist ( ) ;
const pc2 = E . get _deskinfo ( setup ( "00001" , pl2 . pj ) . o _room , 1 ) . PushCards ;
const plv2 = drivePushlist ( "00001" , 2 ) ;
const pc2 = E . get _deskinfo ( plv2 . c . 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 ( '重连 不查牌 当前轮桌面牌仍恢复(playproc.cards)' ,
JS ( pc2 . playproc . cards [ plv2 . played [ 0 ] . seat ] ) , JS ( plv2 . played [ 0 ] . cards ) ) ;
const pc1 = E . get _deskinfo ( plv . c . o _room , 1 ) . PushCards ;
t . eq ( '重连 可查牌 有出牌历史 pushlist' , Array . isArray ( pc1 . pushlist ) , true ) ;
t . eq ( '重连 可查牌 当前轮桌面牌也在' , pc1 . playproc . cards [ 0 ] , [ pl . cur ] ) ;
// ===================== 重连 PushCards.baozhu:与 chupai 的 baozhu 同源同门控 =====================
// baozhu 是「余主公示 + 明牌按钮」的开关(design §9)。它只在 chupai 包里给、重连包不给的话,
// 重连后按钮凭空消失——状态变更没有包承载(server 红线:发全下发面)。
function mkBaozhuRoom ( roomtype ) {
const o _desk _stub = { paiju _list : [ ] } ;
const pj = P . new ( o _desk _stub , 0 ) ;
pj . banker = 0 ; pj . call = 65 ; pj . flower = 1 ; pj . step = 5 ;
P . new _playround ( pj , 1 , 0 ) ;
const lead = id ( 1 , 3 , 9 ) ;
pj . cards [ lead ] . dealowner = 1 ; pj . cards [ lead ] . playround = - 1 ;
for ( const cc of P . get _seat _zhucards ( pj , 1 ) ) { pj . cards [ cc ] . playround = 1 ; }
pj . seatlist [ 1 ] [ 4 ] [ 0 ] = 0 ; // seat1 报无主 → have_baofu()=true
const c = setup ( roomtype , pj ) ;
mod . chupai ( pack ( 0 , { cards : [ lead ] } ) ) ;
return c ;
}
const bzOn = mkBaozhuRoom ( "00000" ) ;
t . eq ( '重连 可查牌 PushCards 带 baozhu,且与 chupai 同值' ,
E . get _deskinfo ( bzOn . o _room , 1 ) . PushCards . baozhu ,
bzOn . sent . filter ( m => / ^ chupai [ 123 ] $ / . test ( m . rpc ) ) [ 0 ] . data . baozhu ) ;
t . eq ( '重连 可查牌 已报无主 → baozhu=1' , E . get _deskinfo ( bzOn . o _room , 1 ) . PushCards . baozhu , 1 ) ;
const bzOff = mkBaozhuRoom ( "00001" ) ;
t . eq ( '重连 不查牌 baozhu 恒 0' , E . get _deskinfo ( bzOff . o _room , 1 ) . PushCards . baozhu , 0 ) ;
// ===================== §7.3 下发面 multiple 必须随「爬坡」开关取值(与结算 aset.multiple 同源)=====================
// 叫 45 分:常规算子基础子数 6(design §7.1「50 及以下统一 6」),爬坡 7( design §7.3.1)
@@ -506,7 +578,7 @@ t.eq('叫分 反 已不叫者再叫 未产生庄家', bd.banker, -1);
// 桩要"完整到能走完成功路径":否则一旦校验被改松,这里会**抛异常打断整个文件**,
// 后面的断言全不执行——看到的是崩溃而不是某条断言转红,定位与信号都变差。
const mkBury = ( step , banker ) => ( {
step , banker , cards : make108 ( ) , playproc : { currseat : banker } ,
step , banker , cards : make108 ( ) , playproc : { currseat : banker } , playhistory : [ ] ,
method : {
check _cards _valid : c => P . check _cards _valid ( { cards : make108 ( ) } , c ) ,
check _cards _inhand : ( ) => true ,