谁最大、这墩闲家得几分是【一次性表现】(收牌动画、得分飘字),任何 deskinfo 都不下发它,
写进 GameState 就多出一处「只活在前端、服务端不知道」的对局态——出牌中途断线重连即归零,
违反红线「丢弃 this.data、仅凭最近一次服务端快照重画,界面必须一致」。
三处一起改,缺一不可:
- PlayHandler chupai3:删 result.chupai 写入,改随 EQW_TRICK_END 下发 {seat,cards,maxseat?,grade?};
aset.grade 的累加保留(它有服务端来源 PushCards.grade / chupai3.grade,是镜像量)
- ResultHandler:_apply 的 map 删掉 chupai。收尾墩不发 chupai3、整包换成 jiesuan,
不改这里则删 EXCLUDE 后必转红。收尾墩改为与 chupai3 走同一对事件、同一份载荷字段
(补 emit EQW_TRICK_END 并带上 grade),否则最后一墩的得分没有出口
- GameState:删掉 result.chupai 字段本身(声明 + reset),不留永远为 null 的死字段
test_consistency 的 EXCLUDE 随之删到只剩 table.ancard3s 一条,27 checks 仍全绿;
该条改注释标明「当前夹具(65 分坐庄、单局)无法触发,此条未经验证」,不假装它在守什么。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
110 lines
6.3 KiB
JavaScript
110 lines
6.3 KiB
JavaScript
///////////////////////////////////////////////////////////////
|
||
////////// EQW_PlayHandler: 出牌(chupai1 / 2 / 3)///////////
|
||
///////////////////////////////////////////////////////////////
|
||
// 协议 §11–§13。三个包字段不完全相同:
|
||
// chupai1 首家独有:shuaicuo 甩错标志 / count 出牌数量 / flower 出牌花色 /
|
||
// cardtype 牌型 / shuai 甩牌分量构成
|
||
// chupai3 末家独有:maxseat 本轮谁最大 / grade 本轮闲家得分
|
||
// 共有:seat / cards / seatlist(仅可查牌) / countdown / baozhu / nextseat /
|
||
// cardsinhand(仅出牌者自己有) / curmultiple(1 和 3 有)
|
||
// mustcard:三个包都可能带,但只发给 nextseat 那一家(其余两家无此属性)
|
||
//
|
||
// 【cardsinhand 只有出牌者自己有】:收到就更新自己的手牌;别人出牌时没这个字段,
|
||
// 绝不能抹掉自己的手牌——这正是「缺字段不兜底」要防的。
|
||
//
|
||
// 【mustcard 没有就要清空】:它是「本轮跟牌的必出牌」,只对当前这一轮有效。
|
||
// 上一轮的建议残留下来会让界面自动选中错误的牌。
|
||
//
|
||
// 【playproc 是服务端的「本轮进行态」,原样拷贝】:它的结构是
|
||
// {round, start, currseat, startcount, startflower, starttype, maxseat, maxcard, cards, shuai_demand}
|
||
// (协议 §11),与重连包 PushCards.playproc 同源同结构(服务端同一个 get_playproc())。
|
||
// 注意它的 cards 是【本轮三家各自出的牌、下标 = 座位】,与本包的 cards(这一手出的牌)
|
||
// 语义相反,绝不能混写。轮次/最大者由服务端给,前端不累积、不推导。
|
||
//
|
||
// 【这一手的信息走事件载荷,不进 GameState】:谁出了什么、几张、什么牌型是【一次性表现】
|
||
// (落牌动画),不是对局状态;deskinfo 也不下发它。放进 GameState 会立刻制造一处
|
||
// 增量/全量不一致。故随 EQW_CARD_PLAYED 事件下发给 UI。
|
||
// 【本墩结果同理】:chupai3 的 maxseat / grade(谁最大、这墩闲家得几分)也是一次性表现,
|
||
// 随 EQW_TRICK_END 事件载荷下发,同样不进 GameState。
|
||
var EQW_PlayHandler = EQW_PlayHandler || {
|
||
|
||
//order: 1/2/3,对应 chupai1/2/3
|
||
handle: function (data, order) {
|
||
if (!data.hasOwnProperty('seat') || !data.hasOwnProperty('cards')) {
|
||
console.error('[EQW_PlayHandler] chupai' + order + ' 缺 seat 或 cards,已跳过该包');
|
||
return;
|
||
}
|
||
|
||
//—— 本轮进行态:服务端权威快照,原样拷贝,前端零推导 ——
|
||
EQW_GameState._apply(EQW_GameState.table, data, { playproc: 'playproc' });
|
||
|
||
//—— 出牌历史:把这一手记进本轮(chupai1 = 一轮的第一手,开新的一轮)——
|
||
EQW_GameState.pushPlay(data.seat, data.cards, order === 1);
|
||
|
||
//—— 自己的手牌:只有出牌者本人才收到 cardsinhand ——
|
||
EQW_GameState._apply(EQW_GameState.my, data, { cards: 'cardsinhand' });
|
||
|
||
//—— 必出牌:本轮有效,没给就清空 ——
|
||
if (data.hasOwnProperty('mustcard')) {
|
||
EQW_GameState.my.mustCard = data.mustcard;
|
||
} else {
|
||
EQW_GameState.my.mustCard = [];
|
||
}
|
||
|
||
//—— 牌况与报无主(仅可查牌模式下发)——
|
||
EQW_GameState._apply(EQW_GameState.table, data, { seatlist: 'seatlist' });
|
||
EQW_GameState._apply(EQW_GameState.aset, data, {
|
||
baozhu: 'baozhu',
|
||
curmultiple: 'curmultiple'
|
||
});
|
||
|
||
//—— 控制权与倒计时 ——
|
||
EQW_GameState._apply(EQW_GameState.turn, data, {
|
||
seat: 'nextseat',
|
||
countdown: 'countdown'
|
||
});
|
||
|
||
EventBus.emit(EQW_Events.EQW_CARD_PLAYED, this._playPayload(data, order));
|
||
if (data.hasOwnProperty('cardsinhand')) { EventBus.emit(EQW_Events.EQW_HAND_CHANGED); }
|
||
|
||
//—— 末家出完:本轮结束 ——
|
||
//【本墩结果走事件载荷,不进 GameState】谁最大、这墩闲家得了几分,是【一次性表现】
|
||
//(收牌动画、得分飘字),与 EQW_CARD_PLAYED 同类;任何阶段的 deskinfo 都不下发它,
|
||
//写进 GameState 就多出一处「只活在前端、服务端不知道」的对局态——出牌中途断线重连
|
||
//即归零,违反前端红线「丢弃 this.data、仅凭最近一次服务端快照重画,界面必须一致」。
|
||
//小局累计捡分 aset.grade 不同:它有服务端来源(PushCards.grade / chupai3.grade),
|
||
//属于镜像量,仍留在 GameState。
|
||
if (order === 3) {
|
||
//本轮闲家得分累加进小局捡分;没得分时不带 grade,累计值保持不变
|
||
if (data.hasOwnProperty('grade')) {
|
||
EQW_GameState.aset.grade = EQW_GameState.aset.grade + data.grade;
|
||
}
|
||
EventBus.emit(EQW_Events.EQW_TRICK_END, this._trickPayload(data));
|
||
}
|
||
|
||
EventBus.emit(EQW_Events.EQW_TURN_CHANGED);
|
||
},
|
||
|
||
//EQW_CARD_PLAYED 的事件载荷:这一手的一次性信息(落牌动画用),不落 GameState。
|
||
//【缺字段不兜底】:包里没有的键就不带(count/flower/cardtype 只有 chupai1 有,
|
||
//shuai 仅合法甩牌时有,shuaicuo 仅甩错时有),订阅方按 hasOwnProperty 判断。
|
||
_playPayload: function (data, order) {
|
||
var payload = { order: order, seat: data.seat, cards: data.cards };
|
||
var keys = ['count', 'flower', 'cardtype', 'shuai', 'shuaicuo'];
|
||
for (var i = 0; i < keys.length; i++) {
|
||
if (data.hasOwnProperty(keys[i])) { payload[keys[i]] = data[keys[i]]; }
|
||
}
|
||
return payload;
|
||
},
|
||
|
||
//EQW_TRICK_END 的事件载荷:这一墩的一次性结果(收牌动画、得分飘字用),不落 GameState。
|
||
//【缺字段不兜底】grade 只在这墩被闲家收走且确实有分时才有,包里没有就不带这个键。
|
||
//与 ResultHandler 为收尾墩发的那份载荷字段一致(收尾墩不发 chupai3,整包换成 jiesuan)。
|
||
_trickPayload: function (data) {
|
||
var payload = { seat: data.seat, cards: data.cards };
|
||
if (data.hasOwnProperty('maxseat')) { payload.maxseat = data.maxseat; }
|
||
if (data.hasOwnProperty('grade')) { payload.grade = data.grade; }
|
||
return payload;
|
||
}
|
||
};
|