文章目录

  • 1. 项目设计
  • 2. 效果图展示
  • 3. 创建项目以及配置文件
    • 3.1 创建项目
    • 3.2 配置文件
      • 3.2.1 在 application.properties 中添加配置文件
      • 3.2.2 在 resources 目录下创建mapper
  • 4. 数据库设计与实现
  • 5. 登录注册模块
    • 5.1 设计登录注册交互接口
    • 5.2 设置登录注册功能返回的响应类
    • 5.3 使用 BCrypt 对密码进行加密
    • 5.4 完成 MyBatis 操作
    • 5.5 后端的实现
      • 5.5.1 登录功能后端实现
      • 5.5.2 注册功能后端实现
      • 5.5.3 注销功能
    • 5.6 前端的实现
      • 5.6.1 登录前端实现
      • 5.6.2 注册前端实现
    • 5.7 添加拦截器
  • 6. 大厅界面
    • 6.1 交互接口设计
    • 6.2 用户加载前后交互接口
    • 6.3 前端和后端实现用户信息加载
      • 6.3.1 后端的实现
      • 6.3.2 前端的实现
    • 6.4 实现匹配功能的前端代码
    • 6.5 实现匹配功能的后端代码
      • 6.5.1 创建在线状态
      • 6.5.2 创建房间对象
      • 6.5.3 创建房间管理器
      • 6.5.4 创建匹配队列
      • 6.5.5 写完 MatchController
    • 6.6 大厅界面总结
  • 7. 房间界面
    • 7.1 交互接口设计
    • 7.2 实现房间界面前端代码
      • 7.2.1 设置棋盘界面, 以及显示框.
      • 7.2.2 对应的js文件
      • 7.2.3 初始化 websocket
      • 7.2.4 落子时,发送落子请求
      • 7.2.5 落子时, 发送落子响应
    • 7.3 实现房间界面后端代码
      • 7.3.1 注册GameController
      • 7.3.2 创建GameController
      • 7.3.3 创建对应的响应类和请求类
      • 7.3.4 完成用户房间在线状态管理
      • 7.3.5 添加 MyBatis 用来更新玩家积分
      • 7.3.6 完成处理连接方法
      • 7.3.7 完成处理连接断开的方法和连接异常的方法
      • 7.3.8 在房间管理器中添加代码
      • 7.3.9 Room类添加棋盘代码
      • 7.3.10 实现handleTextMessage方法
      • 7.3.11 实现putChess方法
      • 7.3.12 完成用户胜负判断

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.Drivermybatis.mapper-locations=classpath:mapper/**Mapper.xml

3.2.2 在 resources 目录下创建mapper

mapper下添加 目录 **.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.onlinemusicserver.mapper."对应的Mapper""></mapper>

4. 数据库设计与实现


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

create database if not exists onlineGobang;use onlineGobang;drop table if exists user;
create table user(userId int primary key auto_increment,username varchar(20) unique,password varchar(255) not null,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 设置登录注册功能返回的响应类

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

@Data
public class ResponseBodyMessage<T> {private int status;private String message;private T data;public ResponseBodyMessage(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.

@Configuration
public class AppConfig implements WebMvcConfigurer {@Beanpublic BCryptPasswordEncoder getBCryptPasswordEncoder() {return new BCryptPasswordEncoder();}
}

5.4 完成 MyBatis 操作

在 model 包中, 创建 User 实体类

@Data
public class User {private int userId;private String username;private String password;private int score;private int totalCount;private int winCount;
}

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

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

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

<?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.gobang.mapper.UserMapper"><insert id="insert">insert into user values(null,#{username},#{password},1000,0,0)</insert><select id="selectByName" resultType="com.example.gobang.model.User">select * from user where username = #{username}</select>
</mapper>

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


@Service
public class UserService {@Autowiredprivate UserMapper userMapper;public int insert(User user){return userMapper.insert(user);}public User selectByName(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")public ResponseBodyMessage<User> login(@RequestBody User user, HttpServletRequest request) {User truUser = userService.selectByName(user.getUsername());if (truUser == null) {System.out.println("登录失败!");return new ResponseBodyMessage<>(-1,"用户名密码错误!",user);}else {boolean flg = bCryptPasswordEncoder.matches(user.getPassword(),truUser.getPassword());if (!flg) {return new ResponseBodyMessage<>(-1,"用户名密码错误!",user);}System.out.println("登录成功!");HttpSession session = request.getSession(true);session.setAttribute(Constant.USER_SESSION_KEY,truUser);return new ResponseBodyMessage<>(1,"登录成功!",truUser);}}

5.5.2 注册功能后端实现

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

5.5.3 注销功能

直接删除对应session 为 Constant.USER_SESSION_KEY, 然后跳转到login.html

@RequestMapping("/logout")public void userLogout(HttpServletRequest request, HttpServletResponse response) throws IOException, 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

public class LoginInterceptor implements HandlerInterceptor {@Overridepublic 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 true;}response.sendRedirect("/login.html");return false;}
}

AppConfig

@Configuration
@EnableWebSocket
public class AppConfig implements WebSocketConfigurer,WebMvcConfigurer{@Overridepublic void addInterceptors(InterceptorRegistry registry) {LoginInterceptor loginInterceptor = new LoginInterceptor();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")public ResponseBodyMessage<User> getUserInfo(HttpServletRequest request) {HttpSession session = request.getSession(false);User user = (User) session.getAttribute(Constant.USER_SESSION_KEY);if (user == null) {return new ResponseBodyMessage<>(-1,"当前用户不存在",null);}else{return new ResponseBodyMessage<>(1,"查找成功!", newUser);}}

6.3.2 前端的实现

      load();function load() {$.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 = new WebSocket(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 = '匹配中...(点击停止)';}else if(resp.message == 'stopMatch') {//结束匹配console.log("结束匹配");play.innerHTML = '开始匹配';}else if(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',}))}else if(play.innerHTML == '匹配中...(点击停止)'){// 发送停止匹配的请求websocket.send(JSON.stringify({message: 'stopMatch',}))}}else{// 这里就是链接异常的情况alert('当前您的链接已经断开, 请重新登录');location.assign("login.html");}}

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

这里是触发url的响应地址

@Configuration
@EnableWebSocket
public class AppConfig implements WebSocketConfigurer{@Autowiredprivate MatchController matchController;@Overridepublic void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {registry.addHandler(matchController,"/findMatch").addInterceptors(new HttpSessionHandshakeInterceptor());}
}

6.5.1 创建在线状态

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

  1. 这里存储的, key是用户的Id, value是对应的WebSocketSession的信息.
  2. 提供三个方法
    • 进入房间的时候, 将用户的状态存入哈希表中
    • 退出房间的时候, 将用户的状态从哈希表中删除
    • 获取当前用户的 WebSocketSession 信息
@Component
public class OnlineUserManager {// 这个哈希表是表示当前用户在游戏大厅的在线状态private ConcurrentHashMap<Integer, WebSocketSession> gameState = new ConcurrentHashMap<>();public void enterGameIndex(int userId, WebSocketSession webSocketSession) {gameState.put(userId,webSocketSession);}public void exitGameHall(int userId) {gameState.remove(userId);}public WebSocketSession getState(int userId) {return gameState.get(userId);}
}

6.5.2 创建房间对象

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


// 游戏房间
@Data
public class Room {private String roomId;private User user1;private User user2;public Room() {this.roomId = UUID.randomUUID().toString();}
}

6.5.3 创建房间管理器

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

  1. 添加用户进入到房间
  2. 删除房间中的用户
  3. 提供房间Id得到房间对象

// 房间管理器
@Component
public class RoomManager {private ConcurrentHashMap<String,Room> rooms = new ConcurrentHashMap<>();public void insert(Room room) {rooms.put(room.getRoomId(),room);}public void remove(String roomId) {rooms.remove(roomId);}public Room findRoomByRoomId(String roomId) {return rooms.get(roomId);}

6.5.4 创建匹配队列

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

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

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

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

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

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


// 匹配器, 这个类是用来完成匹配功能的
@Component
public class Matcher {// 创建匹配队列 按等级划分// 1. < 2000private Queue<User> simpleQueue = new LinkedList<>();// 2. >= 2000 && < 3000private Queue<User> normalQueue = new LinkedList<>();// 3. >= 3000private Queue<User> highQueue = new LinkedList<>();@Autowiredprivate OnlineUserManager onlineUserManager;@Autowiredprivate ObjectMapper objectMapper;@Autowiredprivate RoomManager roomManager;/*** 将当前玩家添加到匹配队列中* @param user*/public void insert(User user) {// 按等级加入队列中if (user.getScore() < 2000) {synchronized (simpleQueue) {simpleQueue.offer(user);// 只要有用户进入了, 就进行唤醒simpleQueue.notify();}}else if (user.getScore() >= 2000 && user.getScore() < 3000) {synchronized (normalQueue) {normalQueue.offer(user);normalQueue.notify();}}else {synchronized (highQueue) {highQueue.offer(user);highQueue.notify();}}}/*** 将当前玩家匹配队列中删除* @param user*/public void remove(User user) {// 按照当前等级去对应匹配队列中删除if (user.getScore() < 2000) {synchronized (simpleQueue){simpleQueue.remove(user);}}else if (user.getScore() >= 2000 && user.getScore() < 3000) {synchronized (normalQueue) {normalQueue.remove(user);}}else {synchronized (highQueue) {highQueue.remove(user);}}}/*** 这里使用3个线程去一直的进行查看是否有2个以上的人, 如果有进行匹配*/public Matcher() {// 创建三个线程, 操作三个匹配队列Thread t1 = new Thread() {@Overridepublic void run() {while (true) {handlerMatch(simpleQueue);}}};t1.start();Thread t2 = new Thread() {@Overridepublic void run() {while (true) {handlerMatch(normalQueue);}}};t2.start();Thread t3 = new Thread() {@Overridepublic void run() {while (true) {handlerMatch(highQueue);}}};t3.start();}private void handlerMatch(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 = new Room();roomManager.insert(room,player1.getUserId(),player2.getUserId());// 5. 给玩家反馈信息, 通知匹配到了对手MatchResponse response1 = new MatchResponse();response1.setStatus(1);response1.setMessage("matchSuccess");String json1 = objectMapper.writeValueAsString(response1);session1.sendMessage(new TextMessage(json1));MatchResponse response2 = new MatchResponse();response2.setMessage("matchSuccess");response2.setStatus(1);String json2 = objectMapper.writeValueAsString(response2);session2.sendMessage(new TextMessage(json2));} catch (IOException | InterruptedException e) {e.printStackTrace();}}}
}

6.5.5 写完 MatchController

websocket 有4个方法.

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

<!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 href="css/game_room.css" rel="stylesheet" type="text/css" media="all" />
</head>
<body><div class="container"><div class="one"><!-- 棋盘区域, 需要基于 canvas 进行实现 --><canvas id="chess" width="450px" height="450px"></canvas><!-- 显示区域 --><div id="screen"> 等待玩家连接中... </div></div></div><script src="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,
}//
// 设定界面显示相关操作
//function setScreenText(me) {let screen = document.querySelector('#screen');if (me) {screen.innerHTML = "轮到你落子了!";} else {screen.innerHTML = "轮到对方落子了!";}
}//
// 初始化 websocket
//
// TODO//
// 初始化一局游戏
//
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 = "#BFBFBF";// 背景图片let logo = new Image();logo.src = "image/sky.jpeg";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, 430);context.stroke();context.moveTo(15, 15 + i * 30);context.lineTo(435, 15 + i * 30);context.stroke();}}// 绘制一个棋子, me 为 truefunction 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) {// 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 = new WebSocket(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 落子时,发送落子请求

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

function send(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;} else if (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 = "恭喜你, 获胜了!";}else if(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
@EnableWebSocket
public class AppConfig implements WebSocketConfigurer,WebMvcConfigurer{@Autowiredprivate GameController gameController;@Overridepublic void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {registry.addHandler(gameController,"/game").addInterceptors(new HttpSessionHandshakeInterceptor());}

7.3.2 创建GameController

  1. afterConnectionEstablished 这个方法是在建立连接时候的方法.
  2. handleTextMessage 这个方法是接收发送的响应
  3. handleTransportError 这个方法是出现异常的时候执行的
  4. afterConnectionClosed 这个方法是关闭websocket的时候执行的
@Component
public class GameController extends TextWebSocketHandler {@Overridepublic void afterConnectionEstablished(WebSocketSession session) throws Exception {}@Overrideprotected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {}@Overridepublic void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {}@Overridepublic void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {}
}

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

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

// 客户端链接成功后, 返回的响应
@Data
public class GameReadyResponse {private String message;private int status;private String roomId;private int thisUserId;private int thatUserId;private int whiteUser;
}

落子请求

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

落子响应

//落子响应
@Data
public class GameResponse {private String message;private int userId;private int row;private int col;private int winner;
}

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

在之前的 OnlineUserManager 中添加代码

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

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

UserMapper

    // 总场数 + 1, 获胜场数+1, 天梯分数 + 50void userWin(int userId);// 总场数 + 1, 天梯分数 -50void userLose(int userId);

UserMapper.xml

    <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>

UserService

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

7.3.6 完成处理连接方法

  1. 首先获取用户的信息
  2. 判断当前是否已经进入房间了, 防止未匹配成功
  3. 判断是否多开, 这里要查询房间在线情况, 和大厅在线情况.
  4. 然后让用户房间的在线状态处于在线.
  5. 首先判断用户1是否上线, 上线就添加到当前房间来, 用户2再上线的时候也添加房间来, 这里可以设置谁是先手方, 根据自己设定的规则.我这里是随机取0~9的数字, 如果是偶数用户1就是先手, 如果是奇数用户2就是先手
  6. 当用户都进入房间的时候, 通知玩家准备就绪了
  7. 注意这里的线程安全问题. 多个用户进入同一个方法,就有可能出现线程安全问题, 由于是同一个房间的用户进行, 只需要对房间对象加锁就可以了.
 @Overridepublic void afterConnectionEstablished(WebSocketSession session) throws Exception {GameReadyResponse readyResponse = new GameReadyResponse();// 获取用户信息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(new TextMessage(objectMapper.writeValueAsBytes(readyResponse)));return;}// 判断当前是否多开if (onlineUserManager.getRoomState(user.getUserId()) != null || onlineUserManager.getState(user.getUserId()) != null) {readyResponse.setMessage("当前用户已经登录!");readyResponse.setStatus(-1);session.sendMessage(new TextMessage(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 = new Random();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(new TextMessage(objectMapper.writeValueAsBytes(readyResponse)));}
private void noticeGameReady(Room room, User user1, User user2) throws IOException {GameReadyResponse resp = new GameReadyResponse();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(new TextMessage(objectMapper.writeValueAsString(resp)));}

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

  1. 首先获取用户的信息
  2. 然后设置用户房间状态为下线
  3. 注意这里掉线了, 就需要判断对方赢了.
    • 判断对方是否掉线, 如果对方也掉线了, 就无需通知谁赢了
    • 如果对方没有掉线, 就通知对方赢了
    • 获胜之后, 要对玩家的信息, 场次, 胜场进行更新. 然后关闭房间
@Overridepublic void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {// 异常下线// 下线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);}
    @Overridepublic void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {// 下线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);}
private void noticeThatUserWin(User user) throws IOException {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 = new GameResponse();gameResponse.setMessage("putChess");gameResponse.setUserId(thatUser.getUserId());gameResponse.setWinner(thatUser.getUserId());session.sendMessage(new TextMessage(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为用户的对应房间号
    private ConcurrentHashMap<Integer,String> Ids = new ConcurrentHashMap<>();public void insert(Room room,int userId1, int userId2) {Ids.put(userId1,room.getRoomId());Ids.put(userId2,room.getRoomId());}public void remove(String roomId,int userId1, int userId2) {Ids.remove(userId1);Ids.remove(userId2);}public Room findRoomByUserId(int userId) {String roomId = Ids.get(userId);if (roomId == null) {return null;}return rooms.get(roomId);}

7.3.9 Room类添加棋盘代码

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

修改启动类

public class GobangApplication {public static ConfigurableApplicationContext context;public static void main(String[] args) {context = SpringApplication.run(GobangApplication.class, args);}}
// 游戏房间
@Data
public class Room {private String roomId;private User user1;private User user2;private int whiteUser;private OnlineUserManager onlineUserManager;private RoomManager roomManager;private UserService userService;public Room() {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落子private int[][] board= new int[Constant.ROW][Constant.COL];private ObjectMapper objectMapper = new ObjectMapper();}

7.3.10 实现handleTextMessage方法

落子请求

    @Overrideprotected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {// 获取用户对象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. 如果胜负已分, 更新玩家获胜的信息, 并销毁房间
    // 这个方法是用来处理一次落子的操作public void putChess(String reqJson) throws IOException {// 1. 记录当前落子的位子GameRequest request = objectMapper.readValue(reqJson,GameRequest.class);GameResponse response = new GameResponse();// 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(new TextMessage(respJson));}if (session2 != null) {session2.sendMessage(new TextMessage(respJson));}// 4. 如果当前获胜, 销毁房间if (response.getWinner() != 0) {System.out.println("游戏结束, 房间即将销毁");// 更新获胜方的信息int winId = response.getWinner();int LoseId = 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, 如果还没有获胜者, 就返回0private int checkWinner(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;}}return 0;}

基于 SpringBoot + MyBatis 的在线五子棋对战相关推荐

  1. 基于 SpringBoot + MyBatis 的在线音乐播放器

    文章目录 4. 数据库的设计与实现 6.3 了解 MD5 加密 和 BCrypt 加密 6.4 在Config中 注入 BCryptPasswordEncoder 对象 6.5.1 LoginInte ...

  2. Jeecg-Boot 1.0 版本发布,基于SpringBoot+Mybatis+AntDesign快速开发平台

    基于SpringBoot+Mybatis+AntDesign企业级快速开发平台 引言:      Jeecg-Boot 一款基于代码生成器的J2EE快速开发框架!  采用前后端分离技术: Spring ...

  3. 基于springboot+mybatis+mysql+html实现宠物医院管理系统(包含实训报告)

    基于springboot+mybatis+mysql+html实现宠物医院管理系统(包含实训报告) 一.系统简介 二.系统主要功能界面 2.1登陆 2.2系统设置 2.3宠物管理 2.4预约管理 2. ...

  4. 基于Springboot+mybatis+mysql+html图书管理系统2

    基于Springboot+mybatis+mysql+html图书管理系统2 一.系统介绍 二.功能展示 1.用户登陆 2.用户主页 3.图书查询 4.还书 5.个人信息修改 6.图书管理(管理员) ...

  5. 基于springboot+mybatis+mysql+vue运动会报名管理系统

    基于springboot+mybatis+mysql+vue运动会报名管理系统 一.系统介绍 二.功能展示 1.用户登陆 2.报名详情(运行员) 3.比赛报名(运动员) 4.个人参赛项目(运动员) 5 ...

  6. 基于springboot+mybatis+mysql+vue房屋租赁管理系统

    基于springboot+mybatis+mysql+vue房屋租赁管理系统 一.系统介绍 二.功能展示 1.用户登陆 2.管理员端主要功能 2.房主角色端主要功能 3.租客角色端主要功能 三.其它系 ...

  7. 基于Springboot+mybatis+mysql+html教育培训中心教学系统

    基于Springboot+mybatis+mysql+html教育培训中心教学系统 一.系统介绍 二.功能展示 1.用户登陆 2.用户注册 3.个人中心 4.人员信息管理 5.课程管理 6.缴费管理 ...

  8. 基于SpringBoot+MyBatis的餐饮点餐系统

    基于SpringBoot+MyBatis的餐饮点餐系统,适用于毕业设计. package com.example.zxdmeal.controller;import com.alibaba.fastj ...

  9. 基于springboot+mybatis+mysql+html企业人事管理系统

    基于springboot+mybatis+mysql+html企业人事管理系统 一.系统介绍 二.功能展示 1.用户登陆 2.员工奖惩--员工 3.合同管理--员工 4.个人薪酬--员工 5.培训管理 ...

  10. 基于Springboot+Mybatis+mysql+vue技术交流博客论坛系统

    基于Springboot+Mybatis+mysql+vue技术交流博客论坛系统 一.系统介绍 二.功能展示 1.主页(普通用户) 2.登陆.注册(普通用户) 3.博客(普通用户) 4.文章详情(点赞 ...

最新文章

  1. Java Websocket实例【服务端与客户端实现全双工通讯】
  2. 字节二面 | 26图揭秘线程安全
  3. Entity Framework 的事务 DbTransaction
  4. sqlite数据库插入和读取图片数据 (for ios)
  5. 自制仿360首页支持拼音输入全模糊搜索和自动换肤
  6. 华科考研834计算机网络,2018年华中科技大学834计算机专业基础综合 (模)...
  7. 《C++ Primer 5th》笔记(8 / 19):IO库
  8. java拦截器_springMVC入门(八)------拦截器
  9. 借条已经收回,他以没收据为由让我继续还款怎样办?
  10. IC设计EDA软件虚拟机环境配置与安装教程
  11. 实验前准备:CPU学习实验的头文件.vh
  12. 互联网发展阶段的三阶段是什么?从门户到搜索,到移动互联网
  13. 【数字基带传输】误码率的分析方法(BER Performance)
  14. IDEA使用Maven构建Spring+SpringMVC+MyBatis整合项目demo成功执行但控制台Tomcat Locahost log输出No Spring WebApplicationIn
  15. aop:aspectj-autoproxy /作用
  16. [保姆级教程]解决Centos 8下无法更新的问题 (附无痕迁徙到Rocky Linux的方法)
  17. Web 框架的替代方案来了!
  18. 强大的多语言版本在线图片处理网站
  19. 韩国计算机专业,韩国计算机专业发展前瞻
  20. refresh方法_Android Shake to Refresh教程

热门文章

  1. python爬虫使用seleium+超级鹰+Image模块自动登录12306(附源码和登录视频)
  2. Sourcetree下载-使用-快速入门
  3. fgo服务器维护更新,fgo命运冠位指定活动维护公告 12月2日更新
  4. Java 如何获取线程状态呢?
  5. hpm1005能扫描不能打印_「惠普m1005怎么扫描」HP惠普M1005打印机不能扫描文件该怎么办? - seo实验室...
  6. VIM 插件管理工具——vim-plug
  7. 开氏温度与摄氏度换算_8789 单位换算小技巧
  8. 【Word】论文公式居中,编号右对齐
  9. 运筹优化算法工程师面试问题汇总
  10. 使用python制作pdf的格式转换程序