【项目】棋海争锋

news2024/11/28 10:45:47

  • 🎥 个人主页:Dikz12
  • 📕格言:吾愚多不敏,而愿加学
  • 欢迎大家👍点赞✍评论⭐收藏

目录

项目介绍

WebSocket介绍

 使用

项目创建 

数据库设计  

用户模块 

登录接口

 注册接口

获取用户信息接口

匹配模块

websocket交互接口

创建并配置 websocket 请求的入口类

创建匹配请求/响应对象

创建用户管理器 

 处理websocket连接成功

处理开始匹配/取消匹配请求

处理连接关闭和连接异常

对战模块 

websocket交互接口

创建并配置 websocket 请求的入口类

创建Room对象(游戏房间) 

创建落子请求/响应对象

 处理连接成功

处理连接异常和连接关闭  

 处理落子请求

实现对弈功能

 主要的判定输赢逻辑

​编辑项目验证


项目介绍

棋海争锋:实现一个网页版的五子棋对战,分为三个模块:用户模块、匹配模块、对战模块.

技术栈:Spring、Spring Boot、SpringMVC、MyBatis、 MySQL、WebSocket

 前端:html、css、js、ajax.

WebSocket介绍

传统的Http协议是一种“一问一答”结构的协议模型,也就是客户端发送一个请求,服务器就会回一个响应,这种情况下,服务器是不能主动给客户端发消息的;

而想这个项目,五子棋双人对战,是需要服务器主动给客户端发消息这种场景的,就可以使用WebSocket来实现“消息推送机制”.

而 WebSocket 则是更接近于 TCP 这种级别的通信方式. 一旦连接建立完成, 客户端或者服务器都可以主动的向对方发送数据.

 使用

 Spring 内置了 Websocket,可以直接使用;

比如:Test 类继承 TextWebsocketHandler,在实现四个方法,还需要把该配置告诉Spring,这是WebSocket配置。

@Component
public class TestSocket extends TextWebSocketHandler {
    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        System.out.println("连接成功");
    }

    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        System.out.println("收到消息: "+message.getPayload());
    }

    @Override
    public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
        System.out.println("连接异常");
    }

    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        System.out.println("连接关闭");
    }
}
@Configuration
@EnableWebSocket //告诉Spring,这是WebSocket配置
public class WebSocketConfig implements WebSocketConfigurer {
    @Autowired
    private TestSocket testSocket;


    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        //当前前断页面访问这个url,就会执行 TestSocket这个类
        registry.addHandler(testSocket, "/test");
    }
}

 效果:

WebSocket实现消息推送最重要的就是掌握四个关键节点,也就是四个核心时机;

建立连接之后、收到消息之后、出现连接异常、出现连接关闭.

项目创建 

在创建项目Spring Boot项目时时引入Spring Web、MyBatis、MySQL、WebSocket、以及Lombok相关依赖.

数据库设计  

数据库就基于项目功能进行简单设计,需要用到id(自增主键)、用户名、用户密码、天梯分、总场数、获胜场数;也就是一个简单的实体表; 

create table user
(
    id          int primary key auto_increment,
    user_name   varchar(20) unique,
    password    varchar(128),
    score       int, -- 积分
    total_count int, -- 总场数
    win_count   int -- 胜利场数
) ENGINE = INNODB DEFAULT CHARSET = utf8mb4;

用户模块 

 该模块用实现用户注册和登录功能,以及管理用户的天梯分数、总场数、获胜场数;使用 MySQL 数据库存储数据,服务器端基于 Spring + MyBatis 来实现数据库的增删改查.

登录接口

[请求]
/user/login?userName=?&&password=?

[响应]
{
    "id": 6,
    "userName": "gaodong",
    "password": "d6ea380dee1f46758bf13b90a9aeb011bb0d40e9d84f042182e6a60cda9a0d27",
    "score": 100,
    "totalCount": 0,
    "winCount": 0
}

 注册接口

[请求]
/user/register?userName=?&&password=?

[响应]
{
    "id": 0,
    "userName": "cboh",
    "password": "b6e029af45e949ad9a221a111c237e6290c0c4620c2fa242fe3daa401092c8c0",
    "score": 0,
    "totalCount": 0,
    "winCount": 0
}

 这里密码进行了加密处理,采用的是摘要算法:MD5算法。(密码加随机盐生成一个MD5)

获取用户信息接口

用来实现登录成功后显示用户分数的信息.

[请求]
/user/getUserInfo

[响应]
{
    "id": 6,
    "userName": "gaodong",
    "password": "d6ea380dee1f46758bf13b90a9aeb011bb0d40e9d84f042182e6a60cda9a0d27",
    "score": 100,
    "totalCount": 4,
    "winCount": 2
}

 后端代码实现:

    @Autowired
    private UserService userService;

    /**
     * 登录
     * @param userName
     * @param password
     * @return
     */
    @ResponseBody
    @RequestMapping(  "/login")
    public Object login(String userName, String password, HttpServletRequest request) {
        UserInfo user = userService.selectByName(userName);
        if (user == null || !SecurityUtils.verifyUser(password,user.getPassword())) {
            return new UserInfo();
        }
        //存session
        HttpSession session = request.getSession(true); //没有会话就创建一个会话
        session.setAttribute("user",user);
        log.info("密码正确");
        return user;
    }

    /**
     * 注册
     * @param userName
     * @param password
     * @return
     */
    @ResponseBody
    @RequestMapping("/register")
    public UserInfo register(String userName,String password) {
        UserInfo user = userService.insertUser(userName, password);
        return user;
    }

    /**
     * 获取用户信息  用来实现登录成功后显示用户分数的信息.
     */
    @RequestMapping("/getUserInfo")
    public Object getUserInfo(HttpServletRequest request) {
        try {
            HttpSession httpSession = request.getSession(false);
            UserInfo user = (UserInfo) httpSession.getAttribute("user");
            // 拿着这个 user 对象, 去数据库中找, 找到最新的数据
            UserInfo newUser = userService.selectByName(user.getUserName());
            return newUser;
        } catch (NullPointerException e) {
            return new UserInfo();
        }
    }

匹配模块

用户登录成功, 则进入游戏大厅页面.

游戏大厅中, 能够显示用户的名字, 天梯分数, 比赛场数和获胜场数.

同时显示一个 "匹配按钮".

点击匹配按钮则用户进入匹配队列, 并且界面上显示为 "取消匹配" .

再次点击则把用户从匹配队列中删除.

如果匹配成功, 则跳转进入到游戏房间页面.

页面加载时和服务器建立 websocket 连接. 双方通过 websocket 来传输 "开始匹配", "取消匹配", "匹配成功" 这样的信息.

websocket交互接口

[连接]
ws://127.0.0.1:8080/findMatch

[请求]
{
    message: 'startMatch' / 'stopMatch',
}

[响应1] //收到请求后响应
{
    ok: true,                // 是否成功. 比如用户 id 不存在, 则返回 false
    reason: '',                // 错误原因
    message: 'startMatch' / 'stopMatch'
}

[响应2] //匹配成功后的响应
{
    ok: true,                // 是否成功. 成功true, 否则 false
    reason: '',                // 错误原因
    message: 'matchSuccess',    
}
  • 页面这端拿到匹配响应之后, 就跳转到游戏房间.
  • 如果返回的响应 ok 为 false, 则弹框的方式显示错误原因, 并跳转到登录页面.

创建并配置 websocket 请求的入口类

该类继承自  TextWebsocketHandler,作为处理 websocket 请求的入口类;

ObjectMapper, 后续用来处理 JSON 数据(字符串和对象之间的转换)

@Component
public class MatchAPI extends TextWebSocketHandler {
    private ObjectMapper objectMapper = new ObjectMapper();

    @Component
    public class MatchAPI extends TextWebSocketHandler {
    }

    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
    }

    @Override
    public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
    }

    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
    }
}

配置: 

在 addHandler 之后,在加上 new HttpSessionHandshakeIntercept0r(), 这样可以把之前登       录过程中往 HttpSession 中存放的session, 放到 WebSocket 的 session 中. 方便后面的代码中获取到当前用户信息.

创建匹配请求/响应对象

@Data
public class MatchRequest {
    /**
     * 表示一个websocket匹配请求
     */
    private String message = "";
}
@Data
public class MatchResponse {
    /**
     * 表示一个websocket匹配响应
     */
    private boolean ok = true;
    private String reason;  //错误信息
    private String message; // 发送的消息
}

创建用户管理器 

 用于管理当前用户的在线状态. 本质上是 哈希表 的结构. key 为用户 id, value 为用户的 WebSocketSession.借助这个类, 一方面可以判定用户是否是在线, 同时也可以进行方便的获取到 Session 从而给客户端回话.

  • 当玩家建立好 websocket 连接, 则将键值对加入 OnlineUserManager 中.
  • 当玩家断开 websocket 连接, 则将键值对从 OnlineUserManager 中删除.
  • 在玩家连接好的过程中, 随时可以通过 userId 来查询到对应的会话, 以便向客户端返回数据.

由于存在两个页面, 游戏大厅和游戏房间, 使用两个 哈希表 来分别存储两部分的会话.

@Component
public class OnlineUserManager {
    /**
     * 使用一个哈希表,来保存当前用户的在线状态:
     *    key -> id    value -> websocket会话
     */
    // 多个用户和服务器建立连接或者断开连接,此时服务器并发执行对HashMap进行修改
     //会产生线程安全问题 改用ConcurrentHashMap
    ConcurrentHashMap<Integer, WebSocketSession> gameHall = new ConcurrentHashMap<>();
    //表示在用户在游戏放假的在线状态
    ConcurrentHashMap<Integer, WebSocketSession> gameRoom = new ConcurrentHashMap<>();

    //上线
    public void enterGameHall(Integer id,WebSocketSession webSocketSession) {
        gameHall.put(id,webSocketSession);

    }
    //退出
    public void exitGameHall(Integer id) {
        gameHall.remove(id);
    }
    //查找
    public WebSocketSession getFromGameHall(Integer id) {
        return gameHall.get(id);
    }

    public void enterGameRoom(Integer id,WebSocketSession webSocketSession) {
        gameRoom.put(id,webSocketSession);
    }

    public void exitGameRoom(Integer id) {
        gameRoom.remove(id);
    }
    public WebSocketSession getSessionFromGameRoom(Integer id) {
        return gameRoom.get(id);
    }
}

 处理websocket连接成功

 实现 afterConnectionEstablished 方法.

  • 通过参数中的 session 对象, 拿到之前登录时设置的 用户 信息.
  • 使用 onlineUserManager 来管理用户的在线状态.
  • 禁止多开,采用账号登录成功之后,禁止在其它地方在登陆的方式;先判定用户是否是已经在线, 如果在线则直接返回出错 .
  • 设置玩家为上线状态.
    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        //玩家上线,添加到 onlineUserManager中
        try{
            // 1.先获取到当前的用户信息(谁在游戏大厅中,建立连接)
            UserInfo userInfo =  (UserInfo) session.getAttributes().get("user");
            // 当用户登录成功之后,禁止多开; 采用的是禁止在其它地方登录的方式(根源解决问题)
            if (onlineUserManager.getFromGameHall(userInfo.getId()) != null
                    || onlineUserManager.getSessionFromGameRoom(userInfo.getId()) != null) {
                //此时,用户已经登录了
                MatchResponse matchResponse = new MatchResponse();
                matchResponse.setOk(true);
                matchResponse.setReason("禁止多开!!");
                matchResponse.setMessage("repeatConnection");
                session.sendMessage(new TextMessage(objectMapper.writeValueAsString(matchResponse)));
                return;
            }
            //userInfo 可能为null,没有经过登录操作,直接通过url访问game_hall.html
            // 2. 拿到用户信息之后,设置成在线状态
            onlineUserManager.enterGameHall(userInfo.getId(),session);
            System.out.println("玩家进入游戏大厅userName: "+userInfo.getUserName());
        } catch (NullPointerException e) {
            System.out.println("[MatchAPI.afterConnectionEstablished] 当前用户未登录!");
//            e.printStackTrace(); //可控异常
            MatchResponse matchResponse = new MatchResponse();
            matchResponse.setOk(false);
            matchResponse.setReason("你尚未登录!不能进行匹配操作!!!");
            //先通过 ObjectMapper把MatchRequest转换成JSON字符串,在包装上一层TextMessage
            session.sendMessage(new TextMessage(objectMapper.writeValueAsString(matchResponse)));
        }
    }

处理开始匹配/取消匹配请求

  • 先从会话中拿到当前玩家的信息.
  • 解析客户端发来的请求
  • 判定请求的类型, 如果是 startMatch, 则把用户对象加入到匹配队列. 如果是 stopMatch, 则把用户对象从匹配队列中删除.
  • 此处需要实现一个 匹配器 对象, 来处理匹配的实际逻辑.

匹配器对象:

1.匹配队列这里采用的是搞三个队列, 根据玩家分数分为三个区间:

Normal:score < 500

High:score >= 500 && score < 1000

VeryHigh:score >= 1000

2.接下来还要在搞三个线程,针对三个队列进行扫描(判断队列是否 > 2),就可以把这一对玩家取出来,放到游戏房间里。

3.采用多线程编程要多考虑,线程安全安全问题(websocket本身就属于一个线程了,有写了一组线程,所以会出现线程安全问题),多个线程访问不同的队列不会出现线程安全问题,必须是多个线程操作同一个队列,才需要进行加锁。所以,使用synchronized选取加锁对象时,就可以选取这三个队列对象本身。

4.避免出现忙等问题。引入wait()和notify(), 线程如入口使用 wait 来等待, 直到队列中达到 2 个元素及其以上, 才唤醒线程消费队列.

 @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        UserInfo userInfo = (UserInfo) session.getAttributes().get("user");
        //获取到客户端发送的数据内容
        String payload = message.getPayload();
        //当前这个数据载荷是一个JSON字符串,就需要转换成Java对象,MatchRequest 对象
        MatchRequest matchRequest = objectMapper.readValue(payload, MatchRequest.class);
        MatchResponse response = new MatchResponse();
        if (matchRequest.getMessage().equals("startMatch")) {
            //进入匹配队列
            //先创建一个匹配队列,把当前用户加进去
            matcher.add(userInfo);
            //就可以个客户端一个响应
            response.setOk(true);
            response.setMessage("startMatch");
        } else if(matchRequest.getMessage().equals("stopMatch")) {
            //退出匹配对队列
            // 把用户从匹配队列中移出
            matcher.remove(userInfo);
            // 返回个客户端一个响应
            response.setOk(true);
            response.setMessage("stopMatch");
        } else {
            //非法请求
            response.setOk(false);
            response.setReason("非法的匹配请求");
        }
        String jsonStr = objectMapper.writeValueAsString(response);
        session.sendMessage(new TextMessage(jsonStr));

    }
   //主要的处理逻辑
    private void handleMatch(Queue<UserInfo> matchQueue) {
        synchronized (matchQueue) {
            try {
                //队列初始情况,可能为null,往队列里添加一个元素是不能往下执行的
                // 所以此处,使用while
                while (matchQueue.size() < 2) {
                    matchQueue.wait();
                }
                // 1.从队列中取出两个玩家
                UserInfo player1 = matchQueue.poll();
                UserInfo player2 = matchQueue.poll();
               log.info("匹配到了两个玩家: " + player1.getUserName() + " ," + player2.getUserName());

                // 2.拿到玩家的websocket会话 目的就是为了告诉玩家,匹配到对手了
                WebSocketSession session1 = onlineUserManager.getFromGameHall(player1.getId());
                WebSocketSession session2 = onlineUserManager.getFromGameHall(player2.getId());
                //理论上,匹配队列中的玩家一定是在线状态, 前面逻辑以及处理过
                // 最后还是在进行判定,尽量避免出现bug
                if (session1 == null) {
                    //玩家1,下线需要把玩家二重新放到匹配队列中
                    matchQueue.offer(player2);
                    return;
                }
                if (session2 == null) {
                    matchQueue.offer(player1);
                    return;
                }
                //这种情况理论上也不存在 1.前面的逻辑已经避免多开 2.玩家下线,就会移出匹配队列
                if (session1 == session2) {
                    matchQueue.offer(player1);
                    return;
                }
                // 3.匹配成功,把两个玩家放到游戏房间
                Room room = new Room();
                roomManager.addRoom(room,player1.getId(), player2.getId());

                // 4.给玩家反馈信息,告诉玩家你匹配成功了
                // 通过websocket 返回一个Message (JSON字符串) 为 "matchSuccess"的响应
                MatchResponse response1 = new MatchResponse();
                response1.setMessage("matchSuccess");
                String json1 = objectMapper.writeValueAsString(response1);
                session1.sendMessage(new TextMessage(json1));

                MatchResponse response2 = new MatchResponse();
                response2.setMessage("matchSuccess");
                String json2 = objectMapper.writeValueAsString(response2);
                session2.sendMessage(new TextMessage(json2));

            } catch (IOException | InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

处理连接关闭和连接异常

  • 主要的工作就是把玩家从 onlineUserManager 中退出.
  • 退出的时候要注意判定, 当前玩家是否是多开的情况(一个userId, 对应到两个 websocket 连接). 如果一个玩家开启了第二个 websocket 连接, 那么这第二个 websocket 连接不会影响到玩家从 OnlineUserManager 中退出.
  • 如果玩家当前在匹配队列中, 则直接从匹配队列里移除.

对战模块 

玩家匹配成功, 则进入游戏房间页面.

每两个玩家在同一个游戏房间中.

在游戏房间页面中, 能够显示五子棋棋盘. 玩家点击棋盘上的位置实现落子功能.

并且五子连珠则触发胜负判定, 显示 "你赢了" "你输了".

页面加载时和服务器建立 websocket 连接. 双方通过 websocket 来传输 "准备就绪", "落子位置", "胜负" 这样的信息.

  • 准备就绪: 两个玩家均连上游戏房间的 websocket 时, 则认为双方准备就绪.
  • 落子位置: 有一方玩家落子时, 会通过 websocket 给服务器发送落子的用户信息和落子位置, 同时服务器再将这样的信息返回给房间内的双方客户端. 然后客户端根据服务器的响应来绘制棋子位置.
  • 胜负: 服务器判定这一局游戏的胜负关系. 如果某一方玩家落子, 产生了五子连珠, 则判定胜负并返回胜负信息. 或者如果某一方玩家掉线(比如关闭页面), 也会判定对方获胜

websocket交互接口

[连接]

ws://127.0.0.1:8080/game
[连接响应]

当两个玩家都连接好了, 则给双方都返回一个数据表示就绪

{
    message: 'gameReady',    // 游戏就绪
    ok: true,                // 是否成功. 
    reason: '',                // 错误原因
    roomId: 'abcdef',        // 房间号. 用来辅助调试. 
    thisUserId: 1,            // 玩家自己的 id
    thatUserId: 2,            // 对手的 id
    whiteUser: 1,            // 先手方的 id
}
[落子请求]

{
    message: 'putChess',
    userId: 1,
    row: 0,
    col: 0
}
[落子响应]

{
    message: 'putChess',
    userId: 1,    
    row: 0,
    col: 0, 
    winner: 0
}

创建并配置 websocket 请求的入口类

 跟匹配模块的操作一样,这里就不在进行过多阐述!!

创建Room对象(游戏房间) 

public class Room {
    /**
     * 这个类表示游戏房间
     */
    //用String,方便保证房间号唯一
    private String roomId;

    //玩家一
    private UserInfo userInfo1;
    //玩家二
    private UserInfo userInfo2;
    // 先手方的id
    private Integer whiteUser;

      public Room() {
        // 生成随机盐,保证房间号唯一
        roomId = UUID.randomUUID().toString();

        //Room对象不能唯一,不能使用注解交给Spring管理
        //所以通过入口类中记录的context来手动获取到前面的RoomManager和 OnlineUserManager
        onlineUserManager = JavaGobangApplication.context.getBean(OnlineUserManager.class);
        roomManager = JavaGobangApplication.context.getBean(RoomManager.class);
        userInfoMapper = JavaGobangApplication.context.getBean(UserInfoMapper.class);
     }
}

创建落子请求/响应对象

@Data
//客户端连接到游戏房间后,服务器返回的响应
public class GameReadyResponse {
    private String message;
    private boolean ok;
    private String reason;
    private String roomId;
    //当前用户id
    private Integer thisUserId;
    //对手id
    private Integer thatUserId;
    // 先手方的id
    private Integer whiteUser;
}
@Data
//落子响应类
public class GameResponse {
    private String message;
    private Integer userId;
    private int row;
    private int col;
    // 判断谁获胜
    private Integer winner;
}
@Data
//表示落子请求类
public class GameRequest {
    private  String message;
    private Integer userId;
    private int row;
    private int col;
}

 处理连接成功

  • 首先需要检测用户的登录状态. 从 Session 中拿到当前用户信息.
  • 然后要判定当前玩家是否是在房间中.
  • 接下来进行多开判定.如果玩家已经在游戏中, 则不能再次连接.
  • 把两个玩家放到对应的房间对象中. 当两个玩家都建立了连接, 房间就放满了.这个时候通知两个玩家双方都准备就绪.
  • 如果有第三个玩家尝试也想加入房间, 则给出一个提示, 房间已经满了.
  public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        GameReadyResponse response = new GameReadyResponse();
        //1.先获取用户信息
        UserInfo userInfo = (UserInfo) session.getAttributes().get("user");
        if (userInfo == null) {
            response.setOk(false);
            response.setReason("用户未登录!");
            session.sendMessage(new TextMessage(objectMapper.writeValueAsString(response)));
            return;
        }
        //2.通过用户id,查询是否已经进入游戏房间
        Room room = roomManager.getRoomByUserId(userInfo.getId());
        if (room == null) {
            //没有找到房间,表示玩家还在匹配中
            response.setOk(false);
            response.setReason("尚未匹配到棋友!");
            session.sendMessage(new TextMessage(objectMapper.writeValueAsString(response)));
            return;
        }
        // 3.判定当前用户是不是多开
        if (onlineUserManager.getFromGameHall(userInfo.getId()) != null
                || onlineUserManager.getSessionFromGameRoom(userInfo.getId()) != null) {
            // 一个账号,一边在游戏大厅,一边在游戏房间,也认为是多开
            response.setOk(true);
            response.setReason("禁止多开游戏页面!");
            response.setMessage("repeatConnection");
            session.sendMessage(new TextMessage(objectMapper.writeValueAsString(response)));
            return;
        }
        // 4.把玩家设置成上线
        onlineUserManager.enterGameRoom(userInfo.getId(), session);

        // 5.把两个玩家加入到游戏房间中
        //    前面的创建房间/匹配过程, 是在 game_hall.html 页面中完成的.
        //    因此前面匹配到对手之后, 需要经过页面跳转, 来到 game_room.html 才算正式进入游戏房间(才算玩家准备就绪)
        //    当前这个逻辑是在 game_room.html 页面加载的时候进行的.
        //    执行到当前逻辑, 说明玩家已经页面跳转成功了!!
        synchronized (room) {
            if (room.getUserInfo1() == null) {
                //第一个玩家加入房间
                room.setUserInfo1(userInfo);
                //先进的设置为先手
                room.setWhiteUser(userInfo.getId());
                log.info("玩家 " + userInfo.getUserName() + " 已经准备就绪! 作为玩家1");
                return;
            }
            if (room.getUserInfo2() == null) {
                //第二个玩家进入房间
                room.setUserInfo2(userInfo);
                room.setWhiteUser(userInfo.getId());
                log.info("玩家 " + userInfo.getUserName() + " 已经准备就绪! 作为玩家2");

                //当两个玩家都准备好了,服务器就需要个两个客户端发送响应
                //玩家1
                noticeGameReady(room,room.getUserInfo1(),room.getUserInfo2());
                //玩家2
                noticeGameReady(room,room.getUserInfo2(),room.getUserInfo1());
                return;
            }
        }
        // 6. 此处如果又有玩家尝试连接同一个房间, 就提示报错.
        //    这种情况理论上是不存在的, 还是做一个判定和提示.
        response.setOk(false);
        response.setReason("当时房间已经满了,加入房间失败");
        session.sendMessage(new TextMessage(objectMapper.writeValueAsString(response)));

处理连接异常和连接关闭  

关闭和异常情况的处理一样! 

    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        UserInfo userInfo = (UserInfo) session.getAttributes().get("user");
        if (userInfo == null) {
            return;
        }
        WebSocketSession existSession = onlineUserManager.getSessionFromGameRoom(userInfo.getId());
        if (existSession == session) {
            onlineUserManager.exitGameRoom(userInfo.getId());
        }
        log.info("用户退出! userId=" + userInfo.getId());

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

 处理落子请求

  •  先获取到用户信息
  • 根据玩家 id 获取到房间对象
  • 通过 room 对象来处理这次具体的请求 
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        // 1.先获取到用户信息
       UserInfo userInfo  = (UserInfo)session.getAttributes().get("user");
       if (userInfo == null) {
           log.info("GameAPI[handleTextMessage]: 玩家未登录!");
           return;
       }
        // 2. 根据玩家 id 获取到房间对象
        Room room = roomManager.getRoomByUserId(userInfo.getId());
       if (room == null) {
           return;
       }
        // 3. 通过 room 对象来处理这次具体的请求
        room.putChess(message.getPayload());
    }

实现对弈功能

实现 room 中的 putChess 方法.

  • 先把请求解析成请求对象.
  • 根据请求对象中的信息, 往棋盘上落子.(判定当前子的位置是用户1的还是用户2的)
  • 落子完毕之后, 为了方便调试, 可以打印出棋盘的当前状况.
  • 判定胜负操作.(约定如果玩家1获胜,返回玩家1的用户id,玩家二获胜就返回玩家二的用户id;胜负未分就返回0)
  • 构造落子响应, 写回给每个玩家.
  • 写回的时候如果发现某个玩家掉线, 则判定另一方为获胜.
  • 如果游戏胜负已分, 则修改玩家的分数, 并销毁房间.

 主要的判定输赢逻辑

输赢逻辑判定操作写在服务器进行. 赢分为如下3中情况:

  1. 对角线(左和右) 

 情况一:

情况二:

情况三: 

项目验证

登录成功界面: 

 

 匹配界面:

判定胜负界面:

 分数更新成功

 

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

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

相关文章

Java每日一题(三道同一类型的题)

前言 本文一共有三道题:1.两数之和 2.三数之和 3. 四数之和 为什么把这三道题放一起呢&#xff0c;因为三数之和是可以根据两数之和进行推导&#xff0c;四数之和可以根据三数之和进行推导。 两数之和 思路分析: 我的思路: 1.排序 2.使用左右指针 3.处理细节问题 先让数组…

【C++成长记】C++入门 |函数重载、引用、内联函数

&#x1f40c;博主主页&#xff1a;&#x1f40c;​倔强的大蜗牛&#x1f40c;​ &#x1f4da;专栏分类&#xff1a;C❤️感谢大家点赞&#x1f44d;收藏⭐评论✍️ 目录 一、函数重载 1、函数重载概念 二、引用 1、引用概念 2、引用特性 3、常引用 4、使用场景 5、…

【C++杂货铺】详解 stack 和 queue

&#x1f308;前言&#x1f308; 欢迎收看本期【C杂货铺】&#xff0c;本期内容将讲解CSTL中stack和queue的内容&#xff0c;其中包含了stack &#xff0c; queue&#xff0c;priority_queue是什么&#xff0c;怎么使用以及模拟实现这些容器。 此外&#xff0c;还将将讲解设计模…

秋叶Stable diffusion的创世工具安装-带安装包链接

来自B站up秋葉aaaki&#xff0c;近期发布了Stable Diffusion整合包v4.7版本&#xff0c;一键在本地部署Stable Diffusion&#xff01;&#xff01; 适用于零基础想要使用AI绘画的小伙伴~本整合包支持SDXL&#xff0c;预装多种必须模型。无需安装git、python、cuda等任何内容&am…

wordpress全站开发指南-面向开发者及深度用户(全中文实操)--wordpress中的著名循环

wordpress中的著名循环 首先&#xff0c;在深入研究任何代码之前&#xff0c;我们首先要确保我们有不止一篇博客文章可以工作。因此&#xff0c;我们要去自己的wordpress站点&#xff0c;从侧边栏单机Posts(文章)&#xff0c;进行创建 在执行代码的时候会优先执行single.php如…

【算法刷题】八大排序算法总结(冒泡、选择、插入、二分插入、归并、快速、希尔、堆排序)

文章目录 八大排序算法总结1.冒泡排序2.选择排序3.插入排序4.二分插入排序5.归并排序6.快速排序7.希尔排序8.堆排序 八大排序算法总结 排序排序方法平均情况最好情况最坏情况空间稳定性1冒泡排序O(n2)O(n)O(n2)O(1)稳定2选择排序O(n2)O(n2)O(n2)O(1)不稳定3插入排序O(n2)O(n)O…

生活中的数学 --- 等额本息贷款和等额本金贷款的月供应该怎么算?

等额本息贷款和等额本金贷款的月供应该怎么算&#xff1f; 从一个例子开始&#xff0c;假设我要从银行贷款36万(即&#xff0c;本金)&#xff0c;银行给出的贷款年利率是12%(月利率为年利率除以12)&#xff0c;贷款半年(6个月)&#xff0c;按月还款&#xff0c;分6期还完。 问分…

go websocket

WebSocket 是一种网络协议&#xff0c;建立在 HTTP 协议之上&#xff0c;允许双向通信。WebSocket 协议允许服务器发送数据到客户端&#xff0c;同时也可以让客户端向服务器发送数据。WebSocket 使用 HTTP 协议的升级请求和响应来建立连接。WebSocket 的主要优点在于它可以通过…

【Node.js】短链接

原文链接&#xff1a;Nodejs 第六十二章&#xff08;短链接&#xff09; - 掘金 (juejin.cn) 短链接是一种缩短长网址的方法&#xff0c;将原始的长网址转换为更短的形式。短链接的主要用途之一是在社交媒体平台进行链接分享。由于这些平台对字符数量有限制&#xff0c;长网址可…

Lua热更新(AssetBundle)

AssetBundle 新版本导入ab包报错,则删除其中的Tests文件夹。 给资源分组 打包设置:平台、路径、重复打包清空文件夹、复制到streaming文件夹 建议勾选 建议使用LZ4压缩方式 用来观察文件中的包大小,不常用 参数总结: 这六个只做了解,重要的是上面的

kubesphere部署(apple m1 m2 m3)

背景&#xff1a;使用一个命令kk(KubeKey)同时快速安装 Kubernetes 和 KubeSphere的集成环境&#xff0c;提高效率&#xff0c;减少部署时所花费的精力。这里环境为apple m2 一、KubeSphere简介 KubeSphere 是在 Kubernetes 之上构建的面向云原生应用的分布式操作系统&#x…

YOLOv7全网独家改进: 卷积魔改 | 变形条状卷积,魔改DCNv3二次创新

💡💡💡本文独家改进: 变形条状卷积,DCNv3改进版本,不降低精度的前提下相比较DCNv3大幅度运算速度 💡💡💡强烈推荐:先到先得,paper级创新,直接使用; 💡💡💡创新点:1)去掉DCNv3中的Mask;2)空间域上的双线性插值转改为轴上的线性插值; 💡💡💡…

人工智能——深度学习

4. 深度学习 4.1. 概念 深度学习是一种机器学习的分支&#xff0c;旨在通过构建和训练多层神经网络模型来实现数据的高级特征表达和复杂模式识别。与传统机器学习算法相比&#xff0c;深度学习具有以下特点&#xff1a; 多层表示学习&#xff1a;深度学习使用深层神经网络&a…

智过网:报考中级注册安全工程师需要什么条件?

随着社会的快速发展和科技的日新月异&#xff0c;安全生产问题越来越受到人们的关注。中级注册安全工程师作为专业安全管理人才&#xff0c;其职责与角色日益凸显。那么&#xff0c;想要报考中级注册安全工程师&#xff0c;需要满足哪些条件呢&#xff1f; 首先&#xff0c;报考…

lanqiao.125卡片换位(2016年蓝桥杯C/C++省赛C组)

题目&#xff1a; 语法点&#xff1a; 1. unordered_map<string,int> dist; //存储图的不同状态及不同状态对应的步数 2. unordered_map的相关操作&#xff0c;详细见C中的unordered_map用法详解-CSDN博客 dist.count(x) //来寻找x出现的次数 dist.find(x) //来…

STM32学习和实践笔记(6):自己进行时钟配置的思路

在《STM32学习和实践笔记&#xff08;4&#xff09;: 分析和理解GPIO_InitTypeDef GPIO_InitStructure (d)-CSDN博客》 中&#xff0c;我了解到&#xff0c;在程序执行我们写的main函数之前&#xff0c;实际上先执行了一个汇编语言所写的启动文件&#xff0c;以完成相应的初始…

51单片机学习笔记15 LCD12864(带字库)显示屏使用

51单片机学习笔记15 LCD12864&#xff08;带字库&#xff09;显示屏使用 一、LCD12864简介二、管脚定义三、命令1. 功能能设定2. 清屏指令&#xff08;0x01&#xff09;3. 地址归位4. 进入设定点5. 显示状态开关6. 设定CGRAM地址7. 设定DDRAM地址8. 写资料到RAM9. 读出RAM 四、…

Flink WordCount实践

目录 前提条件 基本准备 批处理API实现WordCount 流处理API实现WordCount 数据源是文件 数据源是socket文本流 打包 提交到集群运行 命令行提交作业 Web UI提交作业 上传代码到gitee 前提条件 Windows安装好jdk8、Maven3、IDEA Linux安装好Flink集群&#xff0c;可…

37-代码测试(下):Go语言其他测试类型及IAM测试介绍

。 Go中的两类测试&#xff1a;单元测试和性能测试。 我就来介绍下Go 语言中的其他测试类型&#xff1a;示例测试、TestMain函数、Mock测试、Fake测试等&#xff0c; 示例测试 示例测试以Example开头&#xff0c;没有输入和返回参数&#xff0c;通常保存在example_test.go…

Verilog实现手表计时

实现手表的计时功能&#xff1a; 1.具有start启动信号、pause暂停信号&#xff0c;可以自定义其触发机制。 2.具有时间更改接口&#xff0c;可以更改时、分、秒。 3.输出时、分、秒。 Verilog设计 模块端口定义&#xff1a; module watch1(input wire clk …