自学内容网 自学内容网

网页版五子棋——对战模块(服务器端开发①)

前一篇文章:网页版五子棋——对战模块(客户端开发)-CSDN博客

项目源代码:Java: 利用Java解题与实现部分功能及小项目的代码集合 - Gitee.com

目录

·前言

一、修改 OnlineUserManager 类

二、创建响应/请求对象

1.创建连接响应对象

2.创建落子请求对象

3.创建落子响应对象

三、修改 Room 类

1.增加实例对象与属性

2.增加打印棋盘方法

3.增加胜负判定方法

4.增加落子触发方法

·结尾


·前言

        在前一篇文章中介绍了对战模块中前后端交互接口的设计及对战模块客户端代码的开发,本篇文章将继续对五子棋项目中对战模块的代码进行编写,下面要介绍的内容就是服务器端代码的编写了,这里我们将要进行游戏胜负判定的实现,处理落子前后的逻辑,本篇文章中将要新增的代码文件如下图圈起来的文件所示:

        下面就开始本篇文章的内容介绍。 

一、修改 OnlineUserManager 类

        OnlineUserManager 类是用来管理当前用户在线状态的类,关于这个类的详细介绍在前面文章中已经介绍过了,文章链接:网页版五子棋——匹配模块(服务器端开发)-CSDN博客 ,在本篇文章就不再进行介绍,下面我来介绍一下我们为什么要修改这个类,及如何进行修改。

        前面在 OnlineUserManager 中已经维护了一个哈希表来管理玩家的在线状态了,如下图所示:

        但是这个状态仅仅是局限在 game_hall.html 这个页面,当我们玩家匹配成功跳转到 game_room.html 页面后,之前在 game_hall.html 建立的 WebSocket 连接就会断开,并且服务器也会在 OnlineUserManager 中删除对应的元素,如下图所示:

        此时,就不能再通过之前的哈希表来获取玩家的状态信息了,因此,玩家从游戏大厅页面离开之后,进入游戏房间页面的时候,就需要重新管理玩家的在线状态了,所以在 OnlineUserManager 中我们要新增一个哈希表来管理玩家在游戏房间的在线状态,并且实现几个对应的操作方法,修改后的 OnlineUserManager 类的全部代码及详细介绍如下所示: 

// 表现用户的在线状态
@Component
public class OnlineUserManager {
    // 这个哈希表是用来表示当前用户在游戏大厅的在线状态.
    private ConcurrentHashMap<Integer, WebSocketSession> gameHall = new ConcurrentHashMap<>();
    // 这个哈希表用来表示当前用户在游戏房间的在线状态.
    private ConcurrentHashMap<Integer, WebSocketSession> gameRoom = new ConcurrentHashMap<>();
    // 进入游戏大厅,调用这个方法,把玩家的 WebSocket 连接信息保存到哈希表中
    public void enterGameHall(int userId, WebSocketSession webSocketSession) {
        gameHall.put(userId, webSocketSession);
    }

    // 离开游戏大厅,调用这个方法,把玩家的 WebSocket 连接信息从哈希表中删除
    public void exitGameHall(int userId) {
        gameHall.remove(userId);
    }

    // 调用这个方法,把玩家的 WebSocket 连接信息从哈希表中获取到
    public WebSocketSession getFromGameHall (int userId) {
        return gameHall.get(userId);
    }

    // 进入游戏房间,调用这个方法,把玩家的 WebSocket 连接信息保存到哈希表中
    public void enterGameRoom(int userId, WebSocketSession webSocketSession) {
        gameRoom.put(userId, webSocketSession);
    }

    // 离开游戏房间,调用这个方法,把玩家的 WebSocket 连接信息从哈希表中删除
    public void exitGameRoom(int userId) {
        gameRoom.remove(userId);
    }

    // 调用这个方法,把玩家的 WebSocket 连接信息从哈希表中获取到
    public WebSocketSession getFromGameRoom(int userId) {
        return gameRoom.get(userId);
    }
}

二、创建响应/请求对象

1.创建连接响应对象

        创建 GameReadyResponse 类,这是用来返回玩家进入游戏房间成功,准备就绪的响应类,其中的属性与前面文章约定对战模块中前后端交互接口的连接响应一致,对战响应的接口设计如下图所示:

        关于 GameReadyResponse 类的具体代码如下所示:

// 客户端连接到游戏房间后, 服务器返回的响应.
@Data
public class GameReadyResponse {
    private String message;
    private boolean ok;
    private String reason;
    private String roomId;
    private int thisUserId;
    private int thatUserId;
    private int whiteUser;
}

         @Data 注解会自动生成 get 与 set 方法。

2.创建落子请求对象

        创建 GameRequest 类,这是用来获取玩家落子请求的类,其中的属性与前面文章约定对战模块中前后端交互接口的落子请求一致,落子请求的接口设计如下图所示:

        关于 GameRequest 类的具体代码如下所示:

// 这个类表示一个落子请求
@Data
public class GameRequest {
    private String message;
    private int userId;
    private int row;
    private int col;
}

3.创建落子响应对象

        创建 GameResponse 类,这是用来返回玩家落子响应的类,其中的属性与前面文章约定对战模块中前后端交互接口的落子响应一致,落子响应的接口设计如下图所示:

        关于 GameResponse 类的具体代码如下所示:

// 这个类表示一个落子响应
@Data
public class GameResponse {
    private String message;
    private int userId;
    private int row;
    private int col;
    private int winner;
}

三、修改 Room 类

1.增加实例对象与属性

        对战模块中在 Room 类里我们要做的事情有很多,这就需要我们对之前的 Room 类增加一些属性,来供后面的方法使用,新增属性如下:

  • whiteUser:用来表示先手方玩家的 id;
  • 常量 MAX_ROW 表示棋盘最大的行数;
  • 常量 MAX_COL 表示棋盘最大的列数;
  • 二维数组 board 用来表示棋盘;

        新增完这些属性,我们还要给 Room 类中增加一些实例对象,在下面的方法中会要使用到这些实例对象中的相应方法,新增的实例对象如下:

  • 准备一个 ObjectMapper 对象,用来处理 JSON 数据;
  • 引入 OnlineUserManager 对象,用来获取玩家的在线状态,还有获取玩家的连接信息用于给玩家返回响应;
  • 引入 RoomManager 对象,用于在游戏结束时销毁房间;
  • 引入 UserMapper 对象,用于更新对局结束后玩家的信息。

         由于我们的 Room 类并没有通过 Spring 来管理,因此在 Room 类中无法通过 @Autowired 来自动注入,而是需要我们手动通过 SpringBoot 的启动类来获取里面的对象,操作的流程如下:

  1. 修改启动类;
  2. 在 Room 类的构造方法中,手动获取到 Bean。

        修改后的启动类具体代码及介绍如下所示:

@SpringBootApplication
public class SpringGobangApplication {
    // 创建一个 ConfigurableApplicationContext 对象
    public static ConfigurableApplicationContext context;

    public static void main(String[] args) {
        // 使用 context 接收 run 的返回值
        context = SpringApplication.run(SpringGobangApplication.class, args);
    }

}

        下面我们就可以修改 Room 类的构造方法来获取到我们要新增实例的 Bean 了,增加属性与修改构造方法后 Room 类的具体代码及详细介绍如下所示:

// 这个类表示一个游戏房间
@Data
public class Room {
    // 使用字符串类型来表示, 方便生成唯一值.
    private String roomId;
    // 玩家一
    private User user1;
    // 玩家二
    private User user2;
    // 先手方的玩家 id
    private int whiteUser;
    // 行数
    private static final int MAX_ROW = 15;
    // 列数
    private static final int MAX_COL = 15;

    // 这个二维数组用来表示棋盘
    // 约定:
    // 1) 使用 0 表示当前位置未落子, 初始化好的 int 二维数组, 就相当于是全 0
    // 2) 使用 1 表示 user1 的落子位置
    // 3) 使用 2 表示 user2 的落子位置
    private int[][] board = new int[MAX_ROW][MAX_COL];

    // 创建 ObjectMapper 用来转换 JSON
    private ObjectMapper objectMapper = new ObjectMapper();
    // 引入 OnlineUserManager 用于获取玩家的 WebSocketSession
    private OnlineUserManager onlineUserManager;
    // 引入 RoomManager 用于房间销毁
    private RoomManager roomManager;
    // 引入 UserMapper 用于修改对局结束后的玩家信息
    private UserMapper userMapper;

    public Room() {
        // 构造 Room 的时候生成一个唯一的字符串表示房间 id
        // 使用 UUID 来作为房间 id
        roomId = UUID.randomUUID().toString();
        // 通过入口类中记录的 context 来手动获取到前面的 RoomManager, OnlineUserManager 还有 UserMapper 对象
        onlineUserManager = SpringGobangApplication.context.getBean(OnlineUserManager.class);
        roomManager = SpringGobangApplication.context.getBean(RoomManager.class);
        userMapper = SpringGobangApplication.context.getBean(UserMapper.class);
    }

}

2.增加打印棋盘方法

        落子完毕后,为了方便对这部分代码进行调试,可以打印出棋盘当前的信息,我们在 Room 类中新增一个打印棋盘的方法 —— printBoard(),关于这个方法的具体代码及详细介绍如下所示:

    // 通过这个方法来打印当前棋盘信息, 方便查看当前状态
    private void printBoard() {
        // 打印出棋盘
        System.out.println("[打印棋盘信息] 房间号是: " + roomId);
        System.out.println("===============================================");
        for (int r = 0; r < MAX_ROW; r++) {
            for (int c = 0; c < MAX_COL; c++) {
                // 针对一行之内的若干列, 不打印换行
                System.out.print(board[r][c] + " ");
            }
            // 每次遍历完一行之后, 再打印换行.
            System.out.println();
        }
        System.out.println("===============================================");
    }

3.增加胜负判定方法

        落子完毕后,我们要对这次落子进行胜负的判定,我们在 Room 类中新增一个判定胜负的方法 —— checkWinner(),在这个方法中,我们要做以下几件事情:

  • 如果游戏分出胜负,则返回玩家的 id,如果未分出胜负,就返回 0;
  • 棋盘中的值为 1,表示是玩家1 的落子,值为 2 表示是玩家2 的落子;
  • 检查胜负的时候,以当前落子位置为中心,检查所有相关的行,列,对角线即可,不必遍历整个棋盘。

         由于棋盘上出现 “五子连珠” 一定是与最后的落子有关,所以我们才可以不用遍历整个棋盘,只需要以最后落子的位置为中心,来判定其周围的若干个格子,如下图所示:

         关于这个方法具体代码及介绍如下所示:

    // 使用这个方法来判定当前落子是否分出胜负.
    // 约定如果玩家1 获胜, 就返回玩家1 的userId
    // 如果玩家2 获胜, 就返回玩家2 的 userId
    // 如果胜负未分, 就返回 0
    private int checkWinner(int row, int col, int chess) {
        // 1. 检查所有的行
        //    先遍历这五种情况
        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 == 1 ? user1.getUserId() : user2.getUserId();
                }
            } catch (ArrayIndexOutOfBoundsException e) {
                // 如果出现数组下标越界的情况, 就在这里直接忽略这个异常
                continue;
            }
        }

        // 2. 检查所有列
        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;
            }
        }

        // 3. 检查右对角线
        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;
            }
        }

        // 4. 检查左对角线
        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;
            }
        }

        // 5. 胜负未分, 直接返回 0
        return 0;
    }

4.增加落子触发方法

        在客服端发送落子请求时,我们需要在 Room 类中新增一个处理落子相关逻辑的方法 —— putChess(),在这个方法中我们要做以下几件事情:

  1. 把落子请求解析成 GameRequest 对象;
  2. 根据 GameRequest 对象中的信息,在棋盘上落子;
  3. 落子完毕后,调用 printBoard() 方法,打印棋盘当前的状况;
  4. 调用 checkWinner() 方法,来检查游戏是否结束;
  5. 构造 GameResponse 对象,作为响应,返回给每个玩家;
  6. 在返回响应的时候如果发现某个玩家掉线,就直接判定另一个玩家获胜;
  7. 如果游戏胜负已分,就修改玩家的信息,并销毁房间。

        介绍完这个方法要做的事情后,关于这个方法的具体代码与详细介绍如下所示:

    // 通过这个方法来处理一次落子操作
    public void putChess(String reqJson) throws IOException {
        // 1. 记录当前落子的位置
        GameRequest request = objectMapper.readValue(reqJson, GameRequest.class);
        GameResponse response = new GameResponse();
        // 当前这个子是玩家1 落的还是玩家2 落的.
        // 根据这个玩家1 和玩家2 来决定数组中写的是 1 还是 2
        int chess = request.getUserId() == user1.getUserId() ? 1 : 2;
        int row = request.getRow();
        int col = request.getCol();
        if (board[row][col] != 0) {
            // 在客户端已经针对重复落子进行过判定了, 此处为了程序运行的更靠谱, 在服务器这里再判定一次
            System.out.println("当前位置 (" + row + ", " + col + ") 已经有子了!");
            return;
        }
        board[row][col] = chess;

        // 2. 打印出当前的棋盘信息, 方便来观察局势, 也方便后面验证胜负关系的判定.
        printBoard();

        // 3. 进行胜负判定.
        int winner = checkWinner(row, col, chess);

        // 4. 给房间中所有客户端返回响应.
        response.setMessage("putChess");
        response.setCol(col);
        response.setRow(row);
        response.setWinner(winner);
        response.setUserId(request.getUserId());
        // 要想给用户发送 WebSocket 数据, 就需要获取到这个用户的 WebSocketSession
        WebSocketSession session1 = onlineUserManager.getFromGameRoom(user1.getUserId());
        WebSocketSession session2 = onlineUserManager.getFromGameRoom(user2.getUserId());
        // 万一当前查到的会话为空(玩家已经下线了) 特殊处理一下
        if (session1 == null) {
            // 玩家1 已经下线了, 直接认为玩家2 获胜!
            response.setWinner(user2.getUserId());
            System.out.println("玩家1 掉线!");
        }
        if (session2 == null) {
            // 玩家2 已经下线, 直接认为玩家1 获胜!
            response.setWinner(user1.getUserId());
            System.out.println("玩家2 掉线!");
        }
        // 把响应构造成 JSON 字符串, 通过 Session 进行传输
        String respJson = objectMapper.writeValueAsString(response);
        if (session1 != null) {
            session1.sendMessage(new TextMessage(respJson));
        }
        if (session2 != null) {
            session2.sendMessage(new TextMessage(respJson));
        }

        // 5. 如果当前胜负已分, 此时这个房间就没有存在的意义了, 就可以直接销毁房间.
        if (response.getWinner() != 0) {
            // 胜负已分
            System.out.println("游戏结束! 房间即将销毁! 房间号是: " + roomId + " 获胜方是: " + (user1.getUserId() == winner ? user1.getUsername() : user2.getUsername()));
            // 更新获胜方和失败方的玩家信息
            int winUserId = response.getWinner();
            int loseUserId = response.getWinner() == user1.getUserId() ? user2.getUserId() : user1.getUserId();
            userMapper.userWin(winUserId);
            userMapper.userLose(loseUserId);
            // 销毁房间
            roomManager.remove(roomId, user1.getUserId(), user2.getUserId());
        }
    }

·结尾

        文章到此就要结束了,本篇文章介绍了五子棋项目中核心的部分——落子相关操作的处理,其中包括落子后棋盘信息的打印,落子后胜负的判定,落子后响应的返回等,捋清楚代码直接的关系,对理解本项目有很大的帮助,那么到这,五子棋项目中对战模块的核心逻辑代码就基本完成了,下一篇文章中我会对 WebSocket 连接部分的代码进行介绍,还有对战模块功能的测试,如果对本篇文章的内容有所疑惑,欢迎在评论区进行留言,如果感觉本篇文章还不错,希望收到你的三连支持,那么我们下一篇文章再见吧~~~


原文地址:https://blog.csdn.net/HKJ_numb1/article/details/143723865

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!