🤖
AI审核中

金叶炸金花

  Java   14分钟   127浏览   1评论

摘要:这不是一个在浏览器里随机发三张牌的演示页面,而是一套完整的多人在线游戏系统。它支持实时对战、房间管理、断线恢复、AI 机器人、积分流水、历史记录和管理后台,并围绕服务端权威、状态一致性、并发控制与隐私隔离进行了系统设计。

线上体验:金叶炸金花

技术栈:JavaSpring BootWebSocketMySQLSpring SecurityAI

为什么要做这个项目

炸金花的基本规则并不复杂:每名玩家拿到三张牌,通过看牌、跟注、加注、比牌或弃牌等操作,最终决出胜负。

但如果目标不只是做一个单机小游戏,而是让多名玩家通过不同设备,真正坐到同一张线上牌桌,问题就会迅速变得复杂:

  • 谁负责洗牌、发牌和判定胜负?
  • 如何防止客户端修改手牌、积分和下注金额?
  • 多名玩家同时操作时,应该以谁的请求为准?
  • 玩家刷新页面或网络中断后,怎样恢复到原来的牌局?
  • 如何避免重复点击导致二次扣分?
  • AI 机器人能否根据牌力和局面做出合理决策?
  • AI 接口超时后,牌局会不会一直卡住?
  • PC 和手机如何共用同一套牌桌页面?

因此,这个项目从一开始就没有把重点放在“画出一张牌桌”上,而是希望实现一套可信、稳定、能够实际运行的多人实时游戏系统

项目目前能做什么

系统支持 2~6 名玩家参与游戏。

玩家注册并登录后,可以在大厅中创建公开房或好友房。公开房会展示在大厅列表中,其他玩家可以直接加入;好友房则需要通过房间号邀请。

创建房间时,房主可以配置:

  • 房间人数
  • 基础底注
  • 单次操作时限
  • 允许比牌的起始轮次
  • 最大轮数
  • 封顶下注档位
  • 异花 235 是否可以压豹子
  • 金花与顺子的大小关系
  • 同牌型、同点数时是否继续比较花色

房间规则会在牌局开始时冻结。开局后,即使房主仍在房间中,也不能临时修改规则,避免规则变化影响正在进行的对局。

进入房间后,玩家可以选座、准备、聊天和开始游戏。牌局中支持看牌、跟注、加注、比牌与弃牌;结算后,本局参与者可以查看所有参赛玩家的最终牌面和牌型。

系统还提供个人积分、积分流水、历史对局和胜场统计。管理后台则可以查看运营数据、用户状态、房间情况、聊天记录和对局信息,并对积分调整、账号启停等管理操作保留审计记录。

客户端不决定胜负

这个项目最重要的设计原则,是服务端权威

浏览器只负责展示牌桌,并向服务端提交操作意图。真正的游戏规则全部由服务端执行,包括:

  • 洗牌与发牌
  • 当前行动玩家判断
  • 合法操作计算
  • 跟注与加注金额计算
  • 牌型识别与大小比较
  • 底池计算
  • 积分扣除与派奖
  • 超时处理
  • 最终胜负判定

客户端不能指定自己的手牌,也不能决定胜者、扣分金额或下一位行动玩家。

每局游戏使用一副去掉大小王的 52 张牌。服务端以 SecureRandom 作为随机源,通过 Fisher–Yates 算法完成洗牌。

在玩家执行“看牌”之前,客户端甚至不会收到自己的三张牌;其他玩家尚未公开的手牌,则始终不会被下发到当前客户端。

这套设计无法阻止玩家根据公开信息推测牌力,但可以阻止通过修改前端代码直接换牌、改积分或伪造胜负结果。

WebSocket 不只是“实时刷新”

房间状态通过原生 JSON WebSocket 实时同步。

玩家完成一次操作后,服务端会更新底池、下注档位、行动玩家、倒计时和公开状态,然后向房间内的在线玩家推送最新数据。

但这里并不是把同一份完整状态直接广播给所有人。

服务端会根据每名玩家的身份和当前权限,生成一份独立的隐私状态投影

  • 所有人都可以看到公开的下注信息和玩家状态
  • 只有已经看牌的玩家才能收到自己的手牌
  • 其他玩家的暗牌不会出现在消息中
  • 结算牌面只向本局实际参与者开放
  • 结算后新加入房间的玩家无法查看上一局手牌

也就是说,即使多个玩家处于同一个房间,他们收到的 WebSocket 消息也不完全相同。

断线后不立即判负

玩家断线后,系统不会立刻将其判定为弃牌。

服务端会继续保留该玩家的座位和牌局状态,操作倒计时也会正常运行。玩家重新连接后,服务端会直接发送当前完整状态,而不是依赖客户端重新播放断线期间错过的事件。

因此,即使玩家刷新页面、临时切换网络或短暂掉线,仍然可以回到原来的牌局。

这种恢复方式的核心,不是让客户端“补事件”,而是让客户端重新获取一份经过服务端校验的当前状态快照。

如何处理重复请求和并发操作

实时游戏中,一个非常常见的问题,是同一个操作被执行两次。

例如,玩家连续点击两次“跟注”;又或者,玩家的操作请求恰好与服务端超时任务同时到达。

如果只依靠前端禁用按钮,仍然无法彻底避免重复扣分和状态错乱。因为请求可能已经被发送,也可能来自多个浏览器标签页,甚至可能与定时任务发生竞争。

为此,项目为每个游戏命令设置了唯一的 requestId,并同时使用:

  • 房间级互斥锁
  • 状态版本号
  • 数据库事务
  • 数据库行锁
  • 游戏动作唯一索引
  • 积分流水唯一业务编号

这些机制分别解决不同层面的问题。

房间级锁负责控制同一房间内的并发修改;状态版本号用来判断请求是否基于过期状态;数据库事务和行锁保证积分与对局数据的一致性;唯一索引和业务编号则为重复请求提供最后一道数据库级保护。

因此,无论是同一回合的重复点击、多个浏览器标签同时操作,还是用户操作与超时任务之间的竞争,最终都只有第一个合法请求能够成功执行。

在持久化过程中,任何异常都会触发事务回滚。只有数据库事务提交成功后,服务端才会替换内存中的牌局状态,并向玩家广播最新结果。

这样可以避免出现“数据库已经扣分,但牌桌仍停留在旧状态”的不一致情况。

接入真正参与牌局的 AI 机器人

除了真人对战,房主还可以在等待阶段向房间中加入 AI 机器人。

当前接入的模型为 openai/gpt-oss-120b,通过兼容 OpenAI 协议的接口进行实时决策。

线上健康检查接口会公开 AI 是否启用、配置是否完整以及当前使用的模型,但不会返回任何密钥内容:

查看实时健康状态

机器人并不是随机选择跟注或弃牌。

当机器人已经看牌后,服务端会先根据当前房间规则,计算机器人的权威牌型、牌力档位和牌型比较关键字,再将以下信息交给模型:

  • 当前底池
  • 跟注成本
  • 比牌成本
  • 当前轮次
  • 剩余积分
  • 当前合法操作
  • 合法加注档位
  • 合法比牌目标
  • 对手已经公开的行为
  • 机器人自己的手牌和牌型

在机器人尚未执行“看牌”操作之前,模型不会收到它的底牌。其他玩家尚未公开的手牌,在任何情况下都不会发送给 AI。

三种策略强度

系统目前提供三种 AI 策略:

轻松

打法偏保守,行为相对容易预测,不会主动诈唬。持有弱牌且行动成本上升时,更倾向于弃牌。

均衡

在风险与收益之间进行平衡。强牌会进行有限的价值加注,同时保留很低的受控诈唬概率。

高手

综合考虑牌力、底池、行动成本、当前轮次、剩余积分以及对手的公开行为。强牌会更加主动,中等牌则可能根据局面选择比牌或继续观察。

AI 可以思考,但不能越权

AI 返回的动作只是一条建议,不会直接修改牌局。

模型给出结果后,服务端还会重新检查:

  • 动作是否属于当前合法操作
  • 加注金额是否属于当前允许的下注档位
  • 比牌目标是否仍然有效
  • 机器人是否仍然拥有行动权
  • 当前牌局状态版本是否发生变化
  • AI 决策是否已经过期

只有所有检查都通过,动作才会进入正式的游戏状态机。

这一步非常重要。因为 AI 请求本身存在延迟,当模型返回结果时,牌局状态可能已经发生变化。如果直接执行旧状态下生成的动作,就可能造成越权操作或状态错乱。

AI 失败时,牌局仍然继续

外部模型接口并不总是可靠,可能出现超时、网络错误、返回非 JSON 内容,或者返回一个不合法的动作。

因此,系统不能把整张牌桌的可用性建立在一次 AI 请求上。

当模型调用失败时,服务端会立即切换到本地安全策略,根据当前牌力和合法操作生成一个保守动作,保证牌局能够继续运行。

AI 可以提升机器人的决策能力,但不能成为牌局状态机的单点故障。

房间聊天同样需要权限边界

项目中的聊天功能与游戏状态共用同一条 WebSocket 连接。

服务端不会相信客户端提交的用户编号或昵称。每次发送消息之前,都会根据当前认证信息重新确认该用户是否仍然是房间的有效成员。

因此,被房主移出房间、已经主动退出,或者仍然持有旧 WebSocket 连接的玩家,都不能继续读取和发送房间消息。

聊天功能还增加了:

  • 客户端消息幂等编号
  • 服务端频率限制
  • 消息长度限制
  • 最近 50 条聊天记录恢复
  • 房间级消息隔离

这些功能看起来与牌型和下注规则无关,但对于一套真正投入使用的多人在线系统来说,它们同样不可缺少。

PC 和移动端共用一张牌桌

前端没有分别维护桌面版和移动版两套页面,而是使用原生 ES6、HTML5 和 CSS3 实现了一套响应式单页应用。

桌面端会保留完整的房间信息、牌桌区域和聊天区域;移动端则会重新安排座位、手牌、底池、倒计时和操作栏的位置。

适配过程中最难处理的并不是简单缩放,而是如何让 2 人桌到 6 人桌都保持清晰,同时避免玩家卡片、暗牌、下注金额和操作按钮互相遮挡。

登录、注册、大厅、房间、个人中心、历史记录、规则页面和管理后台都属于同一套 SPA 路由,并支持直接刷新子页面。

技术架构

项目后端主要使用:

  • JDK 8
  • Spring Boot 2.7.18
  • Spring Security
  • Spring Data JPA
  • MySQL 8
  • Flyway
  • 原生 JSON WebSocket

前端使用原生 ES6、HTML5 和 CSS3,不需要额外部署 Node.js 服务。

前后端最终会打包成一个可执行 JAR,并通过统一的 /zjh 上下文对外提供页面、REST API 和 WebSocket 服务:

浏览器
  ├─ HTTPS /zjh/*        页面与前端路由
  ├─ JSON  /zjh/api/*    认证、大厅、资料与历史记录
  └─ WSS   /zjh/ws/game  房间、牌局与聊天
                 │
          Spring Boot
  认证 → 房间 → 游戏状态机 → 牌型规则
                 │
       AI 调度 → GPT-OSS 兼容接口
                 │
        JPA + Flyway + MySQL

数据库结构通过 6 个 Flyway 版本逐步演进,从最初的用户、房间和对局表,逐步扩展到好友房、聊天、管理后台和 AI 机器人。

当前代码库包含:

  • 89 个 Java 源文件
  • 7 个前端 JavaScript 模块
  • 74 项自动化测试

测试范围覆盖牌型比较、洗牌、认证、完整多人流程、重复请求、并发竞争、断线恢复、积分一致性、聊天权限、AI 决策边界和管理后台鉴权。

关于积分与项目边界

本项目只使用站内虚拟娱乐积分。

积分不能充值、提现、转让,也不能兑换任何现实权益;系统不抽水,不涉及现金玩法。

项目的核心定位,是一套围绕多人实时游戏、服务端状态管理和 AI 决策边界展开的技术实践。

当前生产版本采用单应用实例运行。房间锁、在线连接和部分运行状态仍然保存在当前 JVM 中,因此暂时不能通过简单启动多个实例的方式实现水平扩容。

如果未来需要支持多实例部署,还需要继续引入:

  • 共享 Session
  • 分布式锁
  • 跨节点消息广播
  • 稳定的 WebSocket 会话路由
  • 节点故障后的房间状态接管机制

这也是项目下一阶段可以继续深入的方向。

写在最后

真正完成这套项目后,我最大的感受是:

多人游戏最难的部分,从来不是把三张牌画在页面上,而是让不同玩家在不同网络、不同设备和不同操作时序下,仍然看到同一场可信的牌局。

一个能够运行的牌桌需要界面;一套能够长期运行的在线系统,则需要状态机、事务、幂等、隐私隔离、断线恢复、安全校验和故障降级。

AI 机器人的加入,让系统不再只是一套固定规则程序。但 AI 仍然被限制在服务端划定的边界内:

它可以思考,但不能越权;
可以提出行动,但不能决定规则。

实际体验地址:

https://www.hqxiaozou.top/zjh/

欢迎来坐一局。

如果你觉得文章对你有帮助,那就请作者喝杯咖啡吧☕
微信
支付宝
  1 条评论
召田最帅boy 博主   湖南省衡阳市

金叶炸金花 x 今夜炸金花 √