摘要:这不是一个在浏览器里随机发三张牌的演示页面,而是一套完整的多人在线游戏系统。它支持实时对战、房间管理、断线恢复、AI 机器人、积分流水、历史记录和管理后台,并围绕服务端权威、状态一致性、并发控制与隐私隔离进行了系统设计。
线上体验:金叶炸金花
技术栈:Java、Spring Boot、WebSocket、MySQL、Spring Security、AI
为什么要做这个项目
炸金花的基本规则并不复杂:每名玩家拿到三张牌,通过看牌、跟注、加注、比牌或弃牌等操作,最终决出胜负。
但如果目标不只是做一个单机小游戏,而是让多名玩家通过不同设备,真正坐到同一张线上牌桌,问题就会迅速变得复杂:
- 谁负责洗牌、发牌和判定胜负?
- 如何防止客户端修改手牌、积分和下注金额?
- 多名玩家同时操作时,应该以谁的请求为准?
- 玩家刷新页面或网络中断后,怎样恢复到原来的牌局?
- 如何避免重复点击导致二次扣分?
- AI 机器人能否根据牌力和局面做出合理决策?
- AI 接口超时后,牌局会不会一直卡住?
- PC 和手机如何共用同一套牌桌页面?
因此,这个项目从一开始就没有把重点放在“画出一张牌桌”上,而是希望实现一套可信、稳定、能够实际运行的多人实时游戏系统。
项目目前能做什么
系统支持 2~6 名玩家参与游戏。
玩家注册并登录后,可以在大厅中创建公开房或好友房。公开房会展示在大厅列表中,其他玩家可以直接加入;好友房则需要通过房间号邀请。
创建房间时,房主可以配置:
- 房间人数
- 基础底注
- 单次操作时限
- 允许比牌的起始轮次
- 最大轮数
- 封顶下注档位
- 异花 235 是否可以压豹子
- 金花与顺子的大小关系
- 同牌型、同点数时是否继续比较花色
房间规则会在牌局开始时冻结。开局后,即使房主仍在房间中,也不能临时修改规则,避免规则变化影响正在进行的对局。
进入房间后,玩家可以选座、准备、聊天和开始游戏。牌局中支持看牌、跟注、加注、比牌与弃牌;结算后,本局参与者可以查看所有参赛玩家的最终牌面和牌型。
系统还提供个人积分、积分流水、历史对局和胜场统计。管理后台则可以查看运营数据、用户状态、房间情况、聊天记录和对局信息,并对积分调整、账号启停等管理操作保留审计记录。
客户端不决定胜负
这个项目最重要的设计原则,是服务端权威。
浏览器只负责展示牌桌,并向服务端提交操作意图。真正的游戏规则全部由服务端执行,包括:
- 洗牌与发牌
- 当前行动玩家判断
- 合法操作计算
- 跟注与加注金额计算
- 牌型识别与大小比较
- 底池计算
- 积分扣除与派奖
- 超时处理
- 最终胜负判定
客户端不能指定自己的手牌,也不能决定胜者、扣分金额或下一位行动玩家。
每局游戏使用一副去掉大小王的 52 张牌。服务端以 SecureRandom 作为随机源,通过 Fisher–Yates 算法完成洗牌。
在玩家执行“看牌”之前,客户端甚至不会收到自己的三张牌;其他玩家尚未公开的手牌,则始终不会被下发到当前客户端。
这套设计无法阻止玩家根据公开信息推测牌力,但可以阻止通过修改前端代码直接换牌、改积分或伪造胜负结果。
WebSocket 不只是“实时刷新”
房间状态通过原生 JSON WebSocket 实时同步。
玩家完成一次操作后,服务端会更新底池、下注档位、行动玩家、倒计时和公开状态,然后向房间内的在线玩家推送最新数据。
但这里并不是把同一份完整状态直接广播给所有人。
服务端会根据每名玩家的身份和当前权限,生成一份独立的隐私状态投影:
- 所有人都可以看到公开的下注信息和玩家状态
- 只有已经看牌的玩家才能收到自己的手牌
- 其他玩家的暗牌不会出现在消息中
- 结算牌面只向本局实际参与者开放
- 结算后新加入房间的玩家无法查看上一局手牌
也就是说,即使多个玩家处于同一个房间,他们收到的 WebSocket 消息也不完全相同。
断线后不立即判负
玩家断线后,系统不会立刻将其判定为弃牌。
服务端会继续保留该玩家的座位和牌局状态,操作倒计时也会正常运行。玩家重新连接后,服务端会直接发送当前完整状态,而不是依赖客户端重新播放断线期间错过的事件。
因此,即使玩家刷新页面、临时切换网络或短暂掉线,仍然可以回到原来的牌局。
这种恢复方式的核心,不是让客户端“补事件”,而是让客户端重新获取一份经过服务端校验的当前状态快照。
如何处理重复请求和并发操作
实时游戏中,一个非常常见的问题,是同一个操作被执行两次。
例如,玩家连续点击两次“跟注”;又或者,玩家的操作请求恰好与服务端超时任务同时到达。
如果只依靠前端禁用按钮,仍然无法彻底避免重复扣分和状态错乱。因为请求可能已经被发送,也可能来自多个浏览器标签页,甚至可能与定时任务发生竞争。
为此,项目为每个游戏命令设置了唯一的 requestId,并同时使用:
- 房间级互斥锁
- 状态版本号
- 数据库事务
- 数据库行锁
- 游戏动作唯一索引
- 积分流水唯一业务编号
这些机制分别解决不同层面的问题。
房间级锁负责控制同一房间内的并发修改;状态版本号用来判断请求是否基于过期状态;数据库事务和行锁保证积分与对局数据的一致性;唯一索引和业务编号则为重复请求提供最后一道数据库级保护。
因此,无论是同一回合的重复点击、多个浏览器标签同时操作,还是用户操作与超时任务之间的竞争,最终都只有第一个合法请求能够成功执行。
在持久化过程中,任何异常都会触发事务回滚。只有数据库事务提交成功后,服务端才会替换内存中的牌局状态,并向玩家广播最新结果。
这样可以避免出现“数据库已经扣分,但牌桌仍停留在旧状态”的不一致情况。
接入真正参与牌局的 AI 机器人
除了真人对战,房主还可以在等待阶段向房间中加入 AI 机器人。
当前接入的模型为 gpt-6-luna(2026-09-29 起;此前是 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 调度 → OpenAI 兼容接口
│
JPA + Flyway + MySQL
数据库结构通过 6 个 Flyway 版本逐步演进,从最初的用户、房间和对局表,逐步扩展到好友房、聊天、管理后台和 AI 机器人。
当前代码库包含:
- 89 个 Java 源文件
- 7 个前端 JavaScript 模块
- 74 项自动化测试
测试范围覆盖牌型比较、洗牌、认证、完整多人流程、重复请求、并发竞争、断线恢复、积分一致性、聊天权限、AI 决策边界和管理后台鉴权。
关于积分与项目边界
本项目只使用站内虚拟娱乐积分。
积分不能充值、提现、转让,也不能兑换任何现实权益;系统不抽水,不涉及现金玩法。
项目的核心定位,是一套围绕多人实时游戏、服务端状态管理和 AI 决策边界展开的技术实践。
当前生产版本采用单应用实例运行。房间锁、在线连接和部分运行状态仍然保存在当前 JVM 中,因此暂时不能通过简单启动多个实例的方式实现水平扩容。
如果未来需要支持多实例部署,还需要继续引入:
- 共享 Session
- 分布式锁
- 跨节点消息广播
- 稳定的 WebSocket 会话路由
- 节点故障后的房间状态接管机制
这也是项目下一阶段可以继续深入的方向。
写在最后
真正完成这套项目后,我最大的感受是:
多人游戏最难的部分,从来不是把三张牌画在页面上,而是让不同玩家在不同网络、不同设备和不同操作时序下,仍然看到同一场可信的牌局。
一个能够运行的牌桌需要界面;一套能够长期运行的在线系统,则需要状态机、事务、幂等、隐私隔离、断线恢复、安全校验和故障降级。
AI 机器人的加入,让系统不再只是一套固定规则程序。但 AI 仍然被限制在服务端划定的边界内:
它可以思考,但不能越权;
可以提出行动,但不能决定规则。
实际体验地址:
https://www.hqxiaozou.top/zjh/
欢迎来坐一局。

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