网页五子棋——对战后端

news/2025/2/22 16:54:33

目录

GameHandler

创建请求响应对象

处理连接成功

玩家下线处理

处理落子请求

handleTextMessage

putChess

落子 

胜负判定 

构造落子响应并返回

更新玩家分数

修改客户端代码

对战模块测试


在本篇文章中,我们继续实现对战模块的后端逻辑

GameHandler

我们创建 GameHandler 处理 WebSocket 请求

java">@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 进行注册

java">@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

java">@Data
public class PutChessParam implements Serializable {
    /**
     * 落子玩家 id
     */
    private Long userId;
    /**
     * 落子位置——行
     */
    private Integer row;
    /**
     * 落子位置——列
     */
    private Integer col;
}

落子响应 MatchResult

java">@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

java">@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. 若有更多玩家尝试加入房间,提示游戏房间已满

java">    @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 对象进行加锁)

 添加错误码:

java">public interface HandlerErrorCodeConstants {
    ErrorCode GET_GAME_ROOM_ERROR = new ErrorCode(300, "用户尚未匹配到游戏房间");
    ErrorCode GAME_ROOM_IS_FULL = new ErrorCode(301, "游戏房间已满");
}

实现通知玩家就绪 notifyGameReady 方法:

java">    /**
     * 通知玩家游戏已准备就绪
     * @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. 通知玩家对手胜利,并修改对应玩家分数

java">    /**
     * 退出游戏房间
     * @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. 更新玩家分数

java">    /**
     * 通知对手胜利
     * @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);
        }
    }

关于玩家分数的更新,我们后续再进行实现

异常情况处理:

java">    @Override
    public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
        // 打印错误信息
        log.error("游戏过程中出现异常: ", exception);
        logoutFromRoom(session);
    }

连接关闭:

java">    @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 方法来进行处理

java">    @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 接口:

java">@Service
public interface GameService {
    /**
     * 处理落子请求
     * @param param
     * @param userInfo
     */
    void putChess(PutChessParam param, UserInfo userInfo);
}

在 GameServiceImpl 中实现具体逻辑:

java">@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. 若胜负已分,则更新相关分数,并销毁游戏房间

落子 

 我们先实现前四个部分相关逻辑:

java">    @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 中提供对应方法:

java">    /**
     * 判断当前位置是否已经有子
     * @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++;
    }

为了方便观察落子请求,我们可以对棋盘进行打印,观察落子结果:

java">    /**
     * 打印棋盘
     */
    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("------------------棋盘-----------------------");
    }

 

胜负判定 

落子成功后,我们进行胜负判定:

java">            // 进行胜负判断
            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 循环来进行判定:

java">        // 行
        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 个棋子是否为同色棋子

java">        // 列
        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;
            }
        }

再来看斜方:

 

 左对角线 和 右对角线 都需要进行判定:

java">        // 左对角线
        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;
            }
        }

此外,若未决出胜负,但此时棋盘已满,则为平局:

java">        if(piecesNumber >= MAX_ROW * MAX_COL) {
            return -1L;
        }

判断落子数量是否已达到最大数量

若以上情况都不满足,则表明当前未决出胜负,还需继续落子,因此返回 0

胜负判断完整代码:

java">    /**
     * 判断胜负
     * @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. 构造响应并返回

java">            // 通知玩家
            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))));
            }

判断胜负是否已分,若胜负已分,则更新玩家分数,并销毁游戏房间

java">            // 若胜负已分,则更新玩家分数,并销毁房间
            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 接口,更新玩家分数:

java">@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 相关方法,更新分数: 

java">    @Autowired
    private UserMapper userMapper;


    @Override
    public void updateScore(Long winner, Long loser) {
        userMapper.updateWinner(winner);
        userMapper.updateLoser(loser);
    }

UserMapper

java">    @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 完整代码:

java">    /**
     * 处理落子请求
     * @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 时,我们让其跳转到 游戏大厅页面

javascript">    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

因此,在标记棋子时,我们需要先进行判断:

javascript">        // 标记此处有棋子
        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 方法:

java">    @Select("select * from user where id = #{id}")
    UserDO selectByUserId(@Param("id") Long id);

http://www.niftyadmin.cn/n/5862545.html

相关文章

计算机视觉基础|卷积神经网络:从数学原理到可视化实战

一、引言 在当今人工智能飞速发展的时代&#xff0c;卷积神经网络&#xff08;Convolutional Neural Network&#xff0c;简称 CNN&#xff09;无疑在诸多领域发挥着关键作用&#xff0c;尤其在计算机视觉领域&#xff0c;如人脸识别、图像分类、目标检测等任务中&#xff0c;…

【SpringBoot教程】SpringBoot整合Caffeine本地缓存及Spring Cache注解的使用

&#x1f64b;大家好&#xff01;我是毛毛张! &#x1f308;个人首页&#xff1a; 神马都会亿点点的毛毛张 毛毛张今天要介绍的是本地缓存之王&#xff01;Caffeine&#xff01;SpringBoot整合Caffeine本地缓存及Spring Cache注解的使用 文章目录 1.Caffeine本地缓存1.1 本地…

Dify怎么创建数据交易的智能体

Dify怎么创建数据交易的智能体 Dify是一个低代码AI应用开发平台,能帮助你快速创建智能体。以下是使用Dify创建一个数据定价智能体的大致步骤和示例: 1. 注册与登录 首先,访问Dify官网(https://dify.ai/ ),完成注册并登录到你的账号。 2. 创建新项目 登录后,点击创建…

Jupyter里面的manim编程学习

1.Jupyterlab的使用 因为我之前一直都是使用的vscode进行manim编程的&#xff0c;但是今天看的这个教程使用的是Jupyter&#xff0c;我也很是好奇这个manim在Jupyter这样的交互式下面会生成怎么样的效果&#xff0c;所以今天尝试了jupyter&#xff0c;并且对于两个进行比较和说…

鸿蒙NEXT应用App测试-通用测试

注意&#xff1a;大家记得学完通用测试记得再学鸿蒙专项测试 https://blog.csdn.net/weixin_51166786/article/details/145768653 注意&#xff1a;博主有个鸿蒙专栏&#xff0c;里面从上到下有关于鸿蒙next的教学文档&#xff0c;大家感兴趣可以学习下 如果大家觉得博主文章…

SWAT| 水文 | SWAT模型(四):气象数据库制备(附Python代码)

Tips&#xff1a; 本期向大家分享SWAT模型的气象数据库的制备方法。最终要制作5项气象数据&#xff0c;分别是日降水量、最高/最低气温、太阳辐射、相对湿度、平均风速&#xff0c;每项气象数据要有①站点文件和②站点气象数据文件&#xff0c;都用txt文本文件来储存。…

adb shell setprop获取日志

adb shell setprop 用于设置系统属性&#xff0c;而获取日志通常使用 logcat。以下是结合 setprop 和 logcat 获取日志的步骤&#xff1a; 1. 设置日志级别 通过 setprop 调整日志级别&#xff0c;例如&#xff1a; adb shell setprop log.tag.MyApp DEBUG这将把 MyApp 的日…

PyTorch gather 方法详解:作用、应用场景与示例解析(中英双语)

PyTorch gather 方法详解&#xff1a;作用、应用场景与示例解析 在深度学习和自然语言处理&#xff08;NLP&#xff09;任务中&#xff0c;我们经常需要从高维张量中提取特定索引的数据。 PyTorch 提供的 torch.gather 方法可以高效地从张量的指定维度收集数据&#xff0c;广泛…