Files
erqiwang_youle/client/js/01_SubGame/codes/net/handlers/PlayHandler.js
T
joywayerandClaude Opus 5 f8a154ff7a 二七王:result.chupai 移出 GameState,本墩结果改走 EQW_TRICK_END 事件载荷
谁最大、这墩闲家得几分是【一次性表现】(收牌动画、得分飘字),任何 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>
2026-08-27 22:06:41 +08:00

110 lines
6.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
///////////////////////////////////////////////////////////////
////////// 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;
}
};