回合制对战流程
概述
回合制是棋牌、桌游、卡牌类游戏的核心玩法:玩家轮流行动,服务器驱动回合流转。本文设计回合制对战流程的三大环节:出牌阶段流转(回合状态机)、超时检测(ScheduledExecutorService / Netty Timer)、掉线重连与状态恢复。
一、回合制对战模型
1.1 核心概念
对局(Game):一次完整对战
回合(Turn):轮到某个玩家行动
阶段(Phase):回合内的细分步骤(思考/行动/结算)
回合制特点:
同一时刻只有一名(或一组)玩家行动
行动结果广播给全房间
服务器权威驱动:谁行动、何时超时都由服务器定1.2 服务端权威
回合制天然适合服务端权威:
所有判定在服务器(出牌合法、胜负)
客户端只负责表现与输入
状态以服务器为准,客户端状态可被纠正二、出牌阶段流转
2.1 回合状态机
以斗地主为例的回合阶段:
DEAL(发牌)→ PLAYER_TURN(玩家出牌)
→ NEXT_TURN(轮换)→ ...
→ ROUND_END(本轮结束)
→ SETTLE(结算)
细化设计:
TurnState 枚举:
DEALING 发牌
THINKING 轮到玩家思考
ACTING 玩家行动(出牌)
TURN_END 本回合收尾
GAME_END 对局结束2.2 回合流转实现
java
public class TurnManager {
private final Room room;
private int currentPlayerIndex;
private TurnState state = TurnState.DEALING;
// 玩家出牌(服务端验证后执行)
public boolean play(long playerId, PlayAction action) {
if (state != TurnState.THINKING) {
return false; // 非思考阶段拒绝
}
if (!isCurrentPlayer(playerId)) {
return false; // 不是当前玩家
}
// 服务端合法性校验(见行为验证篇)
if (!ruleEngine.validate(room, action)) {
return false;
}
// 应用行动
applyAction(action);
// 广播行动结果
room.broadcast(new TurnEvent("PLAY", playerId, action));
// 判断对局是否结束
if (ruleEngine.isGameOver(room)) {
changeState(TurnState.GAME_END);
return true;
}
// 轮到下一位
nextPlayer();
return true;
}
private void nextPlayer() {
currentPlayerIndex = (currentPlayerIndex + 1) % room.memberCount();
changeState(TurnState.THINKING);
startTurnTimer(); // 开启本回合倒计时
room.broadcast(new TurnEvent("TURN", room.currentPlayerId()));
}
}要点:
状态机校验(非法阶段拒绝)
玩家身份校验(非当前玩家拒绝)
行动后广播 + 驱动下一回合
回合切换即开启新倒计时2.3 行动同步
行动后的消息流:
服务器 → 全房间广播:xxx 出了牌
下一玩家收到"轮到你了"提示
客户端本地表现 + 服务器纠错三、超时检测
3.1 为什么需要超时
回合制最怕"挂机不操作":
玩家思考超时 → 整局卡住
掉线玩家 → 回合无人响应
超时策略(按玩法):
超时自动出牌(出最小/最合理牌)
超时跳过/弃权
超时判负
超时托管(AI 代打)3.2 定时器选型
| 方案 | 说明 | 适用 |
|---|---|---|
ScheduledExecutorService | JUC 定时线程池 | 通用、简单 |
Netty HashedWheelTimer | 时间轮,海量定时高效 | 大量回合倒计时 |
Timer | 单线程,不推荐 | 低频 |
选型建议:
回合倒计时量大(每回合都要建)→ HashedWheelTimer
开销低、支持取消、精确到毫秒
ScheduledExecutorService 也够用,但每任务一线程池开销略高3.3 超时实现
java
public class TurnTimer {
private final HashedWheelTimer timer = new HashedWheelTimer();
// 开启本回合倒计时
public void startTurnTimeout(long roomId, long playerId, long millis) {
Timeout timeout = timer.newTimeout(t -> {
// 到点执行:检查当前回合是否仍属于该玩家
TurnManager tm = roomManager.get(roomId).getTurnManager();
if (tm.isCurrentPlayer(playerId)) {
tm.onTimeout(playerId); // 超时处理(自动出牌/托管)
}
}, millis, TimeUnit.MILLISECONDS);
// 保存 timeout 引用,供主动操作时取消
}
// 玩家正常行动 → 取消本回合定时器
public void cancelTurnTimeout(Timeout timeout) {
if (timeout != null) {
timeout.cancel();
}
}
}超时处理要点:
触发时校验"仍在本回合"(防止旧定时器误触发)
超时动作同样要合法(自动出牌也要校验)
超时信息广播(其他玩家看到"xx 超时自动出牌")3.4 并发安全
超时线程 vs 玩家操作线程的竞争:
都在房间锁内执行(串行化)
超时回调里获取房间锁再处理
双重校验:状态机 + 当前玩家四、掉线重连与状态恢复
4.1 掉线时的对局处理
策略(按玩法):
棋牌:托管(AI 自动出牌)/ 等待
桌游:回合跳过(下一轮再回)
卡牌:暂停该玩家回合等待重连(有限时间)
通用设计:
掉线玩家标记 OFFLINE
轮到其行动时:托管 or 超时跳过
宽限期(如 90s)内重连 → 恢复操作权4.2 状态恢复
重连恢复的关键:对局状态重建
服务器内存已有完整对局状态(内存权威)
重连后只需把"当前状态快照"下发给客户端
恢复流程:
1. 玩家重连登录
2. 查到其所在房间
3. 服务器下发对局快照:
手牌、场上牌、当前回合玩家、倒计时
4. 客户端重建界面
5. 若是其回合 → 正常操作java
public class GameSnapshotService {
public GameSnapshot buildSnapshot(Room room, long viewerId) {
TurnManager tm = room.getTurnManager();
// 只给该玩家看自己的手牌,其他玩家信息脱敏
return new GameSnapshot(
tm.getState(),
tm.currentPlayerId(),
tm.remainingTime(),
room.getHandCards(viewerId),
room.getPublicState()
);
}
}安全要点:
快照只包含本人手牌
他人信息按需可见(防透视作弊)
快照 + 消息序号保证一致性(见消息同步篇)4.3 恢复后的状态机同步
重连玩家可能错过期间的消息:
方案一:全量快照(推荐,简单可靠)
方案二:消息补发(需要消息日志,复杂)
休闲游戏用全量快照即可
恢复后继续:
当前回合玩家掉线 → 托管中,重连后立即恢复操作
自己的回合已因超时跳过 → 下一轮正常参与五、完整对局流程串联
一局棋牌的完整流程:
开局:发牌 → 广播手牌 → 决定先手
循环:当前玩家回合(倒计时 15s)
→ 出牌成功 → 广播 → 下一玩家
→ 超时 → 自动出牌/托管 → 广播 → 下一玩家
→ 掉线 → 托管标记 → 恢复时切换
结束:有人出完/牌局结束 → 结算(见结算篇)
收尾:广播结果 → 房间回 Waiting 或解散六、常见问题
| 问题 | 处理 |
|---|---|
| 回合卡死 | 超时兜底 + 托管 |
| 旧定时器误触发 | 触发时校验当前回合 |
| 重连后界面错乱 | 全量快照重建 |
| 掉线玩家坑队友 | 托管 AI + 超时策略 |
| 并发回合流转 | 房间锁串行 + 状态机 |
七、小结
回合制对战是"服务器驱动的状态机游戏":TurnManager 用 TurnState 状态机管理出牌阶段流转,校验阶段与玩家身份后应用行动并广播;超时检测用 HashedWheelTimer 为每回合挂倒计时,触发时校验当前回合再执行自动出牌或托管;掉线重连依赖内存权威的对局状态,重连后通过全量快照恢复客户端,快照只含本人数据防作弊。三个环节环环相扣:状态机保证流程合法,定时器保证流程不卡死,快照保证断线不丢体验。