6571 字
33 分钟
Poker Lab 开发记录

从一个参赛想法开始#

Poker Lab 最初并不叫这个名字。

项目开始时,我希望围绕 MoonBit 开源大赛做一个可以运行、可以验证、也有实际使用场景的作品。德州扑克的牌力计算是一个比较合适的入口:输入是有限的牌,规则比较明确,输出可以用具体结果验证。它又不只有算法,配上界面后,用户能直接看到底牌、公共牌和胜负概率之间的关系。

早期的项目名称是 MoonHold’em,代码和包名使用 MoonHoldem、moonholdem 等形式。核心目标包括牌面解析、五张牌评估、七选五、多人摊牌,以及已知底牌条件下的精确概率分析。网页最初承担演示这些能力的作用。

随着交互逐渐完善,我投入了越来越多时间在选牌、翻牌动画、手机布局和结果展示上。实际使用时,这些部分比一个函数签名更容易被感受到,也更容易发现问题。

后来,项目没有通过大赛的报名初审。反馈认为它更接近游戏 demo,与赛事对生态基础库或开发工具的要求不符。这个结果让我重新审视了项目定位:我可以强调其中有独立计算库,但最终呈现出来的作品,确实已经越来越像一个面向使用者的完整工具。

赛事选择与项目价值不能简单画等号,不过申报定位不匹配也值得承认。我没有继续围绕奖项重写故事,而是决定把它做成一个属于自己的小工具,既可以使用,也可以作为学习和实习展示的一部分。

于是,界面名称先改为“牌局实验室”,后来确定为 Poker Lab,GitHub 仓库也改名为 PokerLab。代码中的计算库名称继续保留,避免把产品改名和程序接口迁移混在一起。

这篇文章记录的,就是这个转变之后,我究竟把它做到了什么程度,又在过程中学到了什么。

一个工具应该先说明自己能做什么#

Poker Lab 的基本使用路径很短:设置玩家,选择底牌与公共牌,点击分析,再查看每个人的结果。

目前界面支持 2–9 名玩家,每个人可以自定义名字。牌可以手动选择,也可以随机发一张;“一键发牌”先补齐玩家底牌,再补齐公共牌。为了降低第一次使用的门槛,项目内置了 AA 对 KK、同花听牌、顺子听牌、A 小顺子、公共牌平局、多人分池、四人对局和九人对局等示例。

分析结果包括当前牌型、最佳五张牌、独赢概率、共同获胜概率、落败概率和权益。点击结果表中的玩家行,可以切换对应的牌力详情和比较说明。

如果公共牌只有四张,界面还会列出不同河牌带来的最终结果。用户可以查看哪些牌会改变局势,点击某张河牌进入最终分析,再返回原来的转牌状态。这让概率不再只是一个百分比,而是能对应到具体的牌。

除了网页,项目也提供 Android 安装包。网页资源与计算引擎随安装包分发,手机不需要连接电脑上的开发服务就能使用。牌局可以导出为 PNG,在确认保存前先看预览。

同时,功能边界必须讲清楚:

  • 所有玩家的两张底牌都必须已知。
  • 概率分析从三张公共牌开始,支持翻牌、转牌和河牌阶段。
  • 不处理下注轮次、弃牌、边池和不同玩家的筹码投入。
  • 不推测对手手牌范围,也不提供策略 AI 或盈利建议。

这些限制决定了算法的输入范围,也决定了界面应该如何表达结果。一个工具的完成度,不是声称覆盖所有场景,而是在声明的范围内让行为尽量一致、结果可以解释。

核心算法:并非所有事情都靠枚举#

开发过程中,我问过一个很直接的问题:这个项目是不是纯粹在枚举?

更准确的回答是,它把规则判断和有限枚举组合起来使用。不同层次采用了不同的方法。

1. 输入校验比牌型判断更靠前#

一张牌不仅有点数,还有花色。项目使用类似 As、Kh、Td、2c 的文本记法,其中 T 表示 10,后缀表示花色。

在开始比较之前,需要检查记法是否合法、数量是否满足接口要求,以及同一张实体牌是否重复出现。比如两个玩家不能同时拥有黑桃 A,公共牌也不能与底牌重复。

这类校验看起来不像“核心算法”,实际上非常重要。输入不合法时,再精确的枚举也没有意义。把错误挡在入口,比在结果异常后猜测原因更可靠。

2. 五张牌通过规则生成可比较的结果#

五张牌的评估先统计点数,再判断同花、顺子以及相同点数的分布,识别高牌、一对、两对、三条、顺子、同花、葫芦、四条、同花顺九类牌型。

仅有牌型名称还不够。两个人都是一对时,要先比较对子点数;对子相同,再依次比较其余牌。葫芦要先比较三条,再比较对子。顺子需要处理 A2345 这种以 5 为最高点数的小顺子,而不能把 QKA23 当成首尾相连的顺子。

因此,评估结果除了类别,还需要保存按优先级排列的比较点数。比较时先看类别,再逐项比较这些点数。花色不会用来决定同牌型的大小。

界面可以把高牌示例里的最高一张突出显示,但算法不能因此只比较一张牌。最高点数相同时,剩余点数仍然决定胜负。展示上的简化不能变成规则上的简化。

3. 七选五使用小规模组合枚举#

玩家的两张底牌加上五张公共牌,共有七张牌。最终比较的是其中最强的五张,而不一定必须使用两张底牌。

从七张牌里选择五张,共有:

C(7, 5) = 21

项目逐个评估这 21 个组合,保留最强结果。对这个规模而言,直接枚举的优点是逻辑直观、容易测试,也能自然处理“完全使用公共牌”等情况。

这里没有必要为了算法看起来更复杂而先引入大规模查表或位运算优化。是否优化,应当由性能测量和实际需求决定。

4. 概率枚举的是还没发出的公共牌#

当所有玩家底牌已知时,先从标准 52 张牌中去掉已知牌,再枚举剩余公共牌的组合。

以两人转牌局面为例:两名玩家共四张底牌,加上四张公共牌,一共知道八张牌。还有 44 张牌可能成为河牌。

项目中的经典例子是:

玩家 1:As Ad
玩家 2:Ks Kd
公共牌:2c 3d 7h 9s

剩下两张 K 会让玩家 2 获胜,其余 42 张河牌由玩家 1 获胜。因此,玩家 1 的独赢概率为 42 / 44,约 95.45%。这个数字对应的是逐一计算出的结局,而不是随机抽样估计。

翻牌阶段还差两张公共牌,需要枚举剩余牌的无序二元组合;河牌已经完整时,只剩当前这一种结局。

“精确”的前提也需要说明:这里采用未见牌等可能的假设,并且输入范围限定为所有玩家底牌已知。它不包含对手行为、下注信息或未知死牌带来的额外条件。

5. 独赢概率与权益是两回事#

如果某个结局里三名玩家共同获胜,每个人在该结局分到的权益是三分之一,而不是各自获得完整的一份。

独赢概率统计“只有我获胜”的结局比例;共同获胜概率统计“我与其他人并列第一”的结局比例;权益则把每种结局中的份额平均起来。

这一区分在多人牌局中尤其重要。项目既返回结局计数,也返回权益,界面用说明区解释这些概念,避免把不同指标混成一个笼统的胜率。

如何确认计算没有写错#

我逐渐意识到,界面里显示两位小数,很容易给人一种结果很可信的感觉。但可信度不能来自小数位数,而应来自验证方法。

项目的测试覆盖牌型顺序、同牌型比较、A2345、小顺子边界、重复牌、输入数量、公共牌平局、多人共同获胜和权益分配等情况。还检查交换玩家顺序或改变输入牌顺序后,结果是否保持应有的一致性。

除此之外,项目提供五张牌穷举验证程序:

Terminal window
moon run cmd/verify --release

全部五张牌组合共有 2,598,960 种。程序对它们逐一分类,检查九种牌型的计数是否与标准组合计数一致。

不过,牌型计数吻合并不自动证明所有比较逻辑、接口和页面交互都正确。它验证的是一层性质,不能替代其他层的测试。因此,工程中还保留了浏览器计算接口测试,以及网页和模拟原生桥接的交互测试。

这也是我对“测试通过”的理解变化:最好说明通过的是哪一组测试、覆盖哪些行为。核心计算通过,不等于 Android 安装流程没有问题;模拟桥接通过,也不等于所有手机上的系统文件选择器都已经验证。

界面打磨:很多问题只有真正点击才会出现#

选牌完成之后,选中状态应该去哪里#

早期选完两张底牌后,界面还可能停留在某个牌位,或者自动跳回玩家 1。这样再点牌池,就有可能覆盖已经选好的牌。

后来调整为:同一玩家还有空位时继续选择;两张都选完后取消选中。公共牌选满后也取消。再点击其他牌位,才明确指定下一个目标。

这次修改让我注意到,很多交互错误来自“系统认为当前正在做什么”与“用户认为当前正在做什么”不一致。选中边框、提示文字和实际写入目标,必须表达同一个状态。

移除牌也经历了类似调整:除了专门的按钮,还支持双击已选牌快捷移除;移除时卡片飞回牌池,原牌池中的暗牌位置保留,让移动具有明确的起点和终点。

手机上的完整牌池与分色牌池#

桌面上按四种花色各列 13 张牌比较直观,但窄屏会把每张牌压得很小。直接增加换行又会拉长页面,尤其九人牌局本来就已经占据较多高度。

我们讨论过底部弹出选牌面板,最终当前版本先保留了原地牌池,并提供两种布局:完整牌池,以及按花色切换的分色牌池。

分色模式里,一种花色按 2–9 与 10–A 排成 8+5 两行,分别居中。用户可以保留原来的全牌视图,也可以用更大的牌面点击目标。这个版本没有把讨论过的底部弹层当作已经实现的功能。

实际做完后,新的问题又出现了:隐藏花色的卡片没有有效的屏幕矩形,移除牌时动画会飞向左上角。解决它需要处理动画坐标,而不是简单调整 CSS。单张移除时切换到对应花色;隐藏牌的坐标计算也需要有可用的回退位置。

统一比例不等于视觉上就统一#

公共牌一度比其他牌更扁。后来统一了扑克牌长宽比,让内部点数和花色随卡片缩放。但翻牌仍然有轻微顿挫,因为牌背、牌面和飞行动画的圆角、边框不完全一致。

这类问题提醒我:一个对象在动画前后应该看起来还是同一个对象。尺寸、圆角、边框、纹理和内部内容的比例,都属于连续性的一部分。

字体也出现过误解。我说“跟随系统字体”,原本指说明性的文字,结果扑克牌上的数字也被替换。之后把品牌、装饰数字、牌面和普通文字分开处理。

Georgia 的旧式数字又让 2、7、9 看起来高低不同。换成另一种等高数字后,我觉得风格不合适,于是保留原字形,对牌面点数的显示高度和位置进行校正。这个处理是视觉取舍,并不等同于拥有一套专门绘制的扑克牌字体;跨设备显示仍值得继续观察。

动效最难的部分,是描述时间关系#

在整个迭代中,翻牌动画是最反复的一部分。

“从上到下、从左到右翻开”听起来很清楚,但至少可以被实现成三种不同效果:每张翻完再翻下一张、一行翻完再翻下一行,或者相邻牌略微错开、整个网格形成波浪。

我真正想要的是第三种。后来只能用非常具体的例子说明:黑桃 5 开始时,红桃 4、方块 3、梅花 2 也应该开始。它们共享同一个启动时刻。

对应到实现,就是以行号和列号之和计算波次:

启动延迟 =(行号 + 列号)× 相邻波次间隔

而玩家与公共牌的顺序不同:从玩家 1 的第一张一路接到最后一张公共牌,只保留相邻牌的小间隔,跨玩家不能额外等待。单张翻转约 480ms,相邻启动间隔约 60ms,动画互相重叠,才有连续的感觉。

滚动又增加了另一层约束。手机上牌局分析在页面下方,如果刷新时就播放完,用户滑过去只会看到静态结果。因此需要区分“已经可以播放”和“已经进入视野”。宽屏分析区可以直接展示,窄屏则等详情牌出现再翻。

过程中还遇到一个很典型的状态错误:动画刚开始就被记录为“已经播放”,随后计算完成重新渲染节点,动画被截断;新节点又因为记录已完成而不再播放。修复的关键是调整完成记录的时机,并处理重绘后的节点替换。

这使我认识到,动效不是加几个 transition 就结束了。只要界面会刷新、计算、隐藏花色、切换布局或重建节点,动画就与状态管理纠缠在一起。

后来加入的花色滑块、领先者强调、增删玩家过渡、空位呼吸和概率数字变化,也都需要考虑是否阻碍交互。特别是无限循环的呼吸动画,不能被“动画未结束,禁止点击”的统一规则永远拦住。

从网页变成 Android 安装包#

决定做 APP 时,我最关心两个问题:改动会不会很大,会不会破坏原来的网页。

最终采用 Capacitor 复用现有界面。核心结构可以这样理解:

MoonBit 规则与概率核心
↓ 编译及接口转换
JavaScript 计算接口 + Web Worker
↓
HTML / CSS / JavaScript 界面
├── 浏览器与 GitHub Pages
└── Capacitor Android 容器
├── 本地资源
├── 系统文件保存
└── 返回键与系统栏适配

这不是把一个远程网址简单做成快捷方式。网页文件和编译后的引擎放在 APK 里,应用使用本地资源运行,所以电脑关机、预览服务关闭,都不影响已经安装的 APP。

Android 侧需要单独处理的内容包括系统文件保存、返回键、状态栏背景、桌面图标和应用签名。比如某些手机顶部出现白条,问题就不只是网页背景,而涉及原生窗口和系统栏的配置。

网页与 APP 的恢复行为也不同:网页新进入链接时从空白二人牌桌开始,当前标签页刷新恢复;APP 会在设备上保存牌局,再次打开时恢复。使用同一界面,并不意味着所有平台行为必须完全一样。

为了减少无意义的重复打包,开发中采用了“先在网页集中调整,再统一同步 APP”的方式。用户确认界面后再递增 Android 版本并生成安装包,效率比每次改一个按钮都重新构建更高。

Git、CI、构建产物和签名:几个容易混淆的概念#

刚开始使用这些工具时,我会把一些动作混在一起:代码已经改了,为什么 GitHub 还没变?有 APK 了,是不是已经上传?仓库出现红叉,是不是 push 失败?

后来逐渐分清了几个独立阶段。

动作实际改变的内容
修改文件本地工作区里的源码
Git commit本地版本历史
Git pushGitHub 上的提交记录
GitHub Pages 部署在线网页实际使用的资源
APK 构建生成某一时刻代码对应的安装包
手机安装用户设备上正在运行的版本

这些动作有关联,但不是自动等价的。本地 APK 生成成功,不代表代码已经推送;GitHub 收到提交后,某个自动任务失败,也不代表推送没有成功。

项目后来就遇到过这样的情况:核心 CI、网页构建和部署都成功,只有 Android 工作流在准备 SDK 时失败。根据失败步骤定位问题,比看到红叉后怀疑所有代码更有用。

本地也遇到过构建隔离环境的问题。工具内执行时,MoonBit 或 Android 资源编译器因权限和环境异常失败;换到电脑普通 PowerShell 后,最终生成了 0.1.3 安装包。这不能被写成“代码修复后所有测试通过”,因为实际解决的是执行环境问题。

仓库改名为 PokerLab 后,需要调整 Git 远程地址、README 的在线体验链接和包配置里的仓库链接。但 Android 包名、签名和 Mooncakes 包名不应随界面品牌随意改变。

尤其是签名文件:它不是可以随时删除重建的缓存。后续版本要覆盖安装旧版,需要保留同一签名。项目把签名放在私有本地目录,构建产物、缓存和申报材料也按用途与源代码分开管理。

README 和进度文档为什么值得认真维护#

随着网页、核心库和 Android 工程同时存在,仅靠记忆很难准确回答“这次修改进入 APP 了吗”。

README 用于说明目标、边界、运行方式和整体结构;Android 文档说明安装、依赖、构建和签名;进度记录则保存当前版本、已完成改动、未完成检查和环境问题。

这几类文档的读者不同。个人博客可以讲经历和取舍,README 则应让陌生人尽快运行项目。进度记录可以保留一次构建失败的具体背景,但 README 不需要充满每一轮对话的细节。

同样,源码注释应解释不容易从语句本身看出的原因,比如为什么隐藏牌需要坐标回退、为什么翻牌队列按牌位去重、为什么数字动画只修改显示文本。不需要把每一行赋值再翻译成一句中文。

与大模型协作:效率提升与责任边界#

这个项目有大量代码修改、文档和排查工作是在与大模型协作中完成的。我不打算把这段过程包装成完全独立手写的项目。

我的工作主要包括确定需求、选择方案、判断审美、在网页和手机上体验、指出不符合预期的地方,以及决定哪些功能值得继续做。模型则参与方案讨论、代码实现、脚本编写、测试和文档整理。

这样的分工确实降低了把想法变成可运行版本的门槛。很多时候,我不必先精通某个 Android 构建细节,就能提出目标并得到可执行的改动。

但开发中反复出现的问题,也让我看到这种协作的边界。

1. 看起来理解了,不代表理解一致#

“丝滑”“简洁”“跟随系统”“全局刷新”都不是足够具体的验收标准。它们需要被拆成可以观察的行为:哪些文字变化、哪些牌保持字体、动画先后关系是什么、刷新保留什么、首次进入清空什么。

翻牌波浪的例子尤其明显。模型可以正确复述“从左到右”,却仍写出逐行串行的效果。最后真正消除歧义的,是列出同一时刻应该启动的几张牌。

2. 工具检查通过,仍可能没有验证到用户关心的问题#

一次检查可能只确认了页面无报错、元素数量正确、最终动画状态恢复,却没有确认玩家之间是否停顿太久,或翻牌是否在视野之外已经结束。

这些检查有价值,但与“效果符合预期”不是同一个结论。更可靠的记录应明确:验证的是输入输出、布局、动画启动时间,还是手机上的实际操作。

这也提醒我,不能只看模型最后一句“已验证”。应该追问验证方法,必要时自己刷新、滚动、切换布局,再观察全过程。

3. 对话记录不能替代项目记录#

长时间开发后,上下文会变长,模型可能遗忘某个约束,或者把已经回退的方案当成当前实现。模型切换和会话延续时,这个问题更明显。

把约束写进代码结构、文档和版本记录,比依赖“之前说过”稳定得多。例如“先改网页,等确认再打包 APP”“申报书不上传”“保持原签名”,都应该成为可检查的工程事实。

4. 我仍然需要为最终作品负责#

模型给出的代码可以加快实现,但我需要理解项目的主要构造,知道结果从哪里来,能解释为什么这个概率是精确枚举,为什么 APK 能离线运行,以及哪些功能没有实现。

如果把项目放进简历,这一点尤其重要。别人不只会问用了什么工具,也会问为什么这样设计、遇到什么问题、如何验证、下一步准备改什么。

AI 参与不是应该掩盖的事实。更值得展示的是,在协作中能否提出明确要求、识别错误、控制范围,并逐步建立自己的理解。

做到这个阶段,我会怎样评价它#

Poker Lab 已经完成了一个小工具的主要闭环:输入牌局、得到结果、解释结果、探索河牌、保存图片,以及在网页和 Android 上使用。

它还不是一个覆盖所有扑克场景的平台。当前代码也仍有可以整理的地方,例如前端状态和动画逻辑比较集中,跨设备字体与动画仍需要更多实测,自动检查对视觉过程的覆盖也有限。

如果继续开发,我会优先整理状态管理和动画调度,补充容易回归的交互测试,然后根据真实使用决定是否加入撤销、牌局收藏等功能。继续堆叠动效,不一定比让已有功能更稳定更有价值。

这次经历最重要的收获,是开始把一个项目看成完整的交付过程。算法正确、页面好看、安装包生成、在线部署成功、用户操作顺畅,分别是不同的工作,需要不同的证据。

大赛申报没有成功,是这段经历的一部分;把项目继续做成自己认可的工具,也是另一部分。最终我希望展示的,不只是一个会计算概率的页面,而是面对问题时如何观察、拆解、调整,并把一个想法逐步做成可以交给别人使用的东西。


阅读与复现入口#

本文中的实现与过程以本次开发记录为依据;在线部署、自动任务和后续版本的最新状态,以仓库实际记录为准。

Poker Lab 开发记录
https://github.com/posts/poker-lab-开发记录/
作者
纸船
发布于
2026-10-09
许可协议
CC BY-NC-SA 4.0