实战篇:房间 + 回合制对战完整实现
概述
本篇把第 4 周的六个模块合成一条可运行的对战链路:房间状态机 → 匹配 → 回合流转 → 消息同步 → 结算。以一个简化版"猜拳 2 人对战"为例实现完整闭环,演示如何把房间、回合、结算组织成一套可扩展的对战框架。所有代码基于 Java 17 + Netty + Spring Boot,跑在骨架工程与玩家系统之上。
一、工程目标与结构
1.1 目标
本篇实现:
房间状态机:Waiting → Ready → Playing → End
匹配:2 人快速配对
回合流转:轮流出拳(石头/剪刀/布)
消息同步:广播 + 序号
结算:胜负判定 + 积分变动 + 幂等1.2 代码结构
com.game.gamemodule.rps
├── model
│ ├── Room.java // 房间(状态机)
│ ├── Member.java // 房间成员
│ └── RpsGame.java // 对局(回合流转)
├── manager
│ └── RoomManager.java // 房间管理
├── service
│ ├── MatchService.java // 匹配服务
│ ├── TurnService.java // 回合服务
│ └── SettlementService.java // 结算服务
├── handler
│ ├── CreateRoomHandler.java // 建房
│ ├── JoinRoomHandler.java // 加入
│ └── PlayHandler.java // 出拳
└── event
└── GameEvents.java // 消息常量与工具二、房间模型与状态机
2.1 Room 实体
java
package com.game.gamemodule.rps.model;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
public class Room {
public static final int ST_WAITING = 0; // 等待
public static final int ST_READY = 1; // 准备
public static final int ST_PLAYING = 2; // 对局中
public static final int ST_END = 3; // 结算
private final long roomId;
private final List<Member> members = new CopyOnWriteArrayList<>();
private volatile int state = ST_WAITING;
private volatile long ownerId;
public Room(long roomId, long ownerId) {
this.roomId = roomId;
this.ownerId = ownerId;
}
public synchronized boolean changeState(int from, int to) {
if (state != from) {
return false;
}
state = to;
return true;
}
public boolean canJoin() {
return state == ST_WAITING && members.size() < 2;
}
public void addMember(Member m) {
members.add(m);
}
public boolean allReady() {
return members.size() == 2 && members.stream().allMatch(Member::isReady);
}
public long getRoomId() { return roomId; }
public int getState() { return state; }
public List<Member> snapshotMembers() { return members; }
}2.2 建房与加入 Handler
java
@MessageMapping(opcode = 2001) // 建房
@Component
public class CreateRoomHandler implements MessageHandler {
private final RoomManager roomManager;
@Override
public void handle(ChannelHandlerContext ctx, GameSession session, byte[] body) {
Room room = roomManager.create(session.getPlayerId());
room.addMember(new Member(session.getPlayerId(), true)); // 房主默认准备
roomManager.bind(session.getPlayerId(), room.getRoomId());
// 返回房间信息
session.send(new GameMessage(2002, ("{\"roomId\":" + room.getRoomId() + "}").getBytes()));
}
}java
@MessageMapping(opcode = 2003) // 加入(按房间号)
@Component
public class JoinRoomHandler implements MessageHandler {
@Override
public void handle(ChannelHandlerContext ctx, GameSession session, byte[] body) {
long roomId = parseRoomId(body);
Room room = roomManager.get(roomId);
if (room == null || !room.canJoin()) {
session.send(new GameMessage(2004, "{\"code\":1}".getBytes())); // 房间不可加入
return;
}
room.addMember(new Member(session.getPlayerId(), true));
roomManager.bind(session.getPlayerId(), roomId);
// 广播:新玩家加入
GameEvents.broadcast(room, new GameMessage(2005, ("{\"roomId\":" + roomId + "}").getBytes()));
// 人齐且全员就绪 → 开局
if (room.allReady() && room.changeState(Room.ST_WAITING, Room.ST_READY)) {
startGame(room);
}
}
}三、匹配服务
3.1 简易 2 人匹配
java
package com.game.gamemodule.rps.service;
import com.game.gamemodule.rps.model.Room;
import com.game.gamemodule.rps.manager.RoomManager;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import java.util.ArrayDeque;
import java.util.Queue;
@Service
public class MatchService {
private final Queue<Long> waitingQueue = new ArrayDeque<>();
private final RoomManager roomManager;
// 入池
public void enqueue(long playerId) {
synchronized (waitingQueue) {
waitingQueue.add(playerId);
}
}
// 每 500ms 撮合一次
@Scheduled(fixedDelay = 500)
public void tick() {
long first;
synchronized (waitingQueue) {
if (waitingQueue.size() < 2) {
return;
}
first = waitingQueue.poll();
}
long second;
synchronized (waitingQueue) {
if (waitingQueue.isEmpty()) {
waitingQueue.add(first); // 回池
return;
}
second = waitingQueue.poll();
}
// 撮合成功:建房并通知双方
Room room = roomManager.create(first);
room.addMember(new Member(first, true));
room.addMember(new Member(second, true));
roomManager.bind(first, room.getRoomId());
roomManager.bind(second, room.getRoomId());
room.changeState(Room.ST_WAITING, Room.ST_READY);
startGame(room);
}
}说明:
生产环境匹配带段位/渐进放宽(见匹配系统篇)
此处演示核心撮合循环四、回合流转
4.1 RpsGame 对局对象
java
package com.game.gamemodule.rps.model;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class RpsGame {
// 石头/剪刀/布
public static final int ROCK = 0;
public static final int SCISSORS = 1;
public static final int PAPER = 2;
private final Map<Long, Integer> choices = new ConcurrentHashMap<>(); // playerId -> 出拳
private final long playerA;
private final long playerB;
public RpsGame(long playerA, long playerB) {
this.playerA = playerA;
this.playerB = playerB;
}
public boolean bothChosen() {
return choices.size() == 2;
}
public void choose(long playerId, int choice) {
choices.put(playerId, choice);
}
// 判定结果:返回胜者 playerId,平局返回 0
public long judge() {
int a = choices.get(playerA);
int b = choices.get(playerB);
if (a == b) {
return 0;
}
boolean aWin = (a == ROCK && b == SCISSORS)
|| (a == SCISSORS && b == PAPER)
|| (a == PAPER && b == ROCK);
return aWin ? playerA : playerB;
}
public long getPlayerA() { return playerA; }
public long getPlayerB() { return playerB; }
}4.2 出拳服务
java
package com.game.gamemodule.rps.service;
import com.game.gamemodule.rps.model.Room;
import com.game.gamemodule.rps.model.RpsGame;
public class TurnService {
// 玩家出拳
public void play(Room room, RpsGame game, long playerId, int choice) {
synchronized (game) { // 同房间串行
// 服务端校验:选择合法
if (choice < RpsGame.ROCK || choice > RpsGame.PAPER) {
return;
}
game.choose(playerId, choice);
// 广播出拳(序号 +1)
GameEvents.broadcast(room, new GameMessage(3002,
("{\"playerId\":" + playerId + ",\"choice\":" + choice + "}").getBytes()));
// 双方都出拳 → 判定
if (game.bothChosen()) {
settle(room, game);
}
}
}
private void settle(Room room, RpsGame game) {
long winner = game.judge();
// 触发结算
GameEvents.broadcast(room, new GameMessage(4001,
("{\"winner\":" + winner + "}").getBytes()));
room.changeState(Room.ST_PLAYING, Room.ST_END);
}
}要点:
出拳在 game 锁内串行(防并发)
服务端校验选择合法
双方出拳后自动判定 → 结算
超时处理:可用 HashedWheelTimer 定时,超时自动随机出拳五、结算服务
5.1 结算实现(幂等)
java
package com.game.gamemodule.rps.service;
import com.game.gamemodule.rps.model.Room;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class SettlementService {
private final Map<Long, Boolean> settledRooms = new ConcurrentHashMap<>();
// 结算:win 1 平 0 负 -1
public void settle(Room room, long winner, long playerA, long playerB) {
if (settledRooms.putIfAbsent(room.getRoomId(), true) != null) {
return; // 已结算过,幂等返回
}
long baseScore = 10;
long deltaA = outcomeDelta(winner, playerA) * baseScore;
long deltaB = outcomeDelta(winner, playerB) * baseScore;
// 走经济系统统一入口(含流水)
economyService.changeCoin(playerA, deltaA);
economyService.changeCoin(playerB, deltaB);
// 广播结算结果
GameEvents.broadcast(room, new GameMessage(4002,
("{\"winner\":" + winner + ",\"a\":" + deltaA + ",\"b\":" + deltaB + "}").getBytes()));
}
private int outcomeDelta(long winner, long playerId) {
if (winner == 0) {
return 0; // 平局
}
return winner == playerId ? 1 : -1;
}
}幂等实现:
putIfAbsent 保证同房间只结算一次
崩溃补结算也走同一入口
economyService 统一记账(含流水,见经济系统篇)六、消息同步与序号
6.1 事件工具
java
package com.game.gamemodule.rps.event;
import com.game.gamemodule.rps.model.Member;
import com.game.gamemodule.rps.model.Room;
import com.game.gateway.codec.GameMessage;
public class GameEvents {
// 房间内广播(带全局序号)
public static void broadcast(Room room, GameMessage msg) {
for (Member m : room.snapshotMembers()) {
GameSession session = sessionManager.getByPlayer(m.getPlayerId());
if (session != null && session.isOnline()) {
session.send(msg);
}
}
}
}说明:
生产环境消息包装 serverSeq(见消息同步篇)
此处演示广播骨架
离线成员重连后由全量快照恢复七、全链路串联
完整流程:
建房(2001)→ 加入(2003)→ 人齐 Ready
→ 开局广播(3001)→ 轮流出拳(3002)
→ 双方出完 → 判定(4001)→ 结算(4002)
→ 房间 End → 玩家离开 → 房间清理7.1 验证清单
1. 建房:返回 roomId,房主入房
2. 加入:第二人加入,人齐自动开局
3. 出拳:服务端校验非法选择被拒
4. 判定:石头胜剪刀、剪刀胜布、布胜石头
5. 平局:同拳不结算金币(或按配置平局处理)
6. 结算幂等:重复结算请求不重复发奖
7. 房间状态:Playing 中不能加入新玩家
8. 掉线:玩家掉线后房间可继续(宽限/判负)7.2 扩展点
把猜拳换成真实玩法:
TurnService.play → 换成玩法规则引擎
RpsGame → 换成玩法对局对象(牌局/棋盘)
房间/匹配/结算/广播骨架全部复用
新增玩法 = 新增 model + validator + 结算逻辑八、小结
本篇用"猜拳 2 人对战"把第 4 周全部模块串成可运行闭环:Room 以状态机管控生命周期,建房/加入 Handler 驱动人齐开局;MatchService 用 500ms 撮合循环完成 2 人配对;RpsGame + TurnService 在房间锁内串行处理出拳、校验合法并自动判定;SettlementService 用 putIfAbsent 保证结算幂等,统一走经济入口;广播工具完成房间内同步。虽然玩法简单,但"状态机 + 回合串行 + 结算幂等 + 广播同步"这套骨架与真实棋牌、桌游完全同构——后续换玩法只需替换对局模型与规则引擎,房间、匹配、结算、同步四层全部复用。