基于 SpringBoot + MyBatis 的网页版五子棋对战

news2024/11/26 4:28:31

目录

一、项目所要实现的功能模块

1、用户模块

2、匹配模块

3、对战模块

二、使用技术

三、项目截图

1、登录页面

2、注册页面

3、游戏大厅页面

4、游戏房间页面

四、创建 SpringBoot 项目

1、在 IDEA 中创建一个 SpringBoot 项目

2、设置项目名称

3、选择项目依赖

4、选择项目存放路径,就可以创建出一个 SpringBoot 项目

五、配置文件

六、数据库设计

七、工具包

1、ResponseBodyMessage 类

2、Constant 类

3、密码加密

(1)在 pom.xml 文件中添加依赖

(2)在springboot启动类添加注解

(3)在 config 包中创建 AppConfig 类

八、配置拦截器

1、创建 config 包,在 config 包中创建 LoginInterceptor 类

2、在 AppConfig 类中添加下面的代码

九、实现登录注册模块

1、登录功能的请求和响应设计

2、注册功能的请求和响应

3、创建 User 类

4、创建对应的 Mapper 和 API

(1)创建接口 UserMapper

(2)创建 UserMapper.xml

5、在 com.example.online_gobang.api 包中创建 UerAPI 类

6、注册登录功能测试

(1)注册功能

(2)登录功能

7、前端代码

(1)注册功能

(2)登录功能

十、实现匹配模块

1、匹配功能的请求和响应设计

(1)匹配请求

(2)匹配响应 1

(3)匹配响应 2

2、获取用户信息的请求和响应设计

3、在 UserAPI 类中添加 getUserInfo 方法

4、用户的在线状态

5、创建游戏房间

6、创建房间管理器

7、创建匹配队列

8、websocket 的匹配请求

9、websocket 的匹配响应

10、处理匹配功能的 websocket 请求

11、触发 websocket 约定好的请求与响应路径

12、前端代码

十一、实现对战模块

1、对战功能的请求和响应设计

(1)建立连接请求

(2)建立连接响应

(3)玩家落子请求

(4)玩家落子响应

2、触发 websocket 约定好的请求与响应路径

3、建立连接的响应

4、websocket 的落子请求

5、websocket 的落子响应

6、玩家在游戏房间的在线状态

7、更新玩家的比赛场次、获胜场次、天梯积分

(1)在 UserMapper 接口中添加代码

(2)在 UserMapper.xml 中添加代码

8、在 Room 类中实现棋局判定胜负的逻辑

(1)修改启动类

(2)获取 RoomManager 与 OnlineUserManager

(3)在 Room 类中创建一个二维数组用来表示棋盘

(4)Room 类的完整代码

9、处理对战功能的 websocket 请求

10、前端代码

(1)使用 canvas 绘制棋盘(game_room.html)

(2)绘制棋盘的 JavaScript 文件(script.js)


一、项目所要实现的功能模块

1、用户模块

  • 用户的注册和登录。
  • 管理用户的天梯积分、比赛场数、获胜场数等信息。

2、匹配模块

  • 游戏大厅页面
  • 依据用户的天梯积分,实现匹配机制。

3、对战模块

  • 游戏房间页面
  • 把两个匹配到的玩家放到一个游戏房间中,双方进行对战比赛。

二、使用技术

  • 前端:HTML、CSS、JavaScript、jQuery、Ajax
  • 后端:Java、SpringBoot、WebSocket
  • 数据库:MySQL、MyBatis

三、项目截图

1、登录页面

2、注册页面

3、游戏大厅页面

4、游戏房间页面

四、创建 SpringBoot 项目

1、在 IDEA 中创建一个 SpringBoot 项目

2、设置项目名称

3、选择项目依赖

4、选择项目存放路径,就可以创建出一个 SpringBoot 项目

五、配置文件

  • 在 application.properties 配置如下信息
#配置数据库
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/online_gobang?characterEncoding=utf8&serverTimezone=UTC
spring.datasource.username=数据库用户名
spring.datasource.password=数据库密码
spring.datasource.driver-class-name=com.mysql.jdbc.Driver

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

六、数据库设计

create database if not exists online_gobang;

use online_gobang;

-- 用户表
drop table if exists user;
create table user(
    userId int primary key auto_increment comment '用户id',
    username varchar(50) unique comment '用户名',
    password varchar(255) not null comment '用户密码',
    score int comment '天梯积分',
    totalCount int comment '比赛总场数',
    winCount int comment '获胜场数'
);

七、工具包

  • 在 package com.example.online_gobang 目录下创建一个 tools 包,在这个包中存放整个项目要使用的工具类。

1、ResponseBodyMessage 类

  • 设计统一的响应体工具类,因为做任何操作时都需要响应,所以封装一个通用的响应工具类,这个工具类设计成一个泛型类。
package com.example.online_gobang.tools;

import lombok.Data;

@Data
public class ResponseBodyMessage<T> {
    private int status; // 状态码
    private String message; // 返回的信息(出错原因等)
    private T data; // 返回给前端的数据(因为返回的数据类型不确定,可能是 String,boolea,int ...,因此使用泛型)

    public ResponseBodyMessage(int status, String message, T data) {
        this.status = status;
        this.message = message;
        this.data = data;
    }
}

2、Constant 类

  • 这个类用来存储不变的常量。 例如:设置 session 对象中的 key 值,key 是一个不变的字符串。
  • 如果在其他地方获取对应的 session 就可以通过这个类中的字符串进行获取。
package com.example.online_gobang.tools;

public class Constant {
    public static final String USER_SESSION_KEY = "user"; // 设置 session 中的 key 值
}

3、密码加密

使用 Bcrypt 对用户密码进行加密

  • Bcrypt 是一款加密工具,可以比较方便地实现数据的加密工作。也可以简单理解为它内部自己实现了随机加盐处理 。
  • 使用MD5加密,每次加密后的密文其实都是一样的,这样就方便了MD5通过大数据的方式进行破解。
  • Bcrypt生成的密文是60位的,而MD5的是32位的,因此 Bcrypt 破解难度更大。

(1)在 pom.xml 文件中添加依赖

  • 添加到 <dependencies> </dependencies> 标签内
<!-- 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>

(2)在springboot启动类添加注解

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

(3)在 config 包中创建 AppConfig 类

package com.example.online_gobang.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;

/**
 * Created with IntelliJ IDEA.
 * Description:
 * User: 74646
 * Date: 2022-11-14
 * Time: 16:35
 */
@Configuration
@EnableWebSocket
public class AppConfig implements WebSocketConfigurer,WebMvcConfigurer {
    @Bean
    public BCryptPasswordEncoder getBCryptPasswordEncoder(){
        return  new BCryptPasswordEncoder();
    }
}

八、配置拦截器

  • 未登录的情况下拦截其他页面,登录成功后才可以访问其他界面

1、创建 config 包,在 config 包中创建 LoginInterceptor 类

package com.example.online_gobang.config;

import com.example.online_gobang.tools.Constant;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

public class LoginInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
            HttpSession session = request.getSession(false);
            if(session == null || session.getAttribute(Constant.USER_SESSION_KEY)==null){
                return false;
            }
            return true;
        }
}

2、在 AppConfig 类中添加下面的代码

@Configuration
@EnableWebSocket
public class AppConfig implements WebSocketConfigurer,WebMvcConfigurer {
    /**
     * 添加拦截器
     * @param registry
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 登录之后才可以访问其他页面
        LoginInterceptor loginInterceptor = new LoginInterceptor();
        registry.addInterceptor(loginInterceptor).
                // 拦截所有的
                 addPathPatterns("/**")
                //排除所有的JS
                .excludePathPatterns("/js/**.js")
                //排除images下所有的元素
                .excludePathPatterns("/images/**")
                .excludePathPatterns("/css/**.css")
                .excludePathPatterns("/fronts/**")
                .excludePathPatterns("/player/**")
                .excludePathPatterns("/login.html")
                .excludePathPatterns("/register.html")
                //排除登录和注册接口
                .excludePathPatterns("/login")
                .excludePathPatterns("/register")
                .excludePathPatterns("/logout");
    }
}

九、实现登录注册模块

1、登录功能的请求和响应设计

请求:
    {
      post, // 使用 post 请求
      /login // 请求路径
      data:{ username, password } // 传入的数据
    }
     
响应:
    {
       "status": 200,
       "message": "登录成功",
       "data": {
                 "id": xxxxx,
                 "username": xxxxxx,
                 "score": 1000,
                 "totalCount": 0,
                 "winCount": 0
       }
     
    }
     
响应设计字段解释:
    {
      状态码为 200 表示成功,-200表示失败
      状态描述信息,描述此次请求成功或者失败的原因
      返回的数据,请求成功后,服务器返回给前端的数据
    }

2、注册功能的请求和响应

请求:
    {
      post, // 使用 post 请求
      /register // 请求路径
      data:{ username, password } // 传入的数据
    }
     
响应:
    {
       "status": 200,
       "message": "注册成功",
       "data": {
                 "id": xxxxx,
                 "username": xxxxxx,
                 "score": 1000,
                 "totalCount": 0,
                 "winCount": 0
       }
     
    }
     
响应设计字段解释:
    {
      状态码为 200 表示成功,-200表示失败
      状态描述信息,描述此次请求成功或者失败的原因
      返回的数据,请求成功后,服务器返回给前端的数据
    }

3、创建 User 类

  • 在 com.example.online_gobang.model 包中创建 User 类

package com.example.online_gobang.model;

import lombok.Data;

@Data
public class User {
    private int userId; // 用户id
    private String username; // 用户名
    private String password; // 密码
    private int score; // 天梯积分
    private int totalCount; // 比赛总场数
    private int winCount; // 获胜场数
}

4、创建对应的 Mapper 和 API

(1)创建接口 UserMapper,实现用户信息的插入与查询

  • 在 com.example.online_gobang.mapper 包中创建 UserMapper 接口
package com.example.online_gobang.mapper;

import com.example.online_gobang.model.User;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface UserMapper {
    /**
     *  数据库中插入用户信息,用于注册功能
     * @param user
     */
    void insert(User user);

    /**
     *  根据用户名,查询用户的详细信息,用户登录功能
     * @param username
     * @return
     */
    User selectByName(String username);
}

(2)创建 UserMapper.xml

  • 在 resource 目录下,创建 mybatis 文件夹,创建 UserMapper.xml,在 UserMapper.xml 文件中写SQL语句。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.online_gobang.mapper.UserMapper">
    <insert id="insert">
        insert into user values(null, #{username}, #{password}, 1000, 0, 0);
    </insert>

    <select id="selectByName" resultType="com.example.online_gobang.model.User">
        select *
        from user
        where username = #{username};
    </select>
</mapper>

5、在 com.example.online_gobang.api 包中创建 UerAPI 类

  • 在 UserAPI 类中实现用户注册、用户登录功能
package com.example.online_gobang.api;

import com.example.online_gobang.mapper.UserMapper;
import com.example.online_gobang.model.User;
import com.example.online_gobang.tools.Constant;
import com.example.online_gobang.tools.ResponseBodyMessage;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;

@RestController
public class UserAPI {

    @Resource
    private UserMapper userMapper;

    // 使用 BCrypt 对密码进行加密
    @Resource
    private BCryptPasswordEncoder bCryptPasswordEncoder;

    @PostMapping("/login")
    @ResponseBody
    public ResponseBodyMessage<User> login(@RequestParam String username,@RequestParam String password,HttpServletRequest request){
        /**
         *  根据 username 到数据库中进行查询
         *  如果能找到匹配的用户,并且密码也一致,就认为登录成功
         */
        User user = userMapper.selectByName(username);
        System.out.println("登录的用户:"+ username);
        if(user != null){
            System.out.println("登录成功");

            // 判断当前用户输入的密码(password) 与 数据库中查询到的密码(加密的密码,getPassword())是否匹配
            boolean flag = bCryptPasswordEncoder.matches(password,user.getPassword());

            if(!flag){
                // 密码不匹配,登录失败
                return new ResponseBodyMessage<>(-200,"用户名或密码错误",user);
            }
            // 如果登录成功就将信息写入到 session 中(在 session 中存储了一个用户信息对象,此后可以随时从 session 中将这个对象取出来进行一些操作)
            request.getSession().setAttribute(Constant.USER_SESSION_KEY,user);
            // 状态码为200,表示登录成功,并返回用户信息
            return   new ResponseBodyMessage<>(200,"登录成功",user);
        }else {
            // 登录失败,返回一个空的对象
            System.out.println("登录失败");
            return new ResponseBodyMessage<>(-200,"用户名或密码错误",user);
        }
    }

    @RequestMapping("/register")
    @ResponseBody
    public ResponseBodyMessage<Boolean> register(@RequestParam String username, @RequestParam String password){
        User user1 = userMapper.selectByName(username);
        if(user1 != null) {
            return new ResponseBodyMessage<>(-200,"当前用户已经存在",false);
        }else {
            User user = new User();
            String newPassword = bCryptPasswordEncoder.encode(password);
            user.setUsername(username);
            user.setPassword(newPassword);
            boolean flag = userMapper.insert(user);
            if(flag == true){
                return new ResponseBodyMessage<>(200,"注册成功",true);
            }else{
                return new ResponseBodyMessage<>(-200,"注册失败",false);
            }
        }
    }
}

6、注册登录功能测试

  • 使用 postman 进行测试。

(1)注册功能

(2)登录功能

7、前端代码

(1)注册功能

    <script>
        $(function(){
            $("#register").click(function(){
                var username = $("#username").val();
                var password = $("#password").val();
                $.ajax({
                 url: "/register",
                 type: "POST",
                 data:{
                        "username":username,
                        "password":password
                    },
                dataType:"json",
                 success: function(data){
                     console.log(data);
                     if(data.status == 200) {
                        // alert("注册成功");
                        location.assign("login.html");
                     }else{
                        alert("注册失败");
                        $("#username").val("");
                        $("#password").val("");
                        $("#repassword").val("");
                     }
                 }
             })
            })
        });

        let register = document.querySelector('#register');
        console.log(register);
         register.onclick = function() {
             let username = document.querySelector('#username');
             let password = document.querySelector('#password');
             let repassword = document.querySelector('#repassword');
             if(username.value.trim() == ""){
                 alert("请输入账号!");
                 username.focus();
                 return;
             }
             if(password.value.trim() == ""){
                 alert('请输入密码!');
                 password.focus();
                 return;
             }
             if(repassword.value.trim() == ""){
                 alert('请再次输入密码!');
                 repassword.focus();
                 return;
             }
             if(username.value.trim().length > 15) {
                 alert("账号长度不可超过15个字符,请重新输入");
                 username.value="";
                 username.focus();
                 return;
             }
             if(password.value.trim() != repassword.value.trim()) {
                 alert('两次输入的密码不同,请重试输入!');
                 passwrod.value="";
                 repassword.value="";
                 return;
             }
             if(password.value.trim().length > 255) {
                 alert("当前密码长度过长!");
                 password.value="";
                 repassword.value="";
                 password.focus();
                 return;
             }
            
         }
    </script>

(2)登录功能

    <script>
        $(function(){
            $("#submit").click(function(){
                // 点击登录按钮,获取用户名和密码
                var username = $("#username").val();
                var password = $("#password").val();
 
                // 判断用户名和密码是否为空(使用 trim 方法,防止输入空格)
                if(username.trim() == "" || password.trim() == ""){
                    alert("账号或密码不能为空");
                    return;
                }
                // 如果用户名和密码不为空,使用 Ajax 传入请求
                $.ajax({
                    type:"POST",
                    url:"/login",
                    data:{
                            "username":username,
                            "password":password
                    },
                    // 服务器返回的数据类型
                    dataType:"json",
                    // 请求成功,服务器返回数据
                    success:function(data){
                        console.log(data);
                        // 如果状态码为 200,表示登录成功
                        if(data.status == 200){
                            alert("登录成功");
                            // 跳转到游戏大厅页面
                            location.assign('/game_hall.html');
                        }else{
                            alert("登录失败,账号或密码错误");
                            // 登录失败,将用户名或密码置空
                            $("#username").val("");
                            $("#password").val("");
                        }
                    }
                });
            });
        });
 
        $(function () {
			  $("#register").click(function () {
				  window.location.href="register.html";
              });
          });
 
    </script>

十、实现匹配模块

  • 让多个用户在游戏大厅进行匹配,系统将天梯积分相近的两个玩家匹配到同一个房间中进行对战。

1、匹配功能的请求和响应设计

  • 玩家发送匹配请求,这个事情是确定的(点击了匹配按钮,就会发送匹配请求) ,但是服务器什么时候告知玩家匹配结果是不确定的。
  • 匹配功能需要依赖消息推送机制(websocket),当服务器匹配成功之后就会主动告诉当前进行匹配的所有玩家“你匹配成功了”。

消息推送机制:

  • 消息推送就是服务器主动将数据发送给客户端(服务器主动发送请求),而 HTTP 协议必须是客户端主动将数据发送给服务器(客户端主动发送请求)。
  • 匹配功能需要服务器主动给客户端发送数据,因此需要使用 websocket,接下来所设计的前后端交互接口都是基于 websocket 这样的交互方式进行的。

(1)匹配请求

  • 客户端通过 websocket 给服务器发送一个 json 格式的文本数据
请求:
{
  ws://127.0.0.1:8081/findMatch // 请求路径
  data:{ message:'startMatch' // 开始匹配
               或 'stopMatch' // 结束匹配
       } // 请求内容
}
 

(2)匹配响应 1

  • 这个响应是客户端给服务器发送匹配请求之后,服务器立即返回的匹配响应
响应:
     {
       ws://127.0.0.1:8081/findMatch // 响应路径
       data:{ 
              status: 200, // 匹配成功
              reason: '', // 如果匹配失败,失败原因的信息
              message: 'startMatch' // 开始匹配
                    或 'stopMatch' // 结束匹配
            } // 响应内容
     }

响应设计字段解释:
    {
      状态码为 200 表示成功,-200表示失败
      状态描述信息,描述此次请求成功或者失败的原因
      返回的数据,请求成功后,服务器返回给前端的数据
    }

(3)匹配响应 2

  • 这个响应是匹配到对手之后,服务器主动给客户端发送的响应。
响应:
     {
       ws://127.0.0.1:8081/findMatch // 响应路径
       data:{ 
              status: 200, // 匹配成功
              reason: '', // 如果匹配失败,失败原因的信息
              message: 'matchSuccess'
            } // 响应内容
     }

响应设计字段解释:
    {
      状态码为 200 表示成功,-200表示失败
      状态描述信息,描述此次请求成功或者失败的原因
      返回的数据,请求成功后,服务器返回给前端的数据
    }

2、获取用户信息的请求和响应设计

  • 用户登录成功之后,让客户端随时通过这个接口来访问服务器,获取到自身的信息。
请求:
    {
      get, // 使用 get 请求
      /userInfo // 请求路径
    }
     
响应:
    {
       "status": 200,
       "message": "相关信息",
       "data": {
                 "id": xxxxx,
                 "username": xxxxxx,
                 "score": 1000,
                 "totalCount": 0,
                 "winCount": 0
       }
     
    }
     
响应设计字段解释:
    {
      状态码为 200 表示成功,-200表示失败
      状态描述信息,描述此次请求成功或者失败的原因
      返回的数据,请求成功后,服务器返回给前端的数据
    }

3、在 UserAPI 类中添加 getUserInfo 方法

  • 根据当前存储的 session 对象, 查找对应的用户
    @RequestMapping("/userInfo")
    @ResponseBody
    public ResponseBodyMessage<User> getUserInfo(HttpServletRequest request){
        HttpSession session = request.getSession(false);
        User user = (User)session.getAttribute("user"); // 从会话中获取 User 对象
        User newUser = userMapper.selectByName(user.getUsername());
        if(newUser != null){
            return new ResponseBodyMessage<>(200,"获取成功",user);
        }else {
            System.out.println("没有该用户");
            return new ResponseBodyMessage<>(-200,"获取失败",user);
        }
    } 

4、用户的在线状态

  • 创建 game 包,在 game 包中创建 OnlineUserManager 类
  • 维护用户的在线状态,目的是为了能够方便获取到某个用户当前的 websocket 会话,从而可以通过这个会话来给这个客户端发送信息,同时也可以感知到当前玩家的在线/离线状态。

使用哈希表来保存当前用户的在线状态,当用户登录的时候, 就将用户状态添加到哈希表中。

  • 玩家的在线状态是多线程的, 很多用户访问同一个哈希表就会出现线程安全的问题, 所以这里就使用 ConcurrentHashMap, 确保了线程安全问题。
  • 这里存储的, key是用户的Id, value是对应的 websocket 会话。

提供三个方法:

  1. 玩家进入游戏大厅的时候, 将用户的状态存入哈希表中
  2. 玩家退出游戏大厅的时候, 将用户的状态从哈希表中删除
  3. 在游戏大厅获取当前用户的信息
package com.example.online_gobang.game;

import org.springframework.stereotype.Component;
import org.springframework.web.socket.WebSocketSession;

import java.util.concurrent.ConcurrentHashMap;

@Component
public class OnlineUserManager {
    // 这个哈希表用来表示当前用户在游戏大厅的在线状态
    private ConcurrentHashMap<Integer, WebSocketSession> gameHall = new ConcurrentHashMap<>();

    // 玩家进入游戏大厅
    public void enterGameHall(int userId,WebSocketSession webSocketSession){
        gameHall.put(userId,webSocketSession);
    }

    // 玩家退出游戏大厅
    public void exitGameHall(int userId){
        gameHall.remove(userId);
    }

    public WebSocketSession getFromGameHall(int userId){
        return gameHall.get(userId);
    }

}

5、创建游戏房间

  • 在 com.example.online_gobang.game 包中创建 Room 类。
  • 将 roomId 设置成字符串类型,使用 UUID 生成唯一的 roomId。

UUID:通用唯一识别码(Universally Unique Identifier)的缩写

  • 表示“唯一的身份标识”。
  • 通过一系列算法能够生成一串字符串(一组十六进制表示的数字)。
  • 每次调用这个算法,得到的结果都是不相同的。
  • Java 中有现成的类可以直接生成 UUID。
package com.example.online_gobang.game;

import com.example.online_gobang.model.User;
import lombok.Data;

import java.util.UUID;

// 这个类表示一个游戏房间
@Data
public class Room {
    private String roomId;

    private User user1;
    private User user2;

    public Room() {
        // 构造 Room 的时候生成一个唯一的字符串表示房间 id.
        // 使用 UUID 来作为房间 id
        roomId = UUID.randomUUID().toString();
    }
}

6、创建房间管理器

  • 在 com.example.online_gobang.game 包中创建 RoomManager 类。
  • 一个游戏服务器上会同时存在多个游戏房间,因此需要一个“游戏房间管理器”来管理多个游戏房间。

使用哈希表存储每个游戏房间并对其进行管理。

private ConcurrentHashMap<String,Room> rooms = new ConcurrentHashMap<>();
  • key:roomId(每个游戏房间的id都是唯一的),value:Room。

使用哈希表维护玩家和游戏房间之间的关系。

private ConcurrentHashMap<Integer,String>  usrIdToRoomId = new ConcurrentHashMap<>();
  • 考虑到线程安全问题,因此使用 ConcurrentHashMap。
  • key:userId,value:roomId。

提供4个方法:

  1. 添加玩家进入到游戏房间中。
  2. 删除游戏房间中的玩家。
  3. 根据游戏房间的Id,获取对应的游戏房间。
  4. 通过用户Id,查找该玩家所在的游戏房间。
package com.example.online_gobang.game;

import org.springframework.stereotype.Component;

import java.util.concurrent.ConcurrentHashMap;

// 房间管理器
@Component
public class RoomManager {
    private ConcurrentHashMap<String,Room> rooms = new ConcurrentHashMap<>();
    // 通过用户id 与房间id 维护玩家和房间之间的关系
    private ConcurrentHashMap<Integer,String>  usrIdToRoomId = new ConcurrentHashMap<>();
    public void add(Room room, int userId1,int userId2){
        // 添加一个房间到房间管理器的同时,也将两个玩家的 userId 添加到 usrIdToRoomId 中,便于维护玩家和房间之间的关系
        rooms.put(room.getRoomId(),room);
        usrIdToRoomId.put(userId1,room.getRoomId());
        usrIdToRoomId.put(userId2,room.getRoomId());
    }

    public void remove(String roomId, int userId1,int userId2){
        // 移除一个房间的同时也要同时移除两个玩家的信息
        rooms.remove(roomId);
        usrIdToRoomId.remove(userId1);
        usrIdToRoomId.remove(userId2);
    }

    public Room getByRoomId(String roomId){
        return rooms.get(roomId);
    }

    // 通过用户id 查找对应的房间
    public Room getRoomByUserId(int userId){
        String rooId = usrIdToRoomId.get(userId);
        if(rooId == null){
            // rooId == null 表示游戏房间不存在
            // userId -> roomId 映射关系不存在,直接返回 null
            return null;
        }
        return rooms.get(rooId);
    }
}

7、创建匹配队列

  • 在 com.example.online_gobang.game 包中创建 Matcher 类。
  • 从待匹配的玩家中选中分数相近的玩家进行匹配。

将所有玩家按照天梯积分划分成三类(基础分:1000):

  • 业余水平:score < 2000
  • 普通水平:score >= 2000 && score < 3000
  • 大师水平:score >= 3000
    // 1. 业余水平:score < 2000
    private Queue<User> amateurQueue = new LinkedList<>();
    // 2. 普通水平:score >= 2000 && score < 3000
    private Queue<User> normalQueue = new LinkedList<>();
    // 3. 大师水平:score >= 3000
    private Queue<User> masterQueue = new LinkedList<>();
  • 给这三个等级分配三个不同的队列,根据当前玩家的天梯积分,将玩家的用户信息放到对应的队列中。
  • 设置一个线程去不停的扫描匹配队列,只要队列中的元素(匹配中的玩家)积分相近,就将这一对玩家放到一个游戏房间中。

1、线程安全问题:

  • 如果多个线程针对同一个队列进行并发修改操作(入队、出队)就会产生线程安装问题。如果是针对多个不同队列进行操作就不会产生线程安全问题。

解决办法:对创建的三个队列对象在进行队列操作时分别进行加锁(synchronized)。

2、忙等问题:

  • 如果当前匹配队列中没有玩家或只有一个玩家在进行匹配,线程就会调用 handlerMatch 方法并且直接返回,然后再次调用 handlerMatch 方法 ...... 一直循环。这个过程中 CPU 占用率会非常高。

解决方法:

  • 在判断当前队列中的元素(正在匹配的玩家)是否有2个以上的时候,如果当前队列中的玩家数 < 2,就调用 wait() 进行匹配等待,如果没有玩家进入匹配队列就会一直等。
  • 直到有玩家进入匹配队列,就调用 notify() 唤醒线程,再次判断当前队列中是否有2个以上的玩家。
package com.example.online_gobang.game;

import com.example.online_gobang.model.User;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;

import java.io.IOException;
import java.util.LinkedList;
import java.util.Queue;

// 匹配器, 这个类是用来完成匹配功能的
@Component
public class Matcher {
    // 创建匹配队列 按等级划分
    // 1. 业余水平:score < 2000
    private Queue<User> amateurQueue = new LinkedList<>();
    // 2. 普通水平:score >= 2000 && score < 3000
    private Queue<User> normalQueue = new LinkedList<>();
    // 3. 大师水平:score >= 3000
    private Queue<User> masterQueue = new LinkedList<>();

    @Autowired
    private OnlineUserManager onlineUserManager;

    @Autowired
    private ObjectMapper objectMapper;

    @Autowired
    private RoomManager roomManager;

    /**
     * 将当前玩家添加到匹配队列中
     * @param user
     */
    public void add(User user) {
        // 按等级加入队列中
        if (user.getScore() < 2000) {
            synchronized (amateurQueue) {
                amateurQueue.offer(user);
                // 只要有用户进入了, 就进行唤醒
                amateurQueue.notify();
            }
            // 打印日志
            System.out.println("把玩家"+user.getUsername()+"加入到了amateurQueue 中");
        }else if (user.getScore() >= 2000 && user.getScore() < 3000) {
            synchronized (normalQueue) {
                normalQueue.offer(user);
                normalQueue.notify();
            }
            System.out.println("把玩家"+user.getUsername()+"加入到了normalQueue 中");
        }else {
            synchronized (masterQueue) {
                masterQueue.offer(user);
                masterQueue.notify();
            }
            System.out.println("把玩家"+user.getUsername()+"加入到了masterQueue 中");
        }
    }

    /**
     *  当玩家点击停止匹配
     * 就把当前玩家匹配队列中删除
     * @param user
     */
    public void remove(User user) {
        // 按照当前等级去对应匹配队列中删除
        if (user.getScore() < 2000) {
            synchronized (amateurQueue){
                amateurQueue.remove(user);
            }
            System.out.println("把玩家"+user.getUsername()+"移除了masterQueue");
        }else if (user.getScore() >= 2000 && user.getScore() < 3000) {
            synchronized (normalQueue) {
                normalQueue.remove(user);
            }
            System.out.println("把玩家"+user.getUsername()+"移除了normalQueue");
        }else {
            synchronized (masterQueue) {
                masterQueue.remove(user);
            }
            System.out.println("把玩家"+user.getUsername()+"移除了masterQueue");
        }
    }

    // 使用3个线程去一直的进行查看是否有2个以上的人, 如果有进行匹配
    public Matcher() {
        // 创建三个线程, 操作三个匹配队列
        Thread t1 = new Thread() {
            @Override
            public void run() {
                // 扫描 amateurQueue
                while (true) {
                    handlerMatch(amateurQueue);
                }
            }
        };
        t1.start();
        Thread t2 = new Thread() {
            @Override
            public void run() {
                // 扫描 normalQueue
                while (true) {
                    handlerMatch(normalQueue);
                }
            }
        };
        t2.start();
        Thread t3 = new Thread() {
            @Override
            public void run() {
                // 扫描 masterQueue
                while (true) {
                    handlerMatch(masterQueue);
                }
            }
        };
        t3.start();
    }

    private void handlerMatch(Queue<User> matchQueue) {
        // 因为三个队列都调用了 handlerMatch 方法,因此对这个方法里面的操作进行加锁即可。
        // 针对形参进行加锁(传入不同的实参就可以对不同的队列对象进行加锁)
        synchronized (matchQueue) {
            try{
                // 1. 先查看当前队列中的元素个数, 是否满足两个
                //    在往队列里添加一个元素后仍然不能进行后续匹配操作,
                //    因此使用 while 循环检测是否有两个元素添加到队列中更合理
                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.getFromGameHall(player1.getUserId());
                WebSocketSession session2 = onlineUserManager.getFromGameHall(player2.getUserId());
                // 再次判断是否为空
                if (session1 == null && session2 != null) {
                    // 如果玩家1 掉线了,就把玩家2 重新放到匹配队列中
                    matchQueue.offer(player2);
                    return;
                }
                if (session1 != null && session2 == null) {
                    // 如果玩家2 掉线了,就把玩家1 重新放到匹配队列中
                    matchQueue.offer(player1);
                    return;
                }
                if (session1 == null && session2 == null) {
                    return;
                }
                if (session1 == session2) {
                    // 如果两个玩家是同一个用户(一个玩家入队了两次,理论上不存在,但还是需要再判定一次)
                    // 就把其中的一个玩家放回到匹配队列
                    matchQueue.offer(player1);
                    return;
                }

                // 4. 把两个玩家放入一个游戏房间中
                Room room = new Room();
                roomManager.add(room, player1.getUserId(), player2.getUserId());

                // 5. 给玩家反馈信息, 通知匹配到了对手
               // 给玩家1返回的响应
                MatchResponse response1 = new MatchResponse();
                response1.setStatus(200);
                response1.setMessage("matchSuccess");
                String json1 = objectMapper.writeValueAsString(response1);
                session1.sendMessage(new TextMessage(json1));

                // 给玩家2返回的响应
                MatchResponse response2 = new MatchResponse();
                response2.setMessage("matchSuccess");
                response2.setStatus(200);
                String json2 = objectMapper.writeValueAsString(response2);
                session2.sendMessage(new TextMessage(json2));

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

8、websocket 的匹配请求

  • 在 com.example.online_gobang.game 包下创建 MatchRequest 类。
package com.example.online_gobang.game;

import lombok.Data;

// 表示 websocket 的一个匹配请求
@Data
public class MatchRequest {
    private String message;
}

9、websocket 的匹配响应

  • 在 com.example.online_gobang.game 包下创建 MatchResponse 类。
package com.example.online_gobang.game;

import lombok.Data;

// 表示 websocket 的一个匹配响应
@Data
public class MatchResponse {
    private int status; // 状态码
    private String reason; // 响应内容(失败的原因)
    private String message; // 匹配信息
}

10、处理匹配功能的 websocket 请求

  • 在 com.example.online_gobang.api 包中创建 MatchAPI 类
  • afterConnectionEstablished 方法:在游戏大厅建立连接
  • handleTextMessage 方法:在游戏大厅中接收发送的响应
  • handleTransportError 方法:处理玩家异常下线
  • afterConnectionClosed 方法:处理玩家正常下线
package com.example.online_gobang.api;

import com.example.online_gobang.game.MatchRequest;
import com.example.online_gobang.game.MatchResponse;
import com.example.online_gobang.game.Matcher;
import com.example.online_gobang.game.OnlineUserManager;
import com.example.online_gobang.model.User;
import com.example.online_gobang.tools.Constant;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;

// 通过这个类来处理匹配功能中的 websocket 请求
@Component
public class MatchAPI extends TextWebSocketHandler {
    private ObjectMapper objectMapper = new ObjectMapper();

    @Autowired
    private OnlineUserManager onlineUserManager;

    @Autowired
    private Matcher matcher;

    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        // 玩家上线,加入到 OnlineUserManager 中

        // 1. 获取当前的用户信息(谁在游戏大厅创建连接)
        User user = (User) session.getAttributes().get(Constant.USER_SESSION_KEY);

        // 2. 判断当前用户是否已经登录
        MatchResponse response = new MatchResponse();
        if (onlineUserManager.getFromGameHall(user.getUserId()) != null ) {
            // 当前用户已经登录
            response.setMessage("当前用户已经登录!");
            response.setStatus(-200);
            response.setReason("禁止游戏多开");
            /**
             *  先通过 ObjectMapper 把 MathResponse 对象转成 JSON 字符串
             *  然后再包装上一层 TextMessage,再进行传输
             *  TextMessage 就表示一个文本格式的 websocket 数据包
              */
            session.sendMessage(new TextMessage(objectMapper.writeValueAsBytes(response)));
            session.close();
            return;
        }

        // 3. 设置在线状态
        onlineUserManager.enterGameHall(user.getUserId(),session);
        System.out.println("玩家"+user.getUsername()+"进入游戏大厅");
    }

    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        // 处理开始匹配 和 停止匹配
        User user = (User) session.getAttributes().get(Constant.USER_SESSION_KEY);
        // 获取到客户端给服务器发送的数据
        String payload = message.getPayload();
        // 当前这个数据载荷是一个 JSON 格式的字符串,需要将它转换成 Java 对象( MatchRequest )
        MatchRequest matchRequest = objectMapper.readValue(payload, MatchRequest.class); // 从客户端获取的数据
        MatchResponse matchResponse = new MatchResponse(); // 给客户端返回的数据
        if (matchRequest.getMessage().equals("startMatch")) {
            // 进入匹配队列, 加入用户
            matcher.add(user);
            // 返回响应给前端
            matchResponse.setStatus(200);
            matchResponse.setMessage("startMatch");
        }else if(matchRequest.getMessage().equals("stopMatch")) {
            // 退出匹配队列, 将用户移除
            matcher.remove(user);
            matchResponse.setMessage("stopMatch");
            matchResponse.setStatus(200);
        }else{
            // 非法情况
            matchResponse.setStatus(-200);
            matchRequest.setMessage("非法匹配");
        }
        session.sendMessage(new TextMessage(objectMapper.writeValueAsBytes(matchResponse)));
    }

    @Override
    public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
        // 玩家下线,从 OnlineUserManager 中删除
        // 1. 获取用户信息
        User user = (User) session.getAttributes().get(Constant.USER_SESSION_KEY);
        WebSocketSession webSocketSession = onlineUserManager.getFromGameHall(user.getUserId());
        if(webSocketSession == session) {
            // 2. 设置在线状态
            onlineUserManager.exitGameHall(user.getUserId());
        }
        // 如果玩家正在匹配中,而 websocket 连接断开了就应该移除匹配队列
        matcher.remove(user);
    }

    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        // 玩家下线,从 OnlineUserManager 中删除
        // 1. 获取用户信息
        User user = (User) session.getAttributes().get(Constant.USER_SESSION_KEY);
        WebSocketSession webSocketSession = onlineUserManager.getFromGameHall(user.getUserId());
        if(webSocketSession == session) {
            // 2. 设置在线状态
            onlineUserManager.exitGameHall(user.getUserId());
        }
        // 如果玩家正在匹配中,而 websocket 连接断开了就应该移除匹配队列
        matcher.remove(user);
    }
}

11、触发 websocket 约定好的请求与响应路径

  • ws://127.0.0.1:8081/findMatch(触发这个路径与服务器建立连接)
  • 在 AppConfig 类中添加代码 websocket 请求路径的代码
@Configuration
@EnableWebSocket
public class AppConfig implements WebSocketConfigurer,WebMvcConfigurer {

    @Autowired
    private MatchAPI matchAPI;

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

12、前端代码

<script>
        $.ajax({
            type:'get',
            url:'/userInfo',
            success: function(data) {
                if(data.status == 200){
                    let screenDiv = document.querySelector('#screen');
                    screenDiv.innerHTML = "玩家:&nbsp" + data.data.username + "<br>分数:&nbsp" + data.data.score
                + "<br>比赛场次:&nbsp" + data.data.totalCount + "<br>获胜场数:&nbsp" + data.data.winCount
                }else{
                    alert(data.message);
                    location.assign("login.html")
                }
            }
        });

         // 进行初始化 websocket, 并且实现前端的匹配逻辑.
        let websocketUrl = 'ws://' + location.host + '/findMatch';
        let websocket = new WebSocket(websocketUrl);
        websocket.onopen = function() {
            console.log("onopen");
        }
        websocket.onclose = function() {
            console.log("onclose");
        }
        websocket.onerror = function() {
            console.log("onerror");
        }
        // 监听页面关闭事件. 在页面关闭之前, 手动调用这里的 websocket 的 close 方法.
        window.onbeforeunload = function() {
            websocket.close();
        }

        // 匹配成功后收到的响应
        websocket.onmessage = function(e) {
            // 处理服务器返回的响应数据. 
            let resp = JSON.parse(e.data);
            // 获取到开始匹配按钮
            let matchButton = document.querySelector('#match-button');
            if(resp.status == -200) {
                console.log("游戏大厅中接收到了非法响应! " + resp.reason);
                alert(resp.message);
                location.assign("login.html");
                return;
            }
            // 判断是开始匹配, 还是结束匹配
            if (resp.message == 'startMatch') {
                // 开始匹配
                console.log("进入匹配队列成功!");
                matchButton.innerHTML = '匹配中...(点击停止)'
            } else if (resp.message == 'stopMatch') {
                // 结束匹配
                console.log("离开匹配队列成功!");
                matchButton.innerHTML = '开始匹配';
            } else if (resp.message == 'matchSuccess') {
                // 匹配成功
                console.log("匹配成功! 进入游戏房间!");
                location.assign("/game_room.html");
            }else {
                alert(resp.message);
                console.log("收到了非法的响应! message=" + resp.message);
            } 
        }


        // 给匹配按钮添加一个点击事件
        let matchButton = document.querySelector('#match-button');
        matchButton.onclick = function() {
            // 在触发 websocket 请求之前, 先确认下 websocket 连接是否正常
            if (websocket.readyState == websocket.OPEN) {
                // 如果当前 readyState 处在 OPEN 状态, 说明连接正常
                // 开始匹配/停止匹配
                if (matchButton.innerHTML == '开始匹配') {
                    console.log("开始匹配");
                    websocket.send(JSON.stringify({
                        message: 'startMatch',
                    }));
                } else if (matchButton.innerHTML == '匹配中...(点击停止)') {
                    console.log("停止匹配");
                    websocket.send(JSON.stringify({
                        message: 'stopMatch',
                    }));
                }
            } else {
                // 连接异常
                alert("当前您的连接已经断开! 请重新登录!");
                location.assign('/login.html');
            }
        }
    </script>

十一、实现对战模块

1、对战功能的请求和响应设计

(1)建立连接请求

  • 客户端通过 websocket 给服务器发送一个 json 格式的文本数据
请求:
    {
      ws://127.0.0.1:8081/game // 请求路径
    }

(2)建立连接响应

  • 玩家匹配成功后服务器生成一些游戏的初始信息返回给客户端。
响应:
    {
	    message: 'gameReady' // 消息的类别(游戏准备就绪) 
	    status: 200  // 200 是正常响应, -200 是异常响应 
        reason: ''  // 报错原因
	    roomId: ''  // 玩家所处的房间 Id
	    thisUserId: 1 // 自己的用户Id
	    thatUserId: 2 // 对手的用户Id
	    whiteUser: 1  // 执白子的玩家先手 (1:先手; 2:后手)
    }

(3)玩家落子请求

请求:
    {
        message: 'putChess'
        userId: 1 // 落子的用户id
        row: 0 // 落子的行
        col: 0 // 落子的列
    }

(4)玩家落子响应

响应:
    {
        message: 'putChess'
        userId: 1 // 落子的用户id
        row: 0 // 落子的行
        col: 0 // 落子的列
        winner:0 // 为0时: 胜负未分; 非0时(获胜者的用户id): 胜负已分
    }

2、触发 websocket 约定好的请求与响应路径

  • ws://127.0.0.1:8081/game(触发这个路径与服务器建立连接)
  • 在 AppConfig 类中添加触发 websocket 请求路径的代码
@Configuration
@EnableWebSocket
public class AppConfig implements WebSocketConfigurer,WebMvcConfigurer {
    @Autowired
    private GameAPI gameAPI;

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

3、建立连接的响应

  • 在 com.example.online_gobang.game 包下创建 GameReadyResponse 类。
package com.example.online_gobang.game;

import lombok.Data;

// 客户端连接游戏房间后,服务器返回的响应
@Data
public class GameReadyResponse {
    private String message; // 消息的类别
    private int status; // 状态码
    private String roomId; // 房间id
    private int thisUserId; // 自己的用户id
    private int thatUserId; // 对手的用户id
    private int whiteUser; // 执白子的玩家先手
}

4、websocket 的落子请求

  • 在 com.example.online_gobang.game 包下创建 GameRequest 类。
package com.example.online_gobang.game;

import lombok.Data;

// 落子请求
@Data
public class GameRequest {
    private String message;
    private int userId; // 玩家id
    private int row; // 落子的行
    private int col; // 落子的列
}

5、websocket 的落子响应

  • 在 com.example.online_gobang.game 包下创建 GameRequest 类。
package com.example.online_gobang.game;

import lombok.Data;

// 落子响应
@Data
public class GameResponse {
    private String message;
    private int userId;
    private int row;
    private int col;
    private int winner; // 判断胜负(未分出胜负为0,分出胜负则为获胜者的id)
}

6、玩家在游戏房间的在线状态

使用哈希表来保存当前玩家在游戏房间的在线状态,当玩家进入游戏房间的时候, 就将玩家的状态添加到哈希表中。

  • key是用户的Id, value是对应的 websocket 会话。
// 这个哈希表用来表示当前用户在游戏房间的在线状态
private ConcurrentHashMap<Integer, WebSocketSession> gameRoom = new ConcurrentHashMap<>();

提供三个方法

  1. 玩家进入游戏房间的时候, 将用户的状态存入哈希表中
  2. 玩家退出游戏房间的时候, 将用户的状态从哈希表中删除
  3. 在游戏房间获取当前用户的信息
  • 在 com.example.online_gobang.game 包中的 OnlineUserManager 类中添加下面的代码
@Component
public class OnlineUserManager {
    // 这个哈希表用来表示当前用户在游戏房间的在线状态
    private ConcurrentHashMap<Integer, WebSocketSession> gameRoom = new ConcurrentHashMap<>();

    // 玩家进入游戏房间
    public void enterGameRoom(int userId,WebSocketSession webSocketSession){
        gameRoom.put(userId,webSocketSession);
    }

    // 玩家退出游戏房间
    public void exitGameRoom(int userId){
        gameRoom.remove(userId);
    }

    public WebSocketSession getFromGameRoom(int userId){
        return gameRoom.get(userId);
    }
}

7、更新玩家的比赛场次、获胜场次、天梯积分

(1)在 UserMapper 接口中添加代码

@Mapper
public interface UserMapper {
    /**
     * 更新的数据:总场数 +1,获胜场数 +1,天梯积分 + 50
     * @param userId
     */
    void userWin(int userId);

    /**
     * 更新的数据:总场数 +1,获胜场数不变,天梯积分 - 50
     * @param userId
     */
    void userLose(int userId);
}

(2)在 UserMapper.xml 中添加代码

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.online_gobang.mapper.UserMapper">
    
    <update id="userWin">
        update user set totalCount = totalCount + 1,winCount = winCount + 1,score = score + 50
        where userId = #{userId};
    </update>

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

</mapper>

8、在 Room 类中实现棋局判定胜负的逻辑

(1)修改启动类

Room 要注入Spring对象, 不能使用@Autowired @Resource注解. 需要使用context

package com.example.online_gobang;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;

public class OnlineGobangApplication {

	public static ConfigurableApplicationContext context;

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

}

(2)获取 RoomManager 与 OnlineUserManager

  • 在 Room 类中通过 Room 类的构造方法获取 RoomManager 与 OnlineUserManager
// 这个类表示一个游戏房间
@Data
public class Room {

    private static final int ROW = 15;
    private static final int COL = 15;

    private OnlineUserManager onlineUserManager;

    // 引入 roomManager 用于房间销毁
    private RoomManager roomManager;

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

(3)在 Room 类中创建一个二维数组用来表示棋盘

  • 用 0 表示没有落棋子,用 1 表示玩家1落的棋子,用 2 表示玩家2落的棋子。
// 这个类表示一个游戏房间
@Data
public class Room {
    private static final int ROW = 15;
    private static final int COL = 15;

    private OnlineUserManager onlineUserManager;

    // 引入 roomManager 用于房间销毁
    private RoomManager roomManager;

    // 用于将 JSON 格式的字符串转换 Java 对象
    private ObjectMapper objectMapper = new ObjectMapper();

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

    // 处理一次落子的操作
    public void putChess(String payload) throws IOException {
        // 1. 记录当前落子的位置
       
        // 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);

        // 5. 如果当前胜负已分, 此时这个房间就失去存在的意义了. 就可以直接销毁房间. (把房间从房间管理器中给移除)
       
        // 打印棋盘内容
        private void printBoard() {
        
        }

        // 使用这个方法来判定当前落子是否分出胜负.
        // 约定如果玩家1 获胜, 就返回玩家1 的 userId
        // 如果玩家2 获胜, 就返回玩家2 的 userId
        // 如果胜负未分, 就返回 0
        private int checkWinner(int row, int col, int chess) {
            return 0;
        }
}

(4)Room 类的完整代码

package com.example.online_gobang.game;

import com.example.online_gobang.OnlineGobangApplication;
import com.example.online_gobang.mapper.UserMapper;
import com.example.online_gobang.model.User;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.Data;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;

import java.io.IOException;
import java.util.UUID;

// 这个类表示一个游戏房间
@Data
public class Room {
    private String roomId;
    private User user1;
    private User user2;
    private int whiteUser; // 先手方的玩家id

    private static final int ROW = 15;
    private static final int COL = 15;

    private OnlineUserManager onlineUserManager;

    // 引入 roomManager 用于房间销毁
    private RoomManager roomManager;

    // 用于将 JSON 格式的字符串转换 Java 对象
    private ObjectMapper objectMapper = new ObjectMapper();

    // 引入 UserMapper,用于更新比赛数据
    private UserMapper userMapper;

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

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

    // 处理一次落子的操作
    public void putChess(String payload) throws IOException {
        // 1. 记录当前落子的位置
        // 将 json 格式的字符串转换成 Java 对象
        GameRequest request = objectMapper.readValue(payload,GameRequest.class);
        GameResponse response = new GameResponse();
        // 当前这个子是玩家1 落的还是玩家2 落的. 根据这个玩家1 和 玩家2 来决定往数组中是写 1 还是 2
        int chess = request.getUserId() == user1.getUserId() ? 1 : 2;
        int row = request.getRow();
        int col = request.getCol();
        if (board[row][col] != 0) {
            System.out.println("当前位置: ("+row+" ," + col+" )" +"已经有子了");
            return;
        }
        board[row][col] = chess;

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

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

        // 4. 给房间里的所有客户端返回响应
        response.setMessage("putChess");
        response.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());
            System.out.println("玩家1 掉线!");
        }
        if (session2 == null) {
            // 玩家2 已经下线. 直接认为玩家1 获胜!
            response.setWinner(user1.getUserId());
            System.out.println("玩家2 掉线!");
        }
        // 把响应构造成 JSON 字符串, 通过 session 进行传输.
        if (session1 != null) {
            session1.sendMessage(new TextMessage(objectMapper.writeValueAsString(response)));
        }
        if (session2 != null) {
            session2.sendMessage(new TextMessage(objectMapper.writeValueAsString(response)));
        }

        // 5. 如果当前胜负已分, 此时这个房间就失去存在的意义了. 就可以直接销毁房间. (把房间从房间管理器中给移除)
        if (response.getWinner() != 0) {
            // 胜负已分
            System.out.println("游戏结束! 房间即将销毁! roomId=" + 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());
        }
    }

    // 打印棋盘内容
    private void printBoard() {
        System.out.println("打印棋盘信息, 当前房间: " + roomId);
        System.out.println("====================================================");
        for (int i = 0; i < ROW; i++) {
            for (int j = 0; j < COL; j++) {
                System.out.print(board[i][j] + " ");
            }
            System.out.println();
        }
        System.out.println("====================================================");
    }


    // 使用这个方法来判定当前落子是否分出胜负.
    // 如果玩家1 获胜, 就返回玩家1的userId
    // 如果玩家2 获胜, 就返回玩家2的userId
    // 如果胜负未分, 就返回 0
    private int checkWinner(int row, int col, int chess) {
        //  判断当前是谁获胜
        // 1. 一行五子连珠
        for (int c = col -4; c <= col && c <= COL-5; 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) {
                        // 构成五子连珠,chess == 1 获胜者是玩家1;chess == 2 获胜者是玩家2
                        return chess == 1 ? user1.getUserId() : user2.getUserId();
                }
            }catch (ArrayIndexOutOfBoundsException e){
                // 如果出现数组下标越界的情况,就忽略这个异常
                continue;
            }
        }
        // 2. 一列五子连珠
        for (int r = row - 4; r <= row && r <= ROW-5; r++) {
            try{
                if (board[r][col] == chess
                        && board[r +1][col] == chess
                        && board[r +2][col] == chess
                        && board[r +3][col] == chess
                        && board[r +4][col] == chess) {
                    return chess == 1 ? user1.getUserId() : user2.getUserId();
                }
            }catch (ArrayIndexOutOfBoundsException e){
                continue;
            }
        }
        // 3. 斜着五子连珠 -> 左上到右下(左对角线)
        for (int r = row - 4, c = col - 4; r <= row && c <= col; c++, r++){
            try {
                if (board[r][c] == chess
                        && board[r +1][c +1] == chess
                        && board[r +2][c +2] == chess
                        && board[r +3][c +3] == chess
                        && board[r +4][c +4] == chess) {
                    return chess == 1 ? user1.getUserId() : user2.getUserId();
                }
            }catch (ArrayIndexOutOfBoundsException e) {
                continue;
            }
        }
        // 4. 斜着五子连珠 -> 右上到左下(右对角线)
        for (int r = row - 4, c = col + 4; r <= row && c >= col; r++, c--) {
            try {
                if (board[r][c] == chess
                        && board[r + 1][c - 1] == chess
                        && board[r + 2][c - 2] == chess
                        && board[r + 3][c - 3] == chess
                        && board[r + 4][c - 4] == chess) {
                    return chess == 1 ? user1.getUserId() : user2.getUserId();
                }
            } catch (ArrayIndexOutOfBoundsException e) {
                continue;
            }
        }

        // 胜负未分返回0
        return 0;
    }
}

9、处理对战功能的 websocket 请求

  • 在 com.example.online_gobang.api 包中创建 GameAPI 类。
  • afterConnectionEstablished 方法:游戏房间建立连接.
  • handleTextMessage 方法:在游戏房间中接收发送的响应
  • handleTransportError 方法:玩家异常下线
  • afterConnectionClosed 方法:玩家正常下线
package com.example.online_gobang.api;

import com.example.online_gobang.game.*;
import com.example.online_gobang.mapper.UserMapper;
import com.example.online_gobang.model.User;
import com.example.online_gobang.tools.Constant;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;

import javax.annotation.Resource;
import java.io.IOException;
import java.util.Random;

@Component
public class GameAPI extends TextWebSocketHandler {

    @Autowired
    private OnlineUserManager onlineUserManager;

    @Autowired
    private RoomManager roomManager;

    @Autowired
    private ObjectMapper objectMapper;

    @Resource
    private UserMapper userMapper;

    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        GameReadyResponse gameReadyResponse = new GameReadyResponse();

        // 1. 获取玩家信息
        User user = (User) session.getAttributes().get(Constant.USER_SESSION_KEY);
        if(user == null){
            gameReadyResponse.setStatus(-200);
            gameReadyResponse.setReason("用户未登录");
            session.sendMessage(new TextMessage(objectMapper.writeValueAsBytes(gameReadyResponse)));
            return;
        }
        // 2. 判断当前玩家是否已经进入房间
        Room room = roomManager.getRoomByUserId(user.getUserId());
        if (room == null) {
            gameReadyResponse.setStatus(-200);
            gameReadyResponse.setMessage("玩家尚未匹配到!");
            session.sendMessage(new TextMessage(objectMapper.writeValueAsBytes(gameReadyResponse)));
            return;
        }
        // 3. 判断当前玩家是否多开(如果一个账号一边在游戏大厅一边在游戏房间,这种也视为多开)
        if (onlineUserManager.getFromGameHall(user.getUserId()) != null || onlineUserManager.getFromGameRoom(user.getUserId()) != null) {
            gameReadyResponse.setReason("禁止玩家多开游戏页面!");
            gameReadyResponse.setStatus(-200);
            gameReadyResponse.setMessage("repeatConnection");
            session.sendMessage(new TextMessage(objectMapper.writeValueAsBytes(gameReadyResponse)));
            return;
        }

        // 4. 设置当前玩家上线(进入游戏房间)
        onlineUserManager.enterGameRoom(user.getUserId(), session);

        // 5. 把两个玩家加入到游戏房间中
        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 = new Random();
                int num = random.nextInt(10);
                if (num % 2 == 0) {
                    room.setWhiteUser(room.getUser1().getUserId());
                } else{
                    room.setWhiteUser(room.getUser2().getUserId());
                }

                // 当两个玩家都加入成功后, 让服务器给这两个玩家都返回 websocket 的响应数据.
                // 通知玩家1
                noticeGameReady(room,room.getUser1(),room.getUser2());
                // 通知玩家2
                noticeGameReady(room,room.getUser2(),room.getUser1());
                return;
            }
        }

        // 6. 如果又有其他玩家连接到已经满了的房间,给出一个提示。(这种情况理论上不存在)
        gameReadyResponse.setStatus(-200);
        gameReadyResponse.setMessage("当前房间已满");
        session.sendMessage(new TextMessage(objectMapper.writeValueAsBytes(gameReadyResponse)));
    }

    private void noticeGameReady(Room room, User thisUser, User thatUser) throws IOException {
        GameReadyResponse resp = new GameReadyResponse();
        resp.setStatus(200);
        resp.setReason("");
        resp.setMessage("gameReady");
        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)));
    }

    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        // 1. 先从 session 中获取当前用户的身份信息
        User user = (User) session.getAttributes().get(Constant.USER_SESSION_KEY);
        if (user == null){
            System.out.println("[handleTextMessage]当前玩家"+ user.getUsername()+"未登录");
            return;
        }
        // 2. 根据玩家id 获取房间对象
        Room room =  roomManager.getRoomByUserId(user.getUserId());

        // 通过room对象处理这次请求
        room.putChess(message.getPayload());
    }

    @Override
    public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
        // 玩家异常下线
        // 1. 获取玩家信息
        User user = (User) session.getAttributes().get(Constant.USER_SESSION_KEY);
        WebSocketSession webSocketSession = onlineUserManager.getFromGameRoom(user.getUserId());
        if(webSocketSession == session){
            // 2. 退出游戏房间
            onlineUserManager.exitGameRoom(user.getUserId());
        }
        System.out.println("当前用户: " + user.getUsername()+" 游戏房间连接异常!");

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

    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        // 玩家正常下线
        // 1. 获取玩家信息
        User user = (User) session.getAttributes().get(Constant.USER_SESSION_KEY);
        WebSocketSession webSocketSession = onlineUserManager.getFromGameRoom(user.getUserId());
        if(webSocketSession == session){
            // 2. 退出游戏房间
            onlineUserManager.exitGameRoom(user.getUserId());
        }
        System.out.println("当前用户: " + user.getUsername()+" 离开房间");

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

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

        // 2. 根据房间找到对手
        User thatUser = (user == room.getUser1()) ? room.getUser2() : room.getUser1();
        // 3. 获取对手的在线状态
        WebSocketSession webSocketSession = onlineUserManager.getFromGameRoom(thatUser.getUserId());
        if (webSocketSession == null) {
            // 对手掉线了!
            System.out.println("对方掉线了, 无需通知!");
            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());
    }
}

10、前端代码

(1)使用 canvas 绘制棋盘(game_room.html)

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>游戏房间</title>
    <link rel="stylesheet" href="css/common.css">
    <link rel="stylesheet" href="css/game_room.css">
</head>
<body>
    <div class="container">
        <div >
            <!-- 棋盘区域,需要基于 canvas 进行实现  -->
            <canvas id="chess" width="450px" height="450px">
            </canvas>
            <!-- 提示区 -->
            <div id="screen">等待玩家连接中...</div>
            <div class="buttons"></div>
        </div>
    </div>
    <script src="js/script.js"></script>
</body>
</html>

(2)绘制棋盘的 JavaScript 文件(script.js)

  • setScreenText 方法:用来将显示框中的内容, 根据当前是哪个玩家下的棋来修改内容。
  • initGame 方法:用来初始画棋盘的, 棋盘大小为 15 * 15。
  • oneStep 方法:当点击下子之后, 会绘制对应颜色的棋子。
  • onmessage 方法:用来处理后端传输的响应(使用 isWhite 判断是否是先手方)。
  • send方法: 通过 websocket 发送落子请求。
  • 棋盘数组:0 表示该位置没有落子, 1表示该位置已经落子,避免一个位置重复落子。
let gameInfo = {
    roomId: null,
    thisUserId: null,
    thatUserId: null,
    isWhite: true,
}


// 设定界面显示的相关操作
function setScreenText(me) {
    let screen = document.querySelector('#screen');
    if (me) {
        screen.innerHTML = "轮到你落子了!";
    } else {
        screen.innerHTML = "轮到对方落子了!";
    }
}

// 初始化 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("和服务器的连接出现异常!");
}

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

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

    if(resp.message != 'gameReady') {
        console.log("响应类型错误");
        location.assign("game_hall.html");
        return;
    }

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

    // 游戏就绪
    if (resp.message == 'gameReady') {
        gameInfo.roomId = resp.roomId;
        gameInfo.thisUserId = resp.thisUserId;
        gameInfo.thatUserId = resp.thatUserId;
        // 判断先手方,如果执白子的Userid是自己,那么就是自己先手
        gameInfo.isWhite = (resp.whiteUser == resp.thisUserId);

        // 初始化棋盘
        initGame();
        // 设置显示区域的内容
        setScreenText(gameInfo.isWhite);
    } else if (resp.message == 'repeatConnection') { // 重复连接
        alert("检测到游戏多开(游戏大厅和游戏房间多开)! 请使用其他账号登录!");
        location.assign("/login.html");
    }
}

// 初始化一局游戏
function initGame() {
    // 是我下还是对方下. 根据服务器分配的先后手情况决定
    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 = "#000000";
    // 背景图片
    let logo = new Image();
    logo.src = "images/game_room2.png";
    logo.onload = function () {
        context.drawImage(logo, 0, 0, 450, 450);
        initChessBoard();
    }

    // 绘制棋盘网格
    function initChessBoard() {
        for (let i = 0; i < 15; i++) {
            context.moveTo(15 + i * 30, 15);
            context.lineTo(15 + i * 30, 435);
            context.stroke();
            context.moveTo(15, 15 + i * 30);
            context.lineTo(435, 15 + i * 30);
            context.stroke();
        }
    }

    // 绘制一个棋子, me 为 true
    function oneStep(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) {
            // 发送坐标给服务器, 服务器要返回结果
            send(row, col);
        }
    }

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

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

    // 之前 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("响应类型错误!");
            location.assign("game_hall.html")
            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);
            }
            // 回到游戏大厅
            let room = document.querySelector(".buttons");
            console.log(room)
            let backButton = document.createElement("button");
            backButton.innerHTML = '返回游戏大厅';
            backButton.className = "backButton";
            backButton.onclick = function() {
                location.assign('/game_hall.html');
            }
            room.appendChild(backButton);
        }
    }
}

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

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

相关文章

第2-4-9章 规则引擎Drools实战(2)-信用卡申请

文章目录9.2 信用卡申请9.2.1 计算规则9.2.2 实现步骤9.2 信用卡申请 全套代码及资料全部完整提供&#xff0c;点此处下载 本小节我们需要通过Drools规则引擎来根据规则进行申请人的合法性检查&#xff0c;检查通过后再根据规则确定信用卡额度&#xff0c;最终页面效果如下&a…

浅谈架构备考.补缺.V1

2022.11.28 可靠性分析 to repair to failure between failure 平均故障间隔时间。平均故障间隔时间&#xff08;Mean Time Between Failure&#xff0c;MTBF&#xff09;常常与 MTTF 发生混淆。 因为两次故障&#xff08;失败&#xff09;之间必然有修复行为&#xff0c;因…

SpringBoot主启动类使用@ComponentScans、@ComponentScan扫描组件类,注意避坑

前言&#xff1a; 1、大家都知道&#xff0c;Springboot主启动加载会默认扫描同级包目录下所有的组件类、配置类&#xff0c;然后进行解析注入到Spring容器中。SpringBootApplication 是个联合注解&#xff0c;里面包含了 ComponentScan 组件扫描注解&#xff0c;所以我们不需要…

沉睡者IT - 什么是NFT?

欢迎关注沉睡者IT&#xff0c;点上面关注我 ↑ ↑ NFT&#xff0c;全称为Non-Fungible Token&#xff0c;指非同质化通证&#xff0c;实质是区块链网络里具有唯一性特点的可信数字权益凭证&#xff0c;是一种可在区块链上记录和处理多维、复杂属性的数据对象。 以上是百度百科…

MongoBd 离线安装与管理

背景&#xff1a; 鉴于内部网络原因&#xff0c;可能一个简单的操作变得复杂化&#xff0c;现在就Mongodb的离线安装分享本人的操作经验: 材料&#xff1a; 操作系统&#xff1a;centos7.6 MongoDB(主程序) : mongodb-linux-x86_64-rhel70-6.0.1.tgz 下载地址&#xff1a;下载…

传输层协议 —— UDP

目录 一、端口号的划分范围 二、认识知名端口号 三、两个问题 四、nestat和pidof命令 五、UDP协议 1. UDP首部格式 2. UDP的特点 3. 面向数据报 4. UDP的缓冲区 5. UDP使用注意事项 6. 基于UDP的应用层协议 一、端口号的划分范围 端口号的长度是16位&#xff0c;因此…

博途PLC和MATLAB矩阵运算存储方法对比

MATLBA不用多说,号称矩阵实验室可想而知在MATLAB里对矩阵的存储、运算非常简单、高效。如下图简单定义一个5*3的矩阵 1、rand(5*3) 上面利用rand()函数简单的实现了内存矩阵存储空间分配+附随机初值,下面我们看下博途里的矩阵定义存储方法。 BP神经网络PID算法的PLC实现过程…

量表如何分析?

一、什么是量表 量表是一种测量工具&#xff0c;通常用来测量人们的主观态度、意见或价值观念。我们经常会在问卷中使用量表对调查对象进行测量&#xff0c;最常见到的就是李克特量表。 ‍1、定义&#xff1a;李克特量表 李克特量表是最常用的量表&#xff0c;是由美国社会心…

基于AD Event日志检测LSASS凭证窃取攻击

01、简介 简单介绍一下&#xff0c;LSASS(本地安全机构子系统服务)在本地或域中登录Windows时&#xff0c;用户生成的各种凭证将会存储在LSASS进程的内存中&#xff0c;以便用户不必每次访问系统时重新登录。 攻击者在获得起始攻击点后&#xff0c;需要获取目标主机上的相关凭证…

AutoCAD Electrical 2022—项目特性

当绘图的过程中如果弹出上面的对话框&#xff0c;就是库和图标菜单途径不对造成的&#xff1b; 点击浏览找到正确的位置或点击默认设置恢复默认的路径&#xff1b; 元件对应原理图的设置&#xff1b; 标记格式&#xff1a;放置元件的代号的格式&#xff1b; 线号&#xff1a;编…

iphone怎么传数据到另一个手机,苹果如何转移数据到新手机,两台iphone怎么同步所有数据

换新手机后&#xff0c;需要迁移旧苹果手机的数据到新苹果手机里面&#xff0c;那么&#xff0c;iphone怎么传数据到另一个手机&#xff1f;本篇文章带您深度了解苹果手机的数据传输技巧。 方法一、通过“快速开始”传输数据 苹果手机如何数据传输&#xff1f;我记得之前换 iP…

【JUC】信号量Semaphore详解

前言 大家应该都用过synchronized 关键字加锁&#xff0c;用来保证某个时刻只允许一个线程运行。那么如果控制某个时刻允许指定数量的线程执行&#xff0c;有什么好的办法呢? 答案就是JUC提供的信号量Semaphore。 介绍和使用 Semaphore&#xff08;信号量&#xff09;可以用…

Servlet API 表白墙

Servlet API 详解 主要三个: 1.HttpServlet 2.HttpServletRequest 3.HttpServletResponse 1.HttpServlet 方法名称 调用时机 init 在 HttpServlet 实例化之后被调用一次 destroy 在 HttpServlet 实例不再使用的时候调用一次 service 收到 HTTP 请求的时候调用 …

vue开发测评系统思路及踩坑

最近公司做了一个测评系统&#xff0c;因为时间很短&#xff0c;本以为会很简单&#xff0c;没有想到踩了很多坑。 先看下部分效果图吧 然后在说下需求 1&#xff1a;所有的答案都是动态的&#xff08;例如选择是出来的是第二题&#xff0c;选择否出来的是第五题&#xff09…

【Linux】文件权限的理解

不用心做一件事情&#xff0c;你永远不知道自己有多么的强大&#xff01; 文章目录一、shell命令以及运行原理(centos7下&#xff0c;shell为命令行解释器bash)1. 什么是shell(Kernel外层的软件层)&#xff1f;2. shell的交互方式存在意义3. windows GUI对比Linux shell(都是Ke…

算法: C# 中将 Dictionary 集合用作 Hashmap 等价类型

一.只出现一次的数字 1.1 题目描述 给你一个整数数组 nums &#xff0c;除某个元素仅出现 一次 外&#xff0c;其余每个元素都恰出现 三次 。请你找出并返回那个只出现了一次的元素。 示例 1&#xff1a; 输入&#xff1a;nums [2,2,3,2] 输出&#xff1a;3 示例 2&#…

Faster RCNN全文翻译

Abstract—State-of-the-art【最先进的】 object detection networks depend on region proposal algorithms to hypothesize【假设、推测】 object locations.Advances like SPPnet [1] and Fast R-CNN [2] have reduced the running time of these detection networks, expos…

赞叹AI的力量-TopazLabs 全家桶使用经历

一、Topaz Gigapixel AI 之前有用过日本的一个2x提升的在线网站服务waifu2x 是通过深度卷积神经网络来实现的&#xff0c;对于anime-style的图片效果是非常好的&#xff0c;使用过之后发现对于一些真实图片效果也不错&#xff0c;只是放大之后能明显的看到局部失真。 效果图&…

详解nginx的root与alias

文章目录1. 结论2. 详解root2.1 基本用法2.2 location的最左匹配原则2.3 index2.4 nginx location解析url工作流程2.5 末尾/3. 详解alias3.1 基本用法4. 特殊情况4.1 alias指定文件4.2 root指定文件nginx版本: 1.18.0 1. 结论 location命中后 如果是root&#xff0c;会把请求…

Anaconda、Conda、pip、Virtualenv的区别

一、Anaconda 1.1 简介 Anaconda是一个包含180的科学包及其依赖项的发行版本。其包含的科学包包括&#xff1a;conda, numpy, scipy, ipython notebook等。 二、Conda 2.1 简述 conda是包及其依赖项和环境的管理工具。 适用语言&#xff1a;Python, R, Ruby, Lua, Scala, …