Files
erqiwang_youle/client/js/01_SubGame/codes/state/GameState.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

137 lines
6.4 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_GameState: 服务端快照的镜像(前端 SSOT)////////
///////////////////////////////////////////////////////////////
// 前端红线「视图 = f(服务端快照)」的落点:本对象只存【服务端下发过的字段】,
// 不存任何前端算出来的结论。手牌排序、牌面标记、花色统计等由 core/ 的纯函数
// 在渲染时现算,不落地成状态。
//
// 三条硬约束:
// 1. 只镜像不派生——存进来的必须是服务端给过的
// 2. 字段名对齐协议——协议叫 currcall 就叫 currcall,便于逐条核对
// 3. 缺字段不兜底——包里没有的保持原值,不填默认值掩盖;必需字段缺失
// 则 console.error 并跳过该包(服务端漏发,修在服务端)
var EQW_GameState = EQW_GameState || {
//数据分组名(snapshot / reset 按此遍历;新增分组要同步加进来)
_GROUPS: ['room', 'aset', 'turn', 'call', 'my', 'table', 'result'],
//—— 房间级(跨小局)——
room: {
asetCount: 0, //总局数 ← fapai.asetcount / deskinfo.count / setRoomDes
asetIdx: 0, //当前第几局 ← fapai.asetidx / deskinfo.idx
mySeat: -1, //自己的座位 ← 平台 C_Player.seat
playerScores: [], //三家总积分 ← deskinfo.PlayerInfo / jiesuan.aset.seatlist[].score,跨局不清
options: null //roomtype 解析结果
},
//—— 小局级 ——
aset: {
step: 0, //1叫分 2选主/投降 3埋牌 5出牌 6结算
banker: -1,
call: -1, //庄家叫分
multiple: 0, //基础子数
flower: 0, //主牌花色(0 = 未选主)
curmultiple: 0, //当前抓分倍数(带符号,服务端权威,前端不自算)
grade: 0, //闲家已捡分
baozhu: 0, //是否已有人报无主
touxiang: 0 //是否允许投降
},
//—— 当前控制权与倒计时(任何阶段都读这里)——
turn: {
seat: -1, //该谁操作
countdown: 0 //展示用秒数;服务端无对应定时器,归零【不做任何界面推进】
},
//—— 叫分过程 ——
call: {
currcall: 0,
calls: [null, null, null] //null 未叫 / 0 不叫 / >0 叫了多少
},
//—— 自己 ——
my: {
cards: [],
mustCard: [], //本轮必出牌(服务端建议,只发给轮到的那家)
bottomCards: [], //底牌(庄家恒有;闲家仅 70 分坐庄时有)
buryCards: [] //埋牌底牌(仅庄家)
},
//—— 桌面 ——
table: {
ancard3s: 0, //开底标志(70 分坐庄)
playproc: null,
pushlist: [],
seatlist: [],
liangpai: null,
mingpai: null
},
//—— 结算 ——
//字段集与 deskinfo step6 的 Balance 严格对齐(aset / bottom / account)。
//【不存 chupai】收尾墩的「谁最大、得几分」是一次性表现,随 EQW_TRICK_END 事件载荷走,
//任何 deskinfo 都不下发它——存进来就是一处服务端不知道的私有对局态(见 ResultHandler / PlayHandler)
result: {
bottom: null,
aset: null,
account: null
},
//新一局开局时清空对局态;room.mySeat / room.options / room.playerScores 跨局不变,不清
//(playerScores 由 deskinfo.PlayerInfo 或 jiesuan.aset.seatlist[].score 增量更新,见 ResyncHandler / ResultHandler)
reset: function () {
this.room.asetIdx = 0;
this.aset = { step: 0, banker: -1, call: -1, multiple: 0, flower: 0,
curmultiple: 0, grade: 0, baozhu: 0, touxiang: 0 };
this.turn = { seat: -1, countdown: 0 };
this.call = { currcall: 0, calls: [null, null, null] };
this.my = { cards: [], mustCard: [], bottomCards: [], buryCards: [] };
this.table = { ancard3s: 0, playproc: null, pushlist: [], seatlist: [],
liangpai: null, mingpai: null };
this.result = { bottom: null, aset: null, account: null };
},
//深拷贝快照(只含数据分组,不含方法),供测试做「增量 vs 全量」比对
snapshot: function () {
var out = {};
for (var i = 0; i < this._GROUPS.length; i++) {
var g = this._GROUPS[i];
out[g] = JSON.parse(JSON.stringify(this[g]));
}
return out;
},
//把「一手牌」记进出牌历史 table.pushlist。
//结构与 deskinfo.PushCards.pushlist 完全一致:外层下标 = 轮次-1,内层恒 3 个数组、
//下标 = 座位序号(协议 §断线重连 pushlist)。
//
//【只记录服务端发来的事实】:谁、出了哪几张。轮次的推进不在这里推导——
//startRound 由调用方按【包类型】给出:chupai1 就是「一轮的第一手」(服务端定义的包,
//不是前端算出来的),其余各手落进当前这一轮。
//
//【为什么不能读 chupai3.playproc.cards 直接归档】:服务端 do_playcard 在第三家出完后
//就地调用 new_playround,chupai3 带的是【下一轮】的进行态(round+1、cards 全空,
//协议 §13 明标),本轮那三手只在 chupai1/2/3 各自的 seat + cards 里。
//
//【仅可查牌模式累积】:pushlist 是「查牌」历史,不查牌模式下服务端的 deskinfo 压根
//不下发它(design §9),前端也不能自己攒一份——否则增量路径会凭空多出重连路径没有的历史。
pushPlay: function (seat, cards, startRound) {
var opts = this.room.options;
if (!opts || opts.nocheck) { return; }
var list = this.table.pushlist;
if (startRound || list.length === 0) { list.push([[], [], []]); }
list[list.length - 1][seat] = cards;
},
//把包里的字段写进目标分组。map = { 目标键: 源键 }。
//【缺字段不兜底】源键在包里不存在就不写,保持原值——不填默认值掩盖漏发。
_apply: function (target, src, map) {
for (var dst in map) {
if (!map.hasOwnProperty(dst)) { continue; }
var srcKey = map[dst];
if (src.hasOwnProperty(srcKey)) { target[dst] = src[srcKey]; }
}
}
};