目录
在本篇文章中,我们继续实现对战模块的后端逻辑
GameHandler
我们创建 GameHandler 处理 WebSocket 请求
@Component
@Slf4j
public class GameHandler extends TextWebSocketHandler {
@Autowired
private OnlineUserManager onlineUserManager;
@Autowired
private RoomManager roomManager;
/**
* 连接建立(游戏准备)
* @param session
* @throws Exception
*/
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
}
/**
* 处理落子请求
* @param session
* @param message
* @throws Exception
*/
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
}
/**
* 处理异常情况
* @param session
* @param exception
* @throws Exception
*/
@Override
public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
}
/**
* 连接关闭
* @param session
* @param status
* @throws Exception
*/
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
}
}
并注入 RoomManager 和 OnlineUserManager
修改 WebSocketConfig 将 GameHandler 进行注册
@EnableWebSocket
@Configuration
public class WebSocketConfig implements WebSocketConfigurer {
@Autowired
private MatchHandler matchHandler;
@Autowired
private GameHandler gameHandler;
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(matchHandler, "/findMatch")
.addInterceptors(new HttpSessionHandshakeInterceptor()); // 添加拦截器
registry.addHandler(gameHandler, "/game")
.addInterceptors(new HttpSessionHandshakeInterceptor());
}
}
根据约定的前后端交互接口创建请求/响应对象
创建请求响应对象
落子请求 PutChessParam:
@Data
public class PutChessParam implements Serializable {
/**
* 落子玩家 id
*/
private Long userId;
/**
* 落子位置——行
*/
private Integer row;
/**
* 落子位置——列
*/
private Integer col;
}
落子响应 MatchResult:
@Data
public class MatchResult implements Serializable {
/**
* 匹配结果
*/
private String matchMessage;
private Rival rival;
@Data
public static class Rival {
/**
* 对手姓名
*/
private String name;
/**
* 天梯分数
*/
private Long score;
}
public MatchResult() {}
public MatchResult(String matchMessage) {
this.matchMessage = matchMessage;
}
}
游戏就绪响应 GameReadyResult:
@Data
public class GameReadyResult implements Serializable {
/**
* 房间号
*/
private String roomId;
/**
* 玩家 id
*/
private Long thisUserId;
/**
* 对手 id
*/
private Long thatUserId;
/**
* 先手 id
*/
private Long whiteUserId;
}
处理连接成功
游戏房间 WebSocket 连接建立之后(afterConnectionEstablished)需要实现的业务逻辑:
1. 从 session 中获取登录时存储的用户信息(UserInfo),检查用户的登录情况
2. 通过 OnlineUserManager 进行多开判定
3. 判断当前玩家是否在游戏房间中
4. 将两个玩家放到对应的房间对象中,当两个玩家都建立了连接时,这个房间就满了,此时就可以通知双方准备就绪
5. 若有更多玩家尝试加入房间,提示游戏房间已满
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
// 从 session 中获取用户信息
UserInfo userInfo = (UserInfo) session.getAttributes().get(USER_INFO);
// 用户是否登录
if (null == userInfo) {
session.sendMessage(new TextMessage(JacksonUtil.writeValueAsString(
CommonResult.noLogin())));
return;
}
// 判断用户是否处于在线状态
if (null != onlineUserManager.getFromHall(userInfo.getUserId())) {
session.sendMessage(new TextMessage(JacksonUtil.writeValueAsString(
CommonResult.repeatConnection())));
return;
}
// 获取游戏房间, 判断当前玩家是否在游戏房间中
Room room = roomManager.getRoomByUserId(userInfo.getUserId());
if (null == room) {
session.sendMessage(new TextMessage(JacksonUtil.writeValueAsString(
CommonResult.fail(HandlerErrorCodeConstants.GET_GAME_ROOM_ERROR))));
return;
}
// 将用户设置为在线状态
onlineUserManager.enterGameRoom(userInfo.getUserId(), session);
// 将玩家加入到游戏房间(加锁)
synchronized (room) {
// 第一个玩家是否加入游戏房间
if (null == room.getUser1()) {
room.setUser1(userInfo);
// 默认玩家1 作为先手
room.setWhiteUserId(userInfo.getUserId());
log.info("房间 {} 玩家1 {} 已就绪", room.getRoomId(), userInfo.getUserName());
return;
}
// 第二个玩家是否加入游戏房间
if (null == room.getUser2()) {
room.setUser2(userInfo);
log.info("房间 {} 玩家2 {} 已就绪", room.getRoomId(), userInfo.getUserName());
// 通知玩家1 和 玩家2 游戏已就绪
notifyGameReady(room, room.getUser1(), room.getUser2());
notifyGameReady(room, room.getUser2(), room.getUser1());
return;
}
}
// 还有玩家尝试进入同一房间,提示房间已满
// 理论上不会出现该情况,为了让程序更加的健壮, 还是进行判定和提示
session.sendMessage(new TextMessage(JacksonUtil.writeValueAsString(
CommonResult.fail(HandlerErrorCodeConstants.GAME_ROOM_IS_FULL))));
// 日志打印
log.info("玩家 {} 进入游戏房间", userInfo.getUserName());
}
在将玩家加入游戏房间时,我们需要考虑到线程安全问题:
若两个玩家同时与 WebSocket 建立连接,那么,在将玩家加入游戏房间时, 就很可能都判定 玩家1 为空,从而将其都设置为玩家1
因此,我们需要对 玩家加入游戏房间操作 进行加锁(可以直接对 Room 对象进行加锁)
添加错误码:
public interface HandlerErrorCodeConstants {
ErrorCode GET_GAME_ROOM_ERROR = new ErrorCode(300, "用户尚未匹配到游戏房间");
ErrorCode GAME_ROOM_IS_FULL = new ErrorCode(301, "游戏房间已满");
}
实现通知玩家就绪 notifyGameReady 方法:
/**
* 通知玩家游戏已准备就绪
* @param room 游戏房间
* @param thisUser 玩家
* @param thatUser 对手
*/
private void notifyGameReady(Room room, UserInfo thisUser, UserInfo thatUser) {
try {
// 构造游戏就绪响应
GameReadyResult gameReadyResult = new GameReadyResult();
gameReadyResult.setRoomId(room.getRoomId());
gameReadyResult.setThisUserId(thisUser.getUserId());
gameReadyResult.setThatUserId(thatUser.getUserId());
gameReadyResult.setWhiteUserId(room.getWhiteUserId());
// 发送游戏就绪响应
WebSocketSession session = onlineUserManager.getFromRoom(thisUser.getUserId());
session.sendMessage(new TextMessage(JacksonUtil.writeValueAsString(
CommonResult.success(gameReadyResult))));
} catch (Exception e) {
log.warn("notifyGameReady 通知玩家游戏准备就绪异常 e: ", e);
}
}
接下来,我们先实现 连接关闭 和 异常关闭 的相关逻辑,最后再实现对落子请求的处理
玩家下线处理
我们实现 logoutFromRoom 方法,来对玩家下线进行处理:
1. 获取用户信息,判断用户是否登录
2. 获取游戏房间中存储的玩家信息,针对多开情况进行处理
3. 当玩家掉线时,此时对手直接胜利
4. 通知玩家对手胜利,并修改对应玩家分数
/**
* 退出游戏房间
* @param session
*/
private void logoutFromRoom(WebSocketSession session) {
// 获取用户信息
UserInfo userInfo = (UserInfo) session.getAttributes().get(USER_INFO);
// 判断用户是否登录
if (null == userInfo) {
return;
}
// 获取游戏房间中存储的玩家信息
WebSocketSession onlineSession = onlineUserManager.getFromRoom(userInfo.getUserId());
// 多开情况,直接退出
if (session != onlineSession) {
return;
}
// 玩家下线
onlineUserManager.exitGameRoom(userInfo.getUserId());
// 当前玩家下线,对手胜利
notifyThatUserWin(userInfo);
log.info("玩家 {} 从游戏房间退出", userInfo.getUserName());
}
我们继续实现通知玩家对手胜利 notifyThatUserWin 方法:
要通知对手,我们首先需要查询到对手的相关 session 信息:
1. 查询玩家所在游戏房间
2. 判断游戏房间是否已经被销毁(表明此时游戏已经结束,或对手已经下线)
3. 判断对手是否下线
4. 为对手玩家返回响应
5. 销毁房间
6. 更新玩家分数
/**
* 通知对手胜利
* @param userInfo
*/
private void notifyThatUserWin(UserInfo userInfo) {
try {
// 查找玩家下线所在房间
Room room = roomManager.getRoomByUserId(userInfo.getUserId());
// 判断游戏房间是否已经销毁
if (null == room) {
return;
}
// 判断对手是否已经下线
UserInfo thatUser = (room.getUser1() == userInfo) ?
room.getUser2() : room.getUser1();
if (null == thatUser) {
return;
}
// 对手会话是否存在
WebSocketSession thatUserSession = onlineUserManager.getFromRoom(thatUser.getUserId());
if (null == thatUserSession) {
return;
}
// 返回响应
PutChessResult result = new PutChessResult();
result.setUserId(thatUser.getUserId());
result.setRow(-1);
result.setCol(-1);
result.setWinner(thatUser.getUserId());
thatUserSession.sendMessage(new TextMessage(JacksonUtil.writeValueAsString(
CommonResult.success(result))));
log.info("房间{} 玩家 {} 胜利!", room.getRoomId(), thatUser.getUserId());
// 销毁游戏房间
roomManager.remove(
room.getRoomId(), room.getUser1().getUserId(), room.getUser2().getUserId());
// TODO 更新玩家分数
} catch (IOException e) {
log.warn("通知玩家下线异常 e: ", e);
} catch (Exception e) {
log.warn("玩家下线异常 e: ", e);
}
}
关于玩家分数的更新,我们后续再进行实现
异常情况处理:
@Override
public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
// 打印错误信息
log.error("游戏过程中出现异常: ", exception);
logoutFromRoom(session);
}
连接关闭:
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
log.info("游戏连接断开, code: {}, reason: {}",
status.getCode(), status.getReason());
// 玩家下线
logoutFromRoom(session);
}
接下来,我们就可以处理落子请求了
处理落子请求
handleTextMessage
handleTextMessage 主要用于接收落子请求并对其进行处理:
1. 判断用户是否登录
2. 获取游戏房间,判断游戏房间是否存在
3. 将接收到的数据转化为 PutChessParam 对象
4. 根据请求进行落子
关于落子相关逻辑,我们定义 GameService,并创建 putChess 方法来进行处理
@Autowired
private GameService gameService;
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
UserInfo userInfo = (UserInfo) session.getAttributes().get(USER_INFO);
// 用户是否登录
if (null == userInfo) {
session.sendMessage(new TextMessage(JacksonUtil.writeValueAsString(
CommonResult.noLogin())));
return;
}
// 获取游戏房间
Room room = roomManager.getRoomByUserId(userInfo.getUserId());
if (null == room) {
session.sendMessage(new TextMessage(JacksonUtil.writeValueAsString(
CommonResult.fail(HandlerErrorCodeConstants.GET_GAME_ROOM_ERROR))));
}
// 获取用户端发送的数据
String payload = message.getPayload();
// 将 JSON 字符串转化为 java 对象
PutChessParam putChessParam = JacksonUtil.readValue(payload, PutChessParam.class);
// 根据请求落子
gameService.putChess(putChessParam, userInfo);
}
GameService 接口:
@Service
public interface GameService {
/**
* 处理落子请求
* @param param
* @param userInfo
*/
void putChess(PutChessParam param, UserInfo userInfo);
}
在 GameServiceImpl 中实现具体逻辑:
@Service
@Slf4j
public class GameServiceImpl implements GameService {
@Autowired
private RoomManager roomManager;
@Autowired
private OnlineUserManager onlineUserManager;
/**
* 处理落子请求
* @param param
* @param userInfo
*/
@Override
public void putChess(PutChessParam param, UserInfo userInfo) {
}
putChess
putChess 需要实现的业务逻辑:
1. 获取玩家所在游戏房间对象
2. 判断当前玩家是 玩家1 还是 玩家2
3. 从请求参数中获取落子位置
4. 判断当前位置是否有子
5. 若无,则落子
6. 进行胜负判定
7. 通知双方玩家落子结果
8. 若胜负已分,则更新相关分数,并销毁游戏房间
落子
我们先实现前四个部分相关逻辑:
@Override
public void putChess(PutChessParam param, UserInfo userInfo) {
try {
// 获取游戏房间
Room room = roomManager.getRoomByUserId(userInfo.getUserId());
// 判断当前玩家是 玩家1 还是 玩家2
int chess = userInfo.getUserId() == room.getUser1().getUserId()
? 1 : 2;
// 获取落子位置
int row = param.getRow();
int col = param.getCol();
// 判断当前位置是否已经有子
if (!room.isEmpty(row, col)) {
log.info("已禁止 玩家 {} 在 {} 行 {} 列重复落子!",
userInfo.getUserName(), row, col);
return;
}
// 落子
room.putChess(row, col, chess);
// 打印棋盘信息,方便观察落子情况
room.printBoard();
}
在对应位置落子需要对 room 对象中 board 的进行修改,因此,我们在 Room 中提供对应方法:
/**
* 判断当前位置是否已经有子
* @param row
* @param col
* @return
*/
public boolean isEmpty(int row, int col) {
return board[row][col] == 0;
}
/**
* 在当前位置落子
* @param row
* @param col
* @param chess
*/
public void putChess(int row, int col, int chess) {
board[row][col] = chess;
piecesNumber++;
}
为了方便观察落子请求,我们可以对棋盘进行打印,观察落子结果:
/**
* 打印棋盘
*/
public void printBoard() {
System.out.println("------------------棋盘-----------------------");
for (int i = 0; i < MAX_ROW; i++) {
for (int j = 0; j < MAX_COL; j++) {
System.out.print(board[i][j] + " ");
}
System.out.println();
}
System.out.println("------------------棋盘-----------------------");
}
胜负判定
落子成功后,我们进行胜负判定:
// 进行胜负判断
Long winner = room.checkWinner(row, col, chess);
若玩家1胜利,则返回玩家1 id;
若玩家2 胜利,则返回玩家2 id;
若为平局,则返回 -1;
若未决出胜负,则返回 0
我们在每次落下棋子之后,进行胜负判定,也就是判定棋盘上形成连续的五个同色棋子(可以是横排、竖排 或 斜排)
在进行判定时,我们并不需要遍历整个棋盘,只需要考虑新落棋子周围位置
若还未落子就已经出现了 连续五个同色棋子 的情况,则说明前面的判定出现了异常
因此,我们只需要以 (row, col) 为中心,判定其是否与周围棋子形成 五个同色棋子
我们首先来考虑 横排:
此时,需要考虑新落的子(x)能否与当前行周围棋子形成五个同色棋子:
新落棋子为 x
判定 x 与 左边 4 个棋子 是否为同色棋子
同理:
判定 x 与 左边 3 个棋子,右边 1 个棋子是否为同色棋子
判定 x 与 左边 2 个棋子,右边 2 个棋子是否为同色棋子
判定 x 与 左边 1 个棋子,右边 3 个棋子是否为同色棋子
判定 x 与 右边 4 个棋子是否为同色棋子
我们通过 for 循环来进行判定:
// 行
for (int c = col - 4; c <= col; c++) {
try {
if(board[row][c] == chess
&& board[row][c + 1] == chess
&& board[row][c + 2] == chess
&& board[row][c + 3] == chess
&& board[row][c + 4] == chess) {
return chess == user1.getUserId() ?
user1.getUserId() : user2.getUserId();
}
} catch (ArrayIndexOutOfBoundsException e) {
continue;
}
}
在判定过程中可能会出现数组越界的异常情况,我们捕获 数组越界异常,并继续判定
我们继续看竖排:
需要判定:
x 与 上方 4 个棋子 是否为同色棋子
x 与 上方 3 个棋子,下方 1 个棋子是否为同色棋子
x 与 上方 2 个棋子,下方 2 个棋子是否为同色棋子
x 与 上方 1 个棋子,下方 3 个棋子是否为同色棋子
x 与 下方 4 个棋子是否为同色棋子
// 列
for (int r = row - 4; r <= row; r++) {
try {
if(board[r][col] == chess
&& board[r + 1][col] == chess
&& board[r + 2][col] == chess
&& board[r + 3][col] == chess
&& board[r + 4][col] == chess) {
return chess == 1 ?
user1.getUserId() : user2.getUserId();
}
} catch (ArrayIndexOutOfBoundsException e) {
// 若出现数据下标越界的情况,则继续判断
continue;
}
}
再来看斜方:
左对角线 和 右对角线 都需要进行判定:
// 左对角线
for (int r = row - 4, c = col - 4; r <= row && c <= col; r++, c++) {
try {
if(board[r][c] == chess
&& board[r + 1][c + 1] == chess
&& board[r + 2][c + 2] == chess
&& board[r + 3][c + 3] == chess
&& board[r + 4][c + 4] == chess) {
return chess == 1 ? user1.getUserId() : user2.getUserId();
}
} catch (ArrayIndexOutOfBoundsException e) {
continue;
}
}
// 右对角线
for (int r = row - 4, c = col + 4; r <= row && c >= col; r++, c--) {
try {
if(board[r][c] == chess
&& board[r + 1][c - 1] == chess
&& board[r + 2][c - 2] == chess
&& board[r + 3][c - 3] == chess
&& board[r + 4][c - 4] == chess) {
return chess == 1 ? user1.getUserId() : user2.getUserId();
}
} catch (ArrayIndexOutOfBoundsException e) {
continue;
}
}
此外,若未决出胜负,但此时棋盘已满,则为平局:
if(piecesNumber >= MAX_ROW * MAX_COL) {
return -1L;
}
判断落子数量是否已达到最大数量
若以上情况都不满足,则表明当前未决出胜负,还需继续落子,因此返回 0
胜负判断完整代码:
/**
* 判断胜负
* @param row
* @param col
* @param chess
* @return 0:尚未分出胜负 -1:平局
*/
public Long checkWinner(int row, int col, int chess) {
// 行
for (int c = col - 4; c <= col; c++) {
try {
if(board[row][c] == chess
&& board[row][c + 1] == chess
&& board[row][c + 2] == chess
&& board[row][c + 3] == chess
&& board[row][c + 4] == chess) {
return chess == user1.getUserId() ?
user1.getUserId() : user2.getUserId();
}
} catch (ArrayIndexOutOfBoundsException e) {
continue;
}
}
// 列
for (int r = row - 4; r <= row; r++) {
try {
if(board[r][col] == chess
&& board[r + 1][col] == chess
&& board[r + 2][col] == chess
&& board[r + 3][col] == chess
&& board[r + 4][col] == chess) {
return chess == 1 ?
user1.getUserId() : user2.getUserId();
}
} catch (ArrayIndexOutOfBoundsException e) {
// 若出现数据下标越界的情况,则继续判断
continue;
}
}
// 左对角线
for (int r = row - 4, c = col - 4; r <= row && c <= col; r++, c++) {
try {
if(board[r][c] == chess
&& board[r + 1][c + 1] == chess
&& board[r + 2][c + 2] == chess
&& board[r + 3][c + 3] == chess
&& board[r + 4][c + 4] == chess) {
return chess == 1 ? user1.getUserId() : user2.getUserId();
}
} catch (ArrayIndexOutOfBoundsException e) {
continue;
}
}
// 右对角线
for (int r = row - 4, c = col + 4; r <= row && c >= col; r++, c--) {
try {
if(board[r][c] == chess
&& board[r + 1][c - 1] == chess
&& board[r + 2][c - 2] == chess
&& board[r + 3][c - 3] == chess
&& board[r + 4][c - 4] == chess) {
return chess == 1 ? user1.getUserId() : user2.getUserId();
}
} catch (ArrayIndexOutOfBoundsException e) {
continue;
}
}
// 平局
if(piecesNumber == MAX_ROW * MAX_COL) {
return -1L;
}
piecesNumber++;
return 0L;
}
构造落子响应并返回
接着,需要构造落子响应并返回:
1. 获取 session1 和 session2
2. 判断是否存在玩家掉线的情况:若双方玩家均掉线,则结果设置为平局;若玩家1掉线,则玩家2胜利;若玩家2掉线,则玩家1胜利
3. 构造响应并返回
// 通知玩家
WebSocketSession session1 = onlineUserManager.getFromRoom(room.getUser1().getUserId());
WebSocketSession session2 = onlineUserManager.getFromRoom(room.getUser2().getUserId());
if (null == session1 && null == session2) {
// 玩家均掉线
winner = -1L;
log.info("游戏房间 {} 玩家1 和 玩家2 均掉线", room.getRoomId());
} else if (null == session1) {
winner = room.getUser2().getUserId();
log.info("游戏房间 {} 玩家1 掉线", room.getRoomId());
} else if (null == session2){
winner = room.getUser1().getUserId();
log.info("游戏房间 {} 玩家2 掉线", room.getRoomId());
}
// 构造响应并返回
PutChessResult result = new PutChessResult(userInfo.getUserId(), row, col, winner);
if (null != session1) {
session1.sendMessage(new TextMessage(
JacksonUtil.writeValueAsString(CommonResult.success(result))));
}
if (null != session2) {
session2.sendMessage(new TextMessage(
JacksonUtil.writeValueAsString(CommonResult.success(result))));
}
判断胜负是否已分,若胜负已分,则更新玩家分数,并销毁游戏房间
// 若胜负已分,则更新玩家分数,并销毁房间
if (winner != 0) {
if (winner == -1) {
log.info("房间 {} 游戏结束, 结果为平局");
} else {
log.info("房间 {} 游戏结束, 获胜方id为 {}", room.getRoomId(), winner);
// TODO 更新分数
}
// 销毁游戏房间
roomManager.remove(room.getRoomId(),
room.getUser1().getUserId(), room.getUser2().getUserId());
}
更新玩家分数
创建 updateScore 接口,更新玩家分数:
@Service
public interface GameService {
/**
* 处理落子请求
* @param param
* @param userInfo
*/
void putChess(PutChessParam param, UserInfo userInfo);
/**
* 更新玩家分数
* @param winner
* @param loser
*/
void updateScore(Long winner, Long loser);
}
调用 UserMapper 相关方法,更新分数:
@Autowired
private UserMapper userMapper;
@Override
public void updateScore(Long winner, Long loser) {
userMapper.updateWinner(winner);
userMapper.updateLoser(loser);
}
UserMapper:
@Update("update user set total_count = total_count + 1, win_count = win_count + 1, score = score + 30 " +
" where id = #{id}")
int updateWinner(@Param("id") Long winner);
@Update("update user set total_count = total_count + 1, score = score - 30 " +
" where id = #{id}")
int updateLoser(@Param("id") Long loser);
更新分数:
putchess 完整代码:
/**
* 处理落子请求
* @param param
* @param userInfo
*/
@Override
public void putChess(PutChessParam param, UserInfo userInfo) {
try {
// 获取游戏房间
Room room = roomManager.getRoomByUserId(userInfo.getUserId());
// 判断当前玩家是 玩家1 还是 玩家2
int chess = userInfo.getUserId() == room.getUser1().getUserId()
? 1 : 2;
// 获取落子位置
int row = param.getRow();
int col = param.getCol();
// 判断当前位置是否已经有子
if (!room.isEmpty(row, col)) {
log.info("已禁止 玩家 {} 在 {} 行 {} 列重复落子!",
userInfo.getUserName(), row, col);
return;
}
// 落子
room.putChess(row, col, chess);
// 打印棋盘信息,方便观察落子情况
room.printBoard();
// 进行胜负判断
Long winner = room.checkWinner(row, col, chess);
// 通知玩家
WebSocketSession session1 = onlineUserManager.getFromRoom(room.getUser1().getUserId());
WebSocketSession session2 = onlineUserManager.getFromRoom(room.getUser2().getUserId());
if (null == session1 && null == session2) {
// 玩家均掉线
winner = -1L;
log.info("游戏房间 {} 玩家1 和 玩家2 均掉线", room.getRoomId());
} else if (null == session1) {
winner = room.getUser2().getUserId();
log.info("游戏房间 {} 玩家1 掉线", room.getRoomId());
} else if (null == session2){
winner = room.getUser1().getUserId();
log.info("游戏房间 {} 玩家2 掉线", room.getRoomId());
}
// 构造响应并返回
PutChessResult result = new PutChessResult(userInfo.getUserId(), row, col, winner);
if (null != session1) {
session1.sendMessage(new TextMessage(
JacksonUtil.writeValueAsString(CommonResult.success(result))));
}
if (null != session2) {
session2.sendMessage(new TextMessage(
JacksonUtil.writeValueAsString(CommonResult.success(result))));
}
// 若胜负已分,则更新玩家分数,并销毁房间
if (winner != 0) {
if (winner == -1) {
log.info("房间 {} 游戏结束, 结果为平局");
} else {
log.info("房间 {} 游戏结束, 获胜方id为 {}", room.getRoomId(), winner);
// 更新分数
Long loser = (room.getUser1().getUserId() == winner) ?
room.getUser2().getUserId() : room.getUser1().getUserId();
updateScore(winner , loser);
}
// 销毁游戏房间
roomManager.remove(room.getRoomId(),
room.getUser1().getUserId(), room.getUser2().getUserId());
}
} catch (IOException e) {
log.warn("发送落子响应异常 e: ", e);
} catch (Exception e) {
log.warn("落子异常 e: ", e);
}
}
此外,在玩家掉线时,我们还遗留了一个部分——更新玩家分数,我们将其补充完整:
至此,对战模块的后端代码就基本实现完毕了,我们修改客户端代码并进行测试
修改客户端代码
修改 code != 200 的情况:
根据我们约定的错误码:
当 code = 401 或 code = 402 时,我们让其跳转到 登录页面
当 code = 300 或 code = 301 时,我们让其跳转到 游戏大厅页面
if (resp.code != 200) {
if (resp.code == 300 || resp.code == 301) {
location.replace("/game_hall.html");
} else if (resp.code == 402 || resp.code == 401){
location.replace("/login.html")
} else {
alert("异常情况:" + resp.errorMessage);
}
return;
}
游戏就绪响应处理 和 落子响应处理都需进行修改
此外,当有玩家退出游戏房间时,我们返回的 row 和 col 为 -1
因此,在标记棋子时,我们需要先进行判断:
// 标记此处有棋子
if (gameRes.row >= 0 && gameRes.col >= 0) {
chessBoard[gameRes.row][gameRes.col] = 1;
} else {
// 提示对手掉线,并返回游戏大厅
alert("对方已掉线,恭喜你!你赢了!");
location.replace("/game_hall.html");
}
接下来,我们就来对对战模块进行测试
对战模块测试
运行程序,登录两个账号,让其进行匹配,匹配成功后,跳转到游戏房间:
当 玩家1 进入游戏大厅时:
玩家2 进入游戏大厅:
玩家1 落子:
玩家2 落子:
游戏胜利:
观察后端落子情况:
与客户端落子情况相同,符合预期
测试玩家掉线:
但此时还存在一个问题,那就是 用户信息显示
上述我们测试了两种情况——玩家胜利 和 玩家掉线
此时,已经进行了两场游戏,但我们观察游戏大厅显示的用户信息:
此时的用户信息并未进行更新,我们观察后端代码:
由于我们是直接从 session 中获取的信息,因此,当我们更新数据库中的对应信息时,session 存储的信息并未改变,因此,我们需要对其进行修改,从数据库中获取用户信息:
根据从 session 中获取的用户 id(用户 id 始终不变),从而在数据库中查询用户信息
添加 selectByUserId 方法:
@Select("select * from user where id = #{id}")
UserDO selectByUserId(@Param("id") Long id);