Skip to content

0.6.0

Choose a tag to compare

@wm-develop wm-develop released this 17 Sep 00:44
· 16 commits to main since this release

好友德州 v0.6.0

v0.6.0 汇总了 v0.4.2 之后的全部改动,包括未在 GitHub 单独发布的 0.4.3、0.5.0 与 0.5.1。这一段的工作集中在两件事上:

  • 进行中的牌局能跨进程重启恢复;移动端被系统回收后回来直接落回原桌,不必重新登录;CI 接入真实 PostgreSQL,此前静默跳过的集成测试每次都真跑。
  • 换座和私下看牌可以在设置里整体关掉,拒绝时可以屏蔽这个人,对方给你看过牌后本手不能再问。

服务端与客户端都需要更新,并且必须先执行数据库迁移。

发布方式

本 Release 不会直接附带 Web、Windows、Android、HarmonyOS 或 iOS 安装包,仅发布 Git 标签对应的源码快照。

客户端需要写入自建游戏服务地址,语音需要部署者自己的腾讯云 TRTC 配置,Android 与 HarmonyOS 正式分发还涉及各自的平台签名,因此各平台产物应由部署者根据自己的环境构建、签名和测试。

从空服务器开始部署,请参阅自建部署指南。已有部署的版本升级流程请参阅生产环境更新手册。

升级须知

  • 必须先迁移再启动服务。 自 v0.4.2 以来新增一个迁移 000011_table_states。服务端启动时会校验迁移版本,未迁移会拒绝启动。若服务器已经部署过 v0.5.0 或 v0.5.1,这个迁移已经执行过,本次只需替换服务端二进制。
  • 服务端与客户端必须一起更新。 本版新增了 WebSocket 命令与事件:旧客户端连新服务端不会出错,只是没有新功能;新客户端连旧服务端会拿到 unsupported_message_type,拒绝范围也会被旧服务端忽略、退化成普通拒绝。
  • 所有人装好后,管理员在「服务器管理 → 最低客户端版本」填入 6000。
  • 崩溃恢复需要接入 PostgreSQL。 未接数据库的部署行为不变:崩溃后进行中的一手仍然作废。
  • 强烈建议在服务器上跑一遍真实 PostgreSQL 集成测试,步骤见更新手册对应章节。自 v0.4.2 以来新增了一张表和一批 SQL,而内存仓储的单元测试拦不住 SQL 与真实库的错配。
  • 协议变化:新增命令 table.request.preferences.set;新增只发给申请者本人的事件 table.request.declined;table.hole_cards.view.respond 与 table.seat.swap.respond 新增可选字段 scope;快照新增 requestPreferences(只发给本人)。详见协议文档。

重要改进

进行中的牌局跨进程重启恢复

在此之前,服务异常退出会让进行中的那一手作废:底池退回上一手结算后的状态,玩家的决策全部白费。优雅停机会等牌局打完,但崩溃、OOM 和宿主机重启不会等,而这几种情况恰恰无法预告。

每次引擎推进后,牌桌的完整状态(底牌、未发的牌堆、底池、行动位、加时卡、观战者看牌权、动作记录)写入新表 table_states,手结束即删除。进程重启后,第一个回到该房间的人触发恢复,那一手照原样继续。

三个关键决定:

  • 写库放到后台。 保存发生在持有牌桌锁的路径上,同步写会让数据库一慢就把整桌人卡住——这是恢复功能引入的新故障模式,之前牌局根本不碰数据库。改成每房间只保留最新一次待写的异步写入,顺序天然正确;优雅停机时会等它刷完。
  • 恢复从严,宁可作废。 状态版本对不上、内容自相矛盾(牌重复、筹码为负、轮到空座位)、或停机期间房间盲注被改过,一律放弃并清除记录,退回旧行为。拿一个不确定的状态继续发牌,比作废一手严重得多。
  • 行动倒计时从恢复时刻重新起算,不沿用崩溃前的截止时间。玩家在服务端停摆期间无法行动,一上来就判他超时弃牌,是替服务端的故障惩罚玩家。

客户端跟着改了一处:原来一看到服务端进程标识变化就提示「本手作废」,现在要等重连后的第一条快照——手号还在就是恢复成功,不再冤枉服务端。

引擎侧有一条按字段数量比对的守卫测试:以后给 holdem.Table 加字段却忘了同步状态序列化,会被它拦住。

登录态持久化(移动端)

移动设备上应用挂后台十几秒就可能被系统回收,而登录态此前只存在内存里:出去回条微信,回来就要重新输账号密码。牌局本身已经能跨重启续上,客户端却因为这一处停在登录页。

刷新令牌存到设备上,启动时自动换会话。牌桌路由本就是「有房间就进牌桌」,因此回来会直接落在原来那一桌。

这项功能的难点不在存取,而在超时不等于取消:

  • 恢复串了三个请求,每个能等 30 秒,因此加了 8 秒总超时——不设上限时服务端挂起会让玩家对着一个没有出口的转圈等一分半,而此前登录页是立刻出现的。
  • 但 Future.timeout 不会中止底层请求。用代次标记让迟到的结果不再落地,否则十几秒后界面会自己跳走,甚至把玩家手动登录的账号覆盖掉。「正在进行中」的标记改由真正执行者释放,避免两次拿同一个令牌去换会话——服务端刷新时会删掉旧会话,后一次必然被拒还会清空存储。
  • 只有服务端明确拒绝令牌才清除;没网、超时、版本过旧一律保留,否则信号不好的地方启动一次就得重新登录。临时失败后回到前台静默重试,不切回等待界面:切界面会把玩家正在填的登录表单连输入框一起卸载。
  • 轮换出来的新令牌一律等写入落盘再继续。服务端刷新时旧会话已被删除,写入没落盘就被系统回收等于丢掉登录态,而那正是本功能要应对的场景。

Web 端刻意不持久化。 那里的存储落在 localStorage,而浏览器常常是公用的:下一个打开页面的人会直接进入上一个人的账号。移动端的设备通常属于一个人,风险完全不同。Web 的第一帧仍是登录页,行为与从前一致。

存储用应用私有的 shared_preferences,内容明文。这是权衡:本项目是熟人私人牌局、不涉真钱,应用私有目录在未 root 的设备上其他应用读不到;而更严格的安全存储在 HarmonyOS 上没有可用适配。过期时间按 UTC 存,避免玩家换时区后有效令牌被误判。

换座与私下看牌的申请偏好和拒绝范围

换座和私下看牌此前只有「同意」和「拒绝」两个选项,同一个人可以一直申请下去,被申请的人只能一次次地关弹窗。现在有三层控制,粒度从粗到细:

  • 设置里整体关闭(房间级)。「允许其他玩家向我申请换座」「允许其他玩家申请私下看我的牌」,关掉后申请在服务端当场被拒,本人不会看到弹窗,已经排队的申请一并撤掉。偏好只在本房间有效,离开房间即恢复为允许——这是刻意的:这类设置多半是针对当下这一桌的某个人,带到下一个房间去反而意外。
  • 拒绝时屏蔽这个人。换座的屏蔽在被申请者离开房间前一直有效,申请者自己退出再进来不会清除它(否则屏蔽形同虚设);看牌的屏蔽只在本手内有效,因为看牌申请本身就只在一手之内有意义。
  • 拒绝所有人(仅看牌,本手)。牌局里被两三个弃牌的人轮流申请时,一次点掉。

还加了一条小规则:对方同意过一次后,本手不能再向同一人申请。牌已经在自己的玩家框里了,再申请只会让对方再被弹一次窗。客户端直接不发这条申请,服务端也有兜底校验。

拒绝范围的选项没有挤进弹窗底部的按钮行:四个按钮在横屏手机开大字号时会折成参差的两行,看不出哪个是「拒绝」哪个是「同意」。范围选项改成整行按钮放在正文里,底部只留「拒绝 / 同意」。

申请被拒的提示只有申请者看到

被拒的提示区分四种情况(这次拒绝 / 不再接受你 / 不接受任何人 / 对方关掉了申请),并且带上对方昵称。

这条提示走的是只发给一个人的定向事件,不进房间事件缓冲。牌桌的事件缓冲是为断线补发准备的,进了缓冲的事件会在别人重连时一并送出——「谁拒绝了谁」如果进了缓冲,等于过一会儿全桌都知道。定向事件不占序号,客户端把序号 0 当作带外消息接受,因此也不会打乱断线补发的序号连续性检查。

CI 接入真实 PostgreSQL

集成测试在缺少 TEST_DATABASE_URL 时会静默跳过——没人跑就等于没有。列数错配那次 P0、观战者的满员计数、以及一条自 000007 起就已过期的迁移测试,都是这么漏掉的。

  • Go 作业加了 postgres 服务容器并设置 TEST_DATABASE_URL,集成测试每次推送都真跑。
  • 新增跳过检测:显式跑一遍 Postgres 用例并检查输出里没有 SKIP,容器或环境变量坏掉时直接失败,而不是继续全绿。
  • 传输层用例连跑 5 轮,钉出只在慢机器上偶发的竞态。
  • history 包此前没有任何集成测试,生产走的那条 SQL 路径上的手牌裁剪从未在真实库验证过;现已补上并纳入 CI。

手牌历史的可见性契约

存下来的每手牌带着所有参与者的底牌(复盘与纠纷裁定需要),而发给玩家的必须裁剪:只有自己的牌和那手真的亮过的牌。两个 Store 实现一直都做了这件事,但这条契约没有任何测试盯着它——新增实现或重构时漏掉不会有任何征兆,而漏掉就等于公开对手从未亮过的底牌。

契约现已写进接口文档,并有一条可复用的契约测试在内存与 Postgres 两个实现上验证。

界面调整与修复

  • 最近牌局补上按街的动作过程。服务端一直在记录每个动作,客户端此前整个丢掉了,复盘时最想知道的「谁在什么时候下了多少」反而看不到。对手盖着牌结束时那一栏原来是一片空白,看着像加载失败,现在写明「未亮牌」。牌面改为复用牌桌的四色与自绘花色。
  • 本房间战绩不再虚高(0.5.1)。服务端的「桌上筹码」取自成员表,只在结算时更新,牌局中途仍含着已经推进底池的那部分。没弃牌时那笔钱归属未定,照常算在桌上;一旦弃牌它就已经输掉了,再算成自己的筹码会让净胜负虚高一截。现在弃牌后战绩里会单列一行扣掉它。
  • 鸿蒙手机上设置弹窗滚不到最后一项(0.4.3):房主看不到「房间管理」,管理员看不到「打开服务器管理」。内容区此前按屏幕高度的 70% 限高,上游 Flutter 会把它压到弹窗真实剩余空间,OpenHarmony 那版不会,于是滚动范围按错误的高度算。新增 DialogScrollArea 按真实可用空间计算,设置、房间管理、房间名单三个弹窗统一使用。
  • 花色符号没有跟着四色染色(0.4.3):♠♥♦♣ 在 Android / HarmonyOS 上被系统彩色 emoji 字体接管,文字颜色对它不起作用。花色改为自绘图形,公共牌与玩家框同一套,四端一致。
  • 玩家框小牌「10」错位(0.4.3):一行放不下时折成两行。小牌改为一律点数在上、花色在下居中。
  • 快照尚未到达时,房主判断退回房间信息里的房主,避免「房间管理」入口短暂缺失(0.4.3)。
  • 房间管理里的看牌费输入框改为与补码弹窗同款的整行带标签输入。

顺手修掉的既有缺陷

申请偏好功能引出的三轮代码审查,在周边既有代码里查出两处问题,一并修了:

  • 行动超时后的状态落盘在解锁之后才执行。那里用的是 defer,函数返回时牌桌锁已经放开,序列化牌桌状态会与其他协程对同一批数据的写入并发。改为解锁前显式落盘。
  • 结束一手的路径有四条,只有一条清理了本手的看牌状态。发牌次数选择、行动超时、选择超时三条路径都漏了,导致结算展示期里还挂着上一手的私下看牌。现在四条路径共用同一个清理函数。

测试

  • 服务端新增:引擎状态往返与恢复用例(含字段数量守卫)、牌桌管理层的重启恢复与降级用例、状态存储用例、手牌裁剪的契约测试;申请偏好的作用范围、离房重置、申请者进出不清屏蔽、拒绝范围校验、本手结束清理、跨重启保留;传输层端到端用例覆盖定向事件只发申请者且不占序号、旁观者拿不到被看的牌、当场拒绝的各个错误码、偏好字段必填。Postgres 集成测试补上牌桌状态往返与级联删除、手牌历史。
  • 客户端新增:登录态持久化 18 项(存储往返与轮换覆盖、UTC 序列化、Web 空操作、恢复成功进大厅、令牌被拒后清除、网络失败后保留、迟到结果不覆盖当前会话、登录表单不被冲掉);申请偏好 24 项(偏好解析、七种提示文案、定向事件去重、答复弹窗的四种决定、断线时不拨开关也不弹申请、拒绝后本地撤回排队申请、跨手边界不误撤、横屏大字号下的弹窗布局);另有最近牌局的动作解析、四色牌、弹窗滚动高度等用例。
  • 客户端 351 项、服务端全量与 go vet 通过,传输层与牌桌管理层各连跑 5 轮稳定;本标签对应提交的 CI(含竞态检测与真实 PostgreSQL 集成测试)全部通过。

已知限制

  • 仍是单游戏服务实例。恢复解决的是「崩溃后那一手不作废」,不是多实例高可用;多实例已在 ADR-002 中评估并决定暂不实施。
  • 恢复是尽力而为:最后一次状态写入未落盘就崩溃,恢复的是稍早的状态或直接作废。这是设计内的降级。
  • 手间重启(包括正常发版)会把所有人的申请偏好与换座屏蔽重置为默认。这两样跟着「进行中的牌桌状态」走,而手间本来就没有牌桌状态行。牌局进行中崩溃恢复能保留。要跨发版保留,需要把偏好挪进房间成员表(新迁移),本版没有做。
  • 被顺带撤回申请的人收不到提示。关掉偏好、或选「本手不再接受任何人」时,其他人已排队的申请会被静默撤掉,他们要等到再次申请时才知道。
  • 偏好弹窗开着时若正好有别的快照到达,开关可能闪一下再回到正确位置,窗口约一个网络往返。
  • 回到前台自动重试登录态无法写自动化测试:生命周期回调在本项目的 Flutter OpenHarmony 分支上无法在测试里驱动,只能真机验证。
  • 观战者的语音限制只能由客户端配合执行;改过的客户端可以绕过。牌局进行中没有「进入观战」入口(需求定义在准备区)。
  • 24 小时稳定性观测与弱网验收仍未实跑。
  • iOS 不在验收范围。