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

目录

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

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 {@Autowiredprivate MatchAPI matchAPI;@Autowiredprivate GameAPI gameAPI;@Overridepublic 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、判定当前是不是多开(该用户是不是已经在其他地方进行游戏了)//    前面多准备了一个 OnlineUserManagerif (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 的响应数据// 通知两个玩家说:游戏双方都已经准备好了// 通知玩家1noticeGameReady(room, room.getUser1(), room.getUser2());// 通知玩家2noticeGameReady(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
//    @Autowiredprivate OnlineUserManager onlineUserManager;// 引入 RoomManager, 用于房间销毁
//    @Autowiredprivate RoomManager roomManager;// 引入 UserMapper, 用于更新用户数据
//    @Autowiredprivate UserMapper userMapper;public Room() {// 构造 Room 的时候生成一个唯一的字符串表示房间 id// 使用 UUID 来作为房间 idroomId = UUID.randomUUID().toString();// 通过入口类记录中的 context,来手动获取到前面的 RoomManager 和 OnlineUserManageronlineUserManager = 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还是2int 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 数据,就需要获取到这个用户的 WebSocketSessionWebSocketSession 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) 如果 胜负未分,就返回 0private 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.rhkb.cn/news/441164.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【Git】vscode链接github拉去镜像

1.拉取别人的项目到自己的仓库 2.回到自己的仓库拉取文件到vscode里面下载 使用vscode进入虚拟机 推送到自己的仓库上面 在 github 页面将修改的内容 PR 到 Tutorial 创建一个个人仓库 代码如下 cd demo git clone https://github.com/3154067760/Tutorial.git cd Tutorial/…

UGUI(三大现成UI控件)

Rawimage 可以是任意类型的图&#xff0c;所以这里的泛型就更宽泛&#xff0c;不止sprite 相比Image唯二的不同 uvrect有点像平铺 Text suddenly come to a Free island. best fit开启后会有范围选择 Image image 组件是挂在RectTransform的ui下的&#xff0c;换句话说&…

域名续签申请步骤

来此加密-申请3个月使用&#xff08;免费&#xff09; 附上链接&#x1f517; 免费申请SSL证书,支持泛域名和多域名: 来此加密. 使用推荐码注册:E69X5K4D, 立刻获得5个积分. 访问:https://letsencrypt.osfipin.com/jump/share?codeE69X5K4D 登陆网站 https://letsencrypt.…

浅谈新能源电动汽车充电站建设与运营模式分析

摘要&#xff1a;电动汽车是当前新能源汽车中重要的组成部分&#xff0c;具有广阔的发展前景&#xff0c;能够实现“以电代油”&#xff0c;与传统的燃油汽车相比&#xff0c;电动汽车在噪音及废气排放量方面相对较少&#xff0c;具有节能环保的显著特点。而电动汽车充电站则是…

强引用、软引用、弱引用、虚引用用法

强引用、软引用、弱引用、虚引用用法 强引用弱引用弱引用虚引用 强引用 强引用是指程序中在程序代码之中类似“Object obj new Object()”的引用关系&#xff0c;无论任何情况下&#xff0c;只要强引用关系还存在&#xff0c;垃圾回收器就不会回收掉被引用的对象。 强引用是我…

【黑马点评】使用RabbitMQ实现消息队列——3.使用Jmeter压力测试,导入批量token,测试异步秒杀下单

3 批量获取用户token&#xff0c;使用jmeter压力测试 3 批量获取用户token&#xff0c;使用jmeter压力测试3.1 需求3.2 实现3.2.1 环境配置3.2.2 修改登录接口UserController和实现类3.2.3 测试类 3.3 使用jmeter进行测试3.4 测试结果3.5 将用户登录逻辑修改回去 3 批量获取用户…

地图可视化的艺术:深入比较Mapbox、OpenLayers、Leaflet和Cesium,不同场景下应如何选择地图库

目录 地图可视化的艺术&#xff1a;深入比较Mapbox、OpenLayers、Leaflet和Cesium 一、总览 二、定制地图美学的先行者——Mapbox 1、主要功能特点 2、开源情况 3、市场与应用人群 4、安装与基础使用代码 三、开源GIS地图库的全能王——OpenLayers 1、主要功能特点 2…

rabbitmq消费者应答模式

1.应答模式 RabbitMQ 中的消息应答模式主要包括两种&#xff1a;自动应答&#xff08;Automatic Acknowledgement&#xff09;和手动应答&#xff08;Manual Acknowledgement&#xff09;。 自动应答&#xff1a; 不在乎消费者对消息处理是否成功&#xff0c;都会告诉队列删…

ComfyUI增强图像细节只需要一个节点(附工作流),SD1.5、SDXL、FLUX.1 全支持,简单好用!

今天给小伙伴们介绍一个非常简单&#xff0c;但又相当好使的一个插件。 功能很简单&#xff0c;就是增加或者减少图像的细节&#xff0c;节点也很简单&#xff0c;就一个节点&#xff0c;只需要嵌入我们的 ComfyUI 的基础工作流中就可以了&#xff0c;随插随用。 而且该插件不…

springboot mail:如何高效管理邮件服务?

springboot mail发邮件教程&#xff1f;怎么实现spring发信功能&#xff1f; SpringBoot Mail作为Spring Boot框架的一部分&#xff0c;提供了一种简单而强大的方式来集成和管理邮件服务。AokSend将探讨如何高效地使用SpringBoot Mail来管理邮件服务&#xff0c;确保邮件发送的…

Qt实现Halcon窗口显示当前图片坐标

一、前言 Halcon加载图片的窗口&#xff0c;不仅能放大和缩小图片&#xff0c;还可以按住Ctrl键显示鼠标下的灰度值&#xff0c;这种方式很方便我们分析缺陷的灰度和对比度。 二、实现方式 ① 创建显示坐标和灰度的widget窗口 下图的是widget部件&#xff0c;使用了4个label控…

二项式定理学习

1.二项式定理 这个就是二项式定理的重要公式&#xff0c;我们的二项式定理的每一项的系数&#xff0c;代表的意思为从n个里面选出k个 &#xff0c;以下是来自于百度百科上面的解释&#xff08;原谅我实在不会数学定义&#xff09; 因此我们可以去讨论二项式定理中的最特殊的一种…

深入解析LlamaIndex Workflows【下篇】:实现ReAct模式AI智能体的新方法

之前我们介绍了来自LLM开发框架LlamaIndex的新特性&#xff1a;Workflows&#xff0c;一种事件驱动、用于构建复杂AI工作流应用的新方法&#xff08;参考&#xff1a;[深入解析LlamaIndex Workflows&#xff1a;构建复杂RAG与智能体工作流的新利器【上篇】]。在本篇中&#xff…

如何自制无人机?

自制无人机是一个既有趣又富有挑战性的项目&#xff0c;它涉及到电子工程、机械工程和航空航天工程等多个领域的知识。以下是一个基本的自制无人机制作步骤和所需材料概览&#xff0c;供您参考&#xff1a; 一、准备阶段 1. 明确目标 - 确定无人机的用途&#xff08;如航拍、…

基于SSM框架和Layui的学院课程安排系统的设计与实现(源码+定制+定制)

博主介绍&#xff1a; ✌我是阿龙&#xff0c;一名专注于Java技术领域的程序员&#xff0c;全网拥有10W粉丝。作为CSDN特邀作者、博客专家、新星计划导师&#xff0c;我在计算机毕业设计开发方面积累了丰富的经验。同时&#xff0c;我也是掘金、华为云、阿里云、InfoQ等平台…

Element UI教程:如何将Radio单选框的圆框改为方框

大家好&#xff0c;今天给大家带来一篇关于Element UI的使用技巧。在项目中&#xff0c;我们经常会用到Radio单选框组件&#xff0c;默认情况下&#xff0c;Radio单选框的样式是圆框。但有时候&#xff0c;为了满足设计需求&#xff0c;我们需要将圆框改为方框&#xff0c;如下…

SkyWalking 自定义链路追踪

对项目中的业务方法&#xff0c;实现链路追踪&#xff0c;方便我们排查问题 引入依赖 <!‐‐ SkyWalking 工具类 ‐‐> <dependency> <groupId>org.apache.skywalking</groupId> <artifactId>apm‐toolkit‐trace</artifactId> <vers…

【算法】博弈论(C/C++)

个人主页&#xff1a;摆烂小白敲代码 创作领域&#xff1a;算法、C/C 持续更新算法领域的文章&#xff0c;让博主在您的算法之路上祝您一臂之力 欢迎各位大佬莅临我的博客&#xff0c;您的关注、点赞、收藏、评论是我持续创作最大的动力 目录 博弈论&#xff1a; 1. Grundy数…

蓝牙模块(BT04/HC05)

目录 一、介绍 二、模块原理 1.原理图与外形尺寸 2.引脚描述 3.蓝牙模块基础AT指令介绍 三、程序设计 usart3.h文件 usart3.c文件 四、实验效果 五、资料获取 项目分享 一、介绍 BT04A是一款蓝牙低功耗&#xff08;Bluetooth Low Energy, BLE&#xff09;模块&…

继电保护之电压重动、电压并列和电压切换

实践&#xff1a;以某开关室10kV母联隔离柜为例&#xff1a; ZYQ-824为PT并列装置&#xff0c;装置内包含一系列继电器&#xff0c;用于PT重动及并列。按照装置编号原则&#xff0c;交流电压切换箱一般命名为7n。 ​下图为装置内继电器线圈部分接线&#xff1a; 下图为装置内…