0


基于 SpringBoot + MyBatis 的在线五子棋对战

文章目录

1. 项目设计

前端 :

HTML + CSS + JavaScript + Jquery + AJAX

后端 :

Spring MVC + Spring Boot + MyBatis

在这里插入图片描述

2. 效果图展示

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

3. 创建项目以及配置文件

3.1 创建项目

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

3.2 配置文件

3.2.1 在 application.properties 中添加配置文件

spring.datasource.url=jdbc:mysql://localhost:3306/onlineGobang?characterEncoding=utf8&useSSL=true
spring.datasource.username=root
spring.datasource.password=0000
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

mybatis.mapper-locations=classpath:mapper/**Mapper.xml

3.2.2 在 resources 目录下创建mapper

mapper下添加 目录 **.xml 并添加代码

<?xml version="1.0" encoding="UTF-8"?><!DOCTYPEmapperPUBLIC"-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mappernamespace="com.example.onlinemusicserver.mapper."对应的Mapper""></mapper>

4. 数据库设计与实现

在这里插入图片描述
这里使用数据库存储每一个用户的信息, 初始的时候, 天梯分和场次都是默认的.

createdatabaseifnotexists onlineGobang;use onlineGobang;droptableifexistsuser;createtableuser(
    userId intprimarykeyauto_increment,
    username varchar(20)unique,
    password varchar(255)notnull,
    score int,
    totalCount int,
    winCount int);

5. 登录注册模块

5.1 设计登录注册交互接口

登录功能

请求
POST /user/login HTTP/1.1

{username: "",password: ""}

响应
{
    status: 1/-1,
    message: "",
    data: ""
}

注销功能

请求
GET /user/logout HTTP/1.1

响应
HTTP/1.1 200

注册功能

请求
POST /user/register HTTP/1.1

{username: "",password: ""}

响应
{
    status: 1/-1,
    message: "",
    data: ""
}

5.2 设置登录注册功能返回的响应类

通过这个类, 方便前端接收内容

@DatapublicclassResponseBodyMessage<T>{privateint status;privateString message;privateT data;publicResponseBodyMessage(int status,String message,T data){this.status = status;this.message = message;this.data = data;}}

5.3 使用 BCrypt 对密码进行加密

在 pom.xml中添加依赖

<!-- security依赖包 (加密)--><dependency><groupId>org.springframework.security</groupId><artifactId>spring-security-web</artifactId></dependency><dependency><groupId>org.springframework.security</groupId><artifactId>spring-security-config</artifactId></dependency>

在启动类中添加注解

@SpringBootApplication(exclude ={org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration.class})

在 cofig 包下, 创建一个类 AppConfig.

@ConfigurationpublicclassAppConfigimplementsWebMvcConfigurer{@BeanpublicBCryptPasswordEncodergetBCryptPasswordEncoder(){returnnewBCryptPasswordEncoder();}}

5.4 完成 MyBatis 操作

在 model 包中, 创建 User 实体类

@DatapublicclassUser{privateint userId;privateString username;privateString password;privateint score;privateint totalCount;privateint winCount;}

在 mapper 包中, 创建 UserMapper 接口
这个接口中 主要是完成

  1. 注册, 插入一个用户
  2. 登录的时候, 通过名字查询当前用户是否存在.
@MapperpublicinterfaceUserMapper{// 注册一个用户, 初始的天梯积分默认为1000, 场次默认为0intinsert(User user);// 通过username查询当前用户是否存在UserselectByName(String username);}

在 resources 目录下, 创建一个目录 mapper, 在目录下创建 UserMapper.xml
在 UserMapper.xml 中写对应UserMapper接口中对应的操作

<?xml version="1.0" encoding="UTF-8"?><!DOCTYPEmapperPUBLIC"-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mappernamespace="com.example.gobang.mapper.UserMapper"><insertid="insert">
        insert into user values(null,#{username},#{password},1000,0,0)
    </insert><selectid="selectByName"resultType="com.example.gobang.model.User">
        select * from user where username = #{username}
    </select></mapper>

创建 service 包, 在包下创建 UserService 类, 这个类调用 Mapper接口中的方法

@ServicepublicclassUserService{@AutowiredprivateUserMapper userMapper;publicintinsert(User user){return userMapper.insert(user);}publicUserselectByName(String username){return userMapper.selectByName(username);}}

5.5 后端的实现

创建 controller包, 在包下创建一个 UserController 类
这个类是实现登录模块的功能的

  1. 这里需要注入 UserService, 调用数据库中的方法
  2. 还需要注入 BCryptPasswordEncoder, 对密码进行加密和比较

5.5.1 登录功能后端实现

注意这里的登录.

  1. 首先去数据库根据用户名查询是否存在当前用户.
  2. 如果不存在, 登录失败.
  3. 如果存在, 用输入的密码, 和数据库中的密码进行比较, 看是否相等. (注: 数据中的密码是加密的)
  4. 如果不相等, 登录失败.
  5. 如果相等, 创建 session, 并登录成功.
@RequestMapping("/login")publicResponseBodyMessage<User>login(@RequestBodyUser user,HttpServletRequest request){User truUser = userService.selectByName(user.getUsername());if(truUser ==null){System.out.println("登录失败!");returnnewResponseBodyMessage<>(-1,"用户名密码错误!",user);}else{boolean flg = bCryptPasswordEncoder.matches(user.getPassword(),truUser.getPassword());if(!flg){returnnewResponseBodyMessage<>(-1,"用户名密码错误!",user);}System.out.println("登录成功!");HttpSession session = request.getSession(true);
            session.setAttribute(Constant.USER_SESSION_KEY,truUser);returnnewResponseBodyMessage<>(1,"登录成功!",truUser);}}

5.5.2 注册功能后端实现

  1. 首先查看是否该用户是否存在
  2. 存在, 就注册失败
  3. 不存在, 就进行注册, 首先对当前密码进行加密.
  4. 加密之后对这个用户添加到数据库中.
@RequestMapping("/register")publicResponseBodyMessage<User>register(@RequestBodyUser user){User truUser = userService.selectByName(user.getUsername());if(truUser !=null){returnnewResponseBodyMessage<>(-1,"当前用户名已经存在!",user);}else{String password = bCryptPasswordEncoder.encode(user.getPassword());
            user.setPassword(password);
            userService.insert(user);returnnewResponseBodyMessage<>(1,"注册成功!",user);}}

5.5.3 注销功能

直接删除对应session 为

Constant.USER_SESSION_KEY

, 然后跳转到

login.html
@RequestMapping("/logout")publicvoiduserLogout(HttpServletRequest request,HttpServletResponse response)throwsIOException,IOException{HttpSession session = request.getSession(false);// 拦截器的拦截, 所以不可能出现session为空的情况
        session.removeAttribute(Constant.USER_SESSION_KEY);
        response.sendRedirect("login.html");}

注意: 这里的

Constant.USER_SESSION_KEY

是存储的 session 字符串, 由于该 字符串是不变的, 所以存入 Constant 类中.

5.6 前端的实现

5.6.1 登录前端实现

在这里插入图片描述

let loginButton = document.querySelector('#loginButton');
        loginButton.onclick=function(){let username = document.querySelector('#loginUsername');let password = document.querySelector('#loginPassword');if(username.value.trim()==""){alert('请先输入用户名!');
                username.focus();return;}if(password.value.trim()==""){alert('请先输入密码!');
                password.focus();return;}
            $.ajax({url:"user/login",method:"POST",data:JSON.stringify({username: username.value.trim(),password: password.value.trim()}),contentType:"application/json;charset=utf-8",success:function(data, status){if(data.status ==1){
                        location.assign("index.html");}else{alert(data.message);
                        username.value="";
                        password.value="";
                        username.focus();}}})}

5.6.2 注册前端实现

在这里插入图片描述

let Reg = document.querySelector('#Reg');
        Reg.onclick=function(){let username = document.querySelector('#RegUsername');let password1 = document.querySelector('#RegPassword1');let password2 = document.querySelector('#RegPassword2');if(!$('#checkbox').is(':checked')){alert("请勾选条款");return;}if(username.value.trim()==""){alert("请先输入用户名!");
                username.focus();return;}if(password1.value.trim()==""){alert('请先输入密码!');
                password1.focus();return;}if(password2.value.trim()==""){alert('请再次输入密码!');
                password2.focus();return;}if(username.value.trim().length >20){alert("用户名长度过长");
                username.value="";
                username.focus();return;}if(password1.value.trim()!= password2.value.trim()){alert('两次输入的密码不同!');
                passwrod1.value="";
                password2.value="";return;}if(password1.value.trim().length >255){alert("当前密码长度过长!");
                password1.value="";
                password2.value="";
                password1.focus();return;}
            $.ajax({url:"user/register",method:"POST",data:JSON.stringify({username: username.value.trim(),password: password1.value.trim()}),contentType:"application/json;charset=utf-8",success:function(data,status){if(data.status ==1){alert(data.message);
                        location.assign("login.html");}else{alert(data.message);
                        username.value="";
                        password1.value="";
                        password2.value="";
                        username.focus();}}})}

5.7 添加拦截器

LoginIntercepter

publicclassLoginInterceptorimplementsHandlerInterceptor{@OverridepublicbooleanpreHandle(HttpServletRequest request,HttpServletResponse response,Object handler)throwsException{HttpSession session = request.getSession(false);if(session !=null&& session.getAttribute(Constant.USER_SESSION_KEY)!=null){returntrue;}
        response.sendRedirect("/login.html");returnfalse;}}

AppConfig

@Configuration@EnableWebSocketpublicclassAppConfigimplementsWebSocketConfigurer,WebMvcConfigurer{@OverridepublicvoidaddInterceptors(InterceptorRegistry registry){LoginInterceptor loginInterceptor =newLoginInterceptor();
        registry.addInterceptor(loginInterceptor).addPathPatterns("/**").excludePathPatterns("/**/login.html").excludePathPatterns("/**/css/**.css").excludePathPatterns("/**/images/**").excludePathPatterns("/**/fonts/**").excludePathPatterns("/**/js/**.js").excludePathPatterns("/**/scss/**").excludePathPatterns("/**/user/login").excludePathPatterns("/**/user/register").excludePathPatterns("/**/user/logout");}}

6. 大厅界面

6.1 交互接口设计

这里客户端1, 点击匹配发送消息给服务器, 客户端2, 也点击匹配发送消息给服务器, 当服务器收到两个人的请求之后, 就需要服务器主动向客户端发送消息, 这里就需要用到 websocket
在这里插入图片描述

URL: ws://127.0.0.1:8080/findMatch

匹配请求

{
    message: ' startMatch ' / ' stopMatch'
}

匹配响应1 这个响应是点击匹配之后, 立刻返回的响应

{
    status: '1' / '-1'  
    message: ' startMatch ' / ' stopMatch '
}

匹配响应2 这个响应是匹配成功之后的响应

{
    status: '1' / '-1'
    message: 'matchSuccess'
}

6.2 用户加载前后交互接口

请求
GET /user/userInfo HTTP/1.1

响应
{
    status: 1/-1 (1 为成功, -1 为失败),
    message: "对应信息",
    data: "内容",  (用户信息)
}

6.3 前端和后端实现用户信息加载

6.3.1 后端的实现

根据当前存储的session对象, 来查找对应的用户

@RequestMapping("/userInfo")publicResponseBodyMessage<User>getUserInfo(HttpServletRequest request){HttpSession session = request.getSession(false);User user =(User) session.getAttribute(Constant.USER_SESSION_KEY);if(user ==null){returnnewResponseBodyMessage<>(-1,"当前用户不存在",null);}else{returnnewResponseBodyMessage<>(1,"查找成功!", newUser);}}

6.3.2 前端的实现

在这里插入图片描述

load();functionload(){
        $.ajax({url:"user/userInfo",method:"GET",success:function(data){if(data.status ==1){let h2 = document.querySelector('#myname');
              h2.innerHTML ="你好! "+ data.data.username;let game = document.querySelector('#gameMes');
              game.innerHTML ="天梯分数: "+ data.data.score +" | "+"场数: "+ data.data.totalCount +" | "+"获胜场数: "+ data.data.winCount;}else{alert(data.message);
              location.assign("login.html");}}})}

6.4 实现匹配功能的前端代码

let websocketUrl ='ws://'+ location.host +'/findMatch';let websocket =newWebSocket(websocketUrl);// 连接成功的时候调用的方法
        websocket.onopen=function(){
          console.log("onopen");}// 连接关闭的时候调用的方法
        websocket.onclose=function(){
          console.log("onclose");}// 连接异常的时候调用的方法
        websocket.onerror=function(){
          console.log("onerrot");}// 监听整个窗口关闭的事件, 当窗口关闭, 主动的去关闭websocket连接
        window.onbeforeunload=function(){
          websocket.close();}// 连接成功收到的响应
        websocket.onmessage=function(e){// 先将Json格式 e 化为 响应对象let resp =JSON.parse(e.data);// 获取到 匹配按钮let play = document.querySelector('#beginPlay');// 等于-1是错误的起来, 打印错误的信息, 并跳转到登录页面if(resp.status ==-1){alert(resp.message);
            location.assign("login.html");return;}// 这里就都是正常的响应, 那么就判断是开始匹配, 还是结束匹配if(resp.message =='startMatch'){//开始匹配
            console.log("开始匹配");
            play.innerHTML ='匹配中...(点击停止)';}elseif(resp.message =='stopMatch'){//结束匹配
            console.log("结束匹配");
            play.innerHTML ='开始匹配';}elseif(resp.message =='matchSuccess'){//匹配成功
            console.log("匹配成功");
            location.assign('room.html');}else{// 按理不会触发这个elsealert(resp.message);
            console.log("收到非法响应");}}// 获取到匹配按钮let play = document.querySelector('#beginPlay');// 匹配按钮点击事件
        play.onclick=function(){// 判断当前 readyState 是否是OPEN状态的if(websocket.readyState == websocket.OPEN){// 当前 readyState 处于OPEN 状态, 说明链接是好的if(play.innerHTML =='开始匹配'){// 发送开始匹配的请求
              websocket.send(JSON.stringify({message:'startMatch',}))}elseif(play.innerHTML =='匹配中...(点击停止)'){// 发送停止匹配的请求
              websocket.send(JSON.stringify({message:'stopMatch',}))}}else{// 这里就是链接异常的情况alert('当前您的链接已经断开, 请重新登录');
            location.assign("login.html");}}

6.5 实现匹配功能的后端代码

这里是触发url的响应地址

@Configuration@EnableWebSocketpublicclassAppConfigimplementsWebSocketConfigurer{@AutowiredprivateMatchController matchController;@OverridepublicvoidregisterWebSocketHandlers(WebSocketHandlerRegistry registry){
        registry.addHandler(matchController,"/findMatch").addInterceptors(newHttpSessionHandshakeInterceptor());}}

6.5.1 创建在线状态

当用户登录的时候, 就让用户状态添加到哈希表中
由于这里是 多线程的状态下, 很多用户访问同一个哈希表就会出现线程安全的问题, 所以这里就使用 ConcurrentHashMap, 确保了线程安全问题.

  1. 这里存储的, key是用户的Id, value是对应的WebSocketSession的信息.
  2. 提供三个方法 - 进入房间的时候, 将用户的状态存入哈希表中- 退出房间的时候, 将用户的状态从哈希表中删除- 获取当前用户的 WebSocketSession 信息
@ComponentpublicclassOnlineUserManager{// 这个哈希表是表示当前用户在游戏大厅的在线状态privateConcurrentHashMap<Integer,WebSocketSession> gameState =newConcurrentHashMap<>();publicvoidenterGameIndex(int userId,WebSocketSession webSocketSession){
        gameState.put(userId,webSocketSession);}publicvoidexitGameHall(int userId){
        gameState.remove(userId);}publicWebSocketSessiongetState(int userId){return gameState.get(userId);}}

6.5.2 创建房间对象

房间对象, 每一房间中, 会有RoomId, 和2个用户信息.
所以这里需要有一个完全不可重复的RoomId, 这里就使用Java中的 UUID来解决

// 游戏房间@DatapublicclassRoom{privateString roomId;privateUser user1;privateUser user2;publicRoom(){this.roomId = UUID.randomUUID().toString();}}

6.5.3 创建房间管理器

按理 也是使用哈希表存储, 也有线程安全问题, 所以也使用ConcurrentHashMap
提供3个方法

  1. 添加用户进入到房间
  2. 删除房间中的用户
  3. 提供房间Id得到房间对象
// 房间管理器@ComponentpublicclassRoomManager{privateConcurrentHashMap<String,Room> rooms =newConcurrentHashMap<>();publicvoidinsert(Room room){
        rooms.put(room.getRoomId(),room);}publicvoidremove(String roomId){
        rooms.remove(roomId);}publicRoomfindRoomByRoomId(String roomId){return rooms.get(roomId);}

6.5.4 创建匹配队列

匹配队列, 首先按照分数将用户分为三个等级.

  1. <2000 , 属于简单用户
  2. >= 2000 && < 3000 , 属于普通用户
  3. >=3000 , 属于高级用户
// 创建匹配队列 按等级划分// 1. < 2000privateQueue<User> simpleQueue =newLinkedList<>();// 2. >= 2000 && < 3000privateQueue<User> normalQueue =newLinkedList<>();// 3. >= 3000privateQueue<User> highQueue =newLinkedList<>();

这里就通过队列来分为为三个等级, 来完成匹配和退出

  1. 点击匹配的时候, 按照用户当前的等级, 将用户入队
  2. 取消匹配的时候, 按照用户当前的等级, 将用户从队列中删除
  3. 创建三个线程, 一直循环的去对应等级队列中进行获取用户, 如果当前队列中的用户有2个以上的时候, 就进行匹配.

这里也有线程安全的问题, 这里同一个队列中, 用户并发的入队, 和删除用户操作, 就会产生线程安全的问题. 如果是不同的队列, 就不涉及线程安全的问题
解决办法: 对于同一个队列中的操作进行加锁.

问题2: 这里的三个线程, 是循环的去等待, 如果当前队列中迟迟没有人进来, 而线程还是循环的执行下去, 这样的资源消耗就非常的大.
所以在进行判断当前用户是否有2个以上的时候, 如果当前用户小于2个, 就将当前的队列进行wait(), 直到再次有用户加入进来的时候,就解锁, 再去判断当前用户是否有2个以上的用户.

// 匹配器, 这个类是用来完成匹配功能的@ComponentpublicclassMatcher{// 创建匹配队列 按等级划分// 1. < 2000privateQueue<User> simpleQueue =newLinkedList<>();// 2. >= 2000 && < 3000privateQueue<User> normalQueue =newLinkedList<>();// 3. >= 3000privateQueue<User> highQueue =newLinkedList<>();@AutowiredprivateOnlineUserManager onlineUserManager;@AutowiredprivateObjectMapper objectMapper;@AutowiredprivateRoomManager roomManager;/**
     * 将当前玩家添加到匹配队列中
     * @param user
     */publicvoidinsert(User user){// 按等级加入队列中if(user.getScore()<2000){synchronized(simpleQueue){
                simpleQueue.offer(user);// 只要有用户进入了, 就进行唤醒
                simpleQueue.notify();}}elseif(user.getScore()>=2000&& user.getScore()<3000){synchronized(normalQueue){
                normalQueue.offer(user);
                normalQueue.notify();}}else{synchronized(highQueue){
                highQueue.offer(user);
                highQueue.notify();}}}/**
     * 将当前玩家匹配队列中删除
     * @param user
     */publicvoidremove(User user){// 按照当前等级去对应匹配队列中删除if(user.getScore()<2000){synchronized(simpleQueue){
                simpleQueue.remove(user);}}elseif(user.getScore()>=2000&& user.getScore()<3000){synchronized(normalQueue){
                normalQueue.remove(user);}}else{synchronized(highQueue){
                highQueue.remove(user);}}}/**
     * 这里使用3个线程去一直的进行查看是否有2个以上的人, 如果有进行匹配
     */publicMatcher(){// 创建三个线程, 操作三个匹配队列Thread t1 =newThread(){@Overridepublicvoidrun(){while(true){handlerMatch(simpleQueue);}}};
        t1.start();Thread t2 =newThread(){@Overridepublicvoidrun(){while(true){handlerMatch(normalQueue);}}};
        t2.start();Thread t3 =newThread(){@Overridepublicvoidrun(){while(true){handlerMatch(highQueue);}}};
        t3.start();}privatevoidhandlerMatch(Queue<User> matchQueue){synchronized(matchQueue){try{// 1. 先查看当前队列中的元素个数, 是否满足两个// 这里使用while, 以防为0的时候, 被唤醒,然后没有再次判断导致进入下面操作.while(matchQueue.size()<2){// 用户小于2个的时候, 就进行等待, 以免浪费资源
                    matchQueue.wait();}// 2. 尝试从队列中取出两个玩家User player1 = matchQueue.poll();User player2 = matchQueue.poll();// 打印日志System.out.println("匹配到的两个玩家: "+ player1.getUsername()+" , "+ player2.getUsername());// 3. 获取到玩家的 websocket 的会话.WebSocketSession session1 = onlineUserManager.getState(player1.getUserId());WebSocketSession session2 = onlineUserManager.getState(player2.getUserId());// 再次判断是否为空if(session1 ==null&& session2 !=null){
                    matchQueue.offer(player2);return;}if(session1 !=null&& session2 ==null){
                    matchQueue.offer(player1);return;}if(session1 ==null&& session2 ==null){return;}if(session1 == session2){
                    matchQueue.offer(player1);return;}// 4. 把两个玩家放入一个游戏房间中Room room =newRoom();
                roomManager.insert(room,player1.getUserId(),player2.getUserId());// 5. 给玩家反馈信息, 通知匹配到了对手MatchResponse response1 =newMatchResponse();
                response1.setStatus(1);
                response1.setMessage("matchSuccess");String json1 = objectMapper.writeValueAsString(response1);
                session1.sendMessage(newTextMessage(json1));MatchResponse response2 =newMatchResponse();
                response2.setMessage("matchSuccess");
                response2.setStatus(1);String json2 = objectMapper.writeValueAsString(response2);
                session2.sendMessage(newTextMessage(json2));}catch(IOException|InterruptedException e){
                e.printStackTrace();}}}}

6.5.5 写完 MatchController

websocket 有4个方法.

  1. 连接成功的时候调用的方法, 这里需要去判断多开的问题, 由于用户同时登录一个账号的时候, 就会出现多开, 解决办法就是查询当前用户的在线状态, 如果当前用户在线, 就退出当前登录. 如果没有多开就设置登陆状态
  2. 异常关闭的情况, 获取用户的信息, 然后设置在线状态为不在线, 然后删除匹配队列中的用户
  3. 退出的时候调用的方法, 获取用户的信息, 然后设置在线状态为不在线, 然后删除匹配队列中的用户
  4. 处理收到请求的方法, 通过前端发来的请求, 判断是否是开始匹配还是停止匹配.
@ComponentpublicclassMatchControllerextendsTextWebSocketHandler{privateObjectMapper objectMapper =newObjectMapper();@AutowiredprivateOnlineUserManager onlineUserManager;@AutowiredprivateMatcher matcher;// 连接成功的时候就会调用该方法@OverridepublicvoidafterConnectionEstablished(WebSocketSession session)throwsException{// 玩家上线// 1. 获取用户信息User user =(User) session.getAttributes().get(Constant.USER_SESSION_KEY);// 2. 判断当前用户是否已经登录if(onlineUserManager.getState(user.getUserId())!=null){// 当前用户已经登录MatchResponse message =newMatchResponse();
            message.setMessage("当前用户已经登录!");
            message.setStatus(-1);
            session.sendMessage(newTextMessage(objectMapper.writeValueAsBytes(message)));
            session.close();return;}// 3. 设置在线状态
        onlineUserManager.enterGameIndex(user.getUserId(),session);}@OverrideprotectedvoidhandleTextMessage(WebSocketSession session,TextMessage message)throwsException{// 处理开始匹配 和 停止匹配User user =(User) session.getAttributes().get(Constant.USER_SESSION_KEY);String payload = message.getPayload();MatchRequest matchRequest = objectMapper.readValue(payload,MatchRequest.class);MatchResponse matchResponse =newMatchResponse();if(matchRequest.getMessage().equals("startMatch")){// 进入匹配队列// 创建匹配队列, 加入用户
            matcher.insert(user);// 返回响应给前端
            matchResponse.setStatus(1);
            matchResponse.setMessage("startMatch");}elseif(matchRequest.getMessage().equals("stopMatch")){// 退出匹配队列// 创建匹配队列, 将用户移除
            matcher.remove(user);
            matchResponse.setMessage("stopMatch");
            matchResponse.setStatus(1);}else{
            matchResponse.setStatus(-1);
            matchRequest.setMessage("非法匹配");// 非法情况}
        session.sendMessage(newTextMessage(objectMapper.writeValueAsBytes(matchResponse)));}// 异常情况@OverridepublicvoidhandleTransportError(WebSocketSession session,Throwable exception)throwsException{// 玩家下线// 1. 获取用户信息User user =(User) session.getAttributes().get(Constant.USER_SESSION_KEY);WebSocketSession webSocketSession = onlineUserManager.getState(user.getUserId());if(webSocketSession == session){// 2. 设置在线状态
            onlineUserManager.exitGameHall(user.getUserId());}
        matcher.remove(user);}// 关闭情况@OverridepublicvoidafterConnectionClosed(WebSocketSession session,CloseStatus status)throwsException{// 玩家下线// 1. 获取用户信息User user =(User) session.getAttributes().get(Constant.USER_SESSION_KEY);WebSocketSession webSocketSession = onlineUserManager.getState(user.getUserId());if(webSocketSession == session){// 2. 设置在线状态
            onlineUserManager.exitGameHall(user.getUserId());}
        matcher.remove(user);}}

6.6 大厅界面总结

  1. 这里要注意多线程环境下, 多个用户同时使用同一个哈希表的时候, 进行添加和删除的时候, 会有线程安全的问题, 那么这里就需要使用 ConcurrentHashMap
  2. 在多线程环境下, 按照等级分的队列, 在多线程环境下, 并发的进行入队的时候, 删除的队列中用户的时候, 也会有线程安全问题, 这里针对同一个队列就可以进行加锁.
  3. 由于创建3个线程循环的进入队列中查看是否满足2个用户, 如果当前的环境下, 用户特别少, 一直去循环的进入, 会造成CPU占用率特别高, 所以这里就使用wai()等待, 在有用户进入匹配队列的时候,再去唤醒notify().
  4. 防止多开, 多个地方登录同一个账号就会出现很多问题, 这里在进行连接的时候判断, 如果用户已经在线, 就不让该地方用户登录.
  5. 要想让 房间是第一无二, 就需要使用 UUID, 那么roomId也要使用 字符串的格式.

在这里插入图片描述

7. 房间界面

7.1 交互接口设计

连接URL

ws://127.0.0.1:8080/game

当双方玩家都已经连接好了 发送响应

{
    message: 'gameReady' 
    status: '1 / -1'  (1是正常响应, -1 是错误响应) 
    roomId: ' ' 
    thisUserId: ' ' (自己用户Id)
    thatUserId: ' ' (对方用户Id)
    whiteUser: ' ' (先手方)
}

落子的时候的请求

{
    message: ' putChess ' 
    userId: ' '  (落子的用户Id)
    row: ' ' (落子的第几行)
    col: ' ' (落子的第几列)
}

落子的时候的响应

{
    message: 'putChess;
    userId: ' '
    row: ' '
    col: ' '
    winner: ' ' (获胜者, 和用户Id一致, 如果没有获胜, 就是0)
}

7.2 实现房间界面前端代码

7.2.1 设置棋盘界面, 以及显示框.

这里的 canvas 是用来绘制棋盘的,

room.html
<!DOCTYPEhtml><htmllang="en"><head><metacharset="UTF-8"><metahttp-equiv="X-UA-Compatible"content="IE=edge"><metaname="viewport"content="width=device-width, initial-scale=1.0"><title>游戏房间</title><linkhref="css/game_room.css"rel="stylesheet"type="text/css"media="all"/></head><body><divclass="container"><divclass="one"><!-- 棋盘区域, 需要基于 canvas 进行实现 --><canvasid="chess"width="450px"height="450px"></canvas><!-- 显示区域 --><divid="screen"> 等待玩家连接中... </div></div></div><scriptsrc="js/script.js"></script></body></html>
game_room.css
*{margin: 0;padding: 0;box-sizing: border-box;}html, body{height: 100%;background-image:url(../images/bg.jpg);background-repeat: no-repeat;background-position: center;background-size: cover;}.container{height: 100%;display: flex;justify-content: center;align-items: center;}#screen{width: 450px;height: 50px;margin-top: 10px;background-color: #fff;font-size: 22px;line-height: 50px;text-align: center;}.backButton{width: 450px;height: 50px;font-size: 20px;color: white;background-color: orange;border: none;outline: none;border-radius: 10px;text-align: center;line-height: 50px;margin-top: 20px;}.backButton:active{background-color: gray;}

7.2.2 对应的js文件

  1. 这里的 setScreenText 这个方法是用来将显示框中的内容, 根据当前是谁下棋来改变内容.
  2. 这里的 initGame 这个方法是用来初始画棋盘的, 棋盘大小为 15 * 15
  3. 内部的 oneStep 是当点击下子之后, 会绘制对应颜色的棋子.
  4. 注意这里的棋盘数组, 为0是没有落子, 为1是落子了.
  5. 这里的gameInfo, 内部内容是全局的.用来接收传过来的响应
let gameInfo ={roomId:null,thisUserId:null,thatUserId:null,isWhite:true,}//// 设定界面显示相关操作//functionsetScreenText(me){let screen = document.querySelector('#screen');if(me){
        screen.innerHTML ="轮到你落子了!";}else{
        screen.innerHTML ="轮到对方落子了!";}}//// 初始化 websocket//// TODO//// 初始化一局游戏//functioninitGame(){// 是我下还是对方下. 根据服务器分配的先后手情况决定let me = gameInfo.isWhite;// 游戏是否结束let over =false;let chessBoard =[];//初始化chessBord数组(表示棋盘的数组)for(let i =0; i <15; i++){
        chessBoard[i]=[];for(let j =0; j <15; j++){
            chessBoard[i][j]=0;}}let chess = document.querySelector('#chess');let context = chess.getContext('2d');
    context.strokeStyle ="#BFBFBF";// 背景图片let logo =newImage();
    logo.src ="image/sky.jpeg";
    logo.onload=function(){
        context.drawImage(logo,0,0,450,450);initChessBoard();}// 绘制棋盘网格functioninitChessBoard(){for(let i =0; i <15; i++){
            context.moveTo(15+ i *30,15);
            context.lineTo(15+ i *30,430);
            context.stroke();
            context.moveTo(15,15+ i *30);
            context.lineTo(435,15+ i *30);
            context.stroke();}}// 绘制一个棋子, me 为 truefunctiononeStep(i, j, isWhite){
        context.beginPath();
        context.arc(15+ i *30,15+ j *30,13,0,2* Math.PI);
        context.closePath();var gradient = context.createRadialGradient(15+ i *30+2,15+ j *30-2,13,15+ i *30+2,15+ j *30-2,0);if(!isWhite){
            gradient.addColorStop(0,"#0A0A0A");
            gradient.addColorStop(1,"#636766");}else{
            gradient.addColorStop(0,"#D1D1D1");
            gradient.addColorStop(1,"#F9F9F9");}
        context.fillStyle = gradient;
        context.fill();}

    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){// TODO 发送坐标给服务器, 服务器要返回结果oneStep(col, row, gameInfo.isWhite);
            chessBoard[row][col]=1;}}// TODO 实现发送落子请求逻辑, 和处理落子响应逻辑. }initGame();

7.2.3 初始化 websocket

  1. 在服务器传过来请求的时候, 两个用户都已经准备好了, 首先判断是否是正确的请求.
  2. 在请求是正确的时候, 将传过来的信息存入到gameInfo中, 注意这里的isWhite 是判断是否是先手方.
  3. 注意只有2个人都建立连接了, 才初始画棋盘, 所以在这里初始化棋盘为好.
  4. 棋盘绘制好之后, 在显示框中, 显示对应的信息, 调用对应的 setScreenText 方法
let websocketUrl ='ws://'+ location.host +'/game';let websocket =newWebSocket(websocketUrl);

websocket.onopen=function(){
    console.log("房间链接成功!");}
websocket.onclose=function(){
    console.log("房间断开链接");}
websocket.onerror=function(){
    console.log("房间出现异常");}
window.onbeforeunload=function(){
    websocket.close();}
websocket.onmessage=function(e){
    console.log(e.data);let resp =JSON.parse(e.data);if(resp.message !='gameReady'){
        console.log("响应类型错误");
        location.assign("index.html");return;}if(resp.status ==-1){alert("游戏链接失败!");
        location.assign("index.html");return;}
    
    gameInfo.roomId == resp.roomId;
    gameInfo.thisUserId = resp.thisUserId;
    gameInfo.thatUserId = resp.thatUserId;
    gameInfo.isWhite = resp.whiteUser == resp.thisUserId;// 初始化棋盘initGame();// 设置显示内容setScreenText(gameInfo.isWhite);}

7.2.4 落子时,发送落子请求

在初始化棋盘之后, 在点击的时候, 发送落子请求
注意发送的对应的格式

在这里插入图片描述

functionsend(row,col){let req ={message:'putChess',userId: gameInfo.thisUserId,row: row,col: col
        };

        websocket.send(JSON.stringify(req));}

7.2.5 落子时, 发送落子响应

  1. 注意这里的响应是在落子之后, 所以要写在initGame() 中
  2. 在接收的时候, 首先将JSON格式响应转成可以接收的格式
  3. 判断响应是否正常, 排除响应错误的情况
  4. 判断当前是自己落子还是对方落子, 然后根据落子绘制棋子
  5. 落子之后, 交换落子的权利, 然后将显示的内容改变.
  6. 再次去判断是否游戏结束. 结束的时候,在显示框显示获胜信息, 并添加一个返回大厅的按钮, 以免直接返回了(用户看不到失败的信息.
websocket.onmessage=function(e){
        console.log(e.data);let resp =JSON.parse(e.data);if(resp.message !='putChess'){
            console.log("响应类型错误!");
            location.assign("index.html")return;}if(resp.userId == gameInfo.thisUserId){// 自己落子oneStep(resp.col, resp.row, gameInfo.isWhite);
            chessBoard[resp.row][resp.col]=1;}elseif(resp.userId == gameInfo.thatUserId){// 别人落子oneStep(resp.col, resp.row,!gameInfo.isWhite);
            chessBoard[resp.row][resp.col]=1;}else{// 落子异常
            console.log("userId 异常");return;}// 交换落子
        me =!me;setScreenText(me);// 判断游戏是否结束let screenDiv = document.querySelector('#screen');if(resp.winner !=0){
            console.log(resp.winner+" "+ gameInfo.thisUserId+" "+ gameInfo.thatUserId);if(resp.winner == gameInfo.thisUserId){
                screenDiv.innerHTML ="恭喜你, 获胜了!";}elseif(resp.winner == gameInfo.thatUserId){
                screenDiv.innerHTML ="游戏结束, 失败了!";}else{
                console.log("winner 错误");alert("当前 winner字段错误 winner = "+ resp.winner);}// location.assign('index.html');// 增加一个按钮, 返回游戏大厅let backBtn = document.createElement('button');
            backBtn.innerHTML ="返回游戏大厅";
            backBtn.className ="backButton";let one = document.querySelector('.one');
            backBtn.onclick=function(){
                location.assign("index.html");}
            one.appendChild(backBtn);}

7.3 实现房间界面后端代码

7.3.1 注册GameController

@Configuration@EnableWebSocketpublicclassAppConfigimplementsWebSocketConfigurer,WebMvcConfigurer{@AutowiredprivateGameController gameController;@OverridepublicvoidregisterWebSocketHandlers(WebSocketHandlerRegistry registry){
        registry.addHandler(gameController,"/game").addInterceptors(newHttpSessionHandshakeInterceptor());}

7.3.2 创建GameController

  1. afterConnectionEstablished 这个方法是在建立连接时候的方法.
  2. handleTextMessage 这个方法是接收发送的响应
  3. handleTransportError 这个方法是出现异常的时候执行的
  4. afterConnectionClosed 这个方法是关闭websocket的时候执行的
@ComponentpublicclassGameControllerextendsTextWebSocketHandler{@OverridepublicvoidafterConnectionEstablished(WebSocketSession session)throwsException{}@OverrideprotectedvoidhandleTextMessage(WebSocketSession session,TextMessage message)throwsException{}@OverridepublicvoidhandleTransportError(WebSocketSession session,Throwable exception)throwsException{}@OverridepublicvoidafterConnectionClosed(WebSocketSession session,CloseStatus status)throwsException{}}

7.3.3 创建对应的响应类和请求类

双方进入房间准备就绪的响应

// 客户端链接成功后, 返回的响应@DatapublicclassGameReadyResponse{privateString message;privateint status;privateString roomId;privateint thisUserId;privateint thatUserId;privateint whiteUser;}

落子请求

// 落子的请求@DatapublicclassGameRequest{privateString message;privateint userId;privateint row;privateint col;}

落子响应

//落子响应@DatapublicclassGameResponse{privateString message;privateint userId;privateint row;privateint col;privateint winner;}

7.3.4 完成用户房间在线状态管理

在之前的 OnlineUserManager 中添加代码

  1. enterGameRoom, 进入房间添加到哈希表中(上线)
  2. exitGameRoom, 退出房间从哈希表中删除(下线)
  3. getRoomState, 获取当前用户的websocketsession信息
// 这个哈希表是表示当前用户在游戏房间的在线状态privateConcurrentHashMap<Integer,WebSocketSession> roomState =newConcurrentHashMap<>();publicvoidenterGameRoom(int userId,WebSocketSession webSocketSession){
        roomState.put(userId,webSocketSession);}publicvoidexitGameRoom(int userId){
        roomState.remove(userId);}publicWebSocketSessiongetRoomState(int userId){return roomState.get(userId);}

7.3.5 添加 MyBatis 用来更新玩家积分

UserMapper

// 总场数 + 1, 获胜场数+1, 天梯分数 + 50voiduserWin(int userId);// 总场数 + 1, 天梯分数 -50voiduserLose(int userId);

UserMapper.xml

<updateid="userWin">
        update user set totalCount = totalCount+1 , winCount = winCount+1, score = score + 50 where userId = #{userId}
    </update><updateid="userLose">
        update user set totalCount = totalCount+1, score = score - 50 where userId = #{userId}
    </update>

UserService

// 总场数 + 1, 获胜场数+1, 天梯分数 + 50publicvoiduserWin(int userId){
        userMapper.userWin(userId);}// 总场数 + 1, 天梯分数 -50publicvoiduserLose(int userId){
        userMapper.userLose(userId);}

7.3.6 完成处理连接方法

  1. 首先获取用户的信息
  2. 判断当前是否已经进入房间了, 防止未匹配成功
  3. 判断是否多开, 这里要查询房间在线情况, 和大厅在线情况.
  4. 然后让用户房间的在线状态处于在线.
  5. 首先判断用户1是否上线, 上线就添加到当前房间来, 用户2再上线的时候也添加房间来, 这里可以设置谁是先手方, 根据自己设定的规则.我这里是随机取0~9的数字, 如果是偶数用户1就是先手, 如果是奇数用户2就是先手
  6. 当用户都进入房间的时候, 通知玩家准备就绪了
  7. 注意这里的线程安全问题. 多个用户进入同一个方法,就有可能出现线程安全问题, 由于是同一个房间的用户进行, 只需要对房间对象加锁就可以了.
@OverridepublicvoidafterConnectionEstablished(WebSocketSession session)throwsException{GameReadyResponse readyResponse =newGameReadyResponse();// 获取用户信息User user =(User) session.getAttributes().get(Constant.USER_SESSION_KEY);// 判断当前是否已经进入房间Room room = roomManager.findRoomByUserId(user.getUserId());if(room ==null){
            readyResponse.setStatus(-1);
            readyResponse.setMessage("用户尚未匹配到!");
            session.sendMessage(newTextMessage(objectMapper.writeValueAsBytes(readyResponse)));return;}// 判断当前是否多开if(onlineUserManager.getRoomState(user.getUserId())!=null|| onlineUserManager.getState(user.getUserId())!=null){
            readyResponse.setMessage("当前用户已经登录!");
            readyResponse.setStatus(-1);
            session.sendMessage(newTextMessage(objectMapper.writeValueAsBytes(readyResponse)));return;}// 上线
        onlineUserManager.enterGameRoom(user.getUserId(), session);synchronized(room){if(room.getUser1()==null){
                room.setUser1(user);System.out.println("玩家1 "+ user.getUsername()+" 已经准备好了");return;}if(room.getUser2()==null){
                room.setUser2(user);System.out.println("玩家2 "+ user.getUsername()+" 已经准备好了");Random random =newRandom();int num = random.nextInt(10);if(num %2==0){
                    room.setWhiteUser(room.getUser1().getUserId());}else{
                    room.setWhiteUser(room.getUser2().getUserId());}// 通知玩家1noticeGameReady(room,room.getUser1(),room.getUser2());// 通知玩家2noticeGameReady(room,room.getUser2(),room.getUser1());return;}}

        readyResponse.setStatus(-1);
        readyResponse.setMessage("房间已经满了");
        session.sendMessage(newTextMessage(objectMapper.writeValueAsBytes(readyResponse)));}
privatevoidnoticeGameReady(Room room,User user1,User user2)throwsIOException{GameReadyResponse resp =newGameReadyResponse();
        resp.setStatus(1);
        resp.setMessage("gameReady");
        resp.setRoomId(room.getRoomId());
        resp.setThisUserId(user1.getUserId());
        resp.setThatUserId(user2.getUserId());
        resp.setWhiteUser(room.getWhiteUser());WebSocketSession webSocketSession = onlineUserManager.getRoomState(user1.getUserId());
        webSocketSession.sendMessage(newTextMessage(objectMapper.writeValueAsString(resp)));}

7.3.7 完成处理连接断开的方法和连接异常的方法

  1. 首先获取用户的信息
  2. 然后设置用户房间状态为下线
  3. 注意这里掉线了, 就需要判断对方赢了. - 判断对方是否掉线, 如果对方也掉线了, 就无需通知谁赢了- 如果对方没有掉线, 就通知对方赢了- 获胜之后, 要对玩家的信息, 场次, 胜场进行更新. 然后关闭房间
@OverridepublicvoidhandleTransportError(WebSocketSession session,Throwable exception)throwsException{// 异常下线// 下线User user =(User) session.getAttributes().get(Constant.USER_SESSION_KEY);WebSocketSession exitSession = onlineUserManager.getRoomState(user.getUserId());if(exitSession == session){
            onlineUserManager.exitGameRoom(user.getUserId());}System.out.println("当前用户: "+ user.getUsername()+" 异常下线了");noticeThatUserWin(user);}
@OverridepublicvoidafterConnectionClosed(WebSocketSession session,CloseStatus status)throwsException{// 下线User user =(User) session.getAttributes().get(Constant.USER_SESSION_KEY);WebSocketSession exitSession = onlineUserManager.getRoomState(user.getUserId());if(exitSession == session){
            onlineUserManager.exitGameRoom(user.getUserId());}System.out.println("当前用户: "+ user.getUsername()+" 离开房间");noticeThatUserWin(user);}
privatevoidnoticeThatUserWin(User user)throwsIOException{Room room = roomManager.findRoomByUserId(user.getUserId());if(room ==null){System.out.println("房间已经关闭");return;}// 找到对手User thatUser =(user == room.getUser1())? room.getUser2(): room.getUser1();// 找到对手的状态WebSocketSession session = onlineUserManager.getRoomState(thatUser.getUserId());if(session ==null){// 都掉线了System.out.println("都掉线了, 无需通知");return;}// 这里通知对手获胜GameResponse gameResponse =newGameResponse();
        gameResponse.setMessage("putChess");
        gameResponse.setUserId(thatUser.getUserId());
        gameResponse.setWinner(thatUser.getUserId());
        session.sendMessage(newTextMessage(objectMapper.writeValueAsBytes(gameResponse)));// 更新玩家分数信息int winId = thatUser.getUserId();int loseId = user.getUserId();
        userService.userWin(winId);
        userService.userLose(loseId);// 释放房间对象
        roomManager.remove(room.getRoomId(),room.getUser1().getUserId(),room.getUser2().getUserId());}

7.3.8 在房间管理器中添加代码

  1. 添加哈希表, 管理用户对应的房间号
  2. key为用户的Id, value为用户的对应房间号
privateConcurrentHashMap<Integer,String>Ids=newConcurrentHashMap<>();publicvoidinsert(Room room,int userId1,int userId2){Ids.put(userId1,room.getRoomId());Ids.put(userId2,room.getRoomId());}publicvoidremove(String roomId,int userId1,int userId2){Ids.remove(userId1);Ids.remove(userId2);}publicRoomfindRoomByUserId(int userId){String roomId =Ids.get(userId);if(roomId ==null){returnnull;}return rooms.get(roomId);}

7.3.9 Room类添加棋盘代码

  1. 这里的 Constant.ROW 和 Constant.COL 都是不变的常量. 放到 Constant类中. 这里初始化的棋盘数组也是15 * 15的
  2. 这里Room要注入Spring对象, 不能使用@Autowired @Resource注解. 需要使用context

修改启动类

publicclassGobangApplication{publicstaticConfigurableApplicationContext context;publicstaticvoidmain(String[] args){
        context =SpringApplication.run(GobangApplication.class, args);}}
// 游戏房间@DatapublicclassRoom{privateString roomId;privateUser user1;privateUser user2;privateint whiteUser;privateOnlineUserManager onlineUserManager;privateRoomManager roomManager;privateUserService userService;publicRoom(){this.roomId = UUID.randomUUID().toString();

        onlineUserManager =GobangApplication.context.getBean(OnlineUserManager.class);

        roomManager =GobangApplication.context.getBean(RoomManager.class);

        userService =GobangApplication.context.getBean(UserService.class);}// 为0就是为落子, 为1就是用户1落子, 为2就是用户2落子privateint[][] board=newint[Constant.ROW][Constant.COL];privateObjectMapper objectMapper =newObjectMapper();}

7.3.10 实现handleTextMessage方法

落子请求

@OverrideprotectedvoidhandleTextMessage(WebSocketSession session,TextMessage message)throwsException{// 获取用户对象User user =(User) session.getAttributes().get(Constant.USER_SESSION_KEY);// 根据 玩家 Id 获取房间对象Room room = roomManager.findRoomByUserId(user.getUserId());// 通过room对象处理这次请求
        room.putChess(message.getPayload());}

7.3.11 实现putChess方法

  1. 注意这里的落子, 与前端不同, 这里的棋盘数组, 为0就是没落子, 为1就是用户1落得子, 为2就是用户2落得子
  2. 每次落子都要进行胜负判断, 使用checkWinner方法来实现
  3. 给房间中的用户返回响应
  4. 注意这里的玩家掉线的情况
  5. 如果胜负已分, 更新玩家获胜的信息, 并销毁房间
// 这个方法是用来处理一次落子的操作publicvoidputChess(String reqJson)throwsIOException{// 1. 记录当前落子的位子GameRequest request = objectMapper.readValue(reqJson,GameRequest.class);GameResponse response =newGameResponse();// 1.1 判断当前落子是谁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. 进行胜负判定int winner =checkWinner(row,col,chess);// 3. 给房间中所有的客户端返回响应
        response.setMessage("putChess");
        response.setRow(row);
        response.setCol(col);
        response.setWinner(winner);
        response.setUserId(request.getUserId());WebSocketSession session1 = onlineUserManager.getRoomState(user1.getUserId());WebSocketSession session2 = onlineUserManager.getRoomState(user2.getUserId());// 这里对下线进行判断if(session1 ==null){// 玩家1下线
            response.setWinner(user2.getUserId());System.out.println("玩家1掉线");}if(session2 ==null){// 玩家2下线, 就认为玩家1获胜System.out.println("玩家2掉线");}String respJson = objectMapper.writeValueAsString(response);if(session1 !=null){
            session1.sendMessage(newTextMessage(respJson));}if(session2 !=null){
            session2.sendMessage(newTextMessage(respJson));}// 4. 如果当前获胜, 销毁房间if(response.getWinner()!=0){System.out.println("游戏结束, 房间即将销毁");// 更新获胜方的信息int winId = response.getWinner();intLoseId= response.getWinner()== user1.getUserId()? user2.getUserId(): user1.getUserId();
            userService.userLose(LoseId);
            userService.userWin(winId);// 销毁房间
            roomManager.remove(roomId,user1.getUserId(),user2.getUserId());}}

7.3.12 完成用户胜负判断

这里要判断四种情况

  1. 一行有五个子连珠
  2. 一列有五个子连珠
  3. 从左到右的斜着的五子连珠
  4. 从右到左的斜着的五子连珠

完成

checkWinner

方法

// 谁获胜就返回谁的Id, 如果还没有获胜者, 就返回0privateintcheckWinner(int row,int col,int chess){//  判断当前是谁获胜// 1. 一行五子连珠for(int i = col -4;i >=0&& i <= col && i <=Constant.COL-5; i++){if(board[row][i]== chess
            && board[row][i+1]== chess
            && board[row][i+2]== chess
            && board[row][i+3]== chess
            && board[row][i+4]== chess){return chess ==1? user1.getUserId(): user2.getUserId();}}// 2. 一列五子连珠for(int i = row -4; i >=0&& i <= row && i <=Constant.ROW-5; i++){if(board[i][col]== chess
            && board[i+1][col]== chess
            && board[i+2][col]== chess
            && board[i+3][col]== chess
            && board[i+4][col]== chess){return chess ==1? user1.getUserId(): user2.getUserId();}}// 3. 斜着五子连珠 -> 左上到右下for(int i = row -4, j = col -4; i <= row && j <= col;j++,i++){try{if(board[i][j]== chess
                        && board[i+1][j+1]== chess
                        && board[i+2][j+2]== chess
                        && board[i+3][j+3]== chess
                        && board[i+4][j+4]== chess){return chess ==1? user1.getUserId(): user2.getUserId();}}catch(ArrayIndexOutOfBoundsException e){continue;}}// 4. 斜着五子连珠 -> 右上到左下for(int i = row+4,j=col-4; i>=row && j <= col; i--,j++){try{if(board[i][j]== chess
                        && board[i-1][j+1]== chess
                        && board[i-2][j+2]== chess
                        && board[i-3][j+3]== chess
                        && board[i-4][j+4]== chess){return chess ==1? user1.getUserId(): user2.getUserId();}}catch(ArrayIndexOutOfBoundsException e){continue;}}return0;}
标签: spring boot java

本文转载自: https://blog.csdn.net/wwzzzzzzzzzzzzz/article/details/126283912
版权归原作者 独一无二的哈密瓜 所有, 如有侵权,请联系我们删除。

“基于 SpringBoot + MyBatis 的在线五子棋对战”的评论:

还没有评论