五子棋双人对战项目(6)——对战模块(解读代码)

news2024/11/24 11:40:25

目录

一、约定前后端交互接口的参数

1、房间准备就绪

(1)配置 websocket 连接路径

(2)构造 游戏就绪 的 响应对象

2、“落子” 的请求和响应

(1)“落子” 请求对象

(2)“落子” 响应对象

二、UserMapper

三、处理 websocket 请求、返回的响应(GameAPI)

1、afterConnectionEstablished

(1)通知玩家玩家进入房间(noticeGameReady)

(2)afterConnectionEstablished

1、首先通过Session拿到玩家信息 

2、通过玩家信息拿到游戏房间

3、判断用户是不是多开了

4、设置房间上线

5、把玩家加入对应的房间中

6、处理不存在的操作

2、handleTextMessage

3、handleTransportError

(1)noticeThatUserWin(通知获胜者)

4、afterConnectionClosed

四、Room

1、棋盘

2、自动注入

3、putChess(处理落子请求、构造返回响应)

4、打印棋盘

5、checkWinner(判断输赢)

行:顶点为左边

列:顶点在上边

主对角线:顶点在左上

副对角线:顶点在右上

五、前端代码的逻辑处理

1、处理游戏就绪响应

2、处理落子请求

3、处理落子响应

六、梳理前后端交互流程

七、代码以及线上云服务器的URL


一、约定前后端交互接口的参数

1、房间准备就绪

        当我们从 游戏大厅页面 跳转到 游戏房间页面,这也意味着:游戏大厅页面的 websocket 连接断开了,跳转到 游戏房间页面,要建立一个新的 websocket 连接

        那么也就说明我们之前的 websocket 连接不能用了,这里新的 websocket 连接最好使用新的 URL,不要和游戏大厅的一样,这样能起到 “解耦合” 的效果

        匹配成功后,是服务器主动给客户端发起响应,客户端就不必发起请求了。约定如图:

(1)配置 websocket 连接路径

        前端代码如下:(使用动态路径,更加灵活,方便后部署在服务器上)

let websocketUrl = "ws://" + location.host + "/game";
let websocket = new WebSocket(websocketUrl);

        后端代码:

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
    @Autowired
    private MatchAPI matchAPI;

    @Autowired
    private GameAPI gameAPI;

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry webSocketHandlerRegistry) {
        webSocketHandlerRegistry.addHandler(matchAPI, "/findMatch")
                .addInterceptors(new HttpSessionHandshakeInterceptor());
        webSocketHandlerRegistry.addHandler(gameAPI, "/game")
                .addInterceptors(new HttpSessionHandshakeInterceptor());
    }
}

注意:配置路径的同时,还要拿到之前HttpSession信息,保存在 新的 websocket连接中

(2)构造 游戏就绪 的 响应对象

        后端需要构造一个响应对象,把其转换为JSON格式的文本信息,再发送给客户端,响应对象如下:

// 客户端连接到游戏房间后,返回的响应
@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;
}

2、“落子” 的请求和响应

        双方玩家进入游戏房间后,也就是准备就绪后,那就要开始对弈了,所以,我们要针对玩家 “落子”的操作,构造请求和响应

        因为玩家是主动落子,所以要发送给服务器请求;服务器接收到请求后,也需要处理这个请求,构造响应,把最新棋盘布局发给另一个玩家,同时这个响应也要发送给我,让我知道到对方落子了。

(1)“落子” 请求对象

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

(2)“落子” 响应对象

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

二、UserMapper

        在游戏结束之后,我们要给玩家结算胜败,那么玩家的天梯积分和游戏场数相对也会改变,说明我们也就要对数据库进行操作了,这里增加两个修改数据库的操作,代码如下:

@Mapper
public interface UserMapper {
    // 根据用户名,查询用户的详情信息,用于登录功能
    @Select("select * from user where user_name = #{username}")
    User selectByName(String username);

    // 往数据库里插入信息,用于注册功能
    @Insert("insert into user values (null, #{username}, #{password}, 1000, 0, 0);")
    void register(User userInfo);

    // 总比赛场数 + 1    获胜场数 + 1    天梯积分 + 30
    @Update("update user set total_count = total_count + 1, " +
            "win_count = win_count + 1, score = score + 30 where user_id = #{userId}")
    void userWin(int userId);

    // 总比赛场数 + 1    获胜场数 不变    天梯积分 - 30
    @Update("update user set total_count = total_count + 1, " +
            "score = score - 30 where user_id = #{userId}")
    void userLose(int userId);
}

        一个是针对玩家 游戏胜利 后的修改操作,一个是针对玩家 对局失败 后的修改操作。


三、处理 websocket 请求、返回的响应(GameAPI)

        这里主要涉及的方法有四个:

afterConnectionEstablished在建立 websocket 连接时,要做的处理

handleTextMessage接收玩家的 请求,返回对应的 响应

handleTransportError当 websocket 连接出现错误时,要做的处理

afterConnectionClosed当 websocket 连接关闭时,要做的处理

1、afterConnectionEstablished

        建立了新的websocket连接,需要把就绪的玩家加入到房间中,这里就是处理玩家加入房间的逻辑。

(1)通知玩家玩家进入房间(noticeGameReady)

        这里把通知玩家的逻辑单独封装成一个方法,代码如下:

    private void noticeGameReady(Room room, User thisUser, User thatUser) throws IOException {
        GameReadyResponse resp = new GameReadyResponse();
        resp.setMessage("gameReady");
        resp.setOk(true);
        resp.setReason("");
        resp.setRoomId(room.getRoomId());
        resp.setThisUserId(thisUser.getUserId());
        resp.setThatUserId(thatUser.getUserId());
        resp.setWhiteUser(room.getWhiteUser());
        // 把当前的响应数据传回给对应的玩家
        WebSocketSession webSocketSession = onlineUserManager.getFromGameRoom(thisUser.getUserId());
        webSocketSession.sendMessage(new TextMessage(objectMapper.writeValueAsString(resp)));
    }

        构造对应的响应数据,然后通过 Session 发送给客户端。

(2)afterConnectionEstablished

        建立连接时,并不可以直接把玩家加入房间,需要经过一系列的校验。

1、首先通过Session拿到玩家信息 

        但拿到后还要判断用户是不是null

        // 1、先获取到用户的身份信息(从 HttpSession 里拿到)
        User user = (User) session.getAttributes().get("user");
        if (user == null) {
            resp.setOk(false);
            resp.setReason("用户尚未登录!");
            String jsonString = objectMapper.writeValueAsString(resp);
            session.sendMessage(new TextMessage(jsonString));
            return;
        }
2、通过玩家信息拿到游戏房间

        也要对房间进行校验,如果为空,就要返回对应响应给前端。

        // 2、判定当前用户是否已经进入房间(拿着房间管理器进行查询)
        Room room = roomManager.getRoomByUserId(user.getUserId());
        if (room == null) {
            // 如果为 null,当前没有找到对应的房间,该玩家还没匹配到
            resp.setOk(false);
            resp.setReason("用户尚未匹配到");
            String jsonString = objectMapper.writeValueAsString(resp);
            session.sendMessage(new TextMessage(objectMapper.writeValueAsBytes(jsonString)));
            return;
        }
3、判断用户是不是多开了

        进行对局时,发现该玩家已经在房间里了,说明前面已经有玩家在登录游戏中,我再登录,就视为多开行为;还有是玩家一边在游戏大厅,一边是在游戏房间,也视为多开。

        // 3、判定当前是不是多开(该用户是不是已经在其他地方进行游戏了)
        //    前面多准备了一个 OnlineUserManager
        if (onlineUserManager.getFromGameHall(user.getUserId()) != null
                || onlineUserManager.getFromGameRoom(user.getUserId()) != null) {
            // 如果一个账号,一边是在游戏大厅,一边是在游戏房间,也视为多开~
            resp.setOk(true);
            resp.setReason("禁止多开游戏页面");
            resp.setMessage("repeatConnection");
            String jsonString = objectMapper.writeValueAsString(resp);
            session.sendMessage(new TextMessage(jsonString));
            return;
        }
4、设置房间上线

        经过上面一系列校验后,没有问题,才能设置玩家在房间中上线。

        // 4、经过一些列校验都没问题后,设置当前玩家上线(房间中上线)
        onlineUserManager.enterGameRoom(user.getUserId(), session);
5、把玩家加入对应的房间中

        把玩家加入对应的房间中,这里设置先手的操作是先加入房间的玩家。

        当双方玩家都加入房间后,要对客户端进行通知,构造对应的响应,发送回去。

        synchronized (room) {
            // 5、把两个玩家加入到游戏房间中
            //    前面的创建房间/匹配过程,是在 game_hall.html 页面中完成的
            //    因此前面匹配到对手之后,需要经过页面跳转,来到 game_room.html 才算正式进入游戏房间(才算玩家准备就绪)
            //    当前这个逻辑是在 game_room.html 页面加载的时候进行的
            //    执行到当前逻辑,说明玩家已经页面跳转成功了
            //    页面跳转,其实是个大活~(很有可能出现 “失败” 的情况的)
            if (room.getUser1() == null) {
                // 第一个玩家尚未加入房间
                // 就把当前连上 WebSocket 的玩家作为 user1,加入到房间中
                room.setUser1(user);
                // 把先连入房间的玩家作为先手方
                room.setWhiteUser(user.getUserId());
                log.info("玩家 " + user.getUsername() + " 已经准备就绪! 作为玩家1");
                return;
            }
            if (room.getUser2() == null) {
                // 如果进入这个房间,说明玩家1 已经加入房间,现在要把玩家2 加入房间
                room.setUser2(user);
                log.info("玩家 " + user.getUsername() + " 已经准备就绪! 作为玩家2");

                // 当两个玩家都加入成功之后,就要让服务器,给这两个玩家都返回 WebSocket 的响应数据
                // 通知两个玩家说:游戏双方都已经准备好了
                // 通知玩家1
                noticeGameReady(room, room.getUser1(), room.getUser2());
                // 通知玩家2
                noticeGameReady(room, room.getUser2(), room.getUser1());
                return;
            }
        }
6、处理不存在的操作

        经过上述操作后,把玩家加入房间中,双方玩家都已经就绪了,这时候还有玩家在尝试连接这个房间,就要给客户端提示报错。(理论上这种情况是不存在的)

        // 6、此处如果又有一个玩家尝试连接同一个房间,就会提示报错
        //    这种情况理论上是不存在的,为了让程序更加健壮,还是做一个判定和提示
        resp.setOk(false);
        resp.setReason("当前房间已满,您不能加入房间");
        session.sendMessage(new TextMessage(objectMapper.writeValueAsString(resp)));

2、handleTextMessage

        在处理请求和构造响应时,根据用户的Session拿到用户信息,然后根据用户再拿到房间,在房间中进行落子的操作。

        其中落子的请求处理和响应构造放在Room里了。

    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        // 1、先从 Session 拿到当前用户的身份信息
        User user = (User) session.getAttributes().get("user");
        if (user == null) {
            log.info("[handleTextMessage] 当前玩家尚未登录");
            return;
        }

        // 2、根据 玩家id 获取到房间对象
        Room room = roomManager.getRoomByUserId(user.getUserId());
        // 3、通过 room对象 来处理这次具体的请求
        room.putChess(message.getPayload());
    }

3、handleTransportError

        出现异常时,根据Session拿到该用户的信息,再去房间管理器中找该用户的Session,看连接中的Session和房间管理器的Session是不是一样的

        是一样的,说明是我们预期想要在房间管理器中删除的;如果不一样,说明用户可能是多开,防止该用户退出导致前面用户的Session被删除。

        当我掉线后,就要判定对方获胜,通知封装成了一个方法,逻辑操作在该方法中完成。

    public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
        User user = (User) session.getAttributes().get("user");
        if (user == null) {
            //此处就简单处理,在断开连接的时候就不给客户端返回响应了
            return;
        }

        WebSocketSession exitSession = onlineUserManager.getFromGameRoom(user.getUserId());
        // 加上这个判定,目的是为了在多开的情况下,第二个用户退出连接动作,导致第一个登录在线的用户会话删除
        if (session == exitSession) {
            onlineUserManager.exitGameRoom(user.getUserId());
            log.info("当前这个用户 {}", user.getUsername() + " 游戏房间连接异常");
        }

        // 通知对手获胜
        noticeThatUserWin(user);
    }

(1)noticeThatUserWin(通知获胜者)

        1、拿到当前玩家,找到该房间

        2、根据房间找到对手

        3、根据房间管理器找到对手的Session

        4、构造响应,告诉客户端,你赢了

        5、更新玩家的分数信息

    private void noticeThatUserWin(User user) throws IOException {
        // 1、根据当前玩家,找到对应房间,再找到当前玩家的对手
        Room room = roomManager.getRoomByUserId(user.getUserId());
        if (room == null) {
            // 这个情况意味着房间已经被释放,也就没有对手了
            log.info("当前房间已经释放, 无需通知对手");
            return;
        }

        // 2、根据房间找到对手
        User thatUser = (user == room.getUser1()) ? room.getUser2() : room.getUser1();

        // 3、找到对手的在线状态
        WebSocketSession webSocketSession = onlineUserManager.getFromGameRoom(thatUser.getUserId());
        if(webSocketSession == null) {
            // 这就意味着对手也掉线了
            log.info("对手也已经掉线了, 无需通知");
            return;
        }

        // 4、构造一个响应,来通知对手,你是获胜方
        GameResponse resp = new GameResponse();
        resp.setMessage("putChess");
        resp.setUserId(thatUser.getUserId());
        resp.setWinner(thatUser.getUserId());
        webSocketSession.sendMessage(new TextMessage(objectMapper.writeValueAsString(resp)));

        // 5、更新玩家的分数信息
        int winUserId = thatUser.getUserId();
        int loseUserId = user.getUserId();
        userMapper.userWin(winUserId);
        userMapper.userLose(loseUserId);
        // 6、释放房间对象
        roomManager.remove(room.getRoomId(), room.getUser1().getUserId(), room.getUser2().getUserId());
    }

4、afterConnectionClosed

        关闭连接后,处理的逻辑也和连接错误一样。

    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        User user = (User) session.getAttributes().get("user");
        if (user == null) {
            //此处就简单处理,在断开连接的时候就不给客户端返回响应了
            return;
        }

        WebSocketSession exitSession = onlineUserManager.getFromGameRoom(user.getUserId());
        // 加上这个判定,目的是为了在多开的情况下,第二个用户退出连接动作,导致第一个登录在线的用户会话删除
        if (session == exitSession) {
            onlineUserManager.exitGameRoom(user.getUserId());
            log.info("当前这个用户 {}", user.getUsername() + " 已经离开游戏房间");
        }

        // 通知对手获胜
        noticeThatUserWin(user);
    }

四、Room

1、棋盘

        在Room对象中,我们把落子的下标存在一个二维数组中:

    // 行 | 列
    private static final int MAX_ROW = 15;
    private static final int MAX_COL = 15;
    // 这个二维数组表示棋盘(服务端这边数组的状态有三种,但客户端那边只有两种,主要是用来判断当前棋盘有没有棋子,用来避免一个问题:同一个位置重复落子的情况)
    // 约定:
    // 1) 使用 0 表示当前位置未落子(初始化好的二维数组,相对于 全都是0)
    // 2) 使用 1 表示 user1 的落子位置
    // 3) 使用 2 表示 user2 的落子位置
    private int[][] board = new int[MAX_ROW][MAX_COL];

2、自动注入

        因为要在落子的请求后,可能会决出胜负,也就是说,处理落子请求时,我们也需要维护roomManager、onlineUserManager,这也意味着我们需要注入这两个对象。还有更新用户信息—— 维护userMapper。

        仔细想想,我们能使用 @Autowired 注入这两个对象吗?如果要使用 @Autowired 注解,也意味着 Room对象 也要被Spring管理起来,但 Room对象 能使用Spring的注解,交给Spring进行管理吗?

        明显是不能的,因为 如果加入对应的注解,那么被Spring管理起来的这个对象,就是单例的了。

        Room对象 能是单例的吗?明显不能吧,因为有很多玩家都在进行游戏时,这时候房间也会有很多,肯定不可能让Room变成单例的。

        那能不能既实现多例,又能注入这两个对象呢?当然也有办法——自动注入

        在Spring启动方法里,添加context:

@SpringBootApplication
public class SpringGobangApplication {

    public static ConfigurableApplicationContext context;

    public static void main(String[] args) {
        context = SpringApplication.run(SpringGobangApplication.class, args);
    }

}

        通过Room的构造方法里,手动注入这两个对象

    // 引入 OnlineUserManager
//    @Autowired
    private OnlineUserManager onlineUserManager;

    // 引入 RoomManager, 用于房间销毁
//    @Autowired
    private RoomManager roomManager;
    // 引入 UserMapper, 用于更新用户数据
//    @Autowired
    private UserMapper userMapper;

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

3、putChess(处理落子请求、构造返回响应)

        1、根据请求,拿到当前落子的位置;然后判断当前这个棋子,是玩家1落的棋子、还是玩家2落的棋子。

        2、在控制台上打印出棋盘信息(方便观察)

        3、进行胜负判断,此时也可能是胜负未分(也封装成一个方法了,后面介绍)

        4、给房间的所有客户端都返回响应(根据房间的用户,分别获取不同用户的Session,有了Session,就能对客户端发送消息了;当获取不到用户的Session,说明用户下线了,那就判断对方赢)

        5、当数据发送完毕后,再判断现在是不是胜负已分,如果比赛已经结束了,那也要更新玩家数据,这个房间也没有必要再存在了,要进行销毁。

    // 通过这个方法来处理一次落子操作
    // 要做的事情:
    // 1、记录当前落子的位置
    // 2、进行胜负判定
    // 3、给客户端返回响应
    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) {
            // 在客户端已经针对重复落子进行判定过了,此处为了程序更加稳健,在服务器再判断一次
            log.info("当前位置 row: " + row + " col: " + col + " 已经有棋子了");
        }
        board[row][col] = chess;

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

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

        // 4、给房间的所有客户端都返回响应
        response.setMessage("putChess");
        response.setUserId(request.getUserId());
        response.setRow(row);
        response.setCol(col);
        response.setWinner(winner);
        // 要想给用户发送 WebSocket 数据,就需要获取到这个用户的 WebSocketSession
        WebSocketSession session1 = onlineUserManager.getFromGameRoom(user1.getUserId());
        WebSocketSession session2 = onlineUserManager.getFromGameRoom(user2.getUserId());
        // 万一当前查到的会话为空(玩家下线了), 特殊处理一下
        if (session1 == null) {
            // 玩家1 下线了,直接认为 玩家2 获胜
            response.setWinner(user2.getUserId());
            log.info("玩家: {}", user1.getUsername() + " 下线, 直接判定玩家1获胜");
        }
        if (session2 == null) {
            // 玩家2 下线了,直接认为 玩家1 获胜
            response.setWinner(user1.getUserId());
            log.info("玩家: {}", user2.getUsername() + " 下线, 直接判定玩家1获胜");
        }
        // 把响应构造成 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) {
            log.info("游戏结束, 当前房间即将销毁! rommId= {}", roomId + " 获胜方为: " + response.getWinner());

            // 更新获胜方和失败方的信息
            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());
        }
    }

4、打印棋盘

        两层for循环,遍历数组

    private void printBoard() {
        // 打印出棋盘
        log.info("[打印棋盘信息] " + "roomId: {}", 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("=======================================================");
    }

5、checkWinner(判断输赢)

        判断输赢,也就是要检查 是否有 “五子连珠” 的情况。

        五子连珠的情况可能是 行有5个子、列有5个子、主对角线有5个子、副对角线有5个子

        那怎么进行判断呢?其实非常简单,在我们拿到落子的下标后,就固定一个顶点,判断这个顶点所在的这一行往下有没有五子连珠的情况(列、对角线同理),没有就继续下一点,直到该坐标。

        其他情况也同理。

        了解了如何判断,那现在我们就固定一下这个 “顶点” 吧。

行:顶点为左边

列:顶点在上边

主对角线:顶点在左上

副对角线:顶点在右上

        具体实现代码如下:(下面的这种方法可能会有空指针异常,但不要紧,我们进行捕获,再continue进入下一个循环就好了)

    // 使用这个方法,来判定当前落子后,是否分出胜负
    // 约定:
    //  1) 如果 玩家1 获胜,就返回 玩家1 的userId
    //  2) 如果 玩家2 获胜,就返回 玩家2 的userId
    //  3) 如果 胜负未分,就返回 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) {
                    // 构成 五子连珠! 胜负已分!
                    log.info("行 五子连珠! 胜负已分!");
                    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) {
                    // 构成 五子连珠! 胜负已分!
                    log.info("列 五子连珠! 胜负已分!");
                    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) {
                    // 构成 五子连珠! 胜负已分!
                    log.info("主对角线 五子连珠! 胜负已分!");
                    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) {
                    // 构成 五子连珠! 胜负已分!
                    log.info("副对角线 五子连珠! 胜负已分!");
                    return chess == 1 ? user1.getUserId() : user2.getUserId();
                }
            } catch (ArrayIndexOutOfBoundsException e) {
                // 如果出现数组下标越界的情况,就在这里直接忽略这个异常,继续下一次循环判断
                continue;
            }
        }
        // 胜负未分,就直接返回 0 了
        return 0;
    }

        如果经过上述逻辑判断,还没返回对应的赢家Id,那么就说明是胜负未分,返回0.


五、前端代码的逻辑处理

1、处理游戏就绪响应

        双方进入房间成功后,就会建立一个房间页面的websocket连接,其代码如下:

//
// 初始化 websocket
//
let websocketUrl = "ws://" + location.host + "/game";
let websocket = new WebSocket(websocketUrl);

websocket.onopen = function () {
    console.log("连接游戏房间成功");
}

websocket.close = function () {
    console.log("和游戏服务器连接断开");
}

websocket.onerror = function () {
    console.log("和服务器的连接出现异常");
}

websocket.onbeforeunload = function () {
    websocket.close();
}

//处理服务器返回的响应数据 (游戏就绪响应)
websocket.onmessage = function (event) {
    console.log("[handlerGameReady] " + event.data);
    let resp = JSON.parse(event.data);

    if (!resp.ok) {
        alert("连接游戏失败! reason: " + resp.reason);
        // 如果出现连接失败的情况,回到游戏大厅
        // location.assign("/game_hall.html");
        location.replace("/game_hall.html");
        return;
    }

    if (resp.message == 'gameReady') {
        // 把后端返回的数据放进 gameInfo 对象中
        gameInfo.roomId = resp.roomId;
        gameInfo.thisUserId = resp.thisUserId;
        gameInfo.thatUserId = resp.thatUserId;
        // 判断自己是不是先手
        gameInfo.isWhite = (resp.whiteUser == resp.thisUserId);
        // 初始化棋盘
        initGame();
        //设置显示区域的内容(轮到谁落子了)
        setScreenText(gameInfo.isWhite);
    } else if (resp.message == 'repeatConnection') {
        console.log("检测到游戏多开!");
        alert("检测到游戏多开, 请使用其他账户进行登录!");
        // location.assign("/login.html");
        location.replace("/login.html");
        return;
    }
}

        接收服务器的响应处理主要在 websocket.message() 上,首先,拿到后端返回来的响应,将其转为 js 对象(原本是JSON文本)。

        判断 resp.ok,如果是false,说明连接异常,跳转到登录页面(可能是还没登录,就直接访问游戏房间页面)。

        然后判断resp.message,如果是 'gameReady',说明该玩家加入房间成功,进行判断先手,设置我方和对方的userId,初始化棋盘,设置显示区域(轮到谁落子了)

        resp.message,如果是 'repeatConnection',说明玩家有多开行为,那就给个提示弹窗,返回到登录页面。

2、处理落子请求

        前端发送的请求有用户Id、落子位置。

        这里有一个点击事件,点击对应位置,就会落子,然后给后端发送一个落子请求,代码如下:

        这里也就会处理:如果不是我落子,你在棋盘上再怎么点也没有用,只有到我落子了,才能继续落子;还有一种是游戏结束了,我也不能继续在这个棋盘上落子了。

    chess.onclick = function (e) {
        if (over) {
            return;
        }
        if (!me) {
            return;
        }
        let x = e.offsetX;
        let y = e.offsetY;
        // 注意, 横坐标是列, 纵坐标是行
        let col = Math.floor(x / 30);
        let row = Math.floor(y / 30);
        // 客户端的棋盘状态只有两种,主要是用来判断当前棋盘有没有棋子,用来避免一个问题:同一个位置重复落子的情况
        if (chessBoard[row][col] == 0) {
            // 发送坐标给服务器, 服务器要返回结果
            send(row, col);
        }
    }

    function send(row, col) {
        let req = {
            message: 'putChess',
            userId: gameInfo.thisUserId,
            row: row,
            col: col
        };
        websocket.send(JSON.stringify(req));
    }

3、处理落子响应

        返回来的响应,如果 resp.message != "putChess",就说明响应数据有问题。

        然后判断这个响应是不是自己落的子,如果是自己落的子,就绘制一个自己的棋子,如果不是自己落的子,那就说明是对方落的子。

        落子之后,要把对应的位置设为1,说明该位置已经有棋子了,不能再在这个位置落子。

        后端返回的响应有用户Id、落子位置、输赢状态。落子后,就要判断输赢了,如果返回resp.winner 是我的 userId,那就说明我赢了,反之则是对方赢了,然后改变对应的提示信息。还有一种情况,就是0,说明胜负未分。

        胜负分出后,就给页面新增一个按钮,点击后返回到游戏大厅

    // 之前 websocket.onmessage 主要是用来处理了游戏就绪响应,在游戏就绪之后,初始化完毕之后,也就不再有这个 游戏就绪响应 了
    // 就在这个 initGame 内部修改 websocket.onmessage 方法~~,让这个方法里面针 对落子响应 进行处理
    websocket.onmessage = function (event) {
        console.log("[handlerPutChess]: " + event.data);

        let resp = JSON.parse(event.data);

        if (resp.message != "putChess") {
            console.log("响应类型错误");
            return;
        }

        // 先判定当前这个响应时否为自己逻的子,还是对方落的子
        if (resp.userId == gameInfo.thisUserId) {
            // 我自己落的子
            // 根据我自己棋子的颜色,来绘制一个棋子
            oneStep(resp.col, resp.row, gameInfo.isWhite);
        } else if (resp.userId == gameInfo.thatUserId) {
            // 我的对手落的子
            oneStep(resp.col, resp.row, !gameInfo.isWhite);
        } else {
            // 响应错误! userId 是有问题的
            console.log("[handlerPutChess resp userId 错误");
            return;
        }
        // 给对应的位置设为 1,方便后续逻辑判定当前位置是否已经有棋子了
        chessBoard[resp.row][resp.col] = 1;

        // 交换双方的落子轮次
        me = !me;
        setScreenText(me);

        // 判定游戏是否结束
        let screenDiv = document.querySelector('#screen');
        if (resp.winner != 0) {
            if (resp.winner == gameInfo.thisUserId) {
                // alert('你赢了!');
                screenDiv.innerHTML = '你赢了!';
            } else if (resp.winner == gameInfo.thatUserId) {
                // alert('你输了');
                screenDiv.innerHTML = '你输了!';
            } else {
                alert('winner 字段错误 ' + resp.winner);
            }
            // 回到游戏大厅
            // location.assign('/game_hall.html');

            // 增加一个按钮,让玩家点击之后,再回到游戏大厅~
            let backButton = document.createElement('button');
            backButton.innerHTML = '回到游戏大厅'
            backButton.className = 'button';  // 添加样式类
            backButton.onclick = function() {
                location.replace('/game_hall.html');
            }
            let fatherDiv = document.querySelector('.container>div');
            fatherDiv.appendChild(backButton);
        }
    }

六、梳理前后端交互流程

        双方点击开始匹配按钮。

        进入游戏房间:

        进入房间后,建立 websocket 连接,代码逻辑如:

        我落子后,服务器接收到请求,会给双方都发送响应。

        前后端代码处理逻辑,前后端就依次往下看代码,这里就不展开了


七、代码以及线上云服务器的URL

        URL:http://120.79.61.184:9090/login.html

        Gitte:spring-gobang · taotao/Studying JavaEE Advanced - 码云 - 开源中国 (gitee.com)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2188253.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

如何使用ssm实现基于java的实验室设备管理系统

TOC ssm768基于java的实验室设备管理系统jsp 绪论 1.1研究背景与意义 1.1.1研究背景 近年来&#xff0c;第三产业发展非常迅速&#xff0c;诸如计算机服务、旅游、娱乐、体育等服务行业&#xff0c;对整个社会的经济建设起到了极大地促进作用&#xff0c;这一点是毋庸置疑…

SpringBoot与舞蹈艺术:古典舞在线交流平台开发记

第二章 相关技术介绍 2.1Java技术 Java是一种非常常用的编程语言&#xff0c;在全球编程语言排行版上总是前三。在方兴未艾的计算机技术发展历程中&#xff0c;Java的身影无处不在&#xff0c;并且拥有旺盛的生命力。Java的跨平台能力十分强大&#xff0c;只需一次编译&#xf…

【基础算法总结】链表篇

目录 一&#xff0c; 链表常用技巧和操作总结二&#xff0c;算法原理和代码实现2.两数相加24.两两交换链表中的节点143.重排链表23.合并k个升序链表25.k个一组翻转链表 三&#xff0c;算法总结 一&#xff0c; 链表常用技巧和操作总结 有关链表的算法题也是一类常见并且经典的题…

案例-猜数字游戏

文章目录 效果展示初始画面演示视频 代码区 效果展示 初始画面 演示视频 猜数字游戏 代码区 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width,…

【题解】【动态规划,最长上升子序列LIS】—— [CSP-J 2022] 上升点列

【题解】【动态规划&#xff0c;最长上升子序列LIS】—— [CSP-J 2022] 上升点列 [CSP-J 2022] 上升点列题目描述输入格式输出格式输入输出样例输入 #1输出 #1输入 #2输出 #2 提示 1.题意解析2.AC代码 [CSP-J 2022] 上升点列 通往洛谷的传送门 题目描述 在一个二维平面内&am…

GAMES101(19节,相机)

相机 synthesis合成成像&#xff1a;比如光栅化&#xff0c;光线追踪&#xff0c;相机是capture捕捉成像&#xff0c; 但是在合成渲染时&#xff0c;有时也会模拟捕捉成像方式&#xff08;包括一些技术 动态模糊 / 景深等&#xff09;&#xff0c;这时会有涉及很多专有名词&a…

确保接口安全:六大方案有效解决幂等性问题

文章目录 六大方案解决接口幂等问题什么是接口幂等&#xff1f;天然幂等不做幂等会怎么样&#xff1f; 解决方案1&#xff09;insert前先select2&#xff09;使用唯一索引3&#xff09;去重表加悲观锁4&#xff09;加乐观锁之版本号机制5&#xff09;使用 Redisson 分布式锁6&a…

银河麒麟系统内存清理

银河麒麟系统内存清理 1、操作步骤2、注意事项 &#x1f490;The Begin&#x1f490;点点关注&#xff0c;收藏不迷路&#x1f490; 当银河麒麟系统运行较长时间&#xff0c;内存中的缓存可能会积累过多&#xff0c;影响系统性能。此时&#xff0c;你可以通过简单的命令来清理这…

问:LINUXWINDOWS线程CPU时间如何排序?

Linux 在Linux上&#xff0c;你可以使用ps命令结合sort命令来查看和排序进程或线程的CPU使用时间。 查看进程的CPU使用时间并按时间排序 使用ps命令的-o选项可以自定义输出格式&#xff0c;-e选项表示显示所有进程&#xff0c;--sort选项用于排序。 ps -e -o pid,tid,comm,…

排序大全(干货)

目录 1. 插入排序步骤&#xff1a; 2.选择排序思路&#xff1a;每次从待排序列中选出一个最小值&#xff0c;然后放在序列的起始位置&#xff0c;直到全部待排数据排完即可。实际上&#xff0c;我们可以一趟选出两个值&#xff0c;一个最大值一个最小值&#xff0c;然后将其放…

【springboot】整合LoadBalancer

目录 问题产生背景解决方案&#xff1a;实现LoadBalancer1. 添加依赖2. 配置文件3. 使用LoadBalancer4. 使用 RestTemplate 进行服务调用5. 测试 问题产生背景 以下是一个购物车项目&#xff0c;通过调用外部接口获取商品信息&#xff0c;并添加到购物车中&#xff0c;这段代码…

如何使用ssm实现中学生课后服务的信息管理与推荐+vue

TOC ssm766中学生课后服务的信息管理与推荐vue 第一章 绪论 1.1 选题背景 目前整个社会发展的速度&#xff0c;严重依赖于互联网&#xff0c;如果没有了互联网的存在&#xff0c;市场可能会一蹶不振&#xff0c;严重影响经济的发展水平&#xff0c;影响人们的生活质量。计算…

查缺补漏----I/O中断处理过程

中断优先级包括响应优先级和处理优先级&#xff0c;响应优先级由硬件线路或查询程序的查询顺序决定&#xff0c;不可动态改变。处理优先级可利用中断屏蔽技术动态调整&#xff0c;以实现多重中断。下面来看他们如何运用在中断处理过程中&#xff1a; 中断控制器位于CPU和外设之…

SpringBoot开发:古典舞在线交流平台的架构与实现

第三章 系统分析 3.1 可行性分析 需要使用大部分精力开发的古典舞在线交流平台为了充分降低开发风险&#xff0c;特意在开发之前进行可行性分析这个验证系统开发是否可行的步骤。本文就会从技术角度&#xff0c;经济角度&#xff0c;还有操作角度等进行综合阐述。 3.1.1技术可行…

排序01 多目标模型

引入 使用机器学习方法对指标做预估&#xff0c;再对预估分数做融合。融合方法&#xff1a;加权和方法给不同指标赋予不同的权重&#xff0c;权重是做A/B test调试得到的。还有更好地融合方法。 多目标模型 排序模型的输入是各种各样的特征&#xff0c;用户特征主要是用户id和…

易趋(EasyTrack)资深顾问唐颖受邀为第四届中国项目经理大会演讲嘉宾

全国项目经理专业人士年度盛会 易趋&#xff08;EasyTrack&#xff09;资深顾问唐颖女士受邀为PMO评论主办的全国项目经理专业人士年度盛会——2024第四届中国项目经理大会演讲嘉宾&#xff0c;演讲议题为“隐形翅膀——数字化项目管理助力项目经理鹏程万里”。大会将于10月26-…

古典舞在线互动:SpringBoot平台设计与功能实现

第三章 系统分析 3.1 可行性分析 需要使用大部分精力开发的古典舞在线交流平台为了充分降低开发风险&#xff0c;特意在开发之前进行可行性分析这个验证系统开发是否可行的步骤。本文就会从技术角度&#xff0c;经济角度&#xff0c;还有操作角度等进行综合阐述。 3.1.1技术可行…

Prometheus之Pushgateway使用

Pushgateway属于整个架构图的这一部分 The Pushgateway is an intermediary service which allows you to push metrics from jobs which cannot be scraped. The Prometheus Pushgateway exists to allow ephemeral and batch jobs to expose their metrics to Prometheus. S…

扩散引导语言建模(DGLM):一种可控且高效的AI对齐方法

随着大型语言模型(LLMs)的迅速普及,如何有效地引导它们生成安全、适合特定应用和目标受众的内容成为一个关键挑战。例如,我们可能希望语言模型在与幼儿园孩子互动时使用不同的语言,或在撰写喜剧小品、提供法律支持或总结新闻文章时采用不同的风格。 目前,最成功的LLM范式是训练…

使用python基于DeepLabv3实现对图片进行语义分割

DeepLabv3 介绍 DeepLabv3 是一种先进的语义分割模型&#xff0c;由 Google Research 团队提出。它在 DeepLab 系列模型的基础上进行了改进&#xff0c;旨在提高图像中像素级分类的准确性。以下是 DeepLabv3 的详细介绍&#xff1a; 概述DeepLabv3 是 DeepLab 系列中的第三代…